Compare commits

..

2 Commits

Author SHA1 Message Date
Cody Henthorne
4e15bcc5f0 Bump version to 5.30.6 2022-02-03 14:59:48 -05:00
Cody Henthorne
b77abf6045 Fix sending reactions to note to self. 2022-02-03 14:57:27 -05:00
560 changed files with 7384 additions and 21040 deletions

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

@@ -62,31 +62,31 @@ ktlint {
version = "0.43.2"
}
def canonicalVersionCode = 1015
def canonicalVersionName = "5.32.12"
def canonicalVersionCode = 996
def canonicalVersionName = "5.30.6"
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',
'websiteProdFlipper',
'websiteProdRelease',
]
@@ -117,48 +117,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
@@ -232,6 +190,38 @@ 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) {
@@ -247,6 +237,7 @@ android {
'proguard/proguard-appcompat-v7.pro',
'proguard/proguard-square-okhttp.pro',
'proguard/proguard-square-okio.pro',
'proguard/proguard-spongycastle.pro',
'proguard/proguard-rounded-image-view.pro',
'proguard/proguard-glide.pro',
'proguard/proguard-shortcutbadger.pro',
@@ -262,12 +253,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
@@ -356,9 +347,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 {
@@ -383,6 +371,19 @@ android {
variant.setIgnore(true)
}
}
lintOptions {
checkReleaseBuilds false
abortOnError true
baseline file("lint-baseline.xml")
disable "LintError"
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
@@ -508,8 +509,9 @@ 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
@@ -524,9 +526,6 @@ dependencies {
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

@@ -4,6 +4,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@@ -29,16 +30,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)
}
// ==============================================================
@@ -51,7 +46,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())
}
@@ -61,7 +56,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())
}
@@ -72,7 +67,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. */
@@ -82,7 +77,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. */
@@ -91,7 +86,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())
}
@@ -101,7 +96,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())
}
@@ -112,26 +107,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())
}
@@ -144,7 +139,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())
}
@@ -157,7 +152,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())
}
@@ -174,7 +169,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())
}
@@ -187,12 +182,12 @@ 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. */
@@ -206,12 +201,14 @@ class RecipientDatabaseTest {
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertEquals(ACI_B, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
assertEquals(PNI_A, retrievedRecipient.pni.get())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertEquals(ACI_A, existingRecipient.requireAci())
assertFalse(existingRecipient.hasE164())
assertNull(existingRecipient.pni.orNull())
}
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. And low trust means we cant take the e164. */
@@ -223,11 +220,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())
}
@@ -246,11 +243,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())
}
@@ -267,7 +264,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())
}
@@ -284,7 +281,7 @@ class RecipientDatabaseTest {
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)
@@ -307,7 +304,7 @@ class RecipientDatabaseTest {
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)
@@ -320,19 +317,19 @@ class RecipientDatabaseTest {
/** 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. */
@@ -348,11 +345,11 @@ 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)
@@ -368,11 +365,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())
}
@@ -389,7 +386,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)
@@ -414,11 +411,11 @@ 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())
}
@@ -437,7 +434,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())
}
@@ -456,7 +453,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())
}
@@ -472,7 +469,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())
changeNumberListener.waitForJobManager()
@@ -503,18 +500,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)

View File

@@ -22,7 +22,6 @@ 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
@@ -34,7 +33,6 @@ 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.UUID
@@ -53,9 +51,6 @@ class RecipientDatabaseTest_merges {
private lateinit var reactionDatabase: ReactionDatabase
private lateinit var notificationProfileDatabase: NotificationProfileDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
@@ -70,9 +65,6 @@ class RecipientDatabaseTest_merges {
reactionDatabase = SignalDatabase.reactions
notificationProfileDatabase = SignalDatabase.notificationProfiles
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
ensureDbEmpty()
}
@@ -80,9 +72,9 @@ class RecipientDatabaseTest_merges {
@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
@@ -107,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))
@@ -127,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)
@@ -183,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))

View File

@@ -4,7 +4,7 @@
package="org.thoughtcrime.securesms">
<application
android:name=".SpinnerApplicationContext"
android:name=".FlipperApplicationContext"
tools:replace="android:name">
<activity

View File

@@ -1,37 +1,25 @@
package org.thoughtcrime.securesms
import android.os.Build
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.signal.spinner.Spinner
import org.thoughtcrime.securesms.database.JobDatabase
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
import org.thoughtcrime.securesms.database.LogDatabase
import org.thoughtcrime.securesms.database.MegaphoneDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.util.AppSignatureUtil
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter
import shark.AndroidReferenceMatchers
class SpinnerApplicationContext : ApplicationContext() {
class FlipperApplicationContext : ApplicationContext() {
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
Spinner.init(
this,
Spinner.DeviceInfo(
name = "${Build.MODEL} (Android ${Build.VERSION.RELEASE}, API ${Build.VERSION.SDK_INT})",
packageName = "$packageName (${AppSignatureUtil.getAppSignature(this).or("Unknown")})",
appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.CANONICAL_VERSION_CODE}, ${BuildConfig.GIT_HASH})"
),
linkedMapOf(
"signal" to SignalDatabase.rawDatabase,
"jobmanager" to JobDatabase.getInstance(this).sqlCipherDatabase,
"keyvalue" to KeyValueDatabase.getInstance(this).sqlCipherDatabase,
"megaphones" to MegaphoneDatabase.getInstance(this).sqlCipherDatabase,
"localmetrics" to LocalMetricsDatabase.getInstance(this).sqlCipherDatabase,
"logs" to LogDatabase.getInstance(this).sqlCipherDatabase,
)
)
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 +

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

@@ -308,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"
@@ -316,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" />
@@ -420,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"

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

@@ -36,7 +36,6 @@ 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.jobs.RetrieveReleaseChannelJob;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
@@ -176,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)
@@ -195,7 +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)
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -352,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

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

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

@@ -571,11 +571,11 @@ 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());
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
if (onContactSelectedListener != null) {
@@ -746,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);

View File

@@ -21,11 +21,9 @@ 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;
@@ -188,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

@@ -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;
@@ -555,15 +557,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
int mediaPosition = Objects.requireNonNull(data.second);
CursorPagerAdapter oldAdapter = (CursorPagerAdapter) mediaPager.getAdapter();
if (oldAdapter == null) {
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(), this, cursor, mediaPosition, leftIsRecent);
mediaPager.setAdapter(adapter);
adapter.setActive(true);
} else {
oldAdapter.setCursor(cursor, mediaPosition);
oldAdapter.setActive(true);
}
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(),this, cursor, mediaPosition, leftIsRecent);
mediaPager.setAdapter(adapter);
adapter.setActive(true);
viewModel.setCursor(this, cursor, leftIsRecent);
@@ -719,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,
@@ -743,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

@@ -67,7 +67,7 @@ public class NewConversationActivity extends ContactSelectionActivity
} 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);
@@ -75,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

@@ -22,7 +22,6 @@ 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;
/**
@@ -62,8 +61,7 @@ public class PassphraseCreateActivity extends PassphraseActivity {
passphrase);
MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret);
SignalStore.account().generateAciIdentityKey();
SignalStore.account().generatePniIdentityKey();
IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this);
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
return null;

View File

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

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

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

@@ -46,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;
@@ -163,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_")) {
@@ -182,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));
@@ -442,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) {
@@ -457,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);
}
@@ -485,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());
}
@@ -505,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;
@@ -251,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()) {

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

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

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

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

@@ -354,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
@@ -401,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();
}
@@ -488,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, buttonToggle)) {
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

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

@@ -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;
@@ -186,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);
@@ -218,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() {
@@ -283,8 +283,7 @@ 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);
@@ -303,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

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

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

@@ -22,7 +22,7 @@ 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) {
private var recyclerView: RecyclerView? = null

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

@@ -107,7 +107,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
sectionHeaderPref(R.string.AccountSettingsFragment__account)
if (FeatureFlags.changeNumber() && 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 = {

View File

@@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob
import org.thoughtcrime.securesms.jobs.RetrieveReleaseChannelJob
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
@@ -32,7 +31,6 @@ import org.thoughtcrime.securesms.payments.DataExportUtil
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.concurrent.SimpleTask
import kotlin.math.max
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {
@@ -335,9 +333,9 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
)
if (FeatureFlags.donorBadges() && SignalStore.donationsValues().getSubscriber() != null) {
dividerPref()
dividerPref()
if (FeatureFlags.donorBadges() && SignalStore.donationsValues().getSubscriber() != null) {
sectionHeaderPref(R.string.preferences__internal_badges)
clickPref(
@@ -347,25 +345,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
)
}
dividerPref()
sectionHeaderPref(R.string.preferences__internal_release_channel)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_fetch_release_channel),
onClick = {
SignalStore.releaseChannelValues().previousManifestMd5 = ByteArray(0)
RetrieveReleaseChannelJob.enqueue(force = true)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_release_channel_set_last_version),
onClick = {
SignalStore.releaseChannelValues().highestVersionNoteReceived = max(SignalStore.releaseChannelValues().highestVersionNoteReceived - 10, 0)
}
)
}
}

View File

@@ -6,7 +6,10 @@ import org.thoughtcrime.securesms.badges.models.Badge
* Events that can arise from use of the donations apis.
*/
sealed class DonationEvent {
class GooglePayUnavailableError(val throwable: Throwable) : DonationEvent()
object RequestTokenSuccess : DonationEvent()
class RequestTokenError(val throwable: Throwable) : DonationEvent()
class PaymentConfirmationError(val throwable: Throwable) : DonationEvent()
class PaymentConfirmationSuccess(val badge: Badge) : DonationEvent()
class SubscriptionCancellationFailed(val throwable: Throwable) : DonationEvent()
object SubscriptionCancelled : DonationEvent()

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.components.settings.app.subscription
class DonationExceptions {
class SetupFailed(reason: Throwable) : Exception(reason)
object TimedOutWaitingForTokenRedemption : Exception()
object RedemptionFailed : Exception()
}

View File

@@ -13,8 +13,6 @@ import org.signal.donations.GooglePayApi
import org.signal.donations.GooglePayPaymentSource
import org.signal.donations.StripeApi
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.JobTracker
@@ -58,6 +56,8 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
private val googlePayApi = GooglePayApi(activity, StripeApi.Gateway(Environment.Donations.STRIPE_CONFIGURATION), Environment.Donations.GOOGLE_PAY_CONFIGURATION)
private val stripeApi = StripeApi(Environment.Donations.STRIPE_CONFIGURATION, this, this, ApplicationDependencies.getOkHttpClient())
fun isGooglePayAvailable(): Completable = googlePayApi.queryIsReadyToPay()
fun scheduleSyncForAccountRecordChange() {
SignalExecutors.BOUNDED.execute {
scheduleSyncForAccountRecordChangeSync()
@@ -88,13 +88,13 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
fun continuePayment(price: FiatMoney, paymentData: PaymentData): Completable {
Log.d(TAG, "Creating payment intent for $price...", true)
return stripeApi.createPaymentIntent(price, application.getString(R.string.Boost__thank_you_for_your_donation))
.onErrorResumeNext { Single.error(DonationError.getPaymentSetupError(DonationErrorSource.BOOST, it)) }
.onErrorResumeNext { Single.error(DonationExceptions.SetupFailed(it)) }
.flatMapCompletable { result ->
Log.d(TAG, "Created payment intent for $price.", true)
when (result) {
is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Completable.error(DonationError.boostAmountTooSmall())
is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Completable.error(DonationError.boostAmountTooLarge())
is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Completable.error(DonationError.invalidCurrencyForBoost())
is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Completable.error(DonationExceptions.SetupFailed(Exception("Boost amount is too small")))
is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Completable.error(DonationExceptions.SetupFailed(Exception("Boost amount is too large")))
is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Completable.error(DonationExceptions.SetupFailed(Exception("Boost currency is not supported")))
is StripeApi.CreatePaymentIntentResult.Success -> confirmPayment(paymentData, result.paymentIntent)
}
}
@@ -141,10 +141,7 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
private fun confirmPayment(paymentData: PaymentData, paymentIntent: StripeApi.PaymentIntent): Completable {
Log.d(TAG, "Confirming payment intent...", true)
val confirmPayment = stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), paymentIntent).onErrorResumeNext {
Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.BOOST, it))
}
val confirmPayment = stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), paymentIntent)
val waitOnRedemption = Completable.create {
Log.d(TAG, "Confirmed payment intent.", true)
@@ -167,20 +164,20 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
}
JobTracker.JobState.FAILURE -> {
Log.d(TAG, "Boost request response job chain failed permanently.", true)
it.onError(DonationError.genericBadgeRedemptionFailure(DonationErrorSource.BOOST))
it.onError(DonationExceptions.RedemptionFailed)
}
else -> {
Log.d(TAG, "Boost request response job chain ignored due to in-progress jobs.", true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.BOOST))
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
}
}
} else {
Log.d(TAG, "Boost redemption timed out waiting for job completion.", true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.BOOST))
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
}
} catch (e: InterruptedException) {
Log.d(TAG, "Boost redemption job interrupted", e, true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.BOOST))
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
}
}
@@ -239,20 +236,20 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
}
JobTracker.JobState.FAILURE -> {
Log.d(TAG, "Subscription request response job chain failed permanently.", true)
it.onError(DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION))
it.onError(DonationExceptions.RedemptionFailed)
}
else -> {
Log.d(TAG, "Subscription request response job chain ignored due to in-progress jobs.", true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION))
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
}
}
} else {
Log.d(TAG, "Subscription request response job timed out.", true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION))
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
}
} catch (e: InterruptedException) {
Log.w(TAG, "Subscription request response interrupted.", e, true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION))
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
}
}
}.doOnError {

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
import android.content.DialogInterface
import android.text.SpannableStringBuilder
import android.view.View
import androidx.appcompat.app.AlertDialog
@@ -11,7 +10,6 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.LottieAnimationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
@@ -24,22 +22,20 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFrag
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationExceptions
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.Progress
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyboard.findListener
import org.thoughtcrime.securesms.util.BottomSheetUtil.requireCoordinatorLayout
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.Projection
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
@@ -67,8 +63,6 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
private lateinit var processingDonationPaymentDialog: AlertDialog
private lateinit var donationPaymentComponent: DonationPaymentComponent
private var errorDialog: DialogInterface? = null
private val sayThanks: CharSequence by lazy {
SpannableStringBuilder(requireContext().getString(R.string.BoostFragment__say_thanks_and_earn, 30))
.append(" ")
@@ -80,7 +74,7 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
donationPaymentComponent = requireListener()
donationPaymentComponent = findListener()!!
viewModel.refresh()
CurrencySelection.register(adapter)
@@ -124,7 +118,10 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
lifecycleDisposable += viewModel.events.subscribe { event: DonationEvent ->
when (event) {
is DonationEvent.GooglePayUnavailableError -> Unit
is DonationEvent.PaymentConfirmationError -> onPaymentError(event.throwable)
is DonationEvent.PaymentConfirmationSuccess -> onPaymentConfirmed(event.badge)
is DonationEvent.RequestTokenError -> onPaymentError(DonationExceptions.SetupFailed(event.throwable))
DonationEvent.RequestTokenSuccess -> Log.i(TAG, "Successfully got request token from Google Pay")
DonationEvent.SubscriptionCancelled -> Unit
is DonationEvent.SubscriptionCancellationFailed -> Unit
@@ -133,13 +130,6 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
lifecycleDisposable += donationPaymentComponent.googlePayResultPublisher.subscribe {
viewModel.onActivityResult(it.requestCode, it.resultCode, it.data)
}
lifecycleDisposable += DonationError
.getErrorsForSource(DonationErrorSource.BOOST)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { donationError ->
onPaymentError(donationError)
}
}
override fun onDestroyView() {
@@ -250,21 +240,37 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
}
private fun onPaymentError(throwable: Throwable?) {
Log.w(TAG, "onPaymentError", throwable, true)
if (errorDialog != null) {
Log.i(TAG, "Already displaying an error dialog. Skipping.")
return
}
errorDialog = DonationErrorDialogs.show(
requireContext(), throwable,
object : DonationErrorDialogs.DialogCallback() {
override fun onDialogDismissed() {
if (throwable is DonationExceptions.TimedOutWaitingForTokenRedemption) {
Log.w(TAG, "Timed out while redeeming token", throwable, true)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.DonationsErrors__still_processing)
.setMessage(R.string.DonationsErrors__your_payment_is_still)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
findNavController().popBackStack()
}
}
)
.show()
} else if (throwable is DonationExceptions.SetupFailed) {
Log.w(TAG, "Error occurred while processing payment", throwable, true)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.DonationsErrors__error_processing_payment)
.setMessage(R.string.DonationsErrors__your_payment)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
findNavController().popBackStack()
}
.show()
} else {
Log.w(TAG, "Error occurred while trying to redeem token", throwable, true)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.DonationsErrors__couldnt_add_badge)
.setMessage(R.string.DonationsErrors__your_badge_could_not)
.setPositiveButton(R.string.Subscription__contact_support) { dialog, _ ->
dialog.dismiss()
findNavController().popBackStack()
}
.show()
}
}
private fun startAnimationAboveSelectedBoost(view: View) {

View File

@@ -18,14 +18,12 @@ import org.signal.donations.GooglePayApi
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.InternetConnectionObserver
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
import org.thoughtcrime.securesms.util.StringUtil
import org.thoughtcrime.securesms.util.livedata.Store
import java.lang.NumberFormatException
import java.math.BigDecimal
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
@@ -110,6 +108,11 @@ class BoostViewModel(
}
)
disposables += donationPaymentRepository.isGooglePayAvailable().subscribeBy(
onComplete = { store.update { it.copy(isGooglePayAvailable = true) } },
onError = { eventPublisher.onNext(DonationEvent.GooglePayUnavailableError(it)) }
)
disposables += currencyObservable.subscribeBy { currency ->
store.update {
it.copy(
@@ -143,13 +146,7 @@ class BoostViewModel(
donationPaymentRepository.continuePayment(boost.price, paymentData).subscribeBy(
onError = { throwable ->
store.update { it.copy(stage = BoostState.Stage.READY) }
val donationError: DonationError = if (throwable is DonationError) {
throwable
} else {
Log.w(TAG, "Failed to complete payment or redemption", throwable, true)
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.BOOST)
}
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
eventPublisher.onNext(DonationEvent.PaymentConfirmationError(throwable))
},
onComplete = {
store.update { it.copy(stage = BoostState.Stage.READY) }
@@ -163,7 +160,7 @@ class BoostViewModel(
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
store.update { it.copy(stage = BoostState.Stage.READY) }
DonationError.routeDonationError(ApplicationDependencies.getApplication(), DonationError.getGooglePayRequestTokenError(DonationErrorSource.BOOST, googlePayException))
eventPublisher.onNext(DonationEvent.RequestTokenError(googlePayException))
}
override fun onCancelled() {

View File

@@ -7,7 +7,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFrag
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.keyboard.findListener
import java.util.Locale
/**
@@ -25,7 +25,7 @@ class SetCurrencyFragment : DSLSettingsBottomSheetFragment() {
)
override fun bindAdapter(adapter: DSLSettingsAdapter) {
donationPaymentComponent = requireListener()
donationPaymentComponent = findListener()!!
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())

View File

@@ -1,154 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.errors
import android.content.Context
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.signal.core.util.logging.Log
import org.signal.donations.StripeDeclineCode
import org.signal.donations.StripeError
sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : Exception(cause) {
/**
* Google Pay errors, which happen well before a user would ever be charged.
*/
sealed class GooglePayError(source: DonationErrorSource, cause: Throwable) : DonationError(source, cause) {
class NotAvailableError(source: DonationErrorSource, cause: Throwable) : GooglePayError(source, cause)
class RequestTokenError(source: DonationErrorSource, cause: Throwable) : GooglePayError(source, cause)
}
/**
* Boost validation errors, which occur before the user could be charged.
*/
sealed class BoostError(message: String) : DonationError(DonationErrorSource.BOOST, Exception(message)) {
object AmountTooSmallError : BoostError("Amount is too small")
object AmountTooLargeError : BoostError("Amount is too large")
object InvalidCurrencyError : BoostError("Currency is not supported")
}
/**
* Stripe setup errors, which occur before the user could be charged. These are either
* payment processing handed to Stripe from the CC company (in the case of a Boost payment
* intent confirmation error) or other generic error from Stripe.
*/
sealed class PaymentSetupError(source: DonationErrorSource, cause: Throwable) : DonationError(source, cause) {
/**
* Payment setup failed in some generic fashion.
*/
class GenericError(source: DonationErrorSource, cause: Throwable) : PaymentSetupError(source, cause)
/**
* Payment setup failed in some way, which we are told about by Stripe.
*/
class CodedError(source: DonationErrorSource, cause: Throwable, val errorCode: String) : PaymentSetupError(source, cause)
/**
* Payment failed by the credit card processor, with a specific reason told to us by Stripe.
*/
class DeclinedError(source: DonationErrorSource, cause: Throwable, val declineCode: StripeDeclineCode) : PaymentSetupError(source, cause)
}
/**
* Errors that can be thrown after we submit a payment to Stripe. It is
* assumed that at this point, anything we submit *could* happen, so we can no
* longer safely assume a user has not been charged. Payment errors explicitly
* originate from Signal service.
*/
sealed class PaymentProcessingError(source: DonationErrorSource, cause: Throwable) : DonationError(source, cause) {
class GenericError(source: DonationErrorSource) : DonationError(source, Exception("Generic Payment Error"))
}
/**
* Errors that can occur during the badge redemption process.
*/
sealed class BadgeRedemptionError(source: DonationErrorSource, cause: Throwable) : DonationError(source, cause) {
/**
* Timeout elapsed while the user was waiting for badge redemption to complete. This is not an indication that
* redemption failed, just that it is taking longer than we can reasonably show a spinner.
*/
class TimeoutWaitingForTokenError(source: DonationErrorSource) : BadgeRedemptionError(source, Exception("Timed out waiting for badge redemption to complete."))
/**
* Some generic error not otherwise accounted for occurred during the redemption process.
*/
class GenericError(source: DonationErrorSource) : BadgeRedemptionError(source, Exception("Failed to add badge to account."))
}
companion object {
private val TAG = Log.tag(DonationError::class.java)
private val donationErrorSubjectSourceMap: Map<DonationErrorSource, Subject<DonationError>> = DonationErrorSource.values().associate { source ->
source to PublishSubject.create()
}
@JvmStatic
fun getErrorsForSource(donationErrorSource: DonationErrorSource): Observable<DonationError> {
return donationErrorSubjectSourceMap[donationErrorSource]!!
}
/**
* Route a given donation error, which will either pipe it out to an appropriate subject
* or, if the subject has no observers, post it as a notification.
*/
@JvmStatic
fun routeDonationError(context: Context, error: DonationError) {
val subject: Subject<DonationError> = donationErrorSubjectSourceMap[error.source]!!
when {
subject.hasObservers() -> {
Log.i(TAG, "Routing donation error to subject ${error.source} dialog", error)
subject.onNext(error)
}
else -> {
Log.i(TAG, "Routing donation error to subject ${error.source} notification", error)
DonationErrorNotifications.displayErrorNotification(context, error)
}
}
}
@JvmStatic
fun getGooglePayRequestTokenError(source: DonationErrorSource, throwable: Throwable): DonationError {
return GooglePayError.RequestTokenError(source, throwable)
}
/**
* Converts a throwable into a payment setup error. This should only be used when
* handling errors handed back via the Stripe API, when we know for sure that no
* charge has occurred.
*/
@JvmStatic
fun getPaymentSetupError(source: DonationErrorSource, throwable: Throwable): DonationError {
return if (throwable is StripeError.PostError) {
val declineCode: StripeDeclineCode? = throwable.declineCode
val errorCode: String? = throwable.errorCode
when {
declineCode != null -> PaymentSetupError.DeclinedError(source, throwable, declineCode)
errorCode != null -> PaymentSetupError.CodedError(source, throwable, errorCode)
else -> PaymentSetupError.GenericError(source, throwable)
}
} else {
PaymentSetupError.GenericError(source, throwable)
}
}
@JvmStatic
fun boostAmountTooSmall(): DonationError = BoostError.AmountTooSmallError
@JvmStatic
fun boostAmountTooLarge(): DonationError = BoostError.AmountTooLargeError
@JvmStatic
fun invalidCurrencyForBoost(): DonationError = BoostError.InvalidCurrencyError
@JvmStatic
fun timeoutWaitingForToken(source: DonationErrorSource): DonationError = BadgeRedemptionError.TimeoutWaitingForTokenError(source)
@JvmStatic
fun genericBadgeRedemptionFailure(source: DonationErrorSource): DonationError = BadgeRedemptionError.GenericError(source)
@JvmStatic
fun genericPaymentFailure(source: DonationErrorSource): DonationError = PaymentProcessingError.GenericError(source)
}
}

View File

@@ -1,89 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.errors
import android.content.Context
import android.content.DialogInterface
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.util.CommunicationActions
/**
* Donation Error Dialogs.
*/
object DonationErrorDialogs {
/**
* Displays a dialog, and returns a handle to it for dismissal.
*/
fun show(context: Context, throwable: Throwable?, callback: DialogCallback): DialogInterface {
val builder = MaterialAlertDialogBuilder(context)
builder.setOnDismissListener { callback.onDialogDismissed() }
val params = DonationErrorParams.create(context, throwable, callback)
if (params.title != null) {
builder.setTitle(params.title)
}
if (params.message != null) {
builder.setMessage(params.message)
}
if (params.positiveAction != null) {
builder.setPositiveButton(params.positiveAction.label) { _, _ -> params.positiveAction.action() }
}
if (params.negativeAction != null) {
builder.setNegativeButton(params.negativeAction.label) { _, _ -> params.negativeAction.action() }
}
return builder.show()
}
open class DialogCallback : DonationErrorParams.Callback<Unit> {
override fun onCancel(context: Context): DonationErrorParams.ErrorAction<Unit>? {
return DonationErrorParams.ErrorAction(
label = android.R.string.cancel,
action = {}
)
}
override fun onOk(context: Context): DonationErrorParams.ErrorAction<Unit>? {
return DonationErrorParams.ErrorAction(
label = android.R.string.ok,
action = {}
)
}
override fun onGoToGooglePay(context: Context): DonationErrorParams.ErrorAction<Unit>? {
return DonationErrorParams.ErrorAction(
label = R.string.DeclineCode__go_to_google_pay,
action = {
CommunicationActions.openBrowserLink(context, context.getString(R.string.google_pay_url))
}
)
}
override fun onLearnMore(context: Context): DonationErrorParams.ErrorAction<Unit>? {
return DonationErrorParams.ErrorAction(
label = R.string.DeclineCode__learn_more,
action = {
CommunicationActions.openBrowserLink(context, context.getString(R.string.donation_decline_code_error_url))
}
)
}
override fun onContactSupport(context: Context): DonationErrorParams.ErrorAction<Unit> {
return DonationErrorParams.ErrorAction(
label = R.string.Subscription__contact_support,
action = {
context.startActivity(AppSettingsActivity.help(context, HelpFragment.DONATION_INDEX))
}
)
}
open fun onDialogDismissed() = Unit
}
}

View File

@@ -1,99 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.errors
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.NotificationIds
/**
* Donation-related push notifications.
*/
object DonationErrorNotifications {
fun displayErrorNotification(context: Context, donationError: DonationError) {
val parameters = DonationErrorParams.create(context, donationError, NotificationCallback)
val notification = NotificationCompat.Builder(context, NotificationChannels.FAILURES)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(context.getString(parameters.title))
.setContentText(context.getString(parameters.message)).apply {
if (parameters.positiveAction != null) {
addAction(context, parameters.positiveAction)
}
if (parameters.negativeAction != null) {
addAction(context, parameters.negativeAction)
}
}
.build()
NotificationManagerCompat
.from(context)
.notify(NotificationIds.DONOR_BADGE_FAILURE, notification)
}
private fun NotificationCompat.Builder.addAction(context: Context, errorAction: DonationErrorParams.ErrorAction<PendingIntent>) {
addAction(
NotificationCompat.Action.Builder(
null,
context.getString(errorAction.label),
errorAction.action.invoke()
).build()
)
}
private object NotificationCallback : DonationErrorParams.Callback<PendingIntent> {
override fun onCancel(context: Context): DonationErrorParams.ErrorAction<PendingIntent>? = null
override fun onOk(context: Context): DonationErrorParams.ErrorAction<PendingIntent>? = null
override fun onLearnMore(context: Context): DonationErrorParams.ErrorAction<PendingIntent> {
return createAction(
context = context,
label = R.string.DeclineCode__learn_more,
actionIntent = Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.donation_decline_code_error_url)))
)
}
override fun onGoToGooglePay(context: Context): DonationErrorParams.ErrorAction<PendingIntent> {
return createAction(
context = context,
label = R.string.DeclineCode__go_to_google_pay,
actionIntent = Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.google_pay_url)))
)
}
override fun onContactSupport(context: Context): DonationErrorParams.ErrorAction<PendingIntent> {
return createAction(
context = context,
label = R.string.Subscription__contact_support,
actionIntent = AppSettingsActivity.help(context, HelpFragment.DONATION_INDEX)
)
}
private fun createAction(
context: Context,
label: Int,
actionIntent: Intent
): DonationErrorParams.ErrorAction<PendingIntent> {
return DonationErrorParams.ErrorAction(
label = label,
action = {
PendingIntent.getActivity(
context,
0,
actionIntent,
if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_ONE_SHOT else 0
)
}
)
}
}
}

