mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-13 05:23:18 +01:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e15bcc5f0 | ||
|
|
b77abf6045 |
7
.idea/codeStyles/Project.xml
generated
7
.idea/codeStyles/Project.xml
generated
@@ -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" />
|
||||
|
||||
8
.idea/file.template.settings.xml
generated
8
.idea/file.template.settings.xml
generated
@@ -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>
|
||||
20
.idea/fileTemplates/ViewModel.kt
generated
20
.idea/fileTemplates/ViewModel.kt
generated
@@ -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()
|
||||
}
|
||||
}
|
||||
125
app/build.gradle
125
app/build.gradle
@@ -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")))
|
||||
|
||||
@@ -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 can’t 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 can’t merge. If you’re 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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package="org.thoughtcrime.securesms">
|
||||
|
||||
<application
|
||||
android:name=".SpinnerApplicationContext"
|
||||
android:name=".FlipperApplicationContext"
|
||||
tools:replace="android:name">
|
||||
|
||||
<activity
|
||||
@@ -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 +
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>?
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -35,7 +35,7 @@ class WebRtcCallRepository {
|
||||
recipients = Collections.singletonList(recipient);
|
||||
}
|
||||
|
||||
consumer.accept(ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients));
|
||||
consumer.accept(ApplicationDependencies.getIdentityStore().getIdentityRecords(recipients));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ public final class WebRtcControls {
|
||||
return isOngoing();
|
||||
}
|
||||
|
||||
boolean displayAnswerWithoutVideo() {
|
||||
boolean displayAnswerWithAudio() {
|
||||
return isIncoming() && isRemoteVideoEnabled;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?,
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user