View File

@@ -1,97 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.errors
import android.content.Context
import androidx.annotation.StringRes
import org.signal.donations.StripeDeclineCode
import org.thoughtcrime.securesms.R
class DonationErrorParams<V> private constructor(
@StringRes val title: Int,
@StringRes val message: Int,
val positiveAction: ErrorAction<V>?,
val negativeAction: ErrorAction<V>?
) {
class ErrorAction<V>(
@StringRes val label: Int,
val action: () -> V
)
companion object {
fun <V> create(
context: Context,
throwable: Throwable?,
callback: Callback<V>
): DonationErrorParams<V> {
return when (throwable) {
is DonationError.PaymentSetupError.DeclinedError -> getDeclinedErrorParams(context, throwable, callback)
is DonationError.PaymentSetupError -> DonationErrorParams(
title = R.string.DonationsErrors__error_processing_payment,
message = R.string.DonationsErrors__your_payment,
positiveAction = callback.onOk(context),
negativeAction = null
)
is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError -> DonationErrorParams(
title = R.string.DonationsErrors__still_processing,
message = R.string.DonationsErrors__your_payment_is_still,
positiveAction = callback.onOk(context),
negativeAction = null
)
else -> DonationErrorParams(
title = R.string.DonationsErrors__couldnt_add_badge,
message = R.string.DonationsErrors__your_badge_could_not,
positiveAction = callback.onContactSupport(context),
negativeAction = null
)
}
}
private fun <V> getDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.DeclinedError, callback: Callback<V>): DonationErrorParams<V> {
return when (declinedError.declineCode) {
is StripeDeclineCode.Known -> when (declinedError.declineCode.code) {
StripeDeclineCode.Code.APPROVE_WITH_ID -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again)
StripeDeclineCode.Code.CALL_ISSUER -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again_if_the_problem)
StripeDeclineCode.Code.CARD_NOT_SUPPORTED -> getLearnMoreParams(context, callback, R.string.DeclineCode__your_card_does_not_support_this_type_of_purchase)
StripeDeclineCode.Code.EXPIRED_CARD -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__your_card_has_expired)
StripeDeclineCode.Code.INCORRECT_NUMBER -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__your_card_number_is_incorrect)
StripeDeclineCode.Code.INCORRECT_CVC -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__your_cards_cvc_number_is_incorrect)
StripeDeclineCode.Code.INSUFFICIENT_FUNDS -> getLearnMoreParams(context, callback, R.string.DeclineCode__your_card_does_not_have_sufficient_funds)
StripeDeclineCode.Code.INVALID_CVC -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__your_cards_cvc_number_is_incorrect)
StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__the_expiration_month)
StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__the_expiration_year)
StripeDeclineCode.Code.INVALID_NUMBER -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__your_card_number_is_incorrect)
StripeDeclineCode.Code.ISSUER_NOT_AVAILABLE -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_completing_the_payment_again)
StripeDeclineCode.Code.PROCESSING_ERROR -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_again)
StripeDeclineCode.Code.REENTER_TRANSACTION -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_again)
else -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_another_payment_method_or_contact_your_bank)
}
else -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_another_payment_method_or_contact_your_bank)
}
}
private fun <V> getLearnMoreParams(context: Context, callback: Callback<V>, message: Int): DonationErrorParams<V> {
return DonationErrorParams(
title = R.string.DonationsErrors__error_processing_payment,
message = message,
positiveAction = callback.onOk(context),
negativeAction = callback.onLearnMore(context)
)
}
private fun <V> getGoToGooglePayParams(context: Context, callback: Callback<V>, message: Int): DonationErrorParams<V> {
return DonationErrorParams(
title = R.string.DonationsErrors__error_processing_payment,
message = message,
positiveAction = callback.onGoToGooglePay(context),
negativeAction = callback.onCancel(context)
)
}
}
interface Callback<V> {
fun onOk(context: Context): ErrorAction<V>?
fun onCancel(context: Context): ErrorAction<V>?
fun onLearnMore(context: Context): ErrorAction<V>?
fun onContactSupport(context: Context): ErrorAction<V>?
fun onGoToGooglePay(context: Context): ErrorAction<V>?
}
}

View File

@@ -1,17 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.errors
enum class DonationErrorSource(private val code: String) {
BOOST("boost"),
SUBSCRIPTION("subscription"),
KEEP_ALIVE("keep-alive"),
UNKNOWN("unknown");
fun serialize(): String = code
companion object {
@JvmStatic
fun deserialize(code: String): DonationErrorSource {
return values().firstOrNull { it.code == code } ?: UNKNOWN
}
}
}

View File

@@ -1,19 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.errors
/**
* Error states that can occur if we detect that a user's subscription has been cancelled and the manual
* cancellation flag is not set.
*/
enum class UnexpectedSubscriptionCancellation(val status: String) {
PAST_DUE("past_due"),
CANCELED("canceled"),
UNPAID("unpaid"),
INACTIVE("user-was-inactive");
companion object {
@JvmStatic
fun fromStatus(status: String?): UnexpectedSubscriptionCancellation? {
return values().firstOrNull { it.status == status }
}
}
}

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.subscribe
import android.content.DialogInterface
import android.graphics.Color
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
@@ -9,7 +8,6 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
@@ -22,22 +20,22 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationExceptions
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.Progress
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.keyboard.findListener
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.subscription.Subscription
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import java.util.Calendar
import java.util.Currency
@@ -65,8 +63,6 @@ class SubscribeFragment : DSLSettingsFragment(
private lateinit var processingDonationPaymentDialog: AlertDialog
private lateinit var donationPaymentComponent: DonationPaymentComponent
private var errorDialog: DialogInterface? = null
private val viewModel: SubscribeViewModel by viewModels(
factoryProducer = {
SubscribeViewModel.Factory(SubscriptionsRepository(ApplicationDependencies.getDonationsService()), donationPaymentComponent.donationPaymentRepository, FETCH_SUBSCRIPTION_TOKEN_REQUEST_CODE)
@@ -79,7 +75,7 @@ class SubscribeFragment : DSLSettingsFragment(
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
donationPaymentComponent = requireListener()
donationPaymentComponent = findListener()!!
viewModel.refresh()
BadgePreview.register(adapter)
@@ -101,7 +97,10 @@ class SubscribeFragment : DSLSettingsFragment(
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
lifecycleDisposable += viewModel.events.subscribe {
when (it) {
is DonationEvent.GooglePayUnavailableError -> Unit
is DonationEvent.PaymentConfirmationError -> onPaymentError(it.throwable)
is DonationEvent.PaymentConfirmationSuccess -> onPaymentConfirmed(it.badge)
is DonationEvent.RequestTokenError -> onPaymentError(DonationExceptions.SetupFailed(it.throwable))
DonationEvent.RequestTokenSuccess -> Log.w(TAG, "Successfully got request token from Google Pay")
DonationEvent.SubscriptionCancelled -> onSubscriptionCancelled()
is DonationEvent.SubscriptionCancellationFailed -> onSubscriptionFailedToCancel(it.throwable)
@@ -110,13 +109,6 @@ class SubscribeFragment : DSLSettingsFragment(
lifecycleDisposable += donationPaymentComponent.googlePayResultPublisher.subscribe {
viewModel.onActivityResult(it.requestCode, it.resultCode, it.data)
}
lifecycleDisposable += DonationError
.getErrorsForSource(DonationErrorSource.SUBSCRIPTION)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { donationError ->
onPaymentError(donationError)
}
}
override fun onDestroyView() {
@@ -285,21 +277,49 @@ class SubscribeFragment : DSLSettingsFragment(
}
private fun onPaymentError(throwable: Throwable?) {
Log.w(TAG, "onPaymentError", throwable, true)
if (errorDialog != null) {
Log.i(TAG, "Already displaying an error dialog. Skipping.")
return
}
errorDialog = DonationErrorDialogs.show(
requireContext(), throwable,
object : DonationErrorDialogs.DialogCallback() {
override fun onDialogDismissed() {
findNavController().popBackStack()
if (throwable is DonationExceptions.TimedOutWaitingForTokenRedemption) {
Log.w(TAG, "Timeout occurred while redeeming token", throwable, true)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.DonationsErrors__still_processing)
.setMessage(R.string.DonationsErrors__your_payment_is_still)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
requireActivity().finish()
requireActivity().startActivity(AppSettingsActivity.manageSubscriptions(requireContext()))
}
}
)
.show()
} else if (throwable is DonationExceptions.SetupFailed) {
Log.w(TAG, "Error occurred while processing payment", throwable, true)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.DonationsErrors__error_processing_payment)
.setMessage(R.string.DonationsErrors__your_payment)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
}
.show()
} else if (SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt) {
Log.w(TAG, "Stripe failed to process payment", throwable, true)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.DonationsErrors__error_processing_payment)
.setMessage(R.string.DonationsErrors__your_badge_could_not_be_added)
.setPositiveButton(R.string.Subscription__contact_support) { dialog, _ ->
dialog.dismiss()
requireActivity().finish()
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
}
.show()
} else {
Log.w(TAG, "Error occurred while trying to redeem token", throwable, true)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.DonationsErrors__couldnt_add_badge)
.setMessage(R.string.DonationsErrors__your_badge_could_not)
.setPositiveButton(R.string.Subscription__contact_support) { dialog, _ ->
dialog.dismiss()
requireActivity().finish()
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
}
.show()
}
}
private fun onSubscriptionCancelled() {

View File

@@ -18,11 +18,9 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.GooglePayApi
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationExceptions
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.subscription.LevelUpdate
@@ -131,6 +129,11 @@ class SubscribeViewModel(
onError = this::handleSubscriptionDataLoadFailure
)
disposables += donationPaymentRepository.isGooglePayAvailable().subscribeBy(
onComplete = { store.update { it.copy(isGooglePayAvailable = true) } },
onError = { eventPublisher.onNext(DonationEvent.GooglePayUnavailableError(it)) }
)
disposables += currency.subscribe { selection ->
store.update { it.copy(currencySelection = selection) }
}
@@ -167,7 +170,6 @@ class SubscribeViewModel(
SignalStore.donationsValues().setLastEndOfPeriod(0L)
SignalStore.donationsValues().clearLevelOperations()
SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt = false
SignalStore.donationsValues().unexpectedSubscriptionCancelationReason = null
MultiDeviceSubscriptionSyncRequestJob.enqueue()
}
} else {
@@ -184,7 +186,6 @@ class SubscribeViewModel(
SignalStore.donationsValues().setLastEndOfPeriod(0L)
SignalStore.donationsValues().clearLevelOperations()
SignalStore.donationsValues().markUserManuallyCancelled()
SignalStore.donationsValues().unexpectedSubscriptionCancelationReason = null
refreshActiveSubscription()
MultiDeviceSubscriptionSyncRequestJob.enqueue()
donationPaymentRepository.scheduleSyncForAccountRecordChange()
@@ -221,20 +222,13 @@ class SubscribeViewModel(
val setup = ensureSubscriberId
.andThen(cancelActiveSubscriptionIfNecessary())
.andThen(continueSetup)
.onErrorResumeNext { Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.SUBSCRIPTION, it)) }
.onErrorResumeNext { Completable.error(DonationExceptions.SetupFailed(it)) }
setup.andThen(setLevel).subscribeBy(
onError = { throwable ->
refreshActiveSubscription()
store.update { it.copy(stage = SubscribeState.Stage.READY) }
val donationError: DonationError = if (throwable is DonationError) {
throwable
} else {
Log.w(TAG, "Failed to complete payment or redemption", throwable, true)
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
}
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
eventPublisher.onNext(DonationEvent.PaymentConfirmationError(throwable))
},
onComplete = {
store.update { it.copy(stage = SubscribeState.Stage.READY) }
@@ -248,7 +242,7 @@ class SubscribeViewModel(
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
store.update { it.copy(stage = SubscribeState.Stage.READY) }
DonationError.routeDonationError(ApplicationDependencies.getApplication(), DonationError.getGooglePayRequestTokenError(DonationErrorSource.SUBSCRIPTION, googlePayException))
eventPublisher.onNext(DonationEvent.RequestTokenError(googlePayException))
}
override fun onCancelled() {
@@ -268,13 +262,7 @@ class SubscribeViewModel(
},
onError = { throwable ->
store.update { it.copy(stage = SubscribeState.Stage.READY) }
val donationError: DonationError = if (throwable is DonationError) {
throwable
} else {
Log.w(TAG, "Failed to complete payment or redemption", throwable, true)
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
}
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
eventPublisher.onNext(DonationEvent.PaymentConfirmationError(throwable))
}
)
}

View File

@@ -18,7 +18,6 @@ import androidx.core.content.ContextCompat
import androidx.core.view.doOnPreDraw
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import app.cash.exhaustive.Exhaustive
import com.google.android.flexbox.FlexboxLayoutManager
@@ -93,7 +92,8 @@ private const val REQUEST_CODE_RETURN_FROM_MEDIA = 4
class ConversationSettingsFragment : DSLSettingsFragment(
layoutId = R.layout.conversation_settings_fragment,
menuId = R.menu.conversation_settings
menuId = R.menu.conversation_settings,
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
) {
private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) }
@@ -151,11 +151,6 @@ class ConversationSettingsFragment : DSLSettingsFragment(
toolbarTitle = view.findViewById(R.id.toolbar_title)
toolbarBackground = view.findViewById(R.id.toolbar_background)
val args: ConversationSettingsFragmentArgs = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
if (args.recipientId != null) {
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
}
super.onViewCreated(view, savedInstanceState)
}
@@ -398,32 +393,28 @@ class ConversationSettingsFragment : DSLSettingsFragment(
enabled = it.canEditGroupAttributes
}
if (!state.recipient.isReleaseNotes) {
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__disappearing_messages),
summary = summary,
icon = DSLSettingsIcon.from(icon),
isEnabled = enabled,
onClick = {
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToAppSettingsExpireTimer()
.setInitialValue(state.disappearingMessagesLifespan)
.setRecipientId(state.recipient.id)
.setForResultMode(false)
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__disappearing_messages),
summary = summary,
icon = DSLSettingsIcon.from(icon),
isEnabled = enabled,
onClick = {
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToAppSettingsExpireTimer()
.setInitialValue(state.disappearingMessagesLifespan)
.setRecipientId(state.recipient.id)
.setForResultMode(false)
navController.safeNavigate(action)
}
)
}
navController.safeNavigate(action)
}
)
if (!state.recipient.isReleaseNotes) {
clickPref(
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
icon = DSLSettingsIcon.from(R.drawable.ic_color_24),
onClick = {
startActivity(ChatWallpaperActivity.createIntent(requireContext(), state.recipient.id))
}
)
}
clickPref(
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
icon = DSLSettingsIcon.from(R.drawable.ic_color_24),
onClick = {
startActivity(ChatWallpaperActivity.createIntent(requireContext(), state.recipient.id))
}
)
if (!state.recipient.isSelf) {
clickPref(
@@ -516,7 +507,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
)
}
if (recipientSettingsState.selfHasGroups && !state.recipient.isReleaseNotes) {
if (recipientSettingsState.selfHasGroups) {
dividerPref()
@@ -767,14 +758,9 @@ class ConversationSettingsFragment : DSLSettingsFragment(
private val rect = Rect()
override fun getAnimationState(recyclerView: RecyclerView): AnimationState {
val layoutManager = recyclerView.layoutManager!!
val firstVisibleItemPosition = if (layoutManager is FlexboxLayoutManager) {
layoutManager.findFirstVisibleItemPosition()
} else {
(layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
}
val layoutManager = recyclerView.layoutManager as FlexboxLayoutManager
return if (firstVisibleItemPosition == 0) {
return if (layoutManager.findFirstVisibleItemPosition() == 0) {
val firstChild = requireNotNull(layoutManager.getChildAt(0))
firstChild.getLocalVisibleRect(rect)

View File

@@ -65,7 +65,7 @@ class ConversationSettingsRepository(
fun getIdentity(recipientId: RecipientId, consumer: (IdentityRecord?) -> Unit) {
SignalExecutors.BOUNDED.execute {
consumer(ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipientId).orNull())
consumer(ApplicationDependencies.getIdentityStore().getIdentityRecord(recipientId).orNull())
}
}

View File

@@ -130,8 +130,8 @@ sealed class ConversationSettingsViewModel(
state.copy(
recipient = recipient,
buttonStripState = ButtonStripPreference.State(
isVideoAvailable = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED && !recipient.isSelf && !recipient.isBlocked && !recipient.isReleaseNotes,
isAudioAvailable = !recipient.isGroup && !recipient.isSelf && !recipient.isBlocked && !recipient.isReleaseNotes,
isVideoAvailable = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED && !recipient.isSelf && !recipient.isBlocked,
isAudioAvailable = !recipient.isGroup && !recipient.isSelf && !recipient.isBlocked,
isAudioSecure = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED,
isMuted = recipient.isMuted,
isMuteAvailable = !recipient.isSelf,
@@ -141,7 +141,7 @@ sealed class ConversationSettingsViewModel(
canModifyBlockedState = !recipient.isSelf && RecipientUtil.isBlockable(recipient),
specificSettingsState = state.requireRecipientSettingsState().copy(
contactLinkState = when {
recipient.isSelf || recipient.isReleaseNotes -> ContactLinkState.NONE
recipient.isSelf -> ContactLinkState.NONE
recipient.isSystemContact -> ContactLinkState.OPEN
else -> ContactLinkState.ADD
}

View File

@@ -26,7 +26,8 @@ import org.thoughtcrime.securesms.util.Hex
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.livedata.Store
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import java.util.Objects
/**
@@ -60,11 +61,18 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
)
if (!recipient.isGroup) {
val serviceId = recipient.serviceId.transform(ServiceId::toString).or("null")
val aci = recipient.aci.transform(ACI::toString).or("null")
longClickPref(
title = DSLSettingsText.from("ServiceId"),
summary = DSLSettingsText.from(serviceId),
onLongClick = { copyToClipboard(serviceId) }
title = DSLSettingsText.from("ACI"),
summary = DSLSettingsText.from(aci),
onLongClick = { copyToClipboard(aci) }
)
val pni = recipient.pni.transform(PNI::toString).or("null")
longClickPref(
title = DSLSettingsText.from("PNI"),
summary = DSLSettingsText.from(pni),
onLongClick = { copyToClipboard(pni) }
)
}
@@ -145,8 +153,11 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
if (recipient.hasServiceId()) {
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireServiceId().toString())
if (recipient.hasAci()) {
SignalDatabase.sessions.deleteAllFor(recipient.requireAci().toString())
}
if (recipient.hasE164()) {
SignalDatabase.sessions.deleteAllFor(recipient.requireE164())
}
}
.show()

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.settings.conversation.preferences
import android.content.ClipData
import android.content.Context
import android.text.SpannableStringBuilder
import android.view.View
import android.widget.TextView
import android.widget.Toast
@@ -10,9 +9,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ContextUtil
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
@@ -28,8 +25,8 @@ object BioTextPreference {
}
abstract class BioTextPreferenceModel<T : BioTextPreferenceModel<T>> : PreferenceModel<T>() {
abstract fun getHeadlineText(context: Context): CharSequence
abstract fun getSubhead1Text(context: Context): String?
abstract fun getHeadlineText(context: Context): String
abstract fun getSubhead1Text(): String?
abstract fun getSubhead2Text(): String?
}
@@ -37,24 +34,9 @@ object BioTextPreference {
private val recipient: Recipient,
) : BioTextPreferenceModel<RecipientModel>() {
override fun getHeadlineText(context: Context): CharSequence {
val name = recipient.getDisplayNameOrUsername(context)
return if (recipient.isReleaseNotes) {
SpannableStringBuilder(name).apply {
SpanUtil.appendCenteredImageSpan(this, ContextUtil.requireDrawable(context, R.drawable.ic_official_28), 28, 28)
}
} else {
name
}
}
override fun getHeadlineText(context: Context): String = recipient.getDisplayNameOrUsername(context)
override fun getSubhead1Text(context: Context): String? {
return if (recipient.isReleaseNotes) {
context.getString(R.string.ReleaseNotes__signal_release_notes_and_news)
} else {
recipient.combinedAboutAndEmoji
}
}
override fun getSubhead1Text(): String? = recipient.combinedAboutAndEmoji
override fun getSubhead2Text(): String? = recipient.e164.transform(PhoneNumberFormatter::prettyPrint).orNull()
@@ -71,9 +53,9 @@ object BioTextPreference {
val groupTitle: String,
val groupMembershipDescription: String?
) : BioTextPreferenceModel<GroupModel>() {
override fun getHeadlineText(context: Context): CharSequence = groupTitle
override fun getHeadlineText(context: Context): String = groupTitle
override fun getSubhead1Text(context: Context): String? = groupMembershipDescription
override fun getSubhead1Text(): String? = groupMembershipDescription
override fun getSubhead2Text(): String? = null
@@ -97,7 +79,7 @@ object BioTextPreference {
override fun bind(model: T) {
headline.text = model.getHeadlineText(context)
model.getSubhead1Text(context).let {
model.getSubhead1Text().let {
subhead1.text = it
subhead1.visibility = if (it == null) View.GONE else View.VISIBLE
}

View File

@@ -1,35 +0,0 @@
package org.thoughtcrime.securesms.components.settings.models
import android.view.View
import android.widget.ImageView
import androidx.annotation.DrawableRes
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
/**
* Renders a single image, horizontally centered.
*/
object SplashImage {
fun register(mappingAdapter: MappingAdapter) {
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.splash_image))
}
class Model(@DrawableRes val splashImageResId: Int) : PreferenceModel<Model>() {
override fun areItemsTheSame(newItem: Model): Boolean {
return newItem.splashImageResId == splashImageResId
}
}
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
private val splashImageView: ImageView = itemView as ImageView
override fun bind(model: Model) {
splashImageView.setImageResource(model.splashImageResId)
}
}
}

View File

@@ -191,27 +191,6 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
}
}
/**
* Tells the Media service to resume playback of a given audio slide. If the audio slide is not
* currently paused, playback will be started from the beginning.
*
* @param audioSlideUri The Uri of the desired audio slide
* @param messageId The Message id of the given audio slide
*/
public void resumePlayback(@NonNull Uri audioSlideUri, long messageId) {
if (isCurrentTrack(audioSlideUri)) {
getMediaController().getTransportControls().play();
} else {
Bundle extras = new Bundle();
extras.putLong(EXTRA_MESSAGE_ID, messageId);
extras.putLong(EXTRA_THREAD_ID, -1L);
extras.putDouble(EXTRA_PROGRESS, 0.0);
extras.putBoolean(EXTRA_PLAY_SINGLE, true);
getMediaController().getTransportControls().playFromUri(audioSlideUri, extras);
}
}
/**
* Pauses playback if the given audio slide is playing.
*

View File

@@ -127,10 +127,10 @@ data class CallParticipantsState(
fun getIncomingRingingGroupDescription(context: Context): String? {
if (callState == WebRtcViewModel.State.CALL_INCOMING &&
groupCallState == WebRtcViewModel.GroupCallState.RINGING &&
ringerRecipient.hasServiceId()
ringerRecipient.hasAci()
) {
val ringerName = ringerRecipient.getShortDisplayName(context)
val membersWithoutYouOrRinger: List<GroupMemberEntry.FullMember> = groupMembers.filterNot { it.member.isSelf || ringerRecipient.requireServiceId() == it.member.serviceId.orNull() }
val membersWithoutYouOrRinger: List<GroupMemberEntry.FullMember> = groupMembers.filterNot { it.member.isSelf || ringerRecipient.requireAci() == it.member.aci.orNull() }
return when (membersWithoutYouOrRinger.size) {
0 -> context.getString(R.string.WebRtcCallView__s_is_calling_you, ringerName)

View File

@@ -35,7 +35,7 @@ class WebRtcCallRepository {
recipients = Collections.singletonList(recipient);
}
consumer.accept(ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients));
consumer.accept(ApplicationDependencies.getIdentityStore().getIdentityRecords(recipients));
});
}
}

View File

@@ -96,8 +96,8 @@ public class WebRtcCallView extends ConstraintLayout {
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
private ImageView hangup;
private TextView hangupLabel;
private View answerWithoutVideo;
private View answerWithoutVideoLabel;
private View answerWithAudio;
private View answerWithAudioLabel;
private View topGradient;
private View footerGradient;
private View startCallControls;
@@ -178,8 +178,8 @@ public class WebRtcCallView extends ConstraintLayout {
ringToggleLabel = findViewById(R.id.call_screen_audio_ring_toggle_label);
hangup = findViewById(R.id.call_screen_end_call);
hangupLabel = findViewById(R.id.call_screen_end_call_label);
answerWithoutVideo = findViewById(R.id.call_screen_answer_without_video);
answerWithoutVideoLabel = findViewById(R.id.call_screen_answer_without_video_label);
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
topGradient = findViewById(R.id.call_screen_header_gradient);
footerGradient = findViewById(R.id.call_screen_footer_gradient);
startCallControls = findViewById(R.id.call_screen_start_call_controls);
@@ -255,7 +255,7 @@ public class WebRtcCallView extends ConstraintLayout {
decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed));
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
answerWithoutVideo.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
pictureInPictureExpansionHelper = new PictureInPictureExpansionHelper();
@@ -286,7 +286,7 @@ public class WebRtcCallView extends ConstraintLayout {
rotatableControls.add(hangup);
rotatableControls.add(answer);
rotatableControls.add(answerWithoutVideo);
rotatableControls.add(answerWithAudio);
rotatableControls.add(audioToggle);
rotatableControls.add(micToggle);
rotatableControls.add(videoToggle);
@@ -590,19 +590,19 @@ public class WebRtcCallView extends ConstraintLayout {
if (webRtcControls.displayIncomingCallButtons()) {
visibleViewSet.addAll(incomingCallViews);
incomingRingStatus.setText(webRtcControls.displayAnswerWithoutVideo() ? R.string.WebRtcCallView__signal_video_call: R.string.WebRtcCallView__signal_call);
incomingRingStatus.setText(webRtcControls.displayAnswerWithAudio() ? R.string.WebRtcCallView__signal_call : R.string.WebRtcCallView__signal_video_call);
answer.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer));
}
if (webRtcControls.displayAnswerWithoutVideo()) {
visibleViewSet.add(answerWithoutVideo);
visibleViewSet.add(answerWithoutVideoLabel);
if (webRtcControls.displayAnswerWithAudio()) {
visibleViewSet.add(answerWithAudio);
visibleViewSet.add(answerWithAudioLabel);
answer.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer_with_video));
}
if (!webRtcControls.displayIncomingCallButtons()){
if (!webRtcControls.displayIncomingCallButtons() && !webRtcControls.displayAnswerWithAudio()){
incomingRingStatus.setVisibility(GONE);
}

View File

@@ -165,7 +165,7 @@ public final class WebRtcControls {
return isOngoing();
}
boolean displayAnswerWithoutVideo() {
boolean displayAnswerWithAudio() {
return isIncoming() && isRemoteVideoEnabled;
}

View File

@@ -10,6 +10,7 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
@@ -100,9 +101,9 @@ public class ContactRepository {
}));
}};
public ContactRepository(@NonNull Context context, @NonNull String noteToSelfTitle) {
public ContactRepository(@NonNull Context context) {
this.recipientDatabase = SignalDatabase.recipients();
this.noteToSelfTitle = noteToSelfTitle;
this.noteToSelfTitle = context.getString(R.string.note_to_self);
this.context = context.getApplicationContext();
}

View File

@@ -75,7 +75,7 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
this.mode = mode;
this.recents = recents;
this.contactRepository = new ContactRepository(context, context.getString(R.string.note_to_self));
this.contactRepository = new ContactRepository(context);
}
protected final List<Cursor> getUnfilteredResults() {

View File

@@ -23,12 +23,12 @@ import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.signalservice.api.push.ACI;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
@@ -54,7 +54,6 @@ import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.ServiceResponse;
@@ -112,9 +111,9 @@ public class DirectoryHelper {
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
for (Recipient recipient : recipients) {
if (recipient.hasServiceId() && !recipient.hasE164()) {
if (ApplicationDependencies.getSignalServiceAccountManager().isIdentifierRegistered(recipient.requireServiceId())) {
recipientDatabase.markRegistered(recipient.getId(), recipient.requireServiceId());
if (recipient.hasAci() && !recipient.hasE164()) {
if (ApplicationDependencies.getSignalServiceAccountManager().isIdentifierRegistered(recipient.requireAci())) {
recipientDatabase.markRegistered(recipient.getId(), recipient.requireAci());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
@@ -136,11 +135,11 @@ public class DirectoryHelper {
RegisteredState originalRegisteredState = recipient.resolve().getRegistered();
RegisteredState newRegisteredState;
if (recipient.hasServiceId() && !recipient.hasE164()) {
boolean isRegistered = ApplicationDependencies.getSignalServiceAccountManager().isIdentifierRegistered(recipient.requireServiceId());
if (recipient.hasAci() && !recipient.hasE164()) {
boolean isRegistered = ApplicationDependencies.getSignalServiceAccountManager().isIdentifierRegistered(recipient.requireAci());
stopwatch.split("aci-network");
if (isRegistered) {
boolean idChanged = recipientDatabase.markRegistered(recipient.getId(), recipient.requireServiceId());
boolean idChanged = recipientDatabase.markRegistered(recipient.getId(), recipient.requireAci());
if (idChanged) {
Log.w(TAG, "ID changed during refresh by UUID.");
}
@@ -173,14 +172,14 @@ public class DirectoryHelper {
if (aci != null) {
boolean idChanged = recipientDatabase.markRegistered(recipient.getId(), aci);
if (idChanged) {
recipient = Recipient.resolved(recipientDatabase.getByServiceId(aci).get());
recipient = Recipient.resolved(recipientDatabase.getByAci(aci).get());
}
} else {
Log.w(TAG, "Registered number set had a null ACI!");
}
} else if (recipient.hasServiceId() && recipient.isRegistered() && hasCommunicatedWith(recipient)) {
if (ApplicationDependencies.getSignalServiceAccountManager().isIdentifierRegistered(recipient.requireServiceId())) {
recipientDatabase.markRegistered(recipient.getId(), recipient.requireServiceId());
} else if (recipient.hasAci() && recipient.isRegistered() && hasCommunicatedWith(context, recipient)) {
if (ApplicationDependencies.getSignalServiceAccountManager().isIdentifierRegistered(recipient.requireAci())) {
recipientDatabase.markRegistered(recipient.getId(), recipient.requireAci());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
@@ -465,9 +464,9 @@ public class DirectoryHelper {
for (RecipientId newUser: newUsers) {
Recipient recipient = Recipient.resolved(newUser);
if (!recipient.isSelf() &&
recipient.hasAUserSetDisplayName(context) &&
!hasSession(recipient.getId()))
if (!SessionUtil.hasSession(recipient.getId()) &&
!recipient.isSelf() &&
recipient.hasAUserSetDisplayName(context))
{
IncomingJoinedMessage message = new IncomingJoinedMessage(recipient.getId());
Optional<InsertResult> insertResult = SignalDatabase.sms().insertMessageInbox(message);
@@ -484,19 +483,6 @@ public class DirectoryHelper {
}
}
public static boolean hasSession(@NonNull RecipientId id) {
Recipient recipient = Recipient.resolved(id);
if (!recipient.hasServiceId()) {
return false;
}
SignalProtocolAddress protocolAddress = Recipient.resolved(id).requireServiceId().toProtocolAddress(SignalServiceAddress.DEFAULT_DEVICE_ID);
return ApplicationDependencies.getProtocolStore().aci().containsSession(protocolAddress) ||
ApplicationDependencies.getProtocolStore().pni().containsSession(protocolAddress);
}
private static Set<String> sanitizeNumbers(@NonNull Set<String> numbers) {
return Stream.of(numbers).filter(number -> {
try {
@@ -517,8 +503,8 @@ public class DirectoryHelper {
List<Recipient> possiblyUnlisted = Stream.of(inactiveIds)
.map(Recipient::resolved)
.filter(Recipient::isRegistered)
.filter(Recipient::hasServiceId)
.filter(DirectoryHelper::hasCommunicatedWith)
.filter(Recipient::hasAci)
.filter(r -> hasCommunicatedWith(context, r))
.toList();
ProfileService profileService = new ProfileService(ApplicationDependencies.getGroupsV2Operations().getProfileOperations(),
@@ -551,10 +537,10 @@ public class DirectoryHelper {
.blockingGet();
}
private static boolean hasCommunicatedWith(@NonNull Recipient recipient) {
ACI localAci = SignalStore.account().requireAci();
return SignalDatabase.threads().hasThread(recipient.getId()) || (recipient.hasServiceId() && SignalDatabase.sessions().hasSessionFor(localAci, recipient.requireServiceId().toString()));
private static boolean hasCommunicatedWith(@NonNull Context context, @NonNull Recipient recipient) {
return SignalDatabase.threads().hasThread(recipient.getId()) ||
(recipient.hasAci() && SignalDatabase.sessions().hasSessionFor(recipient.requireAci().toString())) ||
(recipient.hasE164() && SignalDatabase.sessions().hasSessionFor(recipient.requireE164()));
}
static class DirectoryResult {

View File

@@ -6,21 +6,17 @@ import android.view.MotionEvent
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.HidingLinearLayout
import org.thoughtcrime.securesms.components.reminder.ReminderView
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture
import org.thoughtcrime.securesms.util.views.Stub
open class ConversationActivity : PassphraseRequiredActivity(), ConversationParentFragment.Callback, DonationPaymentComponent {
open class ConversationActivity : PassphraseRequiredActivity(), ConversationParentFragment.Callback {
private lateinit var fragment: ConversationParentFragment
@@ -77,7 +73,4 @@ open class ConversationActivity : PassphraseRequiredActivity(), ConversationPare
fun getReminderView(): Stub<ReminderView> {
return fragment.reminderView
}
override val donationPaymentRepository: DonationPaymentRepository by lazy { DonationPaymentRepository(this) }
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
}

View File

@@ -18,7 +18,6 @@ package org.thoughtcrime.securesms.conversation;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -40,7 +39,6 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.MediaItem;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagingController;
@@ -403,9 +401,7 @@ public class ConversationAdapter
}
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
int messagePosition = isTypingViewEnabled ? position - 1 : position;
int count = messagePosition + 1;
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, count, count));
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
if (hasWallpaper) {
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_8);
@@ -708,11 +704,6 @@ public class ConversationAdapter
return getBindable().canPlayContent();
}
@Override
public boolean shouldProjectContent() {
return getBindable().shouldProjectContent();
}
@Override
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
return getBindable().getColorizerProjections(coordinateRoot);
@@ -787,23 +778,7 @@ public class ConversationAdapter
}
public static class FooterViewHolder extends HeaderFooterViewHolder {
FooterViewHolder(@NonNull View itemView) {
super(itemView);
setPaddingTop();
}
@Override
void bind(@Nullable View view) {
super.bind(view);
setPaddingTop();
}
private void setPaddingTop() {
if (Build.VERSION.SDK_INT <= 23) {
int addToPadding = ViewUtil.getStatusBarHeight(itemView) + (int) ThemeUtil.getThemedDimen(itemView.getContext(), android.R.attr.actionBarSize);
ViewUtil.setPaddingTop(itemView, itemView.getPaddingTop() + addToPadding);
}
}
FooterViewHolder(@NonNull View itemView) { super(itemView); }
}
public static class HeaderViewHolder extends HeaderFooterViewHolder {

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
@@ -21,9 +20,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.SpanUtil;
public class ConversationBannerView extends ConstraintLayout {
@@ -81,23 +78,11 @@ public class ConversationBannerView extends ConstraintLayout {
}
}
public String setTitle(@NonNull Recipient recipient) {
SpannableStringBuilder title = new SpannableStringBuilder(recipient.isSelf() ? getContext().getString(R.string.note_to_self) : recipient.getDisplayNameOrUsername(getContext()));
if (recipient.isReleaseNotes()) {
SpanUtil.appendCenteredImageSpan(title, ContextUtil.requireDrawable(getContext(), R.drawable.ic_official_28), 28, 28);
}
public void setTitle(@Nullable CharSequence title) {
contactTitle.setText(title);
return title.toString();
}
public void setAbout(@NonNull Recipient recipient) {
String about;
if (recipient.isReleaseNotes()) {
about = getContext().getString(R.string.ReleaseNotes__signal_release_notes_and_news);
} else {
about = recipient.getCombinedAboutAndEmoji();
}
public void setAbout(@Nullable String about) {
contactAbout.setText(about);
contactAbout.setVisibility(TextUtils.isEmpty(about) ? GONE : VISIBLE);
}

View File

@@ -1,61 +0,0 @@
package org.thoughtcrime.securesms.conversation
import android.content.Context
import android.os.Build
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.PopupWindow
import androidx.core.content.ContextCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.ContextMenuList
/**
* The context menu shown after long pressing a message in ConversationActivity.
*/
class ConversationContextMenu(private val anchor: View, items: List<ActionItem>) : PopupWindow(
LayoutInflater.from(anchor.context).inflate(R.layout.signal_context_menu, null),
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
) {
val context: Context = anchor.context
private val contextMenuList = ContextMenuList(
recyclerView = contentView.findViewById(R.id.signal_context_menu_list),
onItemClick = { dismiss() },
)
init {
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
animationStyle = R.style.ConversationContextMenuAnimation
isFocusable = false
isOutsideTouchable = true
if (Build.VERSION.SDK_INT >= 21) {
elevation = 20f
}
setTouchInterceptor { _, event ->
event.action == MotionEvent.ACTION_OUTSIDE
}
contextMenuList.setItems(items)
contentView.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
}
fun getMaxWidth(): Int = contentView.measuredWidth
fun getMaxHeight(): Int = contentView.measuredHeight
fun show(offsetX: Int, offsetY: Int) {
showAsDropDown(anchor, offsetX, offsetY, Gravity.TOP or Gravity.START)
}
}

View File

@@ -24,7 +24,6 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.net.Uri;
@@ -59,7 +58,6 @@ import androidx.core.view.ViewKt;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
@@ -122,8 +120,8 @@ import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.longmessage.LongMessageFragment;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment;
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
import org.thoughtcrime.securesms.mms.GlideApp;
@@ -150,7 +148,6 @@ import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.HtmlUtil;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
@@ -183,6 +180,7 @@ import java.util.Set;
import java.util.concurrent.ExecutionException;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
@SuppressLint("StaticFieldLeak")
public class ConversationFragment extends LoggingFragment implements MultiselectForwardFragment.Callback {
@@ -198,6 +196,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
private LiveRecipient recipient;
private long threadId;
private boolean isReacting;
private ActionMode actionMode;
private Locale locale;
private FrameLayout videoContainer;
@@ -400,7 +399,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
GiphyMp4PlaybackController.attach(list, callback, maxPlayback);
list.addItemDecoration(new GiphyMp4ItemDecoration(callback, translationY -> {
reactionsShade.setTranslationY(translationY + list.getHeight());
reactionsShade.setTranslationY(translationY);
return Unit.INSTANCE;
}), 0);
@@ -553,8 +552,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
conversationBanner.setAvatar(GlideApp.with(context), recipient);
conversationBanner.showBackgroundBubble(recipient.hasWallpaper());
String title = conversationBanner.setTitle(recipient);
conversationBanner.setAbout(recipient);
String title = isSelf ? context.getString(R.string.note_to_self) : recipient.getDisplayNameOrUsername(context);
conversationBanner.setTitle(title);
conversationBanner.setAbout(recipient.getCombinedAboutAndEmoji());
if (recipient.isGroup()) {
if (pendingMemberCount > 0) {
@@ -768,7 +768,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
if (menuState.shouldShowSaveAttachmentAction()) {
items.add(new ActionItem(R.drawable.ic_save_24_tinted, getResources().getString(R.string.conversation_selection__menu_save), () -> {
items.add(new ActionItem(R.drawable.ic_save_24, getResources().getString(R.string.conversation_selection__menu_save), () -> {
handleSaveAttachment((MediaMmsMessageRecord) getSelectedConversationMessage().getMessageRecord());
actionMode.finish();
}));
@@ -810,21 +810,19 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
ViewUtil.animateIn(bottomActionBar, bottomActionBar.getEnterAnimation());
listener.onBottomActionBarVisibilityChanged(View.VISIBLE);
bottomActionBar.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (bottomActionBar.getHeight() == 0 && bottomActionBar.getVisibility() == View.VISIBLE) {
return false;
ViewKt.doOnPreDraw(bottomActionBar, new Function1<View, Unit>() {
@Override public Unit invoke(View view) {
if (view.getHeight() == 0 && view.getVisibility() == View.VISIBLE) {
ViewKt.doOnPreDraw(bottomActionBar, this);
return Unit.INSTANCE;
}
bottomActionBar.getViewTreeObserver().removeOnPreDrawListener(this);
int bottomPadding = bottomActionBar.getHeight() + (int) DimensionUnit.DP.toPixels(18);
int bottomPadding = view.getHeight() + (int) DimensionUnit.DP.toPixels(18);
list.setPadding(list.getPaddingLeft(), list.getPaddingTop(), list.getPaddingRight(), bottomPadding);
list.scrollBy(0, -(bottomPadding - additionalScrollOffset));
return false;
return Unit.INSTANCE;
}
});
} else {
@@ -900,7 +898,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
inlineDateDecoration = new StickyHeaderDecoration(adapter, false, false, ConversationAdapter.HEADER_TYPE_INLINE_DATE);
list.addItemDecoration(inlineDateDecoration, 0);
list.addItemDecoration(inlineDateDecoration);
}
public void setLastSeen(long lastSeen) {
@@ -909,7 +907,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
lastSeenDecoration = new LastSeenHeader(getListAdapter(), lastSeen);
list.addItemDecoration(lastSeenDecoration, 0);
list.addItemDecoration(lastSeenDecoration);
}
private void handleCopyMessage(final Set<MultiselectPart> multiselectParts) {
@@ -1005,7 +1003,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
private void handleDisplayDetails(ConversationMessage message) {
MessageDetailsFragment.create(message.getMessageRecord(), recipient.getId()).show(getChildFragmentManager(), null);
startActivity(MessageDetailsActivity.getIntentForMessageDetails(requireContext(), message.getMessageRecord(), recipient.getId(), threadId));
}
private void handleForwardMessageParts(Set<MultiselectPart> multiselectParts) {
@@ -1309,26 +1307,23 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
public interface ConversationFragmentListener extends VoiceNoteMediaControllerOwner {
boolean isKeyboardOpen();
void setThreadId(long threadId);
void handleReplyMessage(ConversationMessage conversationMessage);
void onMessageActionToolbarOpened();
void onBottomActionBarVisibilityChanged(int visibility);
void onForwardClicked();
void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
void handleReaction(@NonNull ConversationMessage conversationMessage,
@NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener,
@NonNull SelectedConversationModel selectedConversationModel,
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
void onCursorChanged();
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
void onVoiceNotePause(@NonNull Uri uri);
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress);
void onVoiceNoteResume(@NonNull Uri uri, long messageId);
void onVoiceNoteSeekTo(@NonNull Uri uri, double progress);
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
void setThreadId(long threadId);
void handleReplyMessage(ConversationMessage conversationMessage);
void onMessageActionToolbarOpened();
void onBottomActionBarVisibilityChanged(int visibility);
void onForwardClicked();
void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
void handleReaction(@NonNull ConversationMessage conversationMessage,
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
void onCursorChanged();
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
void onVoiceNotePause(@NonNull Uri uri);
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress);
void onVoiceNoteSeekTo(@NonNull Uri uri, double progress);
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
}
private class ConversationScrollListener extends OnScrollListener {
@@ -1435,91 +1430,16 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
multiselectItemDecoration.setFocusedItem(new MultiselectPart.Message(item.getConversationMessage()));
list.invalidateItemDecorations();
isReacting = true;
reactionsShade.setVisibility(View.VISIBLE);
list.setLayoutFrozen(true);
if (itemView instanceof ConversationItem) {
Uri audioUri = getAudioUriForLongClick(messageRecord);
if (audioUri != null) {
listener.onVoiceNotePause(audioUri);
}
Bitmap videoBitmap = null;
int childAdapterPosition = list.getChildAdapterPosition(itemView);
GiphyMp4ProjectionPlayerHolder mp4Holder = null;
if (childAdapterPosition != RecyclerView.NO_POSITION) {
mp4Holder = giphyMp4ProjectionRecycler.getCurrentHolder(childAdapterPosition);
if (mp4Holder != null && mp4Holder.isVisible()) {
mp4Holder.pause();
videoBitmap = mp4Holder.getBitmap();
mp4Holder.hide();
} else {
mp4Holder = null;
}
}
final GiphyMp4ProjectionPlayerHolder finalMp4Holder = mp4Holder;
ConversationItem conversationItem = (ConversationItem) itemView;
Bitmap bitmap = ConversationItemSelection.snapshotView(conversationItem, list, messageRecord, videoBitmap);
View focusedView = listener.isKeyboardOpen() ? conversationItem.getRootView().findFocus() : null;
final ConversationItemBodyBubble bodyBubble = conversationItem.bodyBubble;
SelectedConversationModel selectedConversationModel = new SelectedConversationModel(bitmap,
itemView.getX(),
itemView.getY() + list.getTranslationY(),
bodyBubble.getX(),
bodyBubble.getY(),
bodyBubble.getWidth(),
audioUri,
messageRecord.isOutgoing(),
focusedView);
bodyBubble.setVisibility(View.INVISIBLE);
conversationItem.reactionsView.setVisibility(View.INVISIBLE);
ViewUtil.hideKeyboard(requireContext(), conversationItem);
boolean showScrollButtons = conversationViewModel.getShowScrollButtons();
if (showScrollButtons) {
conversationViewModel.setShowScrollButtons(false);
}
listener.handleReaction(item.getConversationMessage(),
new ReactionsToolbarListener(item.getConversationMessage()),
selectedConversationModel,
new ConversationReactionOverlay.OnHideListener() {
@Override public void startHide() {
multiselectItemDecoration.hideShade(list);
ViewUtil.fadeOut(reactionsShade, getResources().getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE);
}
@Override public void onHide() {
list.setLayoutFrozen(false);
if (selectedConversationModel.getAudioUri() != null) {
listener.onVoiceNoteResume(selectedConversationModel.getAudioUri(), messageRecord.getId());
}
WindowUtil.setLightStatusBarFromTheme(requireActivity());
WindowUtil.setLightNavigationBarFromTheme(requireActivity());
clearFocusedItem();
if (finalMp4Holder != null) {
finalMp4Holder.show();
finalMp4Holder.resume();
}
bodyBubble.setVisibility(View.VISIBLE);
conversationItem.reactionsView.setVisibility(View.VISIBLE);
if (showScrollButtons) {
conversationViewModel.setShowScrollButtons(true);
}
}
});
}
listener.handleReaction(item.getConversationMessage(), new ReactionsToolbarListener(item.getConversationMessage()), () -> {
isReacting = false;
reactionsShade.setVisibility(View.GONE);
list.setLayoutFrozen(false);
WindowUtil.setLightStatusBarFromTheme(requireActivity());
clearFocusedItem();
});
} else {
clearFocusedItem();
((ConversationAdapter) list.getAdapter()).toggleSelection(item);
@@ -1529,20 +1449,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
}
@Nullable private Uri getAudioUriForLongClick(@NonNull MessageRecord messageRecord) {
VoiceNotePlaybackState playbackState = listener.getVoiceNoteMediaController().getVoiceNotePlaybackState().getValue();
if (playbackState == null || !playbackState.isPlaying()) {
return null;
}
if (!MessageRecordUtil.hasAudio(messageRecord) || !messageRecord.isMms()) {
return null;
}
Uri messageUri = ((MmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide().getUri();
return playbackState.getUri().equals(messageUri) ? messageUri : null;
}
@Override
public void onQuoteClicked(MmsMessageRecord messageRecord) {
if (messageRecord.getQuote() == null) {
@@ -1575,7 +1481,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
@Override
public void onMoreTextClicked(@NonNull RecipientId conversationRecipientId, long messageId, boolean isMms) {
if (getContext() != null && getActivity() != null) {
LongMessageFragment.create(messageId, isMms).show(getChildFragmentManager(), null);
startActivity(LongMessageActivity.getIntent(getContext(), conversationRecipientId, messageId, isMms));
}
}
@@ -1779,7 +1685,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
.setView(R.layout.safety_number_changed_learn_more_dialog)
.setPositiveButton(R.string.ConversationFragment_verify, (d, w) -> {
SimpleTask.run(getLifecycle(), () -> {
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipient.getId());
return ApplicationDependencies.getIdentityStore().getIdentityRecord(recipient.getId());
}, identityRecord -> {
if (identityRecord.isPresent()) {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord.get()));
@@ -1849,25 +1755,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
public void onChangeNumberUpdateContact(@NonNull Recipient recipient) {
startActivity(RecipientExporter.export(recipient).asAddContactIntent());
}
@Override
public void onCallToAction(@NonNull String action) {
}
@Override
public void onDonateClicked() {
if (SignalStore.donationsValues().isLikelyASustainer()) {
NavHostFragment navHostFragment = NavHostFragment.create(R.navigation.boosts);
requireActivity().getSupportFragmentManager()
.beginTransaction()
.add(navHostFragment, "boost_nav")
.commitNow();
} else {
startActivity(AppSettingsActivity.subscriptions(requireContext()));
}
}
}
public void refreshList() {
@@ -1980,7 +1867,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
}
private class ReactionsToolbarListener implements ConversationReactionOverlay.OnActionSelectedListener {
private class ReactionsToolbarListener implements Toolbar.OnMenuItemClickListener {
private final ConversationMessage conversationMessage;
@@ -1989,32 +1876,16 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
@Override
public void onActionSelected(@NonNull ConversationReactionOverlay.Action action) {
switch (action) {
case REPLY:
handleReplyMessage(conversationMessage);
break;
case FORWARD:
handleForwardMessageParts(conversationMessage.getMultiselectCollection().toSet());
break;
case RESEND:
handleResendMessage(conversationMessage.getMessageRecord());
break;
case DOWNLOAD:
handleSaveAttachment((MediaMmsMessageRecord) conversationMessage.getMessageRecord());
break;
case COPY:
handleCopyMessage(conversationMessage.getMultiselectCollection().toSet());
break;
case MULTISELECT:
handleEnterMultiSelect(conversationMessage);
break;
case VIEW_INFO:
handleDisplayDetails(conversationMessage);
break;
case DELETE:
handleDeleteMessages(conversationMessage.getMultiselectCollection().toSet());
break;
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_info: handleDisplayDetails(conversationMessage); return true;
case R.id.action_delete: handleDeleteMessages(conversationMessage.getMultiselectCollection().toSet()); return true;
case R.id.action_copy: handleCopyMessage(conversationMessage.getMultiselectCollection().toSet()); return true;
case R.id.action_reply: handleReplyMessage(conversationMessage); return true;
case R.id.action_multiselect: handleEnterMultiSelect(conversationMessage); return true;
case R.id.action_forward: handleForwardMessageParts(conversationMessage.getMultiselectCollection().toSet()); return true;
case R.id.action_download: handleSaveAttachment((MediaMmsMessageRecord) conversationMessage.getMessageRecord()); return true;
default: return false;
}
}
}

View File

@@ -45,7 +45,6 @@ import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
@@ -123,7 +122,6 @@ import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.PlaceholderURLSpan;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.ProjectionList;
import org.thoughtcrime.securesms.util.SearchUtil;
@@ -144,7 +142,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* A view that displays an individual conversation item within a conversation
@@ -163,10 +160,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private static final Rect SWIPE_RECT = new Rect();
public static final float LONG_PRESS_SCALE_FACTOR = 0.95f;
private static final int SHRINK_BUBBLE_DELAY_MILLIS = 100;
private static final long MAX_CLUSTERING_TIME_DIFF = TimeUnit.MINUTES.toMillis(3);
private ConversationMessage conversationMessage;
private MessageRecord messageRecord;
private Optional<MessageRecord> nextMessageRecord;
@@ -203,7 +196,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private Stub<LinkPreviewView> linkPreviewStub;
private Stub<BorderlessImageView> stickerStub;
private Stub<ViewOnceMessageView> revealableStub;
private Stub<Button> callToActionStub;
private @Nullable EventListener eventListener;
private int defaultBubbleColor;
@@ -232,25 +224,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private float lastYDownRelativeToThis;
private ProjectionList colorizerProjections = new ProjectionList(3);
private final Runnable shrinkBubble = new Runnable() {
@Override
public void run() {
bodyBubble.animate()
.scaleX(LONG_PRESS_SCALE_FACTOR)
.scaleY(LONG_PRESS_SCALE_FACTOR)
.setUpdateListener(animation -> {
View parent = (View) getParent();
if (parent != null) {
parent.invalidate();
}
});
reactionsView.animate()
.scaleX(LONG_PRESS_SCALE_FACTOR)
.scaleY(LONG_PRESS_SCALE_FACTOR);
}
};
public ConversationItem(Context context) {
this(context, null);
}
@@ -286,7 +259,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
this.linkPreviewStub = new Stub<>(findViewById(R.id.link_preview_stub));
this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub));
this.revealableStub = new Stub<>(findViewById(R.id.revealable_view_stub));
this.callToActionStub = ViewUtil.findStubById(this, R.id.conversation_item_call_to_action_stub);
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
this.quoteView = findViewById(R.id.quote_view);
this.reply = findViewById(R.id.reply_icon_wrapper);
@@ -371,27 +343,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
setGroupAuthorColor(messageRecord, hasWallpaper, colorizer);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getHandler().postDelayed(shrinkBubble, SHRINK_BUBBLE_DELAY_MILLIS);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getHandler().removeCallbacks(shrinkBubble);
bodyBubble.animate()
.scaleX(1.0f)
.scaleY(1.0f);
reactionsView.animate()
.scaleX(1.0f)
.scaleY(1.0f);
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
@@ -456,8 +407,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
!hasAudio(messageRecord) &&
isFooterVisible(messageRecord, nextMessageRecord, groupThread) &&
!bodyText.isJumbomoji() &&
conversationMessage.getBottomButton() == null &&
!StringUtil.hasMixedTextDirection(bodyText.getText()) &&
bodyText.getLastLineWidth() > 0)
{
TextView dateView = footer.getDateView();
@@ -937,18 +886,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
bodyText.setText(StringUtil.trim(styledText));
bodyText.setVisibility(View.VISIBLE);
if (conversationMessage.getBottomButton() != null) {
callToActionStub.get().setVisibility(View.VISIBLE);
callToActionStub.get().setText(conversationMessage.getBottomButton().getLabel());
callToActionStub.get().setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onCallToAction(conversationMessage.getBottomButton().getAction());
}
});
} else if (callToActionStub.resolved()) {
callToActionStub.get().setVisibility(View.GONE);
}
}
}
@@ -1353,19 +1290,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
if (conversationMessage.hasStyleLinks()) {
for (PlaceholderURLSpan placeholder : messageBody.getSpans(0, messageBody.length(), PlaceholderURLSpan.class)) {
int start = messageBody.getSpanStart(placeholder);
int end = messageBody.getSpanEnd(placeholder);
URLSpan span = new InterceptableLongClickCopyLinkSpan(placeholder.getValue(),
urlClickListener,
ContextCompat.getColor(getContext(), R.color.signal_accent_primary),
false);
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(messageBody);
for (Annotation annotation : mentionAnnotations) {
messageBody.setSpan(new MentionClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -1563,7 +1487,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
contactPhotoHolder.setVisibility(VISIBLE);
if (!previous.isPresent() || previous.get().isUpdate() || !current.getRecipient().equals(previous.get().getRecipient()) ||
!DateUtils.isSameDay(previous.get().getTimestamp(), current.getTimestamp()) || !isWithinClusteringTime(current, previous.get()))
!DateUtils.isSameDay(previous.get().getTimestamp(), current.getTimestamp()))
{
groupSenderHolder.setVisibility(VISIBLE);
@@ -1577,7 +1501,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
groupSenderHolder.setVisibility(GONE);
}
if (!next.isPresent() || next.get().isUpdate() || !current.getRecipient().equals(next.get().getRecipient()) || !isWithinClusteringTime(current, next.get())) {
if (!next.isPresent() || next.get().isUpdate() || !current.getRecipient().equals(next.get().getRecipient())) {
contactPhoto.setVisibility(VISIBLE);
badgeImageView.setVisibility(VISIBLE);
} else {
@@ -1677,21 +1601,20 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private boolean isStartOfMessageCluster(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, boolean isGroupThread) {
if (isGroupThread) {
return !previous.isPresent() || previous.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), previous.get().getTimestamp()) ||
!current.getRecipient().equals(previous.get().getRecipient()) || !isWithinClusteringTime(current, previous.get());
!current.getRecipient().equals(previous.get().getRecipient());
} else {
return !previous.isPresent() || previous.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), previous.get().getTimestamp()) ||
current.isOutgoing() != previous.get().isOutgoing() || previous.get().isSecure() != current.isSecure() || !isWithinClusteringTime(current, previous.get());
current.isOutgoing() != previous.get().isOutgoing();
}
}
private boolean isEndOfMessageCluster(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
if (isGroupThread) {
return !next.isPresent() || next.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), next.get().getTimestamp()) ||
!current.getRecipient().equals(next.get().getRecipient()) || !current.getReactions().isEmpty() || !isWithinClusteringTime(current, next.get());
!current.getRecipient().equals(next.get().getRecipient()) || !current.getReactions().isEmpty();
} else {
return !next.isPresent() || next.get().isUpdate() || !DateUtils.isSameDay(current.getTimestamp(), next.get().getTimestamp()) ||
current.isOutgoing() != next.get().isOutgoing() || !current.getReactions().isEmpty() || next.get().isSecure() != current.isSecure() ||
!isWithinClusteringTime(current, next.get());
current.isOutgoing() != next.get().isOutgoing() || !current.getReactions().isEmpty();
}
}
@@ -1706,11 +1629,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
current.isFailed() || current.isRateLimited() || differentTimestamps || isEndOfMessageCluster(current, next, isGroupThread);
}
private static boolean isWithinClusteringTime(@NonNull MessageRecord lhs, @NonNull MessageRecord rhs) {
long timeDiff = Math.abs(lhs.getDateSent() - rhs.getDateSent());
return timeDiff <= MAX_CLUSTERING_TIME_DIFF;
}
private void setMessageSpacing(@NonNull Context context, @NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
int spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_collapse);
int spacingBottom = spacingTop;
@@ -1819,11 +1737,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
@Override
public @NonNull Projection getGiphyMp4PlayableProjection(@NonNull ViewGroup recyclerView) {
if (mediaThumbnailStub != null && mediaThumbnailStub.isResolvable()) {
ConversationItemThumbnail thumbnail = mediaThumbnailStub.require();
return Projection.relativeToParent(recyclerView, thumbnail, thumbnail.getCorners())
.scale(bodyBubble.getScaleX())
.translateX(Util.halfOffsetFromScale(thumbnail.getWidth(), bodyBubble.getScaleX()))
.translateY(Util.halfOffsetFromScale(thumbnail.getHeight(), bodyBubble.getScaleY()))
return Projection.relativeToParent(recyclerView, mediaThumbnailStub.require(), mediaThumbnailStub.require().getCorners())
.translateY(getTranslationY())
.translateX(bodyBubble.getTranslationX())
.translateX(getTranslationX());
@@ -1840,11 +1754,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
return mediaThumbnailStub != null && mediaThumbnailStub.isResolvable() && canPlayContent;
}
@Override
public boolean shouldProjectContent() {
return canPlayContent() && bodyBubble.getVisibility() == VISIBLE;
}
@Override
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
colorizerProjections.clear();
@@ -1852,70 +1761,34 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (messageRecord.isOutgoing() &&
!hasNoBubble(messageRecord) &&
!messageRecord.isRemoteDelete() &&
bodyBubbleCorners != null &&
bodyBubble.getVisibility() == VISIBLE)
bodyBubbleCorners != null)
{
Projection bodyBubbleToRoot = Projection.relativeToParent(coordinateRoot, bodyBubble, bodyBubbleCorners).translateX(bodyBubble.getTranslationX());
Projection videoToBubble = bodyBubble.getVideoPlayerProjection();
float translationX = Util.halfOffsetFromScale(bodyBubble.getWidth(), bodyBubble.getScaleX());
float translationY = Util.halfOffsetFromScale(bodyBubble.getHeight(), bodyBubble.getScaleY());
if (videoToBubble != null) {
Projection videoToRoot = Projection.translateFromDescendantToParentCoords(videoToBubble, bodyBubble, coordinateRoot);
List<Projection> projections = Projection.getCapAndTail(bodyBubbleToRoot, videoToRoot);
if (!projections.isEmpty()) {
projections.get(0)
.scale(bodyBubble.getScaleX())
.translateX(translationX)
.translateY(translationY);
projections.get(1)
.scale(bodyBubble.getScaleX())
.translateX(translationX)
.translateY(-translationY);
}
colorizerProjections.addAll(projections);
colorizerProjections.addAll(Projection.getCapAndTail(bodyBubbleToRoot, videoToRoot));
} else {
colorizerProjections.add(
bodyBubbleToRoot.scale(bodyBubble.getScaleX())
.translateX(translationX)
.translateY(translationY)
);
colorizerProjections.add(bodyBubbleToRoot);
}
}
if (messageRecord.isOutgoing() &&
hasNoBubble(messageRecord) &&
hasWallpaper &&
bodyBubble.getVisibility() == VISIBLE)
hasWallpaper)
{
ConversationItemFooter footer = getActiveFooter(messageRecord);
Projection footerProjection = footer.getProjection(coordinateRoot);
Projection footerProjection = getActiveFooter(messageRecord).getProjection(coordinateRoot);
if (footerProjection != null) {
colorizerProjections.add(
footerProjection.translateX(bodyBubble.getTranslationX())
.scale(bodyBubble.getScaleX())
.translateX(Util.halfOffsetFromScale(footer.getWidth(), bodyBubble.getScaleX()))
.translateY(-Util.halfOffsetFromScale(footer.getHeight(), bodyBubble.getScaleY()))
);
colorizerProjections.add(footerProjection.translateX(bodyBubble.getTranslationX()));
}
}
if (!messageRecord.isOutgoing() &&
hasQuote(messageRecord) &&
quoteView != null &&
bodyBubble.getVisibility() == VISIBLE)
quoteView != null)
{
bodyBubble.setQuoteViewProjection(quoteView.getProjection(bodyBubble));
float bubbleOffsetFromScale = Util.halfOffsetFromScale(bodyBubble.getHeight(), bodyBubble.getScaleY());
Projection cProj = quoteView.getProjection(coordinateRoot)
.translateX(bodyBubble.getTranslationX() + this.getTranslationX() + Util.halfOffsetFromScale(quoteView.getWidth(), bodyBubble.getScaleX()))
.translateY(bubbleOffsetFromScale - quoteView.getY() + (quoteView.getY() * bodyBubble.getScaleY()))
.scale(bodyBubble.getScaleX());
colorizerProjections.add(cProj);
colorizerProjections.add(quoteView.getProjection(coordinateRoot).translateX(bodyBubble.getTranslationX() + this.getTranslationX()));
}
for (int i = 0; i < colorizerProjections.size(); i++) {

View File

@@ -1,124 +0,0 @@
package org.thoughtcrime.securesms.conversation
import android.graphics.Bitmap
import android.graphics.Path
import android.view.ViewGroup
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.createBitmap
import androidx.core.graphics.withClip
import androidx.core.graphics.withTranslation
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.util.hasNoBubble
object ConversationItemSelection {
@JvmStatic
fun snapshotView(
conversationItem: ConversationItem,
list: RecyclerView,
messageRecord: MessageRecord,
videoBitmap: Bitmap?,
): Bitmap {
val isOutgoing = messageRecord.isOutgoing
val hasNoBubble = messageRecord.hasNoBubble(conversationItem.context)
return snapshotMessage(
conversationItem = conversationItem,
list = list,
videoBitmap = videoBitmap,
drawConversationItem = !isOutgoing || hasNoBubble,
hasReaction = messageRecord.reactions.isNotEmpty(),
)
}
private fun snapshotMessage(
conversationItem: ConversationItem,
list: RecyclerView,
videoBitmap: Bitmap?,
drawConversationItem: Boolean,
hasReaction: Boolean,
): Bitmap {
val bodyBubble = conversationItem.bodyBubble
val reactionsView = conversationItem.reactionsView
val originalScale = bodyBubble.scaleX
bodyBubble.scaleX = 1.0f
bodyBubble.scaleY = 1.0f
val projections = conversationItem.getColorizerProjections(list)
val path = Path()
val xTranslation = -conversationItem.x - bodyBubble.x
val yTranslation = -conversationItem.y - bodyBubble.y
val mp4Projection = conversationItem.getGiphyMp4PlayableProjection(list)
var scaledVideoBitmap = videoBitmap
if (videoBitmap != null) {
scaledVideoBitmap = Bitmap.createScaledBitmap(
videoBitmap,
(videoBitmap.width / originalScale).toInt(),
(videoBitmap.height / originalScale).toInt(),
true
)
mp4Projection.translateX(xTranslation)
mp4Projection.translateY(yTranslation)
mp4Projection.applyToPath(path)
}
projections.use {
it.forEach { p ->
p.translateX(xTranslation)
p.translateY(yTranslation)
p.applyToPath(path)
}
}
conversationItem.destroyAllDrawingCaches()
var bitmapHeight = bodyBubble.height
if (hasReaction) {
bitmapHeight += (reactionsView.height - DimensionUnit.DP.toPixels(4f)).toInt()
}
return createBitmap(bodyBubble.width, bitmapHeight).applyCanvas {
if (drawConversationItem) {
bodyBubble.draw(this)
}
withClip(path) {
withTranslation(x = xTranslation, y = yTranslation) {
list.draw(this)
if (scaledVideoBitmap != null) {
drawBitmap(scaledVideoBitmap, mp4Projection.x - xTranslation, mp4Projection.y - yTranslation, null)
}
}
}
withTranslation(
x = reactionsView.x - bodyBubble.x,
y = reactionsView.y - bodyBubble.y
) {
reactionsView.draw(this)
}
}.also {
mp4Projection.release()
bodyBubble.scaleX = originalScale
bodyBubble.scaleY = originalScale
}
}
}
private fun ViewGroup.destroyAllDrawingCaches() {
children.forEach {
it.destroyDrawingCache()
if (it is ViewGroup) {
it.destroyAllDrawingCaches()
}
}
}

View File

@@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.database.MentionUtil;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import java.security.MessageDigest;
import java.util.Collections;
@@ -27,11 +26,10 @@ import java.util.List;
* for various presentations.
*/
public class ConversationMessage {
@NonNull private final MessageRecord messageRecord;
@NonNull private final List<Mention> mentions;
@Nullable private final SpannableString body;
@NonNull private final MultiselectCollection multiselectCollection;
@NonNull private final MessageStyler.Result styleResult;
@NonNull private final MessageRecord messageRecord;
@NonNull private final List<Mention> mentions;
@Nullable private final SpannableString body;
@NonNull private final MultiselectCollection multiselectCollection;
private ConversationMessage(@NonNull MessageRecord messageRecord) {
this(messageRecord, null, null);
@@ -42,26 +40,13 @@ public class ConversationMessage {
@Nullable List<Mention> mentions)
{
this.messageRecord = messageRecord;
this.body = body != null ? SpannableString.valueOf(body) : null;
this.mentions = mentions != null ? mentions : Collections.emptyList();
if (body != null) {
this.body = SpannableString.valueOf(body);
} else if (messageRecord.hasMessageRanges()) {
this.body = SpannableString.valueOf(messageRecord.getBody());
} else {
this.body = null;
}
if (!this.mentions.isEmpty() && this.body != null) {
MentionAnnotation.setMentionAnnotations(this.body, this.mentions);
}
if (this.body != null && messageRecord.hasMessageRanges()) {
styleResult = MessageStyler.style(messageRecord.requireMessageRanges(), this.body);
} else {
styleResult = MessageStyler.Result.none();
}
multiselectCollection = Multiselect.getParts(this);
}
@@ -101,14 +86,6 @@ public class ConversationMessage {
return (body != null) ? body : messageRecord.getDisplayBody(context);
}
public boolean hasStyleLinks() {
return styleResult.getHasStyleLinks();
}
public @Nullable BodyRangeList.BodyRange.Button getBottomButton() {
return styleResult.getBottomButton();
}
/**
* Factory providing multiple ways of creating {@link ConversationMessage}s.
*/

View File

@@ -20,7 +20,6 @@ import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -67,7 +66,6 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -200,8 +198,6 @@ import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel;
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment;
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
import org.thoughtcrime.securesms.keyboard.gif.GifKeyboardPageFragment;
import org.thoughtcrime.securesms.keyboard.sticker.StickerKeyboardPageFragment;
import org.thoughtcrime.securesms.keyboard.sticker.StickerSearchDialogFragment;
import org.thoughtcrime.securesms.keyvalue.PaymentsValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
@@ -212,7 +208,7 @@ import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult;
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView;
@@ -340,8 +336,7 @@ public class ConversationParentFragment extends Fragment
EmojiEventListener,
GifKeyboardPageFragment.Host,
EmojiKeyboardPageFragment.Callback,
EmojiSearchFragment.Callback,
StickerKeyboardPageFragment.Callback
EmojiSearchFragment.Callback
{
private static final int SHORTCUT_ICON_SIZE = Build.VERSION.SDK_INT >= 26 ? ViewUtil.dpToPx(72) : ViewUtil.dpToPx(48 + 16 * 2);
@@ -366,9 +361,6 @@ public class ConversationParentFragment extends Fragment
private static final int SMS_DEFAULT = 11;
private static final int MEDIA_SENDER = 12;
private static final int REQUEST_CODE_PIN_SHORTCUT = 902;
private static final String ACTION_PINNED_SHORTCUT = "action_pinned_shortcut";
private GlideRequests glideRequests;
protected ComposeText composeText;
private AnimatingToggle buttonToggle;
@@ -404,14 +396,12 @@ public class ConversationParentFragment extends Fragment
private Stub<TextView> cannotSendInAnnouncementGroupBanner;
private View requestingMemberBanner;
private View cancelJoinRequest;
private Stub<View> releaseChannelUnmute;
private Stub<View> mentionsSuggestions;
private MaterialButton joinGroupCallButton;
private boolean callingTooltipShown;
private ImageView wallpaper;
private View wallpaperDim;
private Toolbar toolbar;
private BroadcastReceiver pinnedShortcutReceiver;
private LinkPreviewViewModel linkPreviewViewModel;
private ConversationSearchViewModel searchViewModel;
@@ -548,13 +538,6 @@ public class ConversationParentFragment extends Fragment
});
initializeInsightObserver();
initializeActionBar();
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
onBackPressed();
}
});
}
// TODO [alex] LargeScreenSupport -- This needs to be fed a stream of intents
@@ -619,10 +602,6 @@ public class ConversationParentFragment extends Fragment
if (searchViewItem != null && searchViewItem.expandActionView()) {
searchViewModel.onSearchOpened();
}
} else {
searchViewModel.onSearchClosed();
viewModel.setSearchQuery(null);
inputPanel.setHideForSearch(false);
}
}
@@ -710,7 +689,6 @@ public class ConversationParentFragment extends Fragment
@Override
public void onDestroy() {
if (securityUpdateReceiver != null) requireActivity().unregisterReceiver(securityUpdateReceiver);
if (pinnedShortcutReceiver != null) requireActivity().unregisterReceiver(pinnedShortcutReceiver);
super.onDestroy();
}
@@ -922,11 +900,16 @@ public class ConversationParentFragment extends Fragment
boolean isActiveV2Group = groupActiveState != null && groupActiveState.isActiveV2Group();
boolean isInActiveGroup = groupActiveState != null && !groupActiveState.isActiveGroup();
if (isInMessageRequest() && recipient != null && !recipient.get().isBlocked()) {
if (isInMessageRequest()) {
if (isActiveGroup) {
inflater.inflate(R.menu.conversation_message_requests_group, menu);
}
inflater.inflate(R.menu.conversation_message_requests, menu);
if (recipient != null && recipient.get().isMuted()) inflater.inflate(R.menu.conversation_muted, menu);
else inflater.inflate(R.menu.conversation_unmuted, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@@ -945,8 +928,8 @@ public class ConversationParentFragment extends Fragment
}
if (isSingleConversation()) {
if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
else if (!recipient.get().isReleaseNotes()) inflater.inflate(R.menu.conversation_callable_insecure, menu);
if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
} else if (isGroupConversation()) {
if (isActiveV2Group && Build.VERSION.SDK_INT > 19) {
inflater.inflate(R.menu.conversation_callable_groupv2, menu);
@@ -972,14 +955,14 @@ public class ConversationParentFragment extends Fragment
inflater.inflate(R.menu.conversation, menu);
if (isSingleConversation() && !isSecureText && !recipient.get().isReleaseNotes()) {
if (isSingleConversation() && !isSecureText) {
inflater.inflate(R.menu.conversation_insecure, menu);
}
if (recipient != null && recipient.get().isMuted()) inflater.inflate(R.menu.conversation_muted, menu);
else inflater.inflate(R.menu.conversation_unmuted, menu);
if (isSingleConversation() && getRecipient().getContactUri() == null && !recipient.get().isReleaseNotes()) {
if (isSingleConversation() && getRecipient().getContactUri() == null) {
inflater.inflate(R.menu.conversation_add_to_contacts, menu);
}
@@ -1007,10 +990,6 @@ public class ConversationParentFragment extends Fragment
hideMenuItem(menu, R.id.menu_mute_notifications);
}
if (recipient != null && recipient.get().isReleaseNotes()) {
hideMenuItem(menu, R.id.menu_add_shortcut);
}
hideMenuItem(menu, R.id.menu_group_recipients);
if (isActiveV2Group) {
@@ -1095,7 +1074,7 @@ public class ConversationParentFragment extends Fragment
}
public void invalidateOptionsMenu() {
if (!isSearchRequested && getActivity() != null) {
if (!isSearchRequested) {
onCreateOptionsMenu(toolbar.getMenu(), requireActivity().getMenuInflater());
}
}
@@ -1123,12 +1102,13 @@ public class ConversationParentFragment extends Fragment
case R.id.menu_expiring_messages_off:
case R.id.menu_expiring_messages: handleSelectMessageExpiration(); return true;
case R.id.menu_create_bubble: handleCreateBubble(); return true;
case android.R.id.home: requireActivity().finish(); return true;
case android.R.id.home: requireActivity().onBackPressed(); return true;
}
return false;
}
// TODO [alex] LargeScreenSupport -- Add to a back handler
public void onBackPressed() {
Log.d(TAG, "onBackPressed()");
if (reactionDelegate.isShowing()) {
@@ -1136,7 +1116,7 @@ public class ConversationParentFragment extends Fragment
} else if (container.isInputOpen()) {
container.hideCurrentInput(composeText);
} else {
requireActivity().finish();
requireActivity().onBackPressed();
}
}
@@ -1289,15 +1269,6 @@ public class ConversationParentFragment extends Fragment
final Context context = requireContext().getApplicationContext();
final Recipient recipient = this.recipient.get();
if (pinnedShortcutReceiver == null) {
pinnedShortcutReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
Toast.makeText(context, context.getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show();
}
};
requireActivity().registerReceiver(pinnedShortcutReceiver, new IntentFilter(ACTION_PINNED_SHORTCUT));
}
GlideApp.with(this)
.asBitmap()
.load(recipient.getContactPhoto())
@@ -1349,10 +1320,9 @@ public class ConversationParentFragment extends Fragment
.setIntent(ShortcutLauncherActivity.createIntent(context, recipient.getId()))
.build();
Intent callbackIntent = new Intent(ACTION_PINNED_SHORTCUT);
PendingIntent shortcutPinnedCallback = PendingIntent.getBroadcast(context, REQUEST_CODE_PIN_SHORTCUT, callbackIntent, 0);
ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, shortcutPinnedCallback.getIntentSender());
if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, null)) {
Toast.makeText(context, context.getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show();
}
bitmap.recycle();
}
@@ -1547,21 +1517,18 @@ public class ConversationParentFragment extends Fragment
sendButton.resetAvailableTransports(isMediaMessage);
boolean smsEnabled = true;
if (recipient.get().isPushGroup() || (!recipient.get().isMmsGroup() && !recipient.get().hasSmsAddress())) {
sendButton.disableTransport(Type.SMS);
smsEnabled = false;
}
if (!isSecureText && !isPushGroupConversation() && !recipient.get().isServiceIdOnly() && !recipient.get().isReleaseNotes() && smsEnabled) {
if (!isSecureText && !isPushGroupConversation() && !recipient.get().isAciOnly()) {
sendButton.disableTransport(Type.TEXTSECURE);
}
if (!recipient.get().isPushGroup() && recipient.get().isForceSmsSelection() && smsEnabled) {
if (recipient.get().isPushGroup() || (!recipient.get().isMmsGroup() && !recipient.get().hasSmsAddress())) {
sendButton.disableTransport(Type.SMS);
}
if (!recipient.get().isPushGroup() && recipient.get().isForceSmsSelection()) {
sendButton.setDefaultTransport(Type.SMS);
} else {
if (isSecureText || isPushGroupConversation() || recipient.get().isServiceIdOnly() || recipient.get().isReleaseNotes() || !smsEnabled) {
if (isSecureText || isPushGroupConversation() || recipient.get().isAciOnly()) {
sendButton.setDefaultTransport(Type.TEXTSECURE);
} else {
sendButton.setDefaultTransport(Type.SMS);
@@ -1970,12 +1937,6 @@ public class ConversationParentFragment extends Fragment
final SettableFuture<Boolean> future = new SettableFuture<>();
final Context context = requireContext().getApplicationContext();
if (SignalStore.account().getAci() == null || SignalStore.account().getPni() == null) {
Log.w(TAG, "Not registered! Skipping initializeIdentityRecords()");
future.set(false);
return future;
}
new AsyncTask<Recipient, Void, Pair<IdentityRecordList, String>>() {
@Override
protected @NonNull Pair<IdentityRecordList, String> doInBackground(Recipient... params) {
@@ -1988,7 +1949,7 @@ public class ConversationParentFragment extends Fragment
}
long startTime = System.currentTimeMillis();
IdentityRecordList identityRecordList = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients);
IdentityRecordList identityRecordList = ApplicationDependencies.getIdentityStore().getIdentityRecords(recipients);
Log.i(TAG, String.format(Locale.US, "Loaded %d identities in %d ms", recipients.size(), System.currentTimeMillis() - startTime));
@@ -2065,7 +2026,6 @@ public class ConversationParentFragment extends Fragment
cannotSendInAnnouncementGroupBanner = ViewUtil.findStubById(view, R.id.conversation_cannot_send_announcement_stub);
requestingMemberBanner = view.findViewById(R.id.conversation_requesting_banner);
cancelJoinRequest = view.findViewById(R.id.conversation_cancel_request);
releaseChannelUnmute = ViewUtil.findStubById(view, R.id.conversation_release_notes_unmute_stub);
joinGroupCallButton = view.findViewById(R.id.conversation_group_call_join);
container.setIsBubble(isInBubble());
@@ -2159,7 +2119,7 @@ public class ConversationParentFragment extends Fragment
int toolbarColor = getResources().getColor(R.color.conversation_toolbar_color_wallpaper);
toolbar.setBackgroundColor(toolbarColor);
// TODO [alex] LargeScreenSupport -- statusBarBox
if (Build.VERSION.SDK_INT > 23) {
if (Build.VERSION.SDK_INT > 21) {
WindowUtil.setStatusBarColor(requireActivity().getWindow(), toolbarColor);
}
} else {
@@ -2173,7 +2133,7 @@ public class ConversationParentFragment extends Fragment
int toolbarColor = getResources().getColor(R.color.conversation_toolbar_color);
toolbar.setBackgroundColor(toolbarColor);
// TODO [alex] LargeScreenSupport -- statusBarBox
if (Build.VERSION.SDK_INT > 23) {
if (Build.VERSION.SDK_INT > 21) {
WindowUtil.setStatusBarColor(requireActivity().getWindow(), toolbarColor);
}
}
@@ -2231,9 +2191,7 @@ public class ConversationParentFragment extends Fragment
}
private void initializeSearchObserver() {
ConversationSearchViewModel.Factory viewModelFactory = new ConversationSearchViewModel.Factory(getString(R.string.note_to_self));
searchViewModel = new ViewModelProvider(this, viewModelFactory).get(ConversationSearchViewModel.class);
searchViewModel = new ViewModelProvider(this).get(ConversationSearchViewModel.class);
searchViewModel.getSearchResults().observe(getViewLifecycleOwner(), result -> {
if (result == null) return;
@@ -2484,11 +2442,6 @@ public class ConversationParentFragment extends Fragment
}
private void onRecipientChanged(@NonNull Recipient recipient) {
if (getContext() == null) {
Log.w(TAG, "onRecipientChanged called in detached state. Ignoring.");
return;
}
Log.i(TAG, "onModified(" + recipient.getId() + ") " + recipient.getRegistered());
titleView.setTitle(glideRequests, recipient);
titleView.setVerified(identityRecords.isVerified());
@@ -2743,20 +2696,6 @@ public class ConversationParentFragment extends Fragment
inputPanel.setHideForBlockedState(true);
makeDefaultSmsButton.setVisibility(View.VISIBLE);
registerButton.setVisibility(View.GONE);
} else if (recipient.isReleaseNotes() && !recipient.isBlocked()) {
unblockButton.setVisibility(View.GONE);
inputPanel.setHideForBlockedState(true);
makeDefaultSmsButton.setVisibility(View.GONE);
registerButton.setVisibility(View.GONE);
if (recipient.isMuted()) {
View unmuteBanner = releaseChannelUnmute.get();
unmuteBanner.setVisibility(View.VISIBLE);
unmuteBanner.findViewById(R.id.conversation_activity_unmute_button)
.setOnClickListener(v -> handleUnmuteNotifications());
} else if (releaseChannelUnmute.resolved()) {
releaseChannelUnmute.get().setVisibility(View.GONE);
}
} else {
boolean inactivePushGroup = isPushGroupConversation() && !recipient.isActiveGroup();
inputPanel.setHideForBlockedState(inactivePushGroup);
@@ -2764,10 +2703,6 @@ public class ConversationParentFragment extends Fragment
makeDefaultSmsButton.setVisibility(View.GONE);
registerButton.setVisibility(View.GONE);
}
if (releaseChannelUnmute.resolved() && !recipient.isReleaseNotes()) {
releaseChannelUnmute.get().setVisibility(View.GONE);
}
}
private void calculateCharactersRemaining() {
@@ -3019,7 +2954,7 @@ public class ConversationParentFragment extends Fragment
return new SettableFuture<>(null);
}
final boolean sendPush = (isSecureText && !forceSms) || recipient.get().isServiceIdOnly();
final boolean sendPush = (isSecureText && !forceSms) || recipient.get().isAciOnly();
final long thread = this.threadId;
if (sendPush) {
@@ -3082,7 +3017,7 @@ public class ConversationParentFragment extends Fragment
final long thread = this.threadId;
final Context context = requireContext().getApplicationContext();
final String messageBody = getMessage();
final boolean sendPush = (isSecureText && !forceSms) || recipient.get().isServiceIdOnly();
final boolean sendPush = (isSecureText && !forceSms) || recipient.get().isAciOnly();
OutgoingTextMessage message;
@@ -3157,7 +3092,7 @@ public class ConversationParentFragment extends Fragment
}
private void updateLinkPreviewState() {
if (SignalStore.settings().isLinkPreviewsEnabled() && isSecureText && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent() && getContext() != null) {
if (SignalStore.settings().isLinkPreviewsEnabled() && isSecureText && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent()) {
linkPreviewViewModel.onEnabled();
linkPreviewViewModel.onTextChanged(requireContext(), composeText.getTextTrimmed().toString(), composeText.getSelectionStart(), composeText.getSelectionEnd());
} else {
@@ -3476,10 +3411,6 @@ public class ConversationParentFragment extends Fragment
return voiceNoteMediaController;
}
@Override public void openStickerSearch() {
StickerSearchDialogFragment.show(getChildFragmentManager());
}
// Listeners
private final class DeleteCanceledVoiceNoteListener implements ListenableFuture.Listener<VoiceNoteDraft> {
@@ -3550,7 +3481,7 @@ public class ConversationParentFragment extends Fragment
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
if (SignalStore.settings().isEnterKeySends() || event.isCtrlPressed()) {
if (SignalStore.settings().isEnterKeySends()) {
sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
return true;
@@ -3724,19 +3655,18 @@ public class ConversationParentFragment extends Fragment
@Override
public void handleReaction(@NonNull ConversationMessage conversationMessage,
@NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener,
@NonNull SelectedConversationModel selectedConversationModel,
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
@NonNull ConversationReactionOverlay.OnHideListener onHideListener)
{
reactionDelegate.setOnActionSelectedListener(onActionSelectedListener);
reactionDelegate.setOnToolbarItemClickedListener(toolbarListener);
reactionDelegate.setOnHideListener(onHideListener);
reactionDelegate.show(requireActivity(), recipient.get(), conversationMessage, groupViewModel.isNonAdminInAnnouncementGroup(), selectedConversationModel);
reactionDelegate.show(requireActivity(), recipient.get(), conversationMessage, groupViewModel.isNonAdminInAnnouncementGroup());
}
@Override
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
if (messageRecord.isIdentityMismatchFailure()) {
SafetyNumberChangeDialog.show(requireContext(), getChildFragmentManager(), messageRecord);
SafetyNumberChangeDialog.show(requireActivity(), messageRecord);
} else if (messageRecord.hasFailedWithNetworkFailures()) {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.conversation_activity__message_could_not_be_sent)
@@ -3744,7 +3674,7 @@ public class ConversationParentFragment extends Fragment
.setPositiveButton(R.string.conversation_activity__send, (dialog, which) -> MessageSender.resend(requireContext(), messageRecord))
.show();
} else {
MessageDetailsFragment.create(messageRecord, recipient.getId()).show(getChildFragmentManager(), null);
startActivity(MessageDetailsActivity.getIntentForMessageDetails(requireContext(), messageRecord, messageRecord.getRecipient().getId(), messageRecord.getThreadId()));
}
}
@@ -3758,11 +3688,6 @@ public class ConversationParentFragment extends Fragment
voiceNoteMediaController.startConsecutivePlayback(uri, messageId, progress);
}
@Override
public void onVoiceNoteResume(@NonNull Uri uri, long messageId) {
voiceNoteMediaController.resumePlayback(uri, messageId);
}
@Override
public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
voiceNoteMediaController.seekToPosition(uri, progress);
@@ -3799,11 +3724,6 @@ public class ConversationParentFragment extends Fragment
});
}
@Override
public boolean isKeyboardOpen() {
return container.isKeyboardOpen();
}
@Override
public void setThreadId(long threadId) {
this.threadId = threadId;
@@ -4014,9 +3934,9 @@ public class ConversationParentFragment extends Fragment
SimpleTask.run(() -> {
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (IdentityRecord identityRecord : unverifiedIdentities) {
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
VerifiedStatus.DEFAULT);
ApplicationDependencies.getIdentityStore().setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
VerifiedStatus.DEFAULT);
}
}
return null;

View File

@@ -48,12 +48,13 @@ public class ConversationPopupActivity extends ConversationActivity {
else getWindow().setLayout((int) (width * .7), (int) (height * .75));
super.onCreate(bundle, ready);
getTitleView().setOnClickListener(null);
}
@Override
protected void onResume() {
super.onResume();
getTitleView().setOnClickListener(null);
getComposeText().requestFocus();
getQuickAttachmentToggle().disable();
}

View File

@@ -5,6 +5,7 @@ import android.graphics.PointF;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -23,7 +24,7 @@ final class ConversationReactionDelegate {
private final PointF lastSeenDownPoint = new PointF();
private ConversationReactionOverlay.OnReactionSelectedListener onReactionSelectedListener;
private ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener;
private Toolbar.OnMenuItemClickListener onToolbarItemClickedListener;
private ConversationReactionOverlay.OnHideListener onHideListener;
ConversationReactionDelegate(@NonNull Stub<ConversationReactionOverlay> overlayStub) {
@@ -37,10 +38,9 @@ final class ConversationReactionDelegate {
void show(@NonNull Activity activity,
@NonNull Recipient conversationRecipient,
@NonNull ConversationMessage conversationMessage,
boolean isNonAdminInAnnouncementGroup,
@NonNull SelectedConversationModel selectedConversationModel)
boolean isNonAdminInAnnouncementGroup)
{
resolveOverlay().show(activity, conversationRecipient, conversationMessage, lastSeenDownPoint, isNonAdminInAnnouncementGroup, selectedConversationModel);
resolveOverlay().show(activity, conversationRecipient, conversationMessage, lastSeenDownPoint, isNonAdminInAnnouncementGroup);
}
void hide() {
@@ -59,11 +59,11 @@ final class ConversationReactionDelegate {
}
}
void setOnActionSelectedListener(@NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener) {
this.onActionSelectedListener = onActionSelectedListener;
void setOnToolbarItemClickedListener(@NonNull Toolbar.OnMenuItemClickListener onToolbarItemClickedListener) {
this.onToolbarItemClickedListener = onToolbarItemClickedListener;
if (overlayStub.resolved()) {
overlayStub.get().setOnActionSelectedListener(onActionSelectedListener);
overlayStub.get().setOnToolbarItemClickedListener(onToolbarItemClickedListener);
}
}
@@ -99,7 +99,7 @@ final class ConversationReactionDelegate {
overlay.requestFitSystemWindows();
overlay.setOnHideListener(onHideListener);
overlay.setOnActionSelectedListener(onActionSelectedListener);
overlay.setOnToolbarItemClickedListener(onToolbarItemClickedListener);
overlay.setOnReactionSelectedListener(onReactionSelectedListener);
return overlay;

View File

@@ -2,42 +2,34 @@ package org.thoughtcrime.securesms.conversation;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.widget.Toolbar;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewKt;
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
import com.annimon.stream.Stream;
import org.signal.core.util.DimensionUnit;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.components.menu.ActionItem;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -47,12 +39,10 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import kotlin.Unit;
public final class ConversationReactionOverlay extends FrameLayout {
public final class ConversationReactionOverlay extends RelativeLayout {
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
@@ -64,45 +54,45 @@ public final class ConversationReactionOverlay extends FrameLayout {
private final Boundary verticalScrubBoundary = new Boundary();
private final PointF deadzoneTouchPoint = new PointF();
private Activity activity;
private Recipient conversationRecipient;
private MessageRecord messageRecord;
private SelectedConversationModel selectedConversationModel;
private OverlayState overlayState = OverlayState.HIDDEN;
private boolean isNonAdminInAnnouncementGroup;
private Activity activity;
private Recipient conversationRecipient;
private MessageRecord messageRecord;
private OverlayState overlayState = OverlayState.HIDDEN;
private boolean isNonAdminInAnnouncementGroup;
private boolean downIsOurs;
private boolean isToolbarTouch;
private int selected = -1;
private int customEmojiIndex;
private int originalStatusBarColor;
private int originalNavigationBarColor;
private View dropdownAnchor;
private View toolbarShade;
private View inputShade;
private View conversationItem;
private View backgroundView;
private ConstraintLayout foregroundView;
private View selectedView;
private EmojiImageView[] emojiViews;
private ConversationContextMenu contextMenu;
private Toolbar toolbar;
private float touchDownDeadZoneSize;
private float distanceFromTouchDownPointToTopOfScrubberDeadZone;
private float distanceFromTouchDownPointToBottomOfScrubberDeadZone;
private int scrubberDistanceFromTouchDown;
private int scrubberHeight;
private int scrubberWidth;
private int actionBarHeight;
private int selectedVerticalTranslation;
private int scrubberHorizontalMargin;
private int animationEmojiStartDelayFactor;
private int statusBarHeight;
private int bottomNavigationBarHeight;
private OnReactionSelectedListener onReactionSelectedListener;
private OnActionSelectedListener onActionSelectedListener;
private Toolbar.OnMenuItemClickListener onToolbarItemClickedListener;
private OnHideListener onHideListener;
private AnimatorSet revealAnimatorSet = new AnimatorSet();
private AnimatorSet hideAnimatorSet = new AnimatorSet();
private AnimatorSet revealAnimatorSet = new AnimatorSet();
private AnimatorSet revealMaskAnimatorSet = new AnimatorSet();
private AnimatorSet hideAnimatorSet = new AnimatorSet();
private AnimatorSet hideAllButMaskAnimatorSet = new AnimatorSet();
private AnimatorSet hideMaskAnimatorSet = new AnimatorSet();
public ConversationReactionOverlay(@NonNull Context context) {
super(context);
@@ -116,13 +106,13 @@ public final class ConversationReactionOverlay extends FrameLayout {
protected void onFinishInflate() {
super.onFinishInflate();
dropdownAnchor = findViewById(R.id.dropdown_anchor);
toolbarShade = findViewById(R.id.toolbar_shade);
inputShade = findViewById(R.id.input_shade);
conversationItem = findViewById(R.id.conversation_item);
backgroundView = findViewById(R.id.conversation_reaction_scrubber_background);
foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground);
selectedView = findViewById(R.id.conversation_reaction_current_selection_indicator);
backgroundView = findViewById(R.id.conversation_reaction_scrubber_background);
foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground);
selectedView = findViewById(R.id.conversation_reaction_current_selection_indicator);
toolbar = findViewById(R.id.conversation_reaction_toolbar);
toolbar.setOnMenuItemClickListener(this::handleToolbarItemClicked);
toolbar.setNavigationOnClickListener(view -> hide());
emojiViews = new EmojiImageView[] { findViewById(R.id.reaction_1),
findViewById(R.id.reaction_2),
@@ -134,12 +124,16 @@ public final class ConversationReactionOverlay extends FrameLayout {
customEmojiIndex = emojiViews.length - 1;
distanceFromTouchDownPointToTopOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_top);
distanceFromTouchDownPointToBottomOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_bottom);
touchDownDeadZoneSize = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_touch_deadzone_size);
scrubberWidth = getResources().getDimensionPixelOffset(R.dimen.reaction_scrubber_width);
selectedVerticalTranslation = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation);
scrubberHorizontalMargin = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin);
touchDownDeadZoneSize = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_touch_deadzone_size);
scrubberDistanceFromTouchDown = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrubber_distance);
scrubberHeight = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrubber_height);
scrubberWidth = getResources().getDimensionPixelOffset(R.dimen.reaction_scrubber_width);
actionBarHeight = (int) ThemeUtil.getThemedDimen(getContext(), R.attr.actionBarSize);
selectedVerticalTranslation = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation);
scrubberHorizontalMargin = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin);
animationEmojiStartDelayFactor = getResources().getInteger(R.integer.reaction_scrubber_emoji_reveal_duration_start_delay_factor);
@@ -150,8 +144,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
@NonNull Recipient conversationRecipient,
@NonNull ConversationMessage conversationMessage,
@NonNull PointF lastSeenDownPoint,
boolean isNonAdminInAnnouncementGroup,
@NonNull SelectedConversationModel selectedConversationModel)
boolean isNonAdminInAnnouncementGroup)
{
if (overlayState != OverlayState.HIDDEN) {
return;
@@ -159,325 +152,77 @@ public final class ConversationReactionOverlay extends FrameLayout {
this.messageRecord = conversationMessage.getMessageRecord();
this.conversationRecipient = conversationRecipient;
this.selectedConversationModel = selectedConversationModel;
this.isNonAdminInAnnouncementGroup = isNonAdminInAnnouncementGroup;
overlayState = OverlayState.UNINITAILIZED;
selected = -1;
setupToolbarMenuItems(conversationMessage);
setupSelectedEmoji();
if (Build.VERSION.SDK_INT >= 21) {
View statusBarBackground = activity.findViewById(android.R.id.statusBarBackground);
statusBarHeight = statusBarBackground == null ? 0 : statusBarBackground.getHeight();
View navigationBarBackground = activity.findViewById(android.R.id.navigationBarBackground);
bottomNavigationBarHeight = navigationBarBackground == null ? 0 : navigationBarBackground.getHeight();
} else {
statusBarHeight = ViewUtil.getStatusBarHeight(this);
bottomNavigationBarHeight = ViewUtil.getNavigationBarHeight(this);
statusBarHeight = ViewUtil.getStatusBarHeight(this);
}
boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
if (isLandscape) {
bottomNavigationBarHeight = 0;
}
final float scrubberTranslationY = Math.max(-scrubberDistanceFromTouchDown + actionBarHeight,
lastSeenDownPoint.y - scrubberHeight - scrubberDistanceFromTouchDown - statusBarHeight);
toolbarShade.setVisibility(VISIBLE);
toolbarShade.setAlpha(1f);
final float halfWidth = scrubberWidth / 2f + scrubberHorizontalMargin;
final float screenWidth = getResources().getDisplayMetrics().widthPixels;
final float downX = ViewUtil.isLtr(this) ? lastSeenDownPoint.x : screenWidth - lastSeenDownPoint.x;
final float scrubberTranslationX = Util.clamp(downX - halfWidth,
scrubberHorizontalMargin,
screenWidth + scrubberHorizontalMargin - halfWidth * 2) * (ViewUtil.isLtr(this) ? 1 : -1);
inputShade.setVisibility(VISIBLE);
inputShade.setAlpha(1f);
backgroundView.setTranslationX(scrubberTranslationX);
backgroundView.setTranslationY(scrubberTranslationY);
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
foregroundView.setTranslationX(scrubberTranslationX);
foregroundView.setTranslationY(scrubberTranslationY);
conversationItem.setLayoutParams(new LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
conversationItem.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot));
verticalScrubBoundary.update(lastSeenDownPoint.y - distanceFromTouchDownPointToTopOfScrubberDeadZone,
lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone);
boolean isMessageOnLeft = selectedConversationModel.isOutgoing() ^ ViewUtil.isLtr(this);
conversationItem.setScaleX(ConversationItem.LONG_PRESS_SCALE_FACTOR);
conversationItem.setScaleY(ConversationItem.LONG_PRESS_SCALE_FACTOR);
setVisibility(View.INVISIBLE);
hideAnimatorSet.end();
toolbar.setVisibility(VISIBLE);
setVisibility(View.VISIBLE);
revealAnimatorSet.start();
if (Build.VERSION.SDK_INT >= 21) {
this.activity = activity;
updateSystemUiOnShow(activity);
}
originalStatusBarColor = activity.getWindow().getStatusBarColor();
WindowUtil.setStatusBarColor(activity.getWindow(), ContextCompat.getColor(getContext(), R.color.action_mode_status_bar));
ViewKt.doOnLayout(this, v -> {
showAfterLayout(activity, conversationMessage, lastSeenDownPoint, isMessageOnLeft);
return Unit.INSTANCE;
});
}
private void showAfterLayout(@NonNull Activity activity,
@NonNull ConversationMessage conversationMessage,
@NonNull PointF lastSeenDownPoint,
boolean isMessageOnLeft) {
updateToolbarShade(activity);
updateInputShade(activity);
contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(conversationMessage));
conversationItem.setX(selectedConversationModel.getBubbleX());
conversationItem.setY(selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() - statusBarHeight);
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
boolean isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < getWidth();
int overlayHeight = getHeight() - bottomNavigationBarHeight;
int bubbleWidth = selectedConversationModel.getBubbleWidth();
float endX = selectedConversationModel.getBubbleX();
float endY = conversationItem.getY();
float endApparentTop = endY;
float endScale = 1f;
float menuPadding = DimensionUnit.DP.toPixels(12f);
float reactionBarTopPadding = DimensionUnit.DP.toPixels(32f);
int reactionBarHeight = backgroundView.getHeight();
float reactionBarBackgroundY;
if (isWideLayout) {
boolean everythingFitsVertically = reactionBarHeight + menuPadding + reactionBarTopPadding + conversationItemSnapshot.getHeight() < overlayHeight;
if (everythingFitsVertically) {
boolean reactionBarFitsAboveItem = conversationItem.getY() > reactionBarHeight + menuPadding + reactionBarTopPadding;
if (reactionBarFitsAboveItem) {
reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
} else {
endY = reactionBarHeight + menuPadding + reactionBarTopPadding;
reactionBarBackgroundY = reactionBarTopPadding;
}
} else {
float spaceAvailableForItem = overlayHeight - reactionBarHeight - menuPadding - reactionBarTopPadding;
endScale = spaceAvailableForItem / conversationItem.getHeight();
endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
endY = reactionBarHeight + menuPadding + reactionBarTopPadding - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
reactionBarBackgroundY = reactionBarTopPadding;
if (!ThemeUtil.isDarkTheme(getContext())) {
WindowUtil.setLightStatusBar(activity.getWindow());
}
} else {
float reactionBarOffset = DimensionUnit.DP.toPixels(48);
float spaceForReactionBar = Math.max(reactionBarHeight + reactionBarOffset - conversationItemSnapshot.getHeight(), 0);
boolean everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.getHeight() + menuPadding + spaceForReactionBar < overlayHeight;
if (everythingFitsVertically) {
float bubbleBottom = selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight();
boolean menuFitsBelowItem = bubbleBottom + menuPadding + contextMenu.getMaxHeight() <= overlayHeight + statusBarHeight;
if (menuFitsBelowItem) {
if (conversationItem.getY() < 0) {
endY = 0;
}
float contextMenuTop = endY + conversationItemSnapshot.getHeight();
reactionBarBackgroundY = getReactionBarOffsetForTouch(lastSeenDownPoint, contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
if (reactionBarBackgroundY <= reactionBarTopPadding) {
endY = backgroundView.getHeight() + menuPadding + reactionBarTopPadding;
}
} else {
endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight();
float contextMenuTop = endY + conversationItemSnapshot.getHeight();
reactionBarBackgroundY = getReactionBarOffsetForTouch(lastSeenDownPoint, contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
}
endApparentTop = endY;
} else if (reactionBarOffset + reactionBarHeight + contextMenu.getMaxHeight() + menuPadding < overlayHeight) {
float spaceAvailableForItem = (float) overlayHeight - contextMenu.getMaxHeight() - menuPadding - spaceForReactionBar;
endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
endY = spaceForReactionBar - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
float contextMenuTop = endY + (conversationItemSnapshot.getHeight() * endScale);
reactionBarBackgroundY = getReactionBarOffsetForTouch(lastSeenDownPoint, contextMenuTop + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale), menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
endApparentTop = endY + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
} else {
contextMenu.setHeight(contextMenu.getMaxHeight() / 2);
int menuHeight = contextMenu.getHeight();
boolean fitsVertically = menuHeight + conversationItem.getHeight() + menuPadding * 2 + reactionBarHeight + reactionBarTopPadding < overlayHeight;
if (fitsVertically) {
float bubbleBottom = selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight();
boolean menuFitsBelowItem = bubbleBottom + menuPadding + menuHeight <= overlayHeight + statusBarHeight;
if (menuFitsBelowItem) {
reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
if (reactionBarBackgroundY < reactionBarTopPadding) {
endY = reactionBarTopPadding + reactionBarHeight + menuPadding;
reactionBarBackgroundY = reactionBarTopPadding;
}
} else {
endY = overlayHeight - menuHeight - menuPadding - conversationItemSnapshot.getHeight();
reactionBarBackgroundY = endY - reactionBarHeight - menuPadding;
}
endApparentTop = endY;
} else {
float spaceAvailableForItem = (float) overlayHeight - menuHeight - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding;
endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
endY = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale) + menuPadding + reactionBarTopPadding;
reactionBarBackgroundY = reactionBarTopPadding;
endApparentTop = reactionBarHeight + menuPadding + reactionBarTopPadding;
}
}
}
reactionBarBackgroundY = Math.max(reactionBarBackgroundY, -statusBarHeight);
hideAnimatorSet.end();
setVisibility(View.VISIBLE);
float scrubberX;
if (isMessageOnLeft) {
scrubberX = scrubberHorizontalMargin;
} else {
scrubberX = getWidth() - scrubberWidth - scrubberHorizontalMargin;
}
foregroundView.setX(scrubberX);
foregroundView.setY(reactionBarBackgroundY + reactionBarHeight / 2f - foregroundView.getHeight() / 2f);
backgroundView.setX(scrubberX);
backgroundView.setY(reactionBarBackgroundY);
verticalScrubBoundary.update(reactionBarBackgroundY,
lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone);
updateBoundsOnLayoutChanged();
revealAnimatorSet.start();
if (isWideLayout) {
float scrubberRight = scrubberX + scrubberWidth;
float offsetX = isMessageOnLeft ? scrubberRight + menuPadding : scrubberX - contextMenu.getMaxWidth() - menuPadding;
contextMenu.show((int) offsetX, (int) Math.min(backgroundView.getY(), overlayHeight - contextMenu.getMaxHeight()));
} else {
float contentX = selectedConversationModel.getBubbleX();
float offsetX = isMessageOnLeft ? contentX : -contextMenu.getMaxWidth() + contentX + bubbleWidth;
float menuTop = endApparentTop + (conversationItemSnapshot.getHeight() * endScale);
contextMenu.show((int) offsetX, (int) (menuTop + menuPadding));
}
int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
conversationItem.animate()
.x(endX)
.y(endY)
.scaleX(endScale)
.scaleY(endScale)
.setDuration(revealDuration);
}
private float getReactionBarOffsetForTouch(@NonNull PointF touchPoint,
float contextMenuTop,
float contextMenuPadding,
float reactionBarOffset,
int reactionBarHeight,
float spaceNeededBetweenTopOfScreenAndTopOfReactionBar,
float messageTop)
{
float adjustedTouchY = touchPoint.y - statusBarHeight;
float reactionStartingPoint = Math.min(adjustedTouchY, contextMenuTop);
float spaceBetweenTopOfMessageAndTopOfContextMenu = Math.abs(messageTop - contextMenuTop);
if (spaceBetweenTopOfMessageAndTopOfContextMenu < DimensionUnit.DP.toPixels(150)) {
float offsetToMakeReactionBarOffsetMatchMenuPadding = reactionBarOffset - contextMenuPadding;
reactionStartingPoint = messageTop + offsetToMakeReactionBarOffsetMatchMenuPadding;
}
return Math.max(reactionStartingPoint - reactionBarOffset - reactionBarHeight, spaceNeededBetweenTopOfScreenAndTopOfReactionBar);
}
private void updateToolbarShade(@NonNull Activity activity) {
View toolbar = activity.findViewById(R.id.toolbar);
View bannerContainer = activity.findViewById(R.id.conversation_banner_container);
LayoutParams layoutParams = (LayoutParams) toolbarShade.getLayoutParams();
layoutParams.height = toolbar.getHeight() + bannerContainer.getHeight();
toolbarShade.setLayoutParams(layoutParams);
}
private void updateInputShade(@NonNull Activity activity) {
LayoutParams layoutParams = (LayoutParams) inputShade.getLayoutParams();
layoutParams.bottomMargin = bottomNavigationBarHeight;
layoutParams.height = getInputPanelHeight(activity);
inputShade.setLayoutParams(layoutParams);
}
private int getInputPanelHeight(@NonNull Activity activity) {
View bottomPanel = activity.findViewById(R.id.conversation_activity_panel_parent);
View emojiDrawer = activity.findViewById(R.id.emoji_drawer);
return bottomPanel.getHeight() + (emojiDrawer != null && emojiDrawer.getVisibility() == VISIBLE ? emojiDrawer.getHeight() : 0);
}
@RequiresApi(api = 21)
private void updateSystemUiOnShow(@NonNull Activity activity) {
Window window = activity.getWindow();
int barColor = ContextCompat.getColor(getContext(), R.color.conversation_item_selected_system_ui);
originalStatusBarColor = window.getStatusBarColor();
WindowUtil.setStatusBarColor(window, barColor);
originalNavigationBarColor = window.getNavigationBarColor();
WindowUtil.setNavigationBarColor(window, barColor);
if (!ThemeUtil.isDarkTheme(getContext())) {
WindowUtil.clearLightStatusBar(window);
WindowUtil.clearLightNavigationBar(window);
}
}
public void hide() {
hideInternal(onHideListener);
hideInternal(hideAnimatorSet, onHideListener);
}
public void hideForReactWithAny() {
hideInternal(onHideListener);
hideInternal(hideAnimatorSet, null);
}
private void hideInternal(@Nullable OnHideListener onHideListener) {
private void hideInternal(@NonNull AnimatorSet hideAnimatorSet, @Nullable OnHideListener onHideListener) {
overlayState = OverlayState.HIDDEN;
AnimatorSet animatorSet = newHideAnimatorSet();
hideAnimatorSet = animatorSet;
revealAnimatorSet.end();
animatorSet.start();
hideAnimatorSet.start();
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
WindowUtil.setStatusBarColor(activity.getWindow(), originalStatusBarColor);
WindowUtil.clearLightStatusBar(activity.getWindow());
activity = null;
}
if (onHideListener != null) {
onHideListener.startHide();
}
if (selectedConversationModel.getFocusedView() != null) {
ViewUtil.focusAndShowKeyboard(selectedConversationModel.getFocusedView());
}
animatorSet.addListener(new AnimationCompleteListener() {
@Override public void onAnimationEnd(Animator animation) {
animatorSet.removeListener(this);
toolbarShade.setVisibility(INVISIBLE);
inputShade.setVisibility(INVISIBLE);
if (onHideListener != null) {
onHideListener.onHide();
}
}
});
if (contextMenu != null) {
contextMenu.dismiss();
onHideListener.onHide();
}
}
@@ -493,10 +238,6 @@ public final class ConversationReactionOverlay extends FrameLayout {
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
updateBoundsOnLayoutChanged();
}
private void updateBoundsOnLayoutChanged() {
backgroundView.getGlobalVisibleRect(emojiStripViewBounds);
emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect);
emojiStripViewBounds.left = getStart(emojiViewGlobalRect);
@@ -559,10 +300,24 @@ public final class ConversationReactionOverlay extends FrameLayout {
}
}
if (isToolbarTouch) {
if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP) {
isToolbarTouch = false;
}
return false;
}
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
selected = getSelectedIndexViaDownEvent(motionEvent);
if (selected == -1) {
if (motionEvent.getY() < toolbar.getHeight() + statusBarHeight) {
isToolbarTouch = true;
return false;
}
}
deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY());
overlayState = OverlayState.DEADZONE;
downIsOurs = true;
@@ -684,7 +439,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
}
private void handleUpEvent() {
if (selected != -1 && onReactionSelectedListener != null && backgroundView.getVisibility() == View.VISIBLE) {
if (selected != -1 && onReactionSelectedListener != null) {
if (selected == customEmojiIndex) {
onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].getTag() != null);
} else {
@@ -699,8 +454,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
this.onReactionSelectedListener = onReactionSelectedListener;
}
public void setOnActionSelectedListener(@Nullable OnActionSelectedListener onActionSelectedListener) {
this.onActionSelectedListener = onActionSelectedListener;
public void setOnToolbarItemClickedListener(@Nullable Toolbar.OnMenuItemClickListener onToolbarItemClickedListener) {
this.onToolbarItemClickedListener = onToolbarItemClickedListener;
}
public void setOnHideListener(@Nullable OnHideListener onHideListener) {
@@ -719,69 +474,29 @@ public final class ConversationReactionOverlay extends FrameLayout {
.orElse(null);
}
private @NonNull List<ActionItem> getMenuActionItems(@NonNull ConversationMessage conversationMessage) {
private void setupToolbarMenuItems(@NonNull ConversationMessage conversationMessage) {
MenuState menuState = MenuState.getMenuState(conversationRecipient, conversationMessage.getMultiselectCollection().toSet(), false, isNonAdminInAnnouncementGroup);
List<ActionItem> items = new ArrayList<>();
if (menuState.shouldShowReplyAction()) {
items.add(new ActionItem(R.drawable.ic_reply_24_tinted, getResources().getString(R.string.conversation_selection__menu_reply), () -> handleActionItemClicked(Action.REPLY)));
}
if (menuState.shouldShowForwardAction()) {
items.add(new ActionItem(R.drawable.ic_forward_24_tinted, getResources().getString(R.string.conversation_selection__menu_forward), () -> handleActionItemClicked(Action.FORWARD)));
}
if (menuState.shouldShowResendAction()) {
items.add(new ActionItem(R.drawable.ic_retry_24, getResources().getString(R.string.conversation_selection__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
}
if (menuState.shouldShowSaveAttachmentAction()) {
items.add(new ActionItem(R.drawable.ic_save_24_tinted, getResources().getString(R.string.conversation_selection__menu_save), () -> handleActionItemClicked(Action.DOWNLOAD)));
}
if (menuState.shouldShowCopyAction()) {
items.add(new ActionItem(R.drawable.ic_copy_24_tinted, getResources().getString(R.string.conversation_selection__menu_copy), () -> handleActionItemClicked(Action.COPY)));
}
items.add(new ActionItem(R.drawable.ic_select_24_tinted, getResources().getString(R.string.conversation_selection__menu_multi_select), () -> handleActionItemClicked(Action.MULTISELECT)));
if (menuState.shouldShowDetailsAction()) {
items.add(new ActionItem(R.drawable.ic_info_tinted_24, getResources().getString(R.string.conversation_selection__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
}
backgroundView.setVisibility(menuState.shouldShowReactions() ? View.VISIBLE : View.INVISIBLE);
foregroundView.setVisibility(menuState.shouldShowReactions() ? View.VISIBLE : View.INVISIBLE);
items.add(new ActionItem(R.drawable.ic_delete_tinted_24, getResources().getString(R.string.conversation_selection__menu_delete), () -> handleActionItemClicked(Action.DELETE)));
return items;
toolbar.getMenu().findItem(R.id.action_copy).setVisible(menuState.shouldShowCopyAction());
toolbar.getMenu().findItem(R.id.action_download).setVisible(menuState.shouldShowSaveAttachmentAction());
toolbar.getMenu().findItem(R.id.action_forward).setVisible(menuState.shouldShowForwardAction());
toolbar.getMenu().findItem(R.id.action_reply).setVisible(menuState.shouldShowReplyAction());
}
private void handleActionItemClicked(@NonNull Action action) {
hideInternal(new OnHideListener() {
@Override public void startHide() {
if (onHideListener != null) {
onHideListener.startHide();
}
}
private boolean handleToolbarItemClicked(@NonNull MenuItem menuItem) {
@Override public void onHide() {
if (onHideListener != null) {
onHideListener.onHide();
}
hide();
if (onActionSelectedListener != null) {
onActionSelectedListener.onActionSelected(action);
}
}
});
if (onToolbarItemClickedListener == null) {
return false;
}
return onToolbarItemClickedListener.onMenuItemClick(menuItem);
}
private void initAnimators() {
int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
int revealOffset = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_offset);
int duration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
List<Animator> reveals = Stream.of(emojiViews)
.mapIndexed((idx, v) -> {
@@ -792,126 +507,81 @@ public final class ConversationReactionOverlay extends FrameLayout {
})
.toList();
Animator overlayRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
overlayRevealAnim.setDuration(duration);
reveals.add(overlayRevealAnim);
Animator backgroundRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
backgroundRevealAnim.setTarget(backgroundView);
backgroundRevealAnim.setDuration(revealDuration);
backgroundRevealAnim.setStartDelay(revealOffset);
backgroundRevealAnim.setDuration(duration);
reveals.add(backgroundRevealAnim);
Animator selectedRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
selectedRevealAnim.setTarget(selectedView);
backgroundRevealAnim.setDuration(revealDuration);
backgroundRevealAnim.setStartDelay(revealOffset);
selectedRevealAnim.setDuration(duration);
reveals.add(selectedRevealAnim);
Animator toolbarRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
toolbarRevealAnim.setTarget(toolbar);
toolbarRevealAnim.setDuration(duration);
reveals.add(toolbarRevealAnim);
revealAnimatorSet.setInterpolator(INTERPOLATOR);
revealAnimatorSet.playTogether(reveals);
}
private @NonNull AnimatorSet newHideAnimatorSet() {
AnimatorSet set = new AnimatorSet();
revealMaskAnimatorSet.setInterpolator(INTERPOLATOR);
revealMaskAnimatorSet.playTogether(overlayRevealAnim);
set.addListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
setVisibility(View.GONE);
}
});
set.setInterpolator(INTERPOLATOR);
set.playTogether(newHideAnimators());
return set;
}
private @NonNull List<Animator> newHideAnimators() {
int duration = getContext().getResources().getInteger(R.integer.reaction_scrubber_hide_duration);
List<Animator> animators = new ArrayList<>(Stream.of(emojiViews)
.mapIndexed((idx, v) -> {
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
anim.setTarget(v);
return anim;
})
.toList());
List<Animator> hides = Stream.of(emojiViews)
.mapIndexed((idx, v) -> {
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
anim.setTarget(v);
anim.setStartDelay(idx * animationEmojiStartDelayFactor);
return anim;
})
.toList();
Animator overlayHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
overlayHideAnim.setDuration(duration);
animators.add(overlayHideAnim);
Animator backgroundHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
backgroundHideAnim.setTarget(backgroundView);
backgroundHideAnim.setDuration(duration);
animators.add(backgroundHideAnim);
hides.add(backgroundHideAnim);
Animator selectedHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
selectedHideAnim.setTarget(selectedView);
selectedHideAnim.setDuration(duration);
animators.add(selectedHideAnim);
hides.add(selectedHideAnim);
ObjectAnimator itemScaleXAnim = new ObjectAnimator();
itemScaleXAnim.setProperty(View.SCALE_X);
itemScaleXAnim.setFloatValues(1f);
itemScaleXAnim.setTarget(conversationItem);
itemScaleXAnim.setDuration(duration);
animators.add(itemScaleXAnim);
Animator toolbarHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
toolbarHideAnim.setTarget(toolbar);
toolbarHideAnim.setDuration(duration);
hides.add(toolbarHideAnim);
ObjectAnimator itemScaleYAnim = new ObjectAnimator();
itemScaleYAnim.setProperty(View.SCALE_Y);
itemScaleYAnim.setFloatValues(1f);
itemScaleYAnim.setTarget(conversationItem);
itemScaleYAnim.setDuration(duration);
animators.add(itemScaleYAnim);
AnimationCompleteListener hideListener = new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
setVisibility(View.GONE);
}
};
ObjectAnimator itemXAnim = new ObjectAnimator();
itemXAnim.setProperty(View.X);
itemXAnim.setFloatValues(selectedConversationModel.getBubbleX());
itemXAnim.setTarget(conversationItem);
itemXAnim.setDuration(duration);
animators.add(itemXAnim);
List<Animator> hideAllAnimators = new LinkedList<>(hides);
hideAllAnimators.add(overlayHideAnim);
ObjectAnimator itemYAnim = new ObjectAnimator();
itemYAnim.setProperty(View.Y);
itemYAnim.setFloatValues(selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() - statusBarHeight);
itemYAnim.setTarget(conversationItem);
itemYAnim.setDuration(duration);
animators.add(itemYAnim);
hideAnimatorSet.addListener(hideListener);
hideAnimatorSet.setInterpolator(INTERPOLATOR);
hideAnimatorSet.playTogether(hideAllAnimators);
ObjectAnimator toolbarShadeAnim = new ObjectAnimator();
toolbarShadeAnim.setProperty(View.ALPHA);
toolbarShadeAnim.setFloatValues(0f);
toolbarShadeAnim.setTarget(toolbarShade);
toolbarShadeAnim.setDuration(duration);
animators.add(toolbarShadeAnim);
hideAllButMaskAnimatorSet.setInterpolator(INTERPOLATOR);
hideAllButMaskAnimatorSet.playTogether(hides);
ObjectAnimator inputShadeAnim = new ObjectAnimator();
inputShadeAnim.setProperty(View.ALPHA);
inputShadeAnim.setFloatValues(0f);
inputShadeAnim.setTarget(inputShade);
inputShadeAnim.setDuration(duration);
animators.add(inputShadeAnim);
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
ValueAnimator statusBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalStatusBarColor);
statusBarAnim.setDuration(duration);
statusBarAnim.addUpdateListener(animation -> {
WindowUtil.setStatusBarColor(activity.getWindow(), (int) animation.getAnimatedValue());
});
animators.add(statusBarAnim);
ValueAnimator navigationBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalNavigationBarColor);
navigationBarAnim.setDuration(duration);
navigationBarAnim.addUpdateListener(animation -> {
WindowUtil.setNavigationBarColor(activity.getWindow(), (int) animation.getAnimatedValue());
});
animators.add(navigationBarAnim);
}
return animators;
hideMaskAnimatorSet.addListener(hideListener);
hideMaskAnimatorSet.setInterpolator(INTERPOLATOR);
hideMaskAnimatorSet.playTogether(overlayHideAnim);
}
public interface OnHideListener {
void startHide();
void onHide();
}
@@ -920,10 +590,6 @@ public final class ConversationReactionOverlay extends FrameLayout {
void onCustomReactionSelected(@NonNull MessageRecord messageRecord, boolean hasAddedCustomEmoji);
}
public interface OnActionSelectedListener {
void onActionSelected(@NonNull Action action);
}
private static class Boundary {
private float min;
private float max;
@@ -955,15 +621,4 @@ public final class ConversationReactionOverlay extends FrameLayout {
SCRUB,
TAP
}
public enum Action {
REPLY,
FORWARD,
RESEND,
DOWNLOAD,
COPY,
MULTISELECT,
VIEW_INFO,
DELETE,
}
}

View File

@@ -1,10 +1,11 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.ThreadUtil;
import org.thoughtcrime.securesms.search.MessageResult;
@@ -14,7 +15,7 @@ import org.thoughtcrime.securesms.util.Debouncer;
import java.util.Collections;
import java.util.List;
public class ConversationSearchViewModel extends ViewModel {
public class ConversationSearchViewModel extends AndroidViewModel {
private final SearchRepository searchRepository;
private final MutableLiveData<SearchResult> result;
@@ -25,10 +26,11 @@ public class ConversationSearchViewModel extends ViewModel {
private String activeQuery;
private long activeThreadId;
public ConversationSearchViewModel(@NonNull String noteToSelfTitle) {
public ConversationSearchViewModel(@NonNull Application application) {
super(application);
result = new MutableLiveData<>();
debouncer = new Debouncer(500);
searchRepository = new SearchRepository(noteToSelfTitle);
searchRepository = new SearchRepository();
}
LiveData<SearchResult> getSearchResults() {
@@ -126,19 +128,4 @@ public class ConversationSearchViewModel extends ViewModel {
return position;
}
}
public static class Factory extends ViewModelProvider.NewInstanceFactory {
private final String noteToSelfTitle;
public Factory(@NonNull String noteToSelfTitle) {
this.noteToSelfTitle = noteToSelfTitle;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationSearchViewModel(noteToSelfTitle));
}
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
@@ -12,21 +13,23 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.widget.TextViewCompat;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.badges.models.Badge;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
public class ConversationTitleView extends RelativeLayout {
private AvatarImageView avatar;
@@ -86,9 +89,9 @@ public class ConversationTitleView extends RelativeLayout {
Drawable endDrawable = null;
if (recipient != null && recipient.isBlocked()) {
startDrawable = ContextUtil.requireDrawable(getContext(), R.drawable.ic_block_white_18dp);
startDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_block_white_18dp);
} else if (recipient != null && recipient.isMuted()) {
startDrawable = ContextUtil.requireDrawable(getContext(), R.drawable.ic_bell_disabled_16);
startDrawable = Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.ic_bell_disabled_16));
startDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18));
}
@@ -96,19 +99,8 @@ public class ConversationTitleView extends RelativeLayout {
endDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_profile_circle_outline_16);
}
if (startDrawable != null) {
startDrawable = DrawableUtil.tint(startDrawable, ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_80));
}
if (endDrawable != null) {
endDrawable = DrawableUtil.tint(endDrawable, ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_80));
}
if (recipient != null && recipient.isReleaseNotes()) {
endDrawable = ContextUtil.requireDrawable(getContext(), R.drawable.ic_official_24);
}
title.setCompoundDrawablesRelativeWithIntrinsicBounds(startDrawable, null, endDrawable, null);
TextViewCompat.setCompoundDrawableTintList(title, ColorStateList.valueOf(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_80)));
if (recipient != null) {
this.avatar.setAvatar(glideRequests, recipient, false);

View File

@@ -12,7 +12,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cardview.widget.CardView;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
@@ -24,9 +23,6 @@ import com.google.common.collect.Sets;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.views.AutoRounder;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
@@ -51,7 +47,6 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.ACI;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.Collection;
import java.util.Locale;
@@ -70,7 +65,6 @@ public final class ConversationUpdateItem extends FrameLayout
private TextView body;
private MaterialButton actionButton;
private Stub<CardView> donateButtonStub;
private View background;
private ConversationMessage conversationMessage;
private Recipient conversationRecipient;
@@ -98,10 +92,9 @@ public final class ConversationUpdateItem extends FrameLayout
@Override
public void onFinishInflate() {
super.onFinishInflate();
this.body = findViewById(R.id.conversation_update_body);
this.actionButton = findViewById(R.id.conversation_update_action);
this.donateButtonStub = ViewUtil.findStubById(this, R.id.conversation_update_donate_action);
this.background = findViewById(R.id.conversation_update_background);
this.body = findViewById(R.id.conversation_update_body);
this.actionButton = findViewById(R.id.conversation_update_action);
this.background = findViewById(R.id.conversation_update_background);
this.setOnClickListener(new InternalClickListener(null));
}
@@ -227,11 +220,6 @@ public final class ConversationUpdateItem extends FrameLayout
return false;
}
@Override
public boolean shouldProjectContent() {
return false;
}
@Override
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
return EMPTY_PROJECTION_LIST;
@@ -351,12 +339,12 @@ public final class ConversationUpdateItem extends FrameLayout
}
});
} else if (conversationMessage.getMessageRecord().isGroupCall()) {
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
Collection<ServiceId> acis = updateDescription.getMentioned();
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
Collection<ACI> acis = updateDescription.getMentioned();
int text = 0;
if (Util.hasItems(acis)) {
if (acis.contains(Recipient.self().requireServiceId())) {
if (acis.contains(Recipient.self().requireAci())) {
text = R.string.ConversationUpdateItem_return_to_call;
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).getIsCallFull()) {
text = R.string.ConversationUpdateItem_call_is_full;
@@ -432,34 +420,6 @@ public final class ConversationUpdateItem extends FrameLayout
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);
}
if (conversationMessage.getMessageRecord().isBoostRequest()) {
actionButton.setVisibility(GONE);
CardView donateButton = donateButtonStub.get();
TextView buttonText = donateButton.findViewById(R.id.conversation_update_donate_action_button);
boolean isSustainer = SignalStore.donationsValues().isLikelyASustainer();
donateButton.setVisibility(VISIBLE);
donateButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onDonateClicked();
}
});
if (isSustainer) {
buttonText.setText(R.string.ConversationUpdateItem_signal_boost);
buttonText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_boost_outline_16, 0, 0, 0);
} else {
buttonText.setText(R.string.ConversationUpdateItem_become_a_sustainer);
buttonText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
}
AutoRounder.autoSetCorners(donateButton, donateButton::setRadius);
} else if (donateButtonStub.resolved()) {
donateButtonStub.get().setVisibility(GONE);
}
}
private void presentBackground(boolean collapseAbove, boolean collapseBelow, boolean hasWallpaper) {

View File

@@ -286,10 +286,6 @@ public class ConversationViewModel extends ViewModel {
this.hasUnreadMentions.setValue(hasUnreadMentions);
}
boolean getShowScrollButtons() {
return this.showScrollButtons.getValue();
}
void setShowScrollButtons(boolean showScrollButtons) {
this.showScrollButtons.setValue(showScrollButtons);
}

View File

@@ -22,7 +22,6 @@ final class MenuState {
private final boolean resend;
private final boolean copy;
private final boolean delete;
private final boolean reactions;
private MenuState(@NonNull Builder builder) {
forward = builder.forward;
@@ -32,7 +31,6 @@ final class MenuState {
resend = builder.resend;
copy = builder.copy;
delete = builder.delete;
reactions = builder.reactions;
}
boolean shouldShowForwardAction() {
@@ -63,10 +61,6 @@ final class MenuState {
return delete;
}
boolean shouldShowReactions() {
return reactions;
}
static MenuState getMenuState(@NonNull Recipient conversationRecipient,
@NonNull Set<MultiselectPart> selectedParts,
boolean shouldShowMessageRequest,
@@ -148,13 +142,12 @@ final class MenuState {
((MediaMmsMessageRecord)messageRecord).containsMediaSlide() &&
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() == null)
.shouldShowForwardAction(shouldShowForwardAction)
.shouldShowDetailsAction(!actionMessage && !conversationRecipient.isReleaseNotes())
.shouldShowDetailsAction(!actionMessage)
.shouldShowReplyAction(canReplyToMessage(conversationRecipient, actionMessage, messageRecord, shouldShowMessageRequest, isNonAdminInAnnouncementGroup));
}
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText)
.shouldShowDeleteAction(!hasInMemory && onlyContainsCompleteMessages(selectedParts))
.shouldShowReactions(!conversationRecipient.isReleaseNotes())
.build();
}
@@ -179,8 +172,7 @@ final class MenuState {
!isDisplayingMessageRequest &&
messageRecord.isSecure() &&
(!conversationRecipient.isGroup() || conversationRecipient.isActiveGroup()) &&
!messageRecord.getRecipient().isBlocked() &&
!conversationRecipient.isReleaseNotes();
!messageRecord.getRecipient().isBlocked();
}
static boolean isActionMessage(@NonNull MessageRecord messageRecord) {
@@ -196,8 +188,7 @@ final class MenuState {
messageRecord.isGroupV1MigrationEvent() ||
messageRecord.isChatSessionRefresh() ||
messageRecord.isInMemoryMessageRecord() ||
messageRecord.isChangeNumber() ||
messageRecord.isBoostRequest();
messageRecord.isChangeNumber();
}
private final static class Builder {
@@ -209,7 +200,6 @@ final class MenuState {
private boolean resend;
private boolean copy;
private boolean delete;
private boolean reactions;
@NonNull Builder shouldShowForwardAction(boolean forward) {
this.forward = forward;
@@ -246,11 +236,6 @@ final class MenuState {
return this;
}
@NonNull Builder shouldShowReactions(boolean reactions) {
this.reactions = reactions;
return this;
}
@NonNull
MenuState build() {
return new MenuState(this);

View File

@@ -1,53 +0,0 @@
package org.thoughtcrime.securesms.conversation
import android.graphics.Typeface
import android.text.SpannableString
import android.text.Spanned
import android.text.style.StyleSpan
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.util.PlaceholderURLSpan
/**
* Helper for applying style-based [BodyRangeList.BodyRange]s to text.
*/
object MessageStyler {
@JvmStatic
fun style(messageRanges: BodyRangeList, span: SpannableString): Result {
var hasLinks = false
var bottomButton: BodyRangeList.BodyRange.Button? = null
for (range in messageRanges.rangesList) {
if (range.hasStyle()) {
val style = range.style?.let {
when (it) {
BodyRangeList.BodyRange.Style.BOLD -> Typeface.BOLD
BodyRangeList.BodyRange.Style.ITALIC -> Typeface.ITALIC
BodyRangeList.BodyRange.Style.UNRECOGNIZED -> Typeface.NORMAL
}
}
if (style != null && style != Typeface.NORMAL) {
span.setSpan(StyleSpan(style), range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
} else if (range.hasLink() && range.link != null) {
span.setSpan(PlaceholderURLSpan(range.link), range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
hasLinks = true
} else if (range.hasButton() && range.button != null) {
bottomButton = range.button
}
}
return Result(hasLinks, bottomButton)
}
data class Result(val hasStyleLinks: Boolean = false, val bottomButton: BodyRangeList.BodyRange.Button? = null) {
companion object {
@JvmStatic
val NO_STYLE = Result()
@JvmStatic
fun none(): Result = NO_STYLE
}
}
}

View File

@@ -1,21 +0,0 @@
package org.thoughtcrime.securesms.conversation
import android.graphics.Bitmap
import android.net.Uri
import android.view.View
/**
* Contains information on a single selected conversation item. This is used when transitioning
* between selected and unselected states.
*/
data class SelectedConversationModel(
val bitmap: Bitmap,
val itemX: Float,
val itemY: Float,
val bubbleX: Float,
val bubbleY: Float,
val bubbleWidth: Int,
val audioUri: Uri? = null,
val isOutgoing: Boolean,
val focusedView: View?,
)

View File

@@ -79,7 +79,6 @@ class RecyclerViewColorizer(private val recyclerView: RecyclerView) {
}
private val colorPaint = Paint()
private val outOfBoundsPaint = Paint()
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
outRect.setEmpty()
@@ -132,17 +131,6 @@ class RecyclerViewColorizer(private val recyclerView: RecyclerView) {
parent.height.toFloat(),
colorPaint
)
val color = chatColors.asSingleColor()
outOfBoundsPaint.color = color
canvas.drawRect(
0f, -parent.height.toFloat(), parent.width.toFloat(), 0f, outOfBoundsPaint
)
canvas.drawRect(
0f, parent.height.toFloat(), parent.width.toFloat(), parent.height * 2f, outOfBoundsPaint
)
}
}
}

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.conversation.mutiselect
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
@@ -13,7 +12,6 @@ import android.graphics.Region
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.core.view.forEach
@@ -56,7 +54,6 @@ class MultiselectItemDecoration(
private val selectedParts: MutableSet<MultiselectPart> = mutableSetOf()
private var enterExitAnimation: ValueAnimator? = null
private var hideShadeAnimation: ValueAnimator? = null
private val multiselectPartAnimatorMap: MutableMap<MultiselectPart, ValueAnimator> = mutableMapOf()
private var checkedBitmap: Bitmap? = null
@@ -80,10 +77,7 @@ class MultiselectItemDecoration(
checkedBitmap = null
}
private val darkShadeColor = ContextCompat.getColor(context, R.color.reactions_screen_dark_shade_color)
private val lightShadeColor = ContextCompat.getColor(context, R.color.reactions_screen_light_shade_color)
private val argbEvaluator = ArgbEvaluator()
private val shadeColor = ContextCompat.getColor(context, R.color.reactions_screen_shade_color)
private val unselectedPaint = Paint().apply {
isAntiAlias = true
@@ -368,7 +362,7 @@ class MultiselectItemDecoration(
}
}
if (child.canPlayContent() && child.shouldProjectContent()) {
if (child.canPlayContent()) {
val mp4GifProjection = child.getGiphyMp4PlayableProjection(child.rootView as ViewGroup)
path.op(mp4GifProjection.path, Path.Op.DIFFERENCE)
mp4GifProjection.release()
@@ -377,7 +371,7 @@ class MultiselectItemDecoration(
}
canvas.clipPath(path)
canvas.drawShade()
canvas.drawColor(shadeColor)
canvas.restore()
}
}
@@ -395,39 +389,11 @@ class MultiselectItemDecoration(
}
canvas.clipPath(path, Region.Op.DIFFERENCE)
canvas.drawShade()
canvas.drawColor(shadeColor)
canvas.restore()
}
}
private fun Canvas.drawShade() {
val progress = hideShadeAnimation?.animatedValue as? Float
if (progress == null) {
drawColor(lightShadeColor)
drawColor(darkShadeColor)
return
}
drawColor(argbEvaluator.evaluate(progress, lightShadeColor, Color.TRANSPARENT) as Int)
drawColor(argbEvaluator.evaluate(progress, darkShadeColor, Color.TRANSPARENT) as Int)
}
fun hideShade(list: RecyclerView) {
hideShadeAnimation = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 150L
addUpdateListener {
invalidateIfAnimatorsAreRunning(list)
}
doOnEnd {
hideShadeAnimation = null
}
start()
}
}
private fun isInitialAnimation(): Boolean {
return (enterExitAnimation?.animatedFraction ?: 0f) < 1f
}
@@ -475,10 +441,7 @@ class MultiselectItemDecoration(
}
private fun invalidateIfAnimatorsAreRunning(parent: RecyclerView) {
if (enterExitAnimation?.isRunning == true ||
multiselectPartAnimatorMap.values.any { it.isRunning } ||
hideShadeAnimation?.isRunning == true
) {
if (enterExitAnimation?.isRunning == true || multiselectPartAnimatorMap.values.any { it.isRunning }) {
parent.invalidate()
}
}

View File

@@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialog
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.keyboard.findListener
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sharing.MultiShareArgs
@@ -40,7 +41,6 @@ import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.fragments.findListener
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
import org.thoughtcrime.securesms.util.visible
import org.whispersystems.libsignal.util.guava.Optional

View File

@@ -30,7 +30,7 @@ class MultiselectForwardRepository(context: Context) {
fun checkForBadIdentityRecords(shareContacts: List<ShareContact>, consumer: Consumer<List<IdentityRecord>>) {
SignalExecutors.BOUNDED.execute {
val recipients: List<Recipient> = shareContacts.map { Recipient.resolved(it.recipientId.get()) }
val identityRecordList: IdentityRecordList = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients)
val identityRecordList: IdentityRecordList = ApplicationDependencies.getIdentityStore().getIdentityRecords(recipients)
consumer.accept(identityRecordList.untrustedRecords)
}

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.ui.error;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -78,9 +77,9 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG);
}
public static void show(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull MessageRecord messageRecord) {
public static void show(@NonNull FragmentActivity fragmentActivity, @NonNull MessageRecord messageRecord) {
List<String> ids = Stream.of(messageRecord.getIdentityKeyMismatches())
.map(mismatch -> mismatch.getRecipientId(context).serialize())
.map(mismatch -> mismatch.getRecipientId(fragmentActivity).serialize())
.distinct()
.toList();
@@ -91,7 +90,7 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
arguments.putInt(CONTINUE_TEXT_RESOURCE_EXTRA, R.string.safety_number_change_dialog__send_anyway);
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
fragment.setArguments(arguments);
fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG);
fragment.show(fragmentActivity.getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
}
public static void showForCall(@NonNull FragmentManager fragmentManager, @NonNull RecipientId recipientId) {

View File

@@ -12,8 +12,9 @@ import com.annimon.stream.Stream;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.SessionUtil;
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.IdentityDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
@@ -67,7 +68,7 @@ final class SafetyNumberChangeRepository {
List<Recipient> recipients = Stream.of(recipientIds).map(Recipient::resolved).toList();
List<ChangedRecipient> changedRecipients = Stream.of(ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients).getIdentityRecords())
List<ChangedRecipient> changedRecipients = Stream.of(ApplicationDependencies.getIdentityStore().getIdentityRecords(recipients).getIdentityRecords())
.map(record -> new ChangedRecipient(Recipient.resolved(record.getRecipientId()), record))
.toList();
@@ -95,7 +96,7 @@ final class SafetyNumberChangeRepository {
@WorkerThread
private TrustAndVerifyResult trustOrVerifyChangedRecipientsInternal(@NonNull List<ChangedRecipient> changedRecipients) {
SignalIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities();
TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getIdentityStore();
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (ChangedRecipient changedRecipient : changedRecipients) {
@@ -103,9 +104,9 @@ final class SafetyNumberChangeRepository {
if (changedRecipient.isUnverified()) {
Log.d(TAG, "Setting " + identityRecord.getRecipientId() + " as verified");
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
ApplicationDependencies.getIdentityStore().setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
} else {
Log.d(TAG, "Setting " + identityRecord.getRecipientId() + " as approved");
identityStore.setApproval(identityRecord.getRecipientId(), true);
@@ -118,24 +119,23 @@ final class SafetyNumberChangeRepository {
@WorkerThread
private TrustAndVerifyResult trustOrVerifyChangedRecipientsAndResendInternal(@NonNull List<ChangedRecipient> changedRecipients,
@NonNull MessageRecord messageRecord)
{
@NonNull MessageRecord messageRecord) {
if (changedRecipients.isEmpty()) {
Log.d(TAG, "No changed recipients to process, will still process message record");
}
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (ChangedRecipient changedRecipient : changedRecipients) {
SignalProtocolAddress mismatchAddress = changedRecipient.getRecipient().requireServiceId().toProtocolAddress(SignalServiceAddress.DEFAULT_DEVICE_ID);
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(changedRecipient.getRecipient().requireServiceId(), SignalServiceAddress.DEFAULT_DEVICE_ID);
Log.d(TAG, "Saving identity for: " + changedRecipient.getRecipient().getId() + " " + changedRecipient.getIdentityRecord().getIdentityKey().hashCode());
SignalIdentityKeyStore.SaveResult result = ApplicationDependencies.getProtocolStore().aci().identities().saveIdentity(mismatchAddress, changedRecipient.getIdentityRecord().getIdentityKey(), true);
TextSecureIdentityKeyStore.SaveResult result = ApplicationDependencies.getIdentityStore().saveIdentity(mismatchAddress, changedRecipient.getIdentityRecord().getIdentityKey(), true);
Log.d(TAG, "Saving identity result: " + result);
if (result == SignalIdentityKeyStore.SaveResult.NO_CHANGE) {
if (result == TextSecureIdentityKeyStore.SaveResult.NO_CHANGE) {
Log.i(TAG, "Archiving sessions explicitly as they appear to be out of sync.");
ApplicationDependencies.getProtocolStore().aci().sessions().archiveSession(changedRecipient.getRecipient().getId(), SignalServiceAddress.DEFAULT_DEVICE_ID);
ApplicationDependencies.getProtocolStore().aci().sessions().archiveSiblingSessions(mismatchAddress);
SessionUtil.archiveSession(changedRecipient.getRecipient().getId(), SignalServiceAddress.DEFAULT_DEVICE_ID);
SessionUtil.archiveSiblingSessions(mismatchAddress);
SignalDatabase.senderKeyShared().deleteAllFor(changedRecipient.getRecipient().getId());
}
}

View File

@@ -86,7 +86,6 @@ import org.thoughtcrime.securesms.NewConversationActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.badges.models.Badge;
import org.thoughtcrime.securesms.badges.self.expired.CantProcessSubscriptionPaymentBottomSheetDialogFragment;
import org.thoughtcrime.securesms.badges.self.expired.ExpiredBadgeBottomSheetDialogFragment;
import org.thoughtcrime.securesms.components.RatingManager;
import org.thoughtcrime.securesms.components.SearchToolbar;
@@ -106,7 +105,6 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment;
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView;
import org.thoughtcrime.securesms.conversation.ConversationFragment;
@@ -302,7 +300,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class)));
cameraFab.setOnClickListener(v -> {
Permissions.with(this)
Permissions.with(requireActivity())
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_camera_24)
@@ -361,20 +359,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
RecaptchaProofBottomSheetFragment.show(getChildFragmentManager());
}
Badge expiredBadge = SignalStore.donationsValues().getExpiredBadge();
String subscriptionCancellationReason = SignalStore.donationsValues().getUnexpectedSubscriptionCancelationReason();
UnexpectedSubscriptionCancellation unexpectedSubscriptionCancellation = UnexpectedSubscriptionCancellation.fromStatus(subscriptionCancellationReason);
Badge expiredBadge = SignalStore.donationsValues().getExpiredBadge();
if (expiredBadge != null) {
SignalStore.donationsValues().setExpiredBadge(null);
if (expiredBadge.isBoost() || !SignalStore.donationsValues().isUserManuallyCancelled()) {
Log.w(TAG, "Displaying bottom sheet for an expired badge", true);
ExpiredBadgeBottomSheetDialogFragment.show(expiredBadge, unexpectedSubscriptionCancellation, getParentFragmentManager());
ExpiredBadgeBottomSheetDialogFragment.show(expiredBadge, getParentFragmentManager());
}
} else if (unexpectedSubscriptionCancellation != null && !SignalStore.donationsValues().isUserManuallyCancelled() && SignalStore.donationsValues().getShowCantProcessDialog()) {
Log.w(TAG, "Displaying bottom sheet for unexpected cancellation: " + unexpectedSubscriptionCancellation, true);
new CantProcessSubscriptionPaymentBottomSheetDialogFragment().show(getChildFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
}
@@ -446,11 +437,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
return false;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (resultCode != RESULT_OK) {
@@ -687,10 +673,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}
private void initializeViewModel() {
ConversationListViewModel.Factory viewModelFactory = new ConversationListViewModel.Factory(isArchived(),
getString(R.string.note_to_self));
viewModel = new ViewModelProvider(this, viewModelFactory).get(ConversationListViewModel.class);
viewModel = new ViewModelProvider(this, new ConversationListViewModel.Factory(isArchived())).get(ConversationListViewModel.class);
viewModel.getSearchResult().observe(getViewLifecycleOwner(), this::onSearchResultChanged);
viewModel.getMegaphone().observe(getViewLifecycleOwner(), this::onMegaphoneChanged);

View File

@@ -525,7 +525,7 @@ public final class ConversationListItem extends ConstraintLayout
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_could_not_be_processed), defaultTint);
} else if (SmsDatabase.Types.isProfileChange(thread.getType())) {
return emphasisAdded(context, "", defaultTint);
} else if (SmsDatabase.Types.isChangeNumber(thread.getType()) || SmsDatabase.Types.isBoostRequest(thread.getType())) {
} else if (SmsDatabase.Types.isChangeNumber(thread.getType())) {
return emphasisAdded(context, "", defaultTint);
} else if (MmsSmsColumns.Types.isBadDecryptType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_delivery_issue), defaultTint);

View File

@@ -290,17 +290,15 @@ class ConversationListViewModel extends ViewModel {
public static class Factory extends ViewModelProvider.NewInstanceFactory {
private final boolean isArchived;
private final String noteToSelfTitle;
public Factory(boolean isArchived, @NonNull String noteToSelfTitle) {
this.isArchived = isArchived;
this.noteToSelfTitle = noteToSelfTitle;
public Factory(boolean isArchived) {
this.isArchived = isArchived;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository(noteToSelfTitle), isArchived));
return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository(), isArchived));
}
}
}

View File

@@ -17,11 +17,27 @@
*/
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.backup.BackupProtos;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECKeyPair;
import org.whispersystems.libsignal.ecc.ECPrivateKey;
import org.whispersystems.libsignal.util.guava.Preconditions;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
/**
* Utility class for working with identity keys.
@@ -31,6 +47,63 @@ import org.whispersystems.libsignal.ecc.ECPrivateKey;
public class IdentityKeyUtil {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(IdentityKeyUtil.class);
private static final String IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF = "pref_identity_public_curve25519";
private static final String IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF = "pref_identity_private_curve25519";
private static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
private static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
public static boolean hasIdentityKey(Context context) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
return
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
preferences.contains(IDENTITY_PRIVATE_KEY_PREF);
}
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_PREF));
return new IdentityKey(publicKeyBytes, 0);
} catch (IOException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public static @NonNull IdentityKeyPair getIdentityKeyPair(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
IdentityKey publicKey = getIdentityKey(context);
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF)));
return new IdentityKeyPair(publicKey, privateKey);
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static void generateIdentityKeys(Context context) {
IdentityKeyPair identityKeyPair = generateIdentityKeyPair();
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPublicKey().serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPrivateKey().serialize()));
}
/**
* Only call when configuring as a secondary linked device.
*/
public static void setIdentityKeys(Context context, IdentityKeyPair identityKeyPair) {
Preconditions.checkState(SignalStore.account().isLinkedDevice(), "Identity keys can only be set directly by a linked device");
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPublicKey().serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(identityKeyPair.getPrivateKey().serialize()));
}
public static IdentityKeyPair generateIdentityKeyPair() {
ECKeyPair djbKeyPair = Curve.generateKeyPair();
IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
@@ -38,4 +111,78 @@ public class IdentityKeyUtil {
return new IdentityKeyPair(djbIdentityKey, djbPrivateKey);
}
public static void migrateIdentityKeys(@NonNull Context context,
@NonNull MasterSecret masterSecret)
{
if (!hasIdentityKey(context)) {
if (hasLegacyIdentityKeys(context)) {
IdentityKeyPair legacyPair = getLegacyIdentityKeyPair(context, masterSecret);
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(legacyPair.getPublicKey().serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(legacyPair.getPrivateKey().serialize()));
delete(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF);
delete(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF);
} else {
generateIdentityKeys(context);
}
}
}
public static List<BackupProtos.SharedPreference> getBackupRecord(@NonNull Context context) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
return new LinkedList<BackupProtos.SharedPreference>() {{
add(BackupProtos.SharedPreference.newBuilder()
.setFile(MasterSecretUtil.PREFERENCES_NAME)
.setKey(IDENTITY_PUBLIC_KEY_PREF)
.setValue(preferences.getString(IDENTITY_PUBLIC_KEY_PREF, null))
.build());
add(BackupProtos.SharedPreference.newBuilder()
.setFile(MasterSecretUtil.PREFERENCES_NAME)
.setKey(IDENTITY_PRIVATE_KEY_PREF)
.setValue(preferences.getString(IDENTITY_PRIVATE_KEY_PREF, null))
.build());
}};
}
private static boolean hasLegacyIdentityKeys(Context context) {
return
retrieve(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF) != null &&
retrieve(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF) != null;
}
private static IdentityKeyPair getLegacyIdentityKeyPair(@NonNull Context context,
@NonNull MasterSecret masterSecret)
{
try {
MasterCipher masterCipher = new MasterCipher(masterSecret);
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF));
IdentityKey identityKey = new IdentityKey(publicKeyBytes, 0);
ECPrivateKey privateKey = masterCipher.decryptKey(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF)));
return new IdentityKeyPair(identityKey, privateKey);
} catch (IOException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private static String retrieve(Context context, String key) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
return preferences.getString(key, null);
}
private static void save(Context context, String key, String value) {
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
Editor preferencesEditor = preferences.edit();
preferencesEditor.putString(key, value);
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
}
private static void delete(Context context, String key) {
context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0).edit().remove(key).commit();
}
}

View File

@@ -17,66 +17,64 @@
package org.thoughtcrime.securesms.crypto;
import androidx.annotation.NonNull;
import android.content.Context;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.libsignal.util.Medium;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class PreKeyUtil {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(PreKeyUtil.class);
private static final int BATCH_SIZE = 100;
private static final long ARCHIVE_AGE = TimeUnit.DAYS.toMillis(30);
public synchronized static @NonNull List<PreKeyRecord> generateAndStoreOneTimePreKeys(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) {
Log.i(TAG, "Generating one-time prekeys...");
private static final int BATCH_SIZE = 100;
public synchronized static List<PreKeyRecord> generatePreKeys(Context context) {
PreKeyStore preKeyStore = ApplicationDependencies.getPreKeyStore();
List<PreKeyRecord> records = new LinkedList<>();
int preKeyIdOffset = metadataStore.getNextOneTimePreKeyId();
int preKeyIdOffset = TextSecurePreferences.getNextPreKeyId(context);
for (int i = 0; i < BATCH_SIZE; i++) {
for (int i=0;i<BATCH_SIZE;i++) {
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
ECKeyPair keyPair = Curve.generateKeyPair();
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
protocolStore.storePreKey(preKeyId, record);
preKeyStore.storePreKey(preKeyId, record);
records.add(record);
}
metadataStore.setNextOneTimePreKeyId((preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE);
TextSecurePreferences.setNextPreKeyId(context, (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE);
return records;
}
public synchronized static @NonNull SignedPreKeyRecord generateAndStoreSignedPreKey(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore, boolean setAsActive) {
Log.i(TAG, "Generating signed prekeys...");
public synchronized static SignedPreKeyRecord generateSignedPreKey(Context context, IdentityKeyPair identityKeyPair, boolean active) {
try {
int signedPreKeyId = metadataStore.getNextSignedPreKeyId();
ECKeyPair keyPair = Curve.generateKeyPair();
byte[] signature = Curve.calculateSignature(protocolStore.getIdentityKeyPair().getPrivateKey(), keyPair.getPublicKey().serialize());
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
SignedPreKeyStore signedPreKeyStore = ApplicationDependencies.getPreKeyStore();
int signedPreKeyId = TextSecurePreferences.getNextSignedPreKeyId(context);
ECKeyPair keyPair = Curve.generateKeyPair();
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
protocolStore.storeSignedPreKey(signedPreKeyId, record);
metadataStore.setNextSignedPreKeyId((signedPreKeyId + 1) % Medium.MAX_VALUE);
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
TextSecurePreferences.setNextSignedPreKeyId(context, (signedPreKeyId + 1) % Medium.MAX_VALUE);
if (setAsActive) {
metadataStore.setActiveSignedPreKeyId(signedPreKeyId);
if (active) {
TextSecurePreferences.setActiveSignedPreKeyId(context, signedPreKeyId);
}
return record;
@@ -85,33 +83,12 @@ public class PreKeyUtil {
}
}
/**
* Finds all of the signed prekeys that are older than the archive age, and archive all but the youngest of those.
*/
public synchronized static void cleanSignedPreKeys(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore) {
Log.i(TAG, "Cleaning signed prekeys...");
int activeSignedPreKeyId = metadataStore.getActiveSignedPreKeyId();
if (activeSignedPreKeyId < 0) {
return;
}
try {
long now = System.currentTimeMillis();
SignedPreKeyRecord currentRecord = protocolStore.loadSignedPreKey(activeSignedPreKeyId);
List<SignedPreKeyRecord> allRecords = protocolStore.loadSignedPreKeys();
allRecords.stream()
.filter(r -> r.getId() != currentRecord.getId())
.filter(r -> (now - r.getTimestamp()) > ARCHIVE_AGE)
.sorted(Comparator.comparingLong(SignedPreKeyRecord::getTimestamp).reversed())
.skip(1)
.forEach(record -> {
Log.i(TAG, "Removing signed prekey record: " + record.getId() + " with timestamp: " + record.getTimestamp());
protocolStore.removeSignedPreKey(record.getId());
});
} catch (InvalidKeyIdException e) {
Log.w(TAG, e);
}
public static synchronized void setActiveSignedPreKeyId(Context context, int id) {
TextSecurePreferences.setActiveSignedPreKeyId(context, id);
}
public static synchronized int getActiveSignedPreKeyId(Context context) {
return TextSecurePreferences.getActiveSignedPreKeyId(context);
}
}

View File

@@ -22,6 +22,18 @@ public final class ProfileKeyUtil {
private ProfileKeyUtil() {
}
/** @deprecated Use strongly typed {@link org.signal.zkgroup.profiles.ProfileKey}
* from {@link #getSelfProfileKey()}
* or {@code getSelfProfileKey().serialize()} if you need the bytes. */
@Deprecated
public static @NonNull byte[] getProfileKey(@NonNull Context context) {
byte[] profileKey = Recipient.self().getProfileKey();
if (profileKey == null) {
throw new AssertionError();
}
return profileKey;
}
public static synchronized @NonNull ProfileKey getSelfProfileKey() {
try {
return new ProfileKey(Recipient.self().getProfileKey());

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