Compare commits
250 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d8501cd64 | ||
|
|
88827e94f5 | ||
|
|
99d2a0c0b6 | ||
|
|
1d0582867b | ||
|
|
0ea6d9205d | ||
|
|
f438ef543b | ||
|
|
61cd9767c8 | ||
|
|
5fbf0a98b9 | ||
|
|
1671518ded | ||
|
|
f628ffca06 | ||
|
|
1d6f4fd4e7 | ||
|
|
9eedc0a36b | ||
|
|
a870fe9e1a | ||
|
|
d3f779cea9 | ||
|
|
fb4f41b996 | ||
|
|
11aac76fb6 | ||
|
|
98424f6cbb | ||
|
|
1cf7c59af9 | ||
|
|
13470fb0c3 | ||
|
|
3aa0fd1937 | ||
|
|
9e6f2336d1 | ||
|
|
8b8d62f598 | ||
|
|
4572ae5886 | ||
|
|
0802d4beb4 | ||
|
|
9361aa700a | ||
|
|
be5cad1cec | ||
|
|
fe20de2995 | ||
|
|
8714e4298e | ||
|
|
1baebe7475 | ||
|
|
6686ae43f3 | ||
|
|
cc2c0e9561 | ||
|
|
34d252a4bd | ||
|
|
025411c9fb | ||
|
|
4edb66d2b9 | ||
|
|
a17033dff4 | ||
|
|
04a5e56da7 | ||
|
|
0e6a3dd408 | ||
|
|
47f48a6a8c | ||
|
|
2ef7fabade | ||
|
|
5c2b475c01 | ||
|
|
559f4bc0d3 | ||
|
|
1357a4816b | ||
|
|
d1b8a56c0f | ||
|
|
7ea38298ea | ||
|
|
c08f1355db | ||
|
|
b6589637fa | ||
|
|
2ee2d2883a | ||
|
|
029c8ba917 | ||
|
|
60e1ee21ed | ||
|
|
a96e9158c4 | ||
|
|
e47765d7d5 | ||
|
|
e807435c8b | ||
|
|
14b41a93e2 | ||
|
|
f51fb9da29 | ||
|
|
df3ca3d3cc | ||
|
|
7786956b11 | ||
|
|
c17d62aeab | ||
|
|
65255121de | ||
|
|
b042945fef | ||
|
|
9d979217fa | ||
|
|
c177de2ec3 | ||
|
|
b4fd57d900 | ||
|
|
92339dfdcf | ||
|
|
8ae115028e | ||
|
|
2dd0221680 | ||
|
|
1a1923c6c0 | ||
|
|
5801ad4bdb | ||
|
|
388f2971e9 | ||
|
|
14c3a36ec0 | ||
|
|
1ad338ce31 | ||
|
|
71981e8a27 | ||
|
|
5cb10cd054 | ||
|
|
ecf576e9b9 | ||
|
|
09d17659b9 | ||
|
|
53673be5cb | ||
|
|
ed4a1d6ddd | ||
|
|
e3044b8b85 | ||
|
|
4ce05a064c | ||
|
|
2fbcc23451 | ||
|
|
3bdffed8c9 | ||
|
|
6cc8e87d46 | ||
|
|
7d8f549d97 | ||
|
|
fb2ef265bd | ||
|
|
96e8256781 | ||
|
|
dd40517f12 | ||
|
|
6c95b766d6 | ||
|
|
feffdcb71e | ||
|
|
f33e2c49ca | ||
|
|
1037acd4a2 | ||
|
|
68042fc755 | ||
|
|
1bdc77affe | ||
|
|
88f50da4fb | ||
|
|
bc1fbd9b6c | ||
|
|
c1b7b7c95e | ||
|
|
b0688eed5c | ||
|
|
e06ed03d33 | ||
|
|
e2a79394ab | ||
|
|
5db770ca44 | ||
|
|
df8aaa2005 | ||
|
|
882748f080 | ||
|
|
15035f4eb3 | ||
|
|
1d0a87f52a | ||
|
|
59b2cc5f79 | ||
|
|
d6758fc264 | ||
|
|
36418bec59 | ||
|
|
07bd8b2fa3 | ||
|
|
d989d02af9 | ||
|
|
503ce13122 | ||
|
|
8f77321adb | ||
|
|
60cdcea791 | ||
|
|
86cd4c5c30 | ||
|
|
62d5f61a0b | ||
|
|
25860867bb | ||
|
|
272860f071 | ||
|
|
767cfbc717 | ||
|
|
55af6ca84e | ||
|
|
88933ae051 | ||
|
|
4781beebee | ||
|
|
0049c74323 | ||
|
|
361727cec6 | ||
|
|
1b95177e0e | ||
|
|
8d34c54de2 | ||
|
|
2fa0eba3db | ||
|
|
6cc41e95c6 | ||
|
|
b1523f5b91 | ||
|
|
d16002546d | ||
|
|
186a93f5d1 | ||
|
|
3d4875bcfe | ||
|
|
441e30971a | ||
|
|
ff115c2349 | ||
|
|
f23e5bdb44 | ||
|
|
d0a232d86a | ||
|
|
9fef8386e6 | ||
|
|
b1680ba5c6 | ||
|
|
6cd59daf0a | ||
|
|
38f2b39ac4 | ||
|
|
51222738df | ||
|
|
b9835584d8 | ||
|
|
cff01021c2 | ||
|
|
d19b8a125c | ||
|
|
4caaa0033b | ||
|
|
f3a0a059ea | ||
|
|
290b0fe46f | ||
|
|
03a212eee4 | ||
|
|
f3b629bc06 | ||
|
|
b9e002f7b1 | ||
|
|
c90779beea | ||
|
|
bc8c8a049f | ||
|
|
1d9dc66265 | ||
|
|
886c149c3f | ||
|
|
369ca189d3 | ||
|
|
02c4bbe816 | ||
|
|
523c9f6576 | ||
|
|
c259430b09 | ||
|
|
fc94b90a03 | ||
|
|
6af521130d | ||
|
|
9c001e4f35 | ||
|
|
9e8dee36a6 | ||
|
|
26b17d8a3c | ||
|
|
1c5e2e3359 | ||
|
|
0437d37f23 | ||
|
|
ec6b1a44de | ||
|
|
08c661bb14 | ||
|
|
62f62d89c5 | ||
|
|
37dd8b40b2 | ||
|
|
15825f6c3f | ||
|
|
ad196bf03c | ||
|
|
332c4ca26e | ||
|
|
305edf1928 | ||
|
|
9c0c25ef99 | ||
|
|
458dae227f | ||
|
|
b1ba9fd54f | ||
|
|
dd42b5b851 | ||
|
|
d8e3edc729 | ||
|
|
36aa8623da | ||
|
|
7e24252447 | ||
|
|
164ce06177 | ||
|
|
c9d298c447 | ||
|
|
5129613ce8 | ||
|
|
b985ace7ed | ||
|
|
d28afac973 | ||
|
|
fd9b5ff7c4 | ||
|
|
75580bea27 | ||
|
|
db7056c53b | ||
|
|
b55181ffe6 | ||
|
|
81149e5aa8 | ||
|
|
3a341eee19 | ||
|
|
e19c7efbfe | ||
|
|
7e7c68321b | ||
|
|
9fa3f54c7c | ||
|
|
3ff273f1f2 | ||
|
|
e607b1962c | ||
|
|
2c4c6bf87c | ||
|
|
bf048e2a75 | ||
|
|
b0c4bb04e7 | ||
|
|
7e0e6c2786 | ||
|
|
d6a03df087 | ||
|
|
cfc89d2a74 | ||
|
|
c491c9dc8c | ||
|
|
eae066b3a2 | ||
|
|
71aa17bad6 | ||
|
|
93df01e266 | ||
|
|
8f96abb41e | ||
|
|
1457a6fe16 | ||
|
|
290c107698 | ||
|
|
bf7aaddbf9 | ||
|
|
59435e49c8 | ||
|
|
c3499e538e | ||
|
|
1d41b1c5a3 | ||
|
|
e303570b2f | ||
|
|
62940893f0 | ||
|
|
f8434bede5 | ||
|
|
c08e108fc3 | ||
|
|
cd9a160cae | ||
|
|
bba8b8be56 | ||
|
|
f56b5d58c6 | ||
|
|
ac4b0ed606 | ||
|
|
d3e71185e6 | ||
|
|
b4f2cd9ff4 | ||
|
|
fd8d305899 | ||
|
|
bde7ae944a | ||
|
|
99f83e5dc9 | ||
|
|
693aef5c04 | ||
|
|
b9ae537706 | ||
|
|
e41dd6d39d | ||
|
|
5d546f46e4 | ||
|
|
2bef5653b4 | ||
|
|
63d8549865 | ||
|
|
f6bac2f476 | ||
|
|
0dd51856d3 | ||
|
|
be01f2b511 | ||
|
|
045d2cf42f | ||
|
|
64ddd982fe | ||
|
|
b785b3f887 | ||
|
|
399421e20e | ||
|
|
a656d65d1d | ||
|
|
291a5d57c4 | ||
|
|
a9a91e3162 | ||
|
|
de4c6ab7b7 | ||
|
|
7ea9fc0c3b | ||
|
|
1965d5879f | ||
|
|
b2b907a86a | ||
|
|
e565de0724 | ||
|
|
7318e676f7 | ||
|
|
7c28d8ad51 | ||
|
|
3e21fb77c7 | ||
|
|
6b91e525db | ||
|
|
0aca03a919 | ||
|
|
e2ef8e2ef9 | ||
|
|
15c248184f |
@@ -1,5 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*.kt]
|
||||
[*.{kt,kts}]
|
||||
indent_size = 2
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = false
|
||||
ij_kotlin_allow_trailing_comma = false
|
||||
ktlint_code_style = intellij_idea
|
||||
twitter_compose_allowed_composition_locals=LocalExtendedColors
|
||||
4
.github/workflows/android.yml
vendored
@@ -19,11 +19,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: set up JDK 11
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
|
||||
82
.github/workflows/diffuse.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: APK Diff
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
pull-requests: write # to comment on PR
|
||||
|
||||
jobs:
|
||||
assemble-base:
|
||||
if: ${{ github.repository != 'signalapp/Signal-Android' }}
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Cache base apk
|
||||
id: cache-base
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: diffuse-base.apk
|
||||
key: diffuse-${{ github.event.pull_request.base.sha }}
|
||||
|
||||
|
||||
- name: Build with Gradle
|
||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||
run: ./gradlew assemblePlayProdRelease --parallel
|
||||
|
||||
- name: Copy base apk
|
||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-base.apk
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
clean: 'false'
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew assemblePlayProdRelease --parallel
|
||||
|
||||
- name: Copy PR apk
|
||||
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-new.apk
|
||||
|
||||
- id: diffuse
|
||||
uses: usefulness/diffuse-action@v1
|
||||
with:
|
||||
old-file-path: diffuse-base.apk
|
||||
new-file-path: diffuse-new.apk
|
||||
|
||||
- uses: peter-evans/find-comment@v2
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: Diffuse output
|
||||
|
||||
- uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
body: |
|
||||
Diffuse output:
|
||||
|
||||
${{ steps.diffuse.outputs.diff-gh-comment }}
|
||||
edit-mode: replace
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: diffuse-output
|
||||
path: ${{ steps.diffuse.outputs.diff-file }}
|
||||
7
.idea/codeStyles/Project.xml
generated
@@ -212,5 +212,12 @@
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
@@ -42,19 +42,18 @@ wire {
|
||||
}
|
||||
|
||||
ktlint {
|
||||
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
|
||||
version = "0.47.1"
|
||||
version = "0.49.1"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1272
|
||||
def canonicalVersionName = "6.22.7"
|
||||
def canonicalVersionCode = 1289
|
||||
def canonicalVersionName = "6.25.5"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 10,
|
||||
'armeabi-v7a' : 11,
|
||||
'arm64-v8a' : 12,
|
||||
'x86' : 13,
|
||||
'x86_64' : 14]
|
||||
def abiPostFix = ['universal' : 0,
|
||||
'armeabi-v7a' : 1,
|
||||
'arm64-v8a' : 2,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4]
|
||||
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
@@ -159,7 +158,7 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = '1.3.2'
|
||||
kotlinCompilerExtensionVersion = '1.4.4'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@@ -185,7 +184,7 @@ android {
|
||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\""
|
||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\", \"Development\"}"
|
||||
@@ -199,9 +198,11 @@ android {
|
||||
buildConfigField "String[]", "SIGNAL_KBS_IPS", kbs_ips
|
||||
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
|
||||
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDSI_IPS", cdsi_ips
|
||||
buildConfigField "String[]", "SIGNAL_SVR2_IPS", svr2_ips
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\""
|
||||
buildConfigField "String", "SVR2_MRENCLAVE", "\"dc9fd472a5a9c871a3c7f76f1af60aa9c1f314abf2e8d1e0c4ba25c8aaa2848c\""
|
||||
buildConfigField "String", "SVR2_MRENCLAVE", "\"6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " +
|
||||
"\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " +
|
||||
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
||||
@@ -378,6 +379,8 @@ android {
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\""
|
||||
buildConfigField "String", "SVR2_MRENCLAVE", "\"a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
|
||||
"\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
|
||||
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
||||
@@ -445,6 +448,14 @@ android {
|
||||
variant.setIgnore(true)
|
||||
}
|
||||
}
|
||||
|
||||
android.buildTypes.each {
|
||||
if (it.name != 'release') {
|
||||
sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/debug/java"
|
||||
} else {
|
||||
sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/release/java"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -468,6 +479,7 @@ dependencies {
|
||||
implementation libs.androidx.gridlayout
|
||||
implementation libs.androidx.exifinterface
|
||||
implementation libs.androidx.compose.rxjava3
|
||||
implementation libs.androidx.compose.runtime.livedata
|
||||
implementation libs.androidx.constraintlayout
|
||||
implementation libs.androidx.multidex
|
||||
implementation libs.androidx.navigation.fragment.ktx
|
||||
@@ -529,13 +541,11 @@ dependencies {
|
||||
|
||||
implementation libs.leolin.shortcutbadger
|
||||
implementation libs.emilsjolander.stickylistheaders
|
||||
implementation libs.jpardogo.materialtabstrip
|
||||
implementation libs.apache.httpclient.android
|
||||
implementation libs.glide.glide
|
||||
implementation libs.roundedimageview
|
||||
implementation libs.materialish.progress
|
||||
implementation libs.greenrobot.eventbus
|
||||
implementation libs.waitingdots
|
||||
implementation libs.google.zxing.android.integration
|
||||
implementation libs.google.zxing.core
|
||||
implementation libs.google.flexbox
|
||||
@@ -578,9 +588,8 @@ dependencies {
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
testImplementation testLibs.robolectric.shadows.multidex
|
||||
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) {
|
||||
force = true
|
||||
}
|
||||
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) { version { strictly "1.70" } } // Used by roboelectric
|
||||
testImplementation (testLibs.bouncycastle.bcpkix.jdk15on) { version { strictly "1.70" } } // Used by roboelectric
|
||||
testImplementation testLibs.hamcrest.hamcrest
|
||||
testImplementation testLibs.mockk
|
||||
|
||||
|
||||
@@ -11,4 +11,11 @@
|
||||
# Protobuf lite
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
||||
-keep class androidx.window.** { *; }
|
||||
-keep class androidx.window.** { *; }
|
||||
|
||||
# AGP generated dont warns
|
||||
-dontwarn com.android.org.conscrypt.SSLParametersImpl
|
||||
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
-dontwarn sun.net.spi.nameservice.NameService
|
||||
-dontwarn sun.net.spi.nameservice.NameServiceDescriptor
|
||||
@@ -142,6 +142,7 @@ class ConversationItemPreviewer {
|
||||
1024,
|
||||
1024,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of("/not-there.jpg"),
|
||||
false,
|
||||
false,
|
||||
|
||||
@@ -47,6 +47,7 @@ import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientTableTest_getAndPossiblyMerge {
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import org.thoughtcrime.securesms.testing.assertIsNot
|
||||
import org.thoughtcrime.securesms.testing.parsedRequestBody
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyState
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyStatus
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PreKeysSyncJobTest {
|
||||
@@ -106,8 +106,8 @@ class PreKeysSyncJobTest {
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
|
||||
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(100)) }
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(OneTimePreKeyCounts(100, 100)) },
|
||||
Get("/v2/keys?identity=pni") { MockResponse().success(OneTimePreKeyCounts(100, 100)) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
@@ -133,7 +133,7 @@ class PreKeysSyncJobTest {
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(OneTimePreKeyCounts(100, 100)) },
|
||||
Put("/v2/keys/signed?identity=pni") { MockResponse().success() }
|
||||
)
|
||||
|
||||
@@ -157,15 +157,15 @@ class PreKeysSyncJobTest {
|
||||
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
val currentNextAciPreKeyId = aciPreKeyMeta.nextOneTimePreKeyId
|
||||
val currentNextPniPreKeyId = pniPreKeyMeta.nextOneTimePreKeyId
|
||||
val currentNextAciPreKeyId = aciPreKeyMeta.nextEcOneTimePreKeyId
|
||||
val currentNextPniPreKeyId = pniPreKeyMeta.nextEcOneTimePreKeyId
|
||||
|
||||
lateinit var aciPreKeyStateRequest: PreKeyState
|
||||
lateinit var pniPreKeyStateRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(5)) },
|
||||
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(5)) },
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(OneTimePreKeyCounts(5, 5)) },
|
||||
Get("/v2/keys?identity=pni") { MockResponse().success(OneTimePreKeyCounts(5, 5)) },
|
||||
Put("/v2/keys/?identity=aci") { r ->
|
||||
aciPreKeyStateRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
@@ -184,8 +184,8 @@ class PreKeysSyncJobTest {
|
||||
aciPreKeyMeta.activeSignedPreKeyId assertIsNot currentAciKeyId
|
||||
pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId
|
||||
|
||||
aciPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextAciPreKeyId
|
||||
pniPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextPniPreKeyId
|
||||
aciPreKeyMeta.nextEcOneTimePreKeyId assertIsNot currentNextAciPreKeyId
|
||||
pniPreKeyMeta.nextEcOneTimePreKeyId assertIsNot currentNextPniPreKeyId
|
||||
|
||||
ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.let { aciIdentityKey ->
|
||||
aciPreKeyStateRequest.identityKey assertIs aciIdentityKey
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@@ -168,6 +169,10 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
override fun getSenderKeySharedWith(distributionId: DistributionId?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
|
||||
override fun markSenderKeySharedWith(distributionId: DistributionId?, addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
||||
override fun clearSenderKeySharedWith(addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
||||
override fun storeLastResortKyberPreKey(kyberPreKeyId: Int, kyberPreKeyRecord: KyberPreKeyRecord) = throw UnsupportedOperationException()
|
||||
override fun removeKyberPreKey(kyberPreKeyId: Int) = throw UnsupportedOperationException()
|
||||
override fun loadLastResortKyberPreKeys(): List<KyberPreKeyRecord> = throw UnsupportedOperationException()
|
||||
|
||||
override fun isMultiDevice(): Boolean = throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@ import org.thoughtcrime.securesms.registration.VerifyResponse
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -88,6 +88,8 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
|
||||
password = Util.getSecret(18),
|
||||
registrationId = registrationRepository.registrationId,
|
||||
profileKey = registrationRepository.getProfileKey("+15555550101"),
|
||||
aciPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.ACI),
|
||||
pniPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.PNI),
|
||||
fcmToken = null,
|
||||
pniRegistrationId = registrationRepository.pniRegistrationId,
|
||||
recoveryPassword = "asdfasdfasdfasdf"
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package org.signal.benchmark
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.account.PreKeyUpload
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
@@ -37,7 +34,7 @@ class DummyAccountManagerFactory : AccountManagerFactory() {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun setPreKeys(serviceIdType: ServiceIdType, identityKey: IdentityKey, signedPreKey: SignedPreKeyRecord, oneTimePreKeys: List<PreKeyRecord>) {
|
||||
override fun setPreKeys(preKeyUpload: PreKeyUpload) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ import org.thoughtcrime.securesms.registration.VerifyResponse
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.util.UUID
|
||||
|
||||
object TestUsers {
|
||||
|
||||
private var generatedOthers: Int = 0
|
||||
@@ -50,6 +50,8 @@ object TestUsers {
|
||||
password = Util.getSecret(18),
|
||||
registrationId = registrationRepository.registrationId,
|
||||
profileKey = registrationRepository.getProfileKey("+15555550101"),
|
||||
aciPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.ACI),
|
||||
pniPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.PNI),
|
||||
fcmToken = "fcm-token",
|
||||
pniRegistrationId = registrationRepository.pniRegistrationId,
|
||||
recoveryPassword = "asdfasdfasdfasdf"
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.conversation
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.IncomingTextOnly
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import java.security.SecureRandom
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Generates random conversation messages via the given set of parameters.
|
||||
*/
|
||||
class ConversationElementGenerator {
|
||||
private val mappingModelCache = mutableMapOf<ConversationElementKey, MappingModel<*>>()
|
||||
private val random = SecureRandom()
|
||||
|
||||
private val wordBank = listOf(
|
||||
"A",
|
||||
"Test",
|
||||
"Message",
|
||||
"To",
|
||||
"Display",
|
||||
"Content",
|
||||
"In",
|
||||
"Bubbles",
|
||||
"User",
|
||||
"Signal",
|
||||
"The"
|
||||
)
|
||||
|
||||
fun getMappingModel(key: ConversationElementKey): MappingModel<*> {
|
||||
val cached = mappingModelCache[key]
|
||||
if (cached != null) {
|
||||
return cached
|
||||
}
|
||||
|
||||
val messageModel = generateMessage(key)
|
||||
mappingModelCache[key] = messageModel
|
||||
return messageModel
|
||||
}
|
||||
|
||||
private fun getIncomingType(): Long {
|
||||
return MessageTypes.BASE_INBOX_TYPE or MessageTypes.SECURE_MESSAGE_BIT
|
||||
}
|
||||
|
||||
private fun getSentOutgoingType(): Long {
|
||||
return MessageTypes.BASE_SENT_TYPE or MessageTypes.SECURE_MESSAGE_BIT
|
||||
}
|
||||
|
||||
private fun generateMessage(key: ConversationElementKey): MappingModel<*> {
|
||||
val messageId = key.requireMessageId()
|
||||
val now = getNow()
|
||||
|
||||
val testMessageWordLength = random.nextInt(40) + 1
|
||||
val testMessage = (0 until testMessageWordLength).map {
|
||||
wordBank.random()
|
||||
}.joinToString(" ")
|
||||
|
||||
val isIncoming = random.nextBoolean()
|
||||
|
||||
val record = MediaMmsMessageRecord(
|
||||
messageId,
|
||||
if (isIncoming) Recipient.UNKNOWN else Recipient.self(),
|
||||
0,
|
||||
if (isIncoming) Recipient.self() else Recipient.UNKNOWN,
|
||||
now,
|
||||
now,
|
||||
now,
|
||||
1,
|
||||
1,
|
||||
testMessage,
|
||||
SlideDeck(),
|
||||
if (isIncoming) getIncomingType() else getSentOutgoingType(),
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
1,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
false,
|
||||
emptyList(),
|
||||
false,
|
||||
false,
|
||||
now,
|
||||
1,
|
||||
now,
|
||||
null,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
-1,
|
||||
null,
|
||||
null,
|
||||
0
|
||||
)
|
||||
|
||||
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
|
||||
ApplicationDependencies.getApplication(),
|
||||
record,
|
||||
Recipient.UNKNOWN
|
||||
)
|
||||
|
||||
return if (isIncoming) {
|
||||
IncomingTextOnly(conversationMessage)
|
||||
} else {
|
||||
OutgoingTextOnly(conversationMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNow(): Long {
|
||||
val now = System.currentTimeMillis()
|
||||
return now - random.nextInt(20.milliseconds.inWholeMilliseconds.toInt()).toLong()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.conversation
|
||||
|
||||
import org.signal.paging.PagedDataSource
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import kotlin.math.min
|
||||
|
||||
class InternalConversationTestDataSource(
|
||||
private val size: Int,
|
||||
private val generator: ConversationElementGenerator
|
||||
) : PagedDataSource<ConversationElementKey, MappingModel<*>> {
|
||||
override fun size(): Int = size
|
||||
|
||||
override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<MappingModel<*>> {
|
||||
val end = min(start + length, totalSize)
|
||||
return (start until end).map {
|
||||
load(ConversationElementKey.forMessage(it.toLong()))!!
|
||||
}.toMutableList()
|
||||
}
|
||||
|
||||
override fun getKey(data: MappingModel<*>): ConversationElementKey {
|
||||
check(data is ConversationMessageElement)
|
||||
|
||||
return ConversationElementKey.forMessage(data.conversationMessage.messageRecord.id)
|
||||
}
|
||||
|
||||
override fun load(key: ConversationElementKey?): MappingModel<*>? {
|
||||
return key?.let { generator.getMappingModel(it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.conversation
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.databinding.ConversationTestFragmentBinding
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.thoughtcrime.securesms.util.doAfterNextLayout
|
||||
|
||||
class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fragment) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(InternalConversationTestFragment::class.java)
|
||||
}
|
||||
|
||||
private val binding by ViewBinderDelegate(ConversationTestFragmentBinding::bind)
|
||||
private val viewModel: InternalConversationTestViewModel by viewModels()
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val adapter = ConversationAdapterV2(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
glideRequests = GlideApp.with(this),
|
||||
clickListener = ClickListener(),
|
||||
hasWallpaper = false,
|
||||
colorizer = Colorizer()
|
||||
)
|
||||
|
||||
var startTime = 0L
|
||||
var firstRender = true
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
adapter.setPagingController(viewModel.controller)
|
||||
lifecycleDisposable += viewModel.data.observeOn(AndroidSchedulers.mainThread()).subscribeBy {
|
||||
if (firstRender) {
|
||||
startTime = System.currentTimeMillis()
|
||||
}
|
||||
adapter.submitList(it) {
|
||||
if (firstRender) {
|
||||
firstRender = false
|
||||
binding.root.doAfterNextLayout {
|
||||
val endTime = System.currentTimeMillis()
|
||||
Log.d(TAG, "First render in ${endTime - startTime} millis")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.root.layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
|
||||
binding.root.adapter = adapter
|
||||
|
||||
RecyclerViewColorizer(binding.root).apply {
|
||||
setChatColors(ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ClickListener : ItemClickListener {
|
||||
override fun onQuoteClicked(messageRecord: MmsMessageRecord?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onLinkPreviewClicked(linkPreview: LinkPreview) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onMoreTextClicked(conversationRecipientId: RecipientId, messageId: Long, isMms: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onStickerClicked(stickerLocator: StickerLocator) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onViewOnceMessageClicked(messageRecord: MmsMessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onSharedContactDetailsClicked(contact: Contact, avatarTransitionView: View) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onAddToContactsClicked(contact: Contact) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onMessageSharedContactClicked(choices: MutableList<Recipient>) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInviteSharedContactClicked(choices: MutableList<Recipient>) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onReactionClicked(multiselectPart: MultiselectPart, messageId: Long, isMms: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onMessageWithErrorClicked(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onIncomingIdentityMismatchClicked(recipientId: RecipientId) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onRegisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onVoiceNotePause(uri: Uri) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onVoiceNotePlay(uri: Uri, messageId: Long, position: Double) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onVoiceNoteSeekTo(uri: Uri, position: Double) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onVoiceNotePlaybackSpeedChanged(uri: Uri, speed: Float) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onChatSessionRefreshLearnMoreClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onBadDecryptLearnMoreClicked(author: RecipientId) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onJoinGroupCallClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onEnableCallNotificationsClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onPlayInlineContent(conversationMessage: ConversationMessage?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInMemoryMessageClicked(messageRecord: InMemoryMessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onViewGroupDescriptionChange(groupId: GroupId?, description: String, isMessageRequestAccepted: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onChangeNumberUpdateContact(recipient: Recipient) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onCallToAction(action: String) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onDonateClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onBlockJoinRequest(recipient: Recipient) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onRecipientNameClicked(target: RecipientId) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInviteToSignalClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onActivatePaymentsClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onSendPaymentClicked(recipientId: RecipientId) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onUrlClicked(url: String): Boolean {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onGiftBadgeRevealed(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MultiselectPart?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.conversation
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.signal.paging.PagedData
|
||||
import org.signal.paging.PagingConfig
|
||||
|
||||
class InternalConversationTestViewModel : ViewModel() {
|
||||
private val generator = ConversationElementGenerator()
|
||||
private val dataSource = InternalConversationTestDataSource(
|
||||
500,
|
||||
generator
|
||||
)
|
||||
|
||||
private val config = PagingConfig.Builder().setPageSize(25)
|
||||
.setBufferPages(2)
|
||||
.build()
|
||||
|
||||
private val pagedData = PagedData.createForObservable(dataSource, config)
|
||||
|
||||
val controller = pagedData.controller
|
||||
val data = pagedData.data
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
<uses-feature android:name="android.hardware.wifi" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.portrait" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.telephony" android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
|
||||
@@ -602,10 +603,13 @@
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https"
|
||||
android:host="signal.link" />
|
||||
|
||||
<data android:scheme="sgnl" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="signal.link" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@@ -1154,6 +1158,11 @@
|
||||
|
||||
<receiver android:name=".payments.backup.phrase.ClearClipboardAlarmReceiver" />
|
||||
|
||||
<provider android:name=".providers.AvatarProvider"
|
||||
android:authorities="${applicationId}.avatar"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
<provider android:name=".providers.PartProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 238 KiB |
BIN
app/src/main/assets/emoji/Objects_0.webp
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
app/src/main/assets/emoji/Objects_1.webp
Normal file
|
After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 92 KiB |
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.signal.glide.transforms
|
||||
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object SignalDownsampleStrategy {
|
||||
/**
|
||||
* Center outside, but don't up-scale, only downscale. You should be setting centerOutside
|
||||
* on the target image view to still maintain center outside behavior.
|
||||
*/
|
||||
@JvmField
|
||||
val CENTER_OUTSIDE_NO_UPSCALE: DownsampleStrategy = CenterOutsideNoUpscale()
|
||||
|
||||
private class CenterOutsideNoUpscale : DownsampleStrategy() {
|
||||
override fun getScaleFactor(
|
||||
sourceWidth: Int,
|
||||
sourceHeight: Int,
|
||||
requestedWidth: Int,
|
||||
requestedHeight: Int
|
||||
): Float {
|
||||
val widthPercentage = requestedWidth / sourceWidth.toFloat()
|
||||
val heightPercentage = requestedHeight / sourceHeight.toFloat()
|
||||
return min(MAX_SCALE_FACTOR, max(widthPercentage, heightPercentage))
|
||||
}
|
||||
|
||||
override fun getSampleSizeRounding(
|
||||
sourceWidth: Int,
|
||||
sourceHeight: Int,
|
||||
requestedWidth: Int,
|
||||
requestedHeight: Int
|
||||
): SampleSizeRounding {
|
||||
return SampleSizeRounding.QUALITY
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_SCALE_FACTOR = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import org.signal.ringrtc.CallLinkRootKey;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
@@ -116,5 +117,6 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
|
||||
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
|
||||
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.view.Window;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
||||
@@ -20,7 +22,7 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.DeviceProvisioningActivity_link_a_signal_device))
|
||||
.setMessage(getString(R.string.DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner))
|
||||
.setPositiveButton(R.string.DeviceProvisioningActivity_continue, (dialog1, which) -> {
|
||||
|
||||
@@ -5,6 +5,8 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||
@@ -26,7 +28,7 @@ public final class GroupMembersDialog {
|
||||
}
|
||||
|
||||
public void display() {
|
||||
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(fragmentActivity)
|
||||
.setTitle(R.string.ConversationActivity_group_members)
|
||||
.setIcon(R.drawable.ic_group_24)
|
||||
.setCancelable(true)
|
||||
|
||||
@@ -21,6 +21,8 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
@@ -217,7 +219,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
private class SmsSendClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
new AlertDialog.Builder(InviteActivity.this)
|
||||
new MaterialAlertDialogBuilder(InviteActivity.this)
|
||||
.setTitle(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_invites,
|
||||
contactsFragment.getSelectedContacts().size(),
|
||||
contactsFragment.getSelectedContacts().size()))
|
||||
|
||||
@@ -101,6 +101,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
handleGroupLinkInIntent(intent);
|
||||
handleProxyInIntent(intent);
|
||||
handleSignalMeIntent(intent);
|
||||
handleCallLinkInIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
public class PlayServicesProblemFragment extends DialogFragment {
|
||||
|
||||
@@ -37,7 +38,7 @@ public class PlayServicesProblemFragment extends DialogFragment {
|
||||
Dialog dialog = GoogleApiAvailability.getInstance().getErrorDialog(getActivity(), code, 9111);
|
||||
|
||||
if (dialog == null) {
|
||||
return new AlertDialog.Builder(requireActivity())
|
||||
return new MaterialAlertDialogBuilder(requireActivity())
|
||||
.setNegativeButton(android.R.string.ok, null)
|
||||
.setMessage(R.string.PlayServicesProblemFragment_the_version_of_google_play_services_you_have_installed_is_not_functioning)
|
||||
.create();
|
||||
|
||||
@@ -55,6 +55,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallLinkInfoSheet;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow;
|
||||
@@ -71,6 +72,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
|
||||
@@ -104,9 +106,17 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private static final int STANDARD_DELAY_FINISH = 1000;
|
||||
private static final int VIBRATE_DURATION = 50;
|
||||
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
|
||||
public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION";
|
||||
/**
|
||||
* ANSWER the call via voice-only.
|
||||
*/
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
|
||||
/**
|
||||
* ANSWER the call via video.
|
||||
*/
|
||||
public static final String ANSWER_VIDEO_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_VIDEO_ACTION";
|
||||
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
|
||||
public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION";
|
||||
|
||||
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
|
||||
public static final String EXTRA_STARTED_FROM_FULLSCREEN = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_FULLSCREEN";
|
||||
@@ -159,10 +169,18 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
initializeViewModel(isLandscapeEnabled);
|
||||
initializePictureInPictureParams();
|
||||
|
||||
processIntent(getIntent());
|
||||
logIntent(getIntent());
|
||||
|
||||
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
|
||||
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
|
||||
if (ANSWER_VIDEO_ACTION.equals(getIntent().getAction())) {
|
||||
enableVideoIfAvailable = true;
|
||||
} else if (ANSWER_ACTION.equals(getIntent().getAction()) || getIntent().getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false)) {
|
||||
enableVideoIfAvailable = false;
|
||||
} else {
|
||||
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
|
||||
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
|
||||
}
|
||||
|
||||
processIntent(getIntent());
|
||||
|
||||
windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
|
||||
|
||||
@@ -211,6 +229,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
public void onNewIntent(Intent intent) {
|
||||
Log.i(TAG, "onNewIntent(" + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false) + ")");
|
||||
super.onNewIntent(intent);
|
||||
logIntent(intent);
|
||||
processIntent(intent);
|
||||
}
|
||||
|
||||
@@ -297,9 +316,17 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
return isSystemPipEnabledAndAvailable() && isInPictureInPictureMode();
|
||||
}
|
||||
|
||||
private void logIntent(@NonNull Intent intent) {
|
||||
Log.d(TAG, "Intent: Action: " + intent.getAction());
|
||||
Log.d(TAG, "Intent: EXTRA_STARTED_FROM_FULLSCREEN: " + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false));
|
||||
Log.d(TAG, "Intent: EXTRA_ENABLE_VIDEO_IF_AVAILABLE: " + intent.getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false));
|
||||
}
|
||||
|
||||
private void processIntent(@NonNull Intent intent) {
|
||||
if (ANSWER_ACTION.equals(intent.getAction())) {
|
||||
handleAnswerWithAudio();
|
||||
} else if (ANSWER_VIDEO_ACTION.equals(intent.getAction())) {
|
||||
handleAnswerWithVideo();
|
||||
} else if (DENY_ACTION.equals(intent.getAction())) {
|
||||
handleDenyCall();
|
||||
} else if (END_CALL_ACTION.equals(intent.getAction())) {
|
||||
@@ -498,26 +525,20 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
private void handleAnswerWithVideo() {
|
||||
Recipient recipient = viewModel.getRecipient().get();
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone_and_camera), R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||
.onAllGranted(() -> {
|
||||
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||
|
||||
if (!recipient.equals(Recipient.UNKNOWN)) {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
|
||||
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||
.onAllGranted(() -> {
|
||||
callScreen.setRecipient(recipient);
|
||||
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||
ApplicationDependencies.getSignalCallManager().acceptCall(true);
|
||||
|
||||
ApplicationDependencies.getSignalCallManager().acceptCall(true);
|
||||
|
||||
handleSetMuteVideo(false);
|
||||
})
|
||||
.onAnyDenied(this::handleDenyCall)
|
||||
.execute();
|
||||
}
|
||||
handleSetMuteVideo(false);
|
||||
})
|
||||
.onAnyDenied(this::handleDenyCall)
|
||||
.execute();
|
||||
}
|
||||
|
||||
private void handleDenyCall() {
|
||||
@@ -901,7 +922,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
@Override
|
||||
public void onCallInfoClicked() {
|
||||
CallParticipantsListDialog.show(getSupportFragmentManager());
|
||||
LiveRecipient liveRecipient = viewModel.getRecipient();
|
||||
|
||||
if (liveRecipient.get().isCallLink()) {
|
||||
CallLinkInfoSheet.show(getSupportFragmentManager(), liveRecipient.get().requireCallLinkRoomId());
|
||||
} else {
|
||||
CallParticipantsListDialog.show(getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -35,6 +35,9 @@ public abstract class Attachment {
|
||||
@Nullable
|
||||
private final byte[] digest;
|
||||
|
||||
@Nullable
|
||||
private final byte[] incrementalDigest;
|
||||
|
||||
@Nullable
|
||||
private final String fastPreflightId;
|
||||
|
||||
@@ -70,6 +73,7 @@ public abstract class Attachment {
|
||||
@Nullable String key,
|
||||
@Nullable String relay,
|
||||
@Nullable byte[] digest,
|
||||
@Nullable byte[] incrementalDigest,
|
||||
@Nullable String fastPreflightId,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
@@ -93,6 +97,7 @@ public abstract class Attachment {
|
||||
this.key = key;
|
||||
this.relay = relay;
|
||||
this.digest = digest;
|
||||
this.incrementalDigest = incrementalDigest;
|
||||
this.fastPreflightId = fastPreflightId;
|
||||
this.voiceNote = voiceNote;
|
||||
this.borderless = borderless;
|
||||
@@ -165,6 +170,11 @@ public abstract class Attachment {
|
||||
return digest;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public byte[] getIncrementalDigest() {
|
||||
return incrementalDigest;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getFastPreflightId() {
|
||||
return fastPreflightId;
|
||||
|
||||
@@ -33,6 +33,7 @@ public class DatabaseAttachment extends Attachment {
|
||||
String key,
|
||||
String relay,
|
||||
byte[] digest,
|
||||
byte[] incrementalDigest,
|
||||
String fastPreflightId,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
@@ -48,7 +49,7 @@ public class DatabaseAttachment extends Attachment {
|
||||
int displayOrder,
|
||||
long uploadTimestamp)
|
||||
{
|
||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, incrementalDigest, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this.attachmentId = attachmentId;
|
||||
this.hasData = hasData;
|
||||
this.hasThumbnail = hasThumbnail;
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.database.MessageTable;
|
||||
public class MmsNotificationAttachment extends Attachment {
|
||||
|
||||
public MmsNotificationAttachment(int status, long size) {
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, false, 0, 0, false, 0, null, null, null, null, null);
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, null, false, false, false, 0, 0, false, 0, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -30,6 +30,7 @@ public class PointerAttachment extends Attachment {
|
||||
@Nullable String key,
|
||||
@Nullable String relay,
|
||||
@Nullable byte[] digest,
|
||||
@Nullable byte[] incrementalDigest,
|
||||
@Nullable String fastPreflightId,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
@@ -41,7 +42,7 @@ public class PointerAttachment extends Attachment {
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
|
||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, incrementalDigest, fastPreflightId, voiceNote, borderless, videoGif, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -112,6 +113,7 @@ public class PointerAttachment extends Attachment {
|
||||
pointer.get().asPointer().getRemoteId().toString(),
|
||||
encodedKey, null,
|
||||
pointer.get().asPointer().getDigest().orElse(null),
|
||||
pointer.get().asPointer().getincrementalDigest().orElse(null),
|
||||
fastPreflightId,
|
||||
pointer.get().asPointer().getVoiceNote(),
|
||||
pointer.get().asPointer().isBorderless(),
|
||||
@@ -137,6 +139,7 @@ public class PointerAttachment extends Attachment {
|
||||
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
|
||||
null,
|
||||
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
|
||||
thumbnail != null ? thumbnail.asPointer().getincrementalDigest().orElse(null) : null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
@@ -166,6 +169,7 @@ public class PointerAttachment extends Attachment {
|
||||
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
|
||||
null,
|
||||
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
|
||||
thumbnail != null ? thumbnail.asPointer().getincrementalDigest().orElse(null) : null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
|
||||
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||
public class TombstoneAttachment extends Attachment {
|
||||
|
||||
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
|
||||
super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null);
|
||||
super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -52,7 +52,7 @@ public class UriAttachment extends Attachment {
|
||||
@Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this.dataUri = Objects.requireNonNull(dataUri);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,14 +48,14 @@ public class AudioRecorder {
|
||||
if (this.uiHandler != null) {
|
||||
onAudioFocusChangeListener = focusChange -> {
|
||||
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
|
||||
Log.i(TAG, "Audio focus change " + focusChange + " stopping recording");
|
||||
Log.i(TAG, "Audio focus change " + focusChange + " stopping recording via UI handler.");
|
||||
this.uiHandler.onRecordCanceled(false);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
onAudioFocusChangeListener = focusChange -> {
|
||||
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
|
||||
Log.i(TAG, "Audio focus change " + focusChange + " stopping recording");
|
||||
Log.i(TAG, "Audio focus change " + focusChange + " stopping recording.");
|
||||
stopRecording();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,14 +25,14 @@ sealed interface BluetoothVoiceNoteUtil {
|
||||
fun destroy()
|
||||
|
||||
companion object {
|
||||
fun create(context: Context, listener: () -> Unit, bluetoothPermissionDeniedHandler: () -> Unit): BluetoothVoiceNoteUtil {
|
||||
fun create(context: Context, listener: (Boolean) -> Unit, bluetoothPermissionDeniedHandler: () -> Unit): BluetoothVoiceNoteUtil {
|
||||
return if (Build.VERSION.SDK_INT >= 31) BluetoothVoiceNoteUtil31(listener) else BluetoothVoiceNoteUtilLegacy(context, listener, bluetoothPermissionDeniedHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
private class BluetoothVoiceNoteUtil31(val listener: () -> Unit) : BluetoothVoiceNoteUtil {
|
||||
private class BluetoothVoiceNoteUtil31(val listener: (Boolean) -> Unit) : BluetoothVoiceNoteUtil {
|
||||
override fun connectBluetoothScoConnection() {
|
||||
val audioManager = ApplicationDependencies.getAndroidCallAudioManager()
|
||||
val device: AudioDeviceInfo? = audioManager.connectedBluetoothDevice
|
||||
@@ -40,13 +40,15 @@ private class BluetoothVoiceNoteUtil31(val listener: () -> Unit) : BluetoothVoic
|
||||
val result: Boolean = audioManager.setCommunicationDevice(device)
|
||||
if (result) {
|
||||
Log.d(TAG, "Successfully set Bluetooth device as active communication device.")
|
||||
listener(true)
|
||||
} else {
|
||||
Log.d(TAG, "Found Bluetooth device but failed to set it as active communication device.")
|
||||
listener(false)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Could not find Bluetooth device in list of communications devices, falling back to current input.")
|
||||
listener(false)
|
||||
}
|
||||
listener()
|
||||
}
|
||||
|
||||
override fun disconnectBluetoothScoConnection() {
|
||||
@@ -64,15 +66,23 @@ private class BluetoothVoiceNoteUtil31(val listener: () -> Unit) : BluetoothVoic
|
||||
* @param listener This will be executed on the main thread after the Bluetooth connection connects, or if it doesn't.
|
||||
* @param bluetoothPermissionDeniedHandler called when we detect the Bluetooth permission has been denied to our app.
|
||||
*/
|
||||
private class BluetoothVoiceNoteUtilLegacy(val context: Context, val listener: () -> Unit, val bluetoothPermissionDeniedHandler: () -> Unit) : BluetoothVoiceNoteUtil {
|
||||
private class BluetoothVoiceNoteUtilLegacy(val context: Context, val listener: (Boolean) -> Unit, val bluetoothPermissionDeniedHandler: () -> Unit) : BluetoothVoiceNoteUtil {
|
||||
private val commandAndControlThread: HandlerThread = SignalExecutors.getAndStartHandlerThread("voice-note-audio", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD)
|
||||
private val uiThreadHandler = Handler(context.mainLooper)
|
||||
private val audioHandler: SignalAudioHandler = SignalAudioHandler(commandAndControlThread.looper)
|
||||
private val deviceUpdatedListener: AudioDeviceUpdatedListener = object : AudioDeviceUpdatedListener {
|
||||
override fun onAudioDeviceUpdated() {
|
||||
if (signalBluetoothManager.state == SignalBluetoothManager.State.CONNECTED) {
|
||||
Log.d(TAG, "Bluetooth SCO connected. Starting voice note recording on UI thread.")
|
||||
uiThreadHandler.post { listener() }
|
||||
when (signalBluetoothManager.state) {
|
||||
SignalBluetoothManager.State.CONNECTED -> {
|
||||
Log.d(TAG, "Bluetooth SCO connected. Starting voice note recording on UI thread.")
|
||||
uiThreadHandler.post { listener(true) }
|
||||
}
|
||||
SignalBluetoothManager.State.ERROR,
|
||||
SignalBluetoothManager.State.PERMISSION_DENIED -> {
|
||||
Log.w(TAG, "Unable to complete Bluetooth connection due to ${signalBluetoothManager.state}. Starting voice note recording anyway on UI thread.")
|
||||
uiThreadHandler.post { listener(false) }
|
||||
}
|
||||
else -> Log.d(TAG, "Current Bluetooth connection state: ${signalBluetoothManager.state}.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +115,7 @@ private class BluetoothVoiceNoteUtilLegacy(val context: Context, val listener: (
|
||||
bluetoothPermissionDeniedHandler()
|
||||
hasWarnedAboutBluetooth = true
|
||||
}
|
||||
listener()
|
||||
listener(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
viewModel.onAvatarEditCompleted(vector)
|
||||
}
|
||||
|
||||
setFragmentResultListener(PhotoEditorFragment.REQUEST_KEY_EDIT) { _, bundle ->
|
||||
setFragmentResultListener(PhotoEditorFragment.REQUEST_KEY_EDIT) { _, _ ->
|
||||
}
|
||||
|
||||
photoEditorLauncher = registerForActivityResult(PhotoEditorActivity.Contract()) { photo ->
|
||||
@@ -155,6 +155,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
ViewUtil.hideKeyboard(requireContext(), requireView())
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_CODE_SELECT_IMAGE && resultCode == Activity.RESULT_OK && data != null) {
|
||||
val media: Media = requireNotNull(data.getParcelableExtraCompat(AvatarSelectionActivity.EXTRA_MEDIA, Media::class.java))
|
||||
@@ -194,7 +195,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
return true
|
||||
}
|
||||
|
||||
fun openEditor(avatar: Avatar) {
|
||||
private fun openEditor(avatar: Avatar) {
|
||||
when (avatar) {
|
||||
is Avatar.Photo -> openPhotoEditor(avatar)
|
||||
is Avatar.Resource -> throw UnsupportedOperationException()
|
||||
@@ -250,6 +251,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
.execute()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package org.thoughtcrime.securesms.avatar.text
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
@@ -12,7 +12,7 @@ class TextAvatarCreationViewModel(initialText: Avatar.Text) : ViewModel() {
|
||||
|
||||
private val store = Store(TextAvatarCreationState(initialText))
|
||||
|
||||
val state: LiveData<TextAvatarCreationState> = Transformations.distinctUntilChanged(store.stateLiveData)
|
||||
val state: LiveData<TextAvatarCreationState> = store.stateLiveData.distinctUntilChanged()
|
||||
|
||||
fun setColor(colorPair: Avatars.ColorPair) {
|
||||
store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) }
|
||||
|
||||
@@ -45,7 +45,7 @@ public class BackupDialog {
|
||||
@NonNull Runnable onBackupsEnabled)
|
||||
{
|
||||
String[] password = BackupUtil.generateBackupPassphrase();
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.BackupDialog_enable_local_backups)
|
||||
.setView(backupDirectorySelectionIntent != null ? R.layout.backup_enable_dialog_v29 : R.layout.backup_enable_dialog)
|
||||
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
|
||||
|
||||
@@ -42,12 +42,20 @@ object BackupVerifier {
|
||||
|
||||
frame = inputStream.readFrame()
|
||||
}
|
||||
if (frame.end == true) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
if (cancellationSignal.isCanceled) {
|
||||
throw FullBackupExporter.BackupCanceledException()
|
||||
}
|
||||
|
||||
if (count != expectedCount) {
|
||||
Log.e(TAG, "Incorrect number of frames expected $expectedCount but only $count")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
outputStream.close();
|
||||
}
|
||||
}
|
||||
return new BackupEvent(BackupEvent.Type.FINISHED, ++count, estimatedCountOutside);
|
||||
return new BackupEvent(BackupEvent.Type.FINISHED, outputStream.frames, estimatedCountOutside);
|
||||
}
|
||||
|
||||
private static long calculateCount(@NonNull Context context, @NonNull SQLiteDatabase input, List<String> tables) {
|
||||
@@ -637,6 +637,8 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
private final byte[] iv;
|
||||
private int counter;
|
||||
|
||||
private int frames;
|
||||
|
||||
private BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String passphrase) throws IOException {
|
||||
try {
|
||||
byte[] salt = Util.getSecretBytes(32);
|
||||
@@ -796,6 +798,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
out.write(length);
|
||||
out.write(frameCiphertext);
|
||||
out.write(frameMac, 0, 10);
|
||||
frames++;
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@ data class GiftFlowState(
|
||||
RECIPIENT_VERIFICATION,
|
||||
TOKEN_REQUEST,
|
||||
PAYMENT_PIPELINE,
|
||||
FAILURE;
|
||||
FAILURE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
class CallLinkJoinButton @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : LinearLayoutCompat(context, attrs) {
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
inflate(context, R.layout.call_link_join_button, this)
|
||||
}
|
||||
|
||||
private val joinButton: MaterialButton = findViewById(R.id.join_button)
|
||||
|
||||
fun setTextColor(@ColorRes textColorResId: Int) {
|
||||
joinButton.setTextColor(ContextCompat.getColor(context, textColorResId))
|
||||
}
|
||||
|
||||
fun setJoinClickListener(onClickListener: OnClickListener) {
|
||||
joinButton.setOnClickListener(onClickListener)
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@
|
||||
package org.thoughtcrime.securesms.calls.links
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.ringrtc.CallException
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
@@ -21,10 +21,12 @@ import java.net.URLDecoder
|
||||
*/
|
||||
object CallLinks {
|
||||
private const val ROOT_KEY = "key"
|
||||
private const val HTTPS_LINK_PREFIX = "https://signal.link/call/#key="
|
||||
private const val SNGL_LINK_PREFIX = "sgnl://signal.link/#key="
|
||||
|
||||
private val TAG = Log.tag(CallLinks::class.java)
|
||||
|
||||
fun url(linkKeyBytes: ByteArray) = "https://signal.link/call/#key=${Hex.dump(linkKeyBytes)}"
|
||||
fun url(linkKeyBytes: ByteArray) = "$HTTPS_LINK_PREFIX${CallLinkRootKey(linkKeyBytes)}"
|
||||
|
||||
fun watchCallLink(roomId: CallLinkRoomId): Observable<CallLinkTable.CallLink> {
|
||||
return Observable.create { emitter ->
|
||||
@@ -49,8 +51,23 @@ object CallLinks {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isCallLink(url: String): Boolean {
|
||||
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
|
||||
Log.w(TAG, "Invalid url prefix.")
|
||||
return false
|
||||
}
|
||||
|
||||
return url.split("#").last().startsWith("key=")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parseUrl(url: String): CallLinkRootKey? {
|
||||
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
|
||||
Log.w(TAG, "Invalid url prefix.")
|
||||
return null
|
||||
}
|
||||
|
||||
val parts = url.split("#")
|
||||
if (parts.size != 2) {
|
||||
Log.w(TAG, "Invalid fragment delimiter count in url.")
|
||||
@@ -77,7 +94,11 @@ object CallLinks {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO Parse the key into a byte array
|
||||
return null
|
||||
return try {
|
||||
CallLinkRootKey(key)
|
||||
} catch (e: CallException) {
|
||||
Log.w(TAG, "Invalid root key found in fragment query string.")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -49,7 +48,6 @@ import java.time.Instant
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SignalCallRowPreview() {
|
||||
val avatarColor = remember { AvatarColor.random() }
|
||||
val callLink = remember {
|
||||
val credentials = CallLinkCredentials.generate()
|
||||
CallLinkTable.CallLink(
|
||||
@@ -61,8 +59,7 @@ private fun SignalCallRowPreview() {
|
||||
restrictions = org.signal.ringrtc.CallLinkState.Restrictions.NONE,
|
||||
expiration = Instant.MAX,
|
||||
revoked = false
|
||||
),
|
||||
avatarColor = avatarColor
|
||||
)
|
||||
)
|
||||
}
|
||||
SignalTheme(false) {
|
||||
@@ -76,7 +73,7 @@ private fun SignalCallRowPreview() {
|
||||
@Composable
|
||||
fun SignalCallRow(
|
||||
callLink: CallLinkTable.CallLink,
|
||||
onJoinClicked: () -> Unit,
|
||||
onJoinClicked: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
@@ -122,13 +119,15 @@ fun SignalCallRow(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
if (onJoinClicked != null) {
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
Buttons.Small(
|
||||
onClick = onJoinClicked,
|
||||
modifier = Modifier.align(CenterVertically)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))
|
||||
Buttons.Small(
|
||||
onClick = onJoinClicked,
|
||||
modifier = Modifier.align(CenterVertically)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.core.app.ShareCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Rows
|
||||
@@ -86,7 +87,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
) {
|
||||
val callLink: CallLinkTable.CallLink by viewModel.callLink
|
||||
|
||||
Handle(modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||
BottomSheets.Handle(modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
@@ -108,7 +109,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name),
|
||||
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked)
|
||||
onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked
|
||||
)
|
||||
|
||||
Rows.ToggleRow(
|
||||
@@ -123,19 +124,19 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_forward_24),
|
||||
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked)
|
||||
onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_copy_android_24),
|
||||
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked)
|
||||
onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_share_android_24),
|
||||
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked)
|
||||
onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked
|
||||
)
|
||||
|
||||
Buttons.MediumTonal(
|
||||
|
||||
@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.calls.links.create
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
@@ -24,7 +23,7 @@ import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||
class CreateCallLinkRepository(
|
||||
private val callLinkManager: SignalCallLinkManager = ApplicationDependencies.getSignalCallManager().callLinkManager
|
||||
) {
|
||||
fun ensureCallLinkCreated(credentials: CallLinkCredentials, avatarColor: AvatarColor): Single<EnsureCallLinkCreatedResult> {
|
||||
fun ensureCallLinkCreated(credentials: CallLinkCredentials): Single<EnsureCallLinkCreatedResult> {
|
||||
val callLinkRecipientId = Single.fromCallable {
|
||||
SignalDatabase.recipients.getByCallLinkRoomId(credentials.roomId)
|
||||
}
|
||||
@@ -41,8 +40,7 @@ class CreateCallLinkRepository(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = credentials.roomId,
|
||||
credentials = credentials,
|
||||
state = it.state,
|
||||
avatarColor = avatarColor
|
||||
state = it.state
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.ringrtc.CallLinkState.Restrictions
|
||||
import org.thoughtcrime.securesms.calls.links.CallLinks
|
||||
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
@@ -29,7 +28,6 @@ class CreateCallLinkViewModel(
|
||||
private val mutationRepository: UpdateCallLinkRepository = UpdateCallLinkRepository()
|
||||
) : ViewModel() {
|
||||
private val credentials = CallLinkCredentials.generate()
|
||||
private val avatarColor = AvatarColor.random()
|
||||
private val _callLink: MutableState<CallLinkTable.CallLink> = mutableStateOf(
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
@@ -40,8 +38,7 @@ class CreateCallLinkViewModel(
|
||||
restrictions = Restrictions.NONE,
|
||||
revoked = false,
|
||||
expiration = Instant.MAX
|
||||
),
|
||||
avatarColor = avatarColor
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -63,7 +60,7 @@ class CreateCallLinkViewModel(
|
||||
}
|
||||
|
||||
fun commitCallLink(): Single<EnsureCallLinkCreatedResult> {
|
||||
return repository.ensureCallLinkCreated(credentials, avatarColor)
|
||||
return repository.ensureCallLinkCreated(credentials)
|
||||
}
|
||||
|
||||
fun setApproveAllMembers(approveAllMembers: Boolean): Single<UpdateCallLinkResult> {
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -200,8 +199,7 @@ private fun CallLinkDetailsPreview() {
|
||||
revoked = false,
|
||||
restrictions = Restrictions.NONE,
|
||||
expiration = Instant.MAX
|
||||
),
|
||||
avatarColor = avatarColor
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -248,7 +246,7 @@ private fun CallLinkDetails(
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__add_call_name),
|
||||
modifier = Modifier.clickable(onClick = callback::onEditNameClicked)
|
||||
onClick = callback::onEditNameClicked
|
||||
)
|
||||
|
||||
Rows.ToggleRow(
|
||||
@@ -262,14 +260,14 @@ private fun CallLinkDetails(
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__share_link),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_link_24),
|
||||
modifier = Modifier.clickable(onClick = callback::onShareClicked)
|
||||
onClick = callback::onShareClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__delete_call_link),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.symbol_trash_24),
|
||||
foregroundTint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.clickable(onClick = callback::onDeleteClicked)
|
||||
onClick = callback::onDeleteClicked
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,17 +5,15 @@
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.MaybeCompat
|
||||
import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.ReadCallLinkResult
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||
@@ -24,7 +22,7 @@ class CallLinkDetailsRepository(
|
||||
private val callLinkManager: SignalCallLinkManager = ApplicationDependencies.getSignalCallManager().callLinkManager
|
||||
) {
|
||||
fun refreshCallLinkState(callLinkRoomId: CallLinkRoomId): Disposable {
|
||||
return Maybe.fromCallable<CallLinkTable.CallLink> { SignalDatabase.callLinks.getCallLinkByRoomId(callLinkRoomId) }
|
||||
return MaybeCompat.fromCallable { SignalDatabase.callLinks.getCallLinkByRoomId(callLinkRoomId) }
|
||||
.flatMapSingle { callLinkManager.readCallLink(it.credentials!!) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribeBy { result ->
|
||||
@@ -36,7 +34,7 @@ class CallLinkDetailsRepository(
|
||||
}
|
||||
|
||||
fun watchCallLinkRecipient(callLinkRoomId: CallLinkRoomId): Observable<Recipient> {
|
||||
return Maybe.fromCallable<RecipientId> { SignalDatabase.recipients.getByCallLinkRoomId(callLinkRoomId).orNull() }
|
||||
return MaybeCompat.fromCallable { SignalDatabase.recipients.getByCallLinkRoomId(callLinkRoomId).orNull() }
|
||||
.flatMapObservable { Recipient.observable(it) }
|
||||
.distinctUntilChanged { a, b -> a.hasSameContent(b) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
@@ -75,12 +75,10 @@ class CallLogAdapter(
|
||||
fun submitCallRows(
|
||||
rows: List<CallLogRow?>,
|
||||
selectionState: CallLogSelectionState,
|
||||
stagedDeletion: CallLogStagedDeletion?,
|
||||
onCommit: () -> Unit
|
||||
): Int {
|
||||
val filteredRows = rows
|
||||
.filterNotNull()
|
||||
.filterNot { stagedDeletion?.isStagedForDeletion(it.id) == true }
|
||||
.map {
|
||||
when (it) {
|
||||
is CallLogRow.Call -> CallModel(it, selectionState, itemCount)
|
||||
@@ -355,6 +353,7 @@ class CallLogAdapter(
|
||||
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_downleft_compact_16
|
||||
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_compact_16
|
||||
MessageTypes.GROUP_CALL_TYPE -> when {
|
||||
call.type == CallTable.Type.AD_HOC_CALL -> R.drawable.symbol_link_compact_16
|
||||
call.event == CallTable.Event.MISSED -> R.drawable.symbol_missed_incoming_24
|
||||
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_compact_16
|
||||
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_compact_16
|
||||
@@ -376,6 +375,7 @@ class CallLogAdapter(
|
||||
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
|
||||
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
|
||||
MessageTypes.GROUP_CALL_TYPE -> when {
|
||||
call.type == CallTable.Type.AD_HOC_CALL -> R.string.CallLogAdapter__call_link
|
||||
call.event == CallTable.Event.MISSED -> R.string.CallLogAdapter__missed
|
||||
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call
|
||||
call.direction == CallTable.Direction.INCOMING -> R.string.CallLogAdapter__incoming
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.log
|
||||
|
||||
sealed interface CallLogDeletionResult {
|
||||
object Success : CallLogDeletionResult
|
||||
|
||||
object Empty : CallLogDeletionResult
|
||||
data class FailedToRevoke(val failedRevocations: Int) : CallLogDeletionResult
|
||||
data class UnknownFailure(val reason: Throwable) : CallLogDeletionResult
|
||||
}
|
||||
@@ -12,5 +12,10 @@ enum class CallLogFilter {
|
||||
/**
|
||||
* Only missed calls will be displayed
|
||||
*/
|
||||
MISSED
|
||||
MISSED,
|
||||
|
||||
/**
|
||||
* Only ad-hoc calls will be returned
|
||||
*/
|
||||
AD_HOC
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
@@ -27,10 +29,13 @@ import io.reactivex.rxjava3.kotlin.Flowables
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.concurrent.addTo
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
|
||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||
import org.thoughtcrime.securesms.components.Material3SearchToolbar
|
||||
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
|
||||
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
@@ -65,6 +70,10 @@ import java.util.concurrent.TimeUnit
|
||||
@SuppressLint("DiscouragedApi")
|
||||
class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Callbacks, CallLogContextMenu.Callbacks {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(CallLogFragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel: CallLogViewModel by viewModels()
|
||||
private val binding: CallLogFragmentBinding by ViewBinderDelegate(CallLogFragmentBinding::bind)
|
||||
private val disposables = LifecycleDisposable()
|
||||
@@ -84,10 +93,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
val isFiltered = viewModel.filterSnapshot == CallLogFilter.MISSED
|
||||
menu.findItem(R.id.action_clear_missed_call_filter).isVisible = isFiltered
|
||||
menu.findItem(R.id.action_filter_missed_calls).isVisible = !isFiltered
|
||||
menu.findItem(R.id.action_clear_call_history).isVisible = !viewModel.isEmpty
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.action_clear_call_history -> clearCallHistory()
|
||||
R.id.action_settings -> startActivity(AppSettingsActivity.home(requireContext()))
|
||||
R.id.action_notification_profile -> NotificationProfileSelectionFragment.show(parentFragmentManager)
|
||||
R.id.action_filter_missed_calls -> filterMissedCalls()
|
||||
@@ -112,24 +123,23 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
)
|
||||
|
||||
disposables += scrollToPositionDelegate
|
||||
disposables += Flowables.combineLatest(viewModel.data, viewModel.selectedAndStagedDeletion)
|
||||
disposables += Flowables.combineLatest(viewModel.data, viewModel.selected)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { (data, selected) ->
|
||||
val filteredCount = adapter.submitCallRows(
|
||||
data,
|
||||
selected.first,
|
||||
selected.second,
|
||||
selected,
|
||||
scrollToPositionDelegate::notifyListCommitted
|
||||
)
|
||||
binding.emptyState.visible = filteredCount == 0
|
||||
}
|
||||
|
||||
disposables += Flowables.combineLatest(viewModel.selectedAndStagedDeletion, viewModel.totalCount)
|
||||
disposables += Flowables.combineLatest(viewModel.selected, viewModel.totalCount)
|
||||
.distinctUntilChanged()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { (selected, totalCount) ->
|
||||
if (selected.first.isNotEmpty(totalCount)) {
|
||||
callLogActionMode.setCount(selected.first.count(totalCount))
|
||||
if (selected.isNotEmpty(totalCount)) {
|
||||
callLogActionMode.setCount(selected.count(totalCount))
|
||||
} else {
|
||||
callLogActionMode.end()
|
||||
}
|
||||
@@ -220,20 +230,9 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
val count = callLogActionMode.getCount()
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, count, count))
|
||||
.setPositiveButton(R.string.CallLogFragment__delete_for_me) { _, _ ->
|
||||
viewModel.stageSelectionDeletion()
|
||||
.setPositiveButton(R.string.CallLogFragment__delete) { _, _ ->
|
||||
performDeletion(count, viewModel.stageSelectionDeletion())
|
||||
callLogActionMode.end()
|
||||
Snackbar
|
||||
.make(
|
||||
binding.root,
|
||||
resources.getQuantityString(R.plurals.CallLogFragment__d_calls_deleted, count, count),
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
.addCallback(SnackbarDeletionCallback())
|
||||
.setAction(R.string.CallLogFragment__undo) {
|
||||
viewModel.cancelStagedDeletion()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
@@ -270,6 +269,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
scrollToPositionDelegate.resetScrollPosition()
|
||||
}
|
||||
}
|
||||
|
||||
FilterPullState.OPENING -> {
|
||||
ViewUtil.setMinimumHeight(collapsingToolbarLayout, openHeight)
|
||||
viewModel.setFilter(CallLogFilter.MISSED)
|
||||
@@ -363,21 +363,9 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
override fun deleteCall(call: CallLogRow) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, 1, 1))
|
||||
.setPositiveButton(R.string.CallLogFragment__delete_for_me) { _, _ ->
|
||||
viewModel.stageCallDeletion(call)
|
||||
Snackbar
|
||||
.make(
|
||||
binding.root,
|
||||
resources.getQuantityString(R.plurals.CallLogFragment__d_calls_deleted, 1, 1),
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
.addCallback(SnackbarDeletionCallback())
|
||||
.setAction(R.string.CallLogFragment__undo) {
|
||||
viewModel.cancelStagedDeletion()
|
||||
}
|
||||
.show()
|
||||
.setPositiveButton(R.string.CallLogFragment__delete) { _, _ ->
|
||||
performDeletion(1, viewModel.stageCallDeletion(call))
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
@@ -386,6 +374,18 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
binding.recyclerCoordinatorAppBar.setExpanded(false, true)
|
||||
}
|
||||
|
||||
private fun clearCallHistory() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.CallLogFragment__clear_call_history_question)
|
||||
.setMessage(R.string.CallLogFragment__this_will_permanently_delete_all_call_history)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
callLogActionMode.end()
|
||||
performDeletion(-1, viewModel.stageDeleteAll())
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun isSearchOpen(): Boolean {
|
||||
return isSearchVisible() || viewModel.hasSearchQuery
|
||||
}
|
||||
@@ -401,7 +401,59 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
|
||||
private fun isSearchVisible(): Boolean {
|
||||
return requireListener<SearchBinder>().getSearchToolbar().resolved() &&
|
||||
requireListener<SearchBinder>().getSearchToolbar().get().getVisibility() == View.VISIBLE
|
||||
requireListener<SearchBinder>().getSearchToolbar().get().visibility == View.VISIBLE
|
||||
}
|
||||
|
||||
private fun performDeletion(count: Int, callLogStagedDeletion: CallLogStagedDeletion) {
|
||||
var progressDialog: ProgressCardDialogFragment? = null
|
||||
var errorDialog: AlertDialog? = null
|
||||
|
||||
fun cleanUp() {
|
||||
progressDialog?.dismissAllowingStateLoss()
|
||||
progressDialog = null
|
||||
errorDialog?.dismiss()
|
||||
errorDialog = null
|
||||
}
|
||||
|
||||
val snackbarMessage = if (count == -1) {
|
||||
getString(R.string.CallLogFragment__cleared_call_history)
|
||||
} else {
|
||||
resources.getQuantityString(R.plurals.CallLogFragment__d_calls_deleted, count, count)
|
||||
}
|
||||
|
||||
viewModel.delete(callLogStagedDeletion)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnSubscribe {
|
||||
progressDialog = ProgressCardDialogFragment.create(getString(R.string.CallLogFragment__deleting))
|
||||
progressDialog?.show(parentFragmentManager, null)
|
||||
}
|
||||
.doOnDispose { cleanUp() }
|
||||
.subscribeBy {
|
||||
cleanUp()
|
||||
when (it) {
|
||||
CallLogDeletionResult.Empty -> Unit
|
||||
is CallLogDeletionResult.FailedToRevoke -> {
|
||||
errorDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(resources.getQuantityString(R.plurals.CallLogFragment__cant_delete_call_link, it.failedRevocations))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
CallLogDeletionResult.Success -> {
|
||||
Snackbar
|
||||
.make(
|
||||
binding.root,
|
||||
snackbarMessage,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
is CallLogDeletionResult.UnknownFailure -> {
|
||||
Log.w(TAG, "Deletion failed.", it.reason)
|
||||
Toast.makeText(requireContext(), R.string.CallLogFragment__deletion_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.addTo(disposables)
|
||||
}
|
||||
|
||||
private inner class BottomActionBarControllerCallback : SignalBottomActionBarController.Callback {
|
||||
@@ -429,12 +481,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SnackbarDeletionCallback : Snackbar.Callback() {
|
||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
||||
viewModel.commitStagedDeletion()
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onMultiSelectStarted()
|
||||
fun onMultiSelectFinished()
|
||||
|
||||
@@ -2,13 +2,20 @@ package org.thoughtcrime.securesms.calls.log
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.CallLinkPeekJob
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
|
||||
class CallLogRepository : CallLogPagedDataSource.CallRepository {
|
||||
class CallLogRepository(
|
||||
private val updateCallLinkRepository: UpdateCallLinkRepository = UpdateCallLinkRepository()
|
||||
) : CallLogPagedDataSource.CallRepository {
|
||||
override fun getCallsCount(query: String?, filter: CallLogFilter): Int {
|
||||
return SignalDatabase.calls.getCallsCount(query, filter)
|
||||
}
|
||||
@@ -20,14 +27,14 @@ class CallLogRepository : CallLogPagedDataSource.CallRepository {
|
||||
override fun getCallLinksCount(query: String?, filter: CallLogFilter): Int {
|
||||
return when (filter) {
|
||||
CallLogFilter.MISSED -> 0
|
||||
CallLogFilter.ALL -> SignalDatabase.callLinks.getCallLinksCount(query)
|
||||
CallLogFilter.ALL, CallLogFilter.AD_HOC -> SignalDatabase.callLinks.getCallLinksCount(query)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCallLinks(query: String?, filter: CallLogFilter, start: Int, length: Int): List<CallLogRow> {
|
||||
return when (filter) {
|
||||
CallLogFilter.MISSED -> emptyList()
|
||||
CallLogFilter.ALL -> SignalDatabase.callLinks.getCallLinks(query, start, length)
|
||||
CallLogFilter.ALL, CallLogFilter.AD_HOC -> SignalDatabase.callLinks.getCallLinks(query, start, length)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +67,8 @@ class CallLogRepository : CallLogPagedDataSource.CallRepository {
|
||||
selectedCallRowIds: Set<Long>
|
||||
): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.calls.deleteCallEvents(selectedCallRowIds)
|
||||
}.observeOn(Schedulers.io())
|
||||
SignalDatabase.calls.deleteNonAdHocCallEvents(selectedCallRowIds)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun deleteAllCallLogsExcept(
|
||||
@@ -69,7 +76,88 @@ class CallLogRepository : CallLogPagedDataSource.CallRepository {
|
||||
missedOnly: Boolean
|
||||
): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.calls.deleteAllCallEventsExcept(selectedCallRowIds, missedOnly)
|
||||
}.observeOn(Schedulers.io())
|
||||
SignalDatabase.calls.deleteAllNonAdHocCallEventsExcept(selectedCallRowIds, missedOnly)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the selected call links. We DELETE those links we don't have admin keys for,
|
||||
* and revoke the ones we *do* have admin keys for. We then perform a cleanup step on
|
||||
* terminate to clean up call events.
|
||||
*/
|
||||
fun deleteSelectedCallLinks(
|
||||
selectedCallRowIds: Set<Long>,
|
||||
selectedRoomIds: Set<CallLinkRoomId>
|
||||
): Single<Int> {
|
||||
return Single.fromCallable {
|
||||
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinks(allCallLinkIds)
|
||||
SignalDatabase.callLinks.getAdminCallLinks(allCallLinkIds)
|
||||
}.flatMap { callLinksToRevoke ->
|
||||
Single.merge(
|
||||
callLinksToRevoke.map {
|
||||
updateCallLinkRepository.revokeCallLink(it.credentials!!)
|
||||
}
|
||||
).reduce(0) { acc, current ->
|
||||
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
|
||||
}
|
||||
}.doOnTerminate {
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||
}.doOnDispose {
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all but the selected call links. We DELETE those links we don't have admin keys for,
|
||||
* and revoke the ones we *do* have admin keys for. We then perform a cleanup step on
|
||||
* terminate to clean up call events.
|
||||
*/
|
||||
fun deleteAllCallLinksExcept(
|
||||
selectedCallRowIds: Set<Long>,
|
||||
selectedRoomIds: Set<CallLinkRoomId>
|
||||
): Single<Int> {
|
||||
return Single.fromCallable {
|
||||
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
|
||||
SignalDatabase.callLinks.deleteAllNonAdminCallLinksExcept(allCallLinkIds)
|
||||
SignalDatabase.callLinks.getAllAdminCallLinksExcept(allCallLinkIds)
|
||||
}.flatMap { callLinksToRevoke ->
|
||||
Single.merge(
|
||||
callLinksToRevoke.map {
|
||||
updateCallLinkRepository.revokeCallLink(it.credentials!!)
|
||||
}
|
||||
).reduce(0) { acc, current ->
|
||||
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
|
||||
}
|
||||
}.doOnTerminate {
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||
}.doOnDispose {
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun peekCallLinks(): Completable {
|
||||
return Completable.fromAction {
|
||||
val callLinks: List<CallLogRow.CallLink> = SignalDatabase.callLinks.getCallLinks(
|
||||
query = null,
|
||||
offset = 0,
|
||||
limit = 10
|
||||
)
|
||||
|
||||
val callEvents: List<CallLogRow.Call> = SignalDatabase.calls.getCalls(
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
searchTerm = null,
|
||||
filter = CallLogFilter.AD_HOC
|
||||
)
|
||||
|
||||
val recipients = (callLinks.map { it.recipient } + callEvents.map { it.peer }).toSet()
|
||||
|
||||
val jobs = recipients.take(10).map {
|
||||
CallLinkPeekJob(it.id)
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().addAll(jobs)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.webrtc.CallLinkPeekInfo
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
|
||||
/**
|
||||
@@ -25,6 +26,7 @@ sealed class CallLogRow {
|
||||
val record: CallLinkTable.CallLink,
|
||||
val recipient: Recipient,
|
||||
val searchQuery: String?,
|
||||
val callLinkPeekInfo: CallLinkPeekInfo?,
|
||||
override val id: Id = Id.CallLink(record.roomId)
|
||||
) : CallLogRow()
|
||||
|
||||
@@ -38,6 +40,7 @@ sealed class CallLogRow {
|
||||
val groupCallState: GroupCallState,
|
||||
val children: Set<Long>,
|
||||
val searchQuery: String?,
|
||||
val callLinkPeekInfo: CallLinkPeekInfo?,
|
||||
override val id: Id = Id.Call(children)
|
||||
) : CallLogRow()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.calls.log
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
|
||||
/**
|
||||
* Encapsulates a single deletion action
|
||||
@@ -13,19 +14,13 @@ class CallLogStagedDeletion(
|
||||
|
||||
private var isCommitted = false
|
||||
|
||||
fun isStagedForDeletion(id: CallLogRow.Id): Boolean {
|
||||
return stateSnapshot.contains(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Single<Int> which contains the number of failed call-link revocations.
|
||||
*/
|
||||
@MainThread
|
||||
fun cancel() {
|
||||
isCommitted = true
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun commit() {
|
||||
fun commit(): Single<Int> {
|
||||
if (isCommitted) {
|
||||
return
|
||||
return Single.just(0)
|
||||
}
|
||||
|
||||
isCommitted = true
|
||||
@@ -35,10 +30,19 @@ class CallLogStagedDeletion(
|
||||
.flatten()
|
||||
.toSet()
|
||||
|
||||
if (stateSnapshot.isExclusionary()) {
|
||||
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).subscribe()
|
||||
val callLinkIds = stateSnapshot.selected()
|
||||
.filterIsInstance<CallLogRow.Id.CallLink>()
|
||||
.map { it.roomId }
|
||||
.toSet()
|
||||
|
||||
return if (stateSnapshot.isExclusionary()) {
|
||||
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).andThen(
|
||||
repository.deleteAllCallLinksExcept(callRowIds, callLinkIds)
|
||||
)
|
||||
} else {
|
||||
repository.deleteSelectedCallLogs(callRowIds).subscribe()
|
||||
repository.deleteSelectedCallLogs(callRowIds).andThen(
|
||||
repository.deleteSelectedCallLinks(callRowIds, callLinkIds)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
package org.thoughtcrime.securesms.calls.log
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.paging.ObservablePagedData
|
||||
import org.signal.paging.PagedData
|
||||
import org.signal.paging.PagingConfig
|
||||
import org.signal.paging.ProxyPagingController
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* ViewModel for call log management.
|
||||
@@ -31,12 +38,14 @@ class CallLogViewModel(
|
||||
|
||||
val controller = ProxyPagingController<CallLogRow.Id>()
|
||||
val data: Flowable<MutableList<CallLogRow?>> = pagedData.switchMap { it.data.toFlowable(BackpressureStrategy.LATEST) }
|
||||
val selectedAndStagedDeletion: Flowable<Pair<CallLogSelectionState, CallLogStagedDeletion?>> = callLogStore
|
||||
.stateFlowable
|
||||
.map { it.selectionState to it.stagedDeletion }
|
||||
val selected: Flowable<CallLogSelectionState> = callLogStore.stateFlowable.map { it.selectionState }
|
||||
|
||||
private val _isEmpty: BehaviorProcessor<Boolean> = BehaviorProcessor.createDefault(false)
|
||||
val isEmpty: Boolean get() = _isEmpty.value ?: false
|
||||
|
||||
val totalCount: Flowable<Int> = Flowable.combineLatest(distinctQueryFilterPairs, data) { a, _ -> a }
|
||||
.map { (query, filter) -> callLogRepository.getCallsCount(query, filter) }
|
||||
.doOnNext { _isEmpty.onNext(it <= 0) }
|
||||
|
||||
val selectionStateSnapshot: CallLogSelectionState
|
||||
get() = callLogStore.state.selectionState
|
||||
@@ -70,10 +79,25 @@ class CallLogViewModel(
|
||||
disposables += callLogRepository.listenForChanges().subscribe {
|
||||
controller.onDataInvalidated()
|
||||
}
|
||||
|
||||
if (FeatureFlags.adHocCalling()) {
|
||||
disposables += Observable
|
||||
.interval(30, TimeUnit.SECONDS, Schedulers.computation())
|
||||
.flatMapCompletable { callLogRepository.peekCallLinks() }
|
||||
.subscribe()
|
||||
|
||||
disposables += ApplicationDependencies
|
||||
.getSignalCallManager()
|
||||
.peekInfoCache
|
||||
.observeOn(Schedulers.computation())
|
||||
.distinctUntilChanged()
|
||||
.subscribe {
|
||||
controller.onDataInvalidated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
commitStagedDeletion()
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
@@ -96,49 +120,52 @@ class CallLogViewModel(
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun stageCallDeletion(call: CallLogRow) {
|
||||
callLogStore.state.stagedDeletion?.commit()
|
||||
callLogStore.update {
|
||||
it.copy(
|
||||
stagedDeletion = CallLogStagedDeletion(
|
||||
it.filter,
|
||||
CallLogSelectionState.empty().toggle(call.id),
|
||||
callLogRepository
|
||||
)
|
||||
)
|
||||
}
|
||||
fun stageCallDeletion(call: CallLogRow): CallLogStagedDeletion {
|
||||
return CallLogStagedDeletion(
|
||||
callLogStore.state.filter,
|
||||
CallLogSelectionState.empty().toggle(call.id),
|
||||
callLogRepository
|
||||
)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun stageSelectionDeletion() {
|
||||
callLogStore.state.stagedDeletion?.commit()
|
||||
callLogStore.update {
|
||||
it.copy(
|
||||
stagedDeletion = CallLogStagedDeletion(
|
||||
it.filter,
|
||||
it.selectionState,
|
||||
callLogRepository
|
||||
)
|
||||
)
|
||||
}
|
||||
fun stageSelectionDeletion(): CallLogStagedDeletion {
|
||||
return CallLogStagedDeletion(
|
||||
callLogStore.state.filter,
|
||||
callLogStore.state.selectionState,
|
||||
callLogRepository
|
||||
)
|
||||
}
|
||||
|
||||
fun commitStagedDeletion() {
|
||||
callLogStore.state.stagedDeletion?.commit()
|
||||
fun stageDeleteAll(): CallLogStagedDeletion {
|
||||
callLogStore.update {
|
||||
it.copy(
|
||||
stagedDeletion = null
|
||||
selectionState = CallLogSelectionState.empty()
|
||||
)
|
||||
}
|
||||
|
||||
return CallLogStagedDeletion(
|
||||
callLogStore.state.filter,
|
||||
CallLogSelectionState.selectAll(),
|
||||
callLogRepository
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelStagedDeletion() {
|
||||
callLogStore.state.stagedDeletion?.cancel()
|
||||
callLogStore.update {
|
||||
it.copy(
|
||||
stagedDeletion = null
|
||||
)
|
||||
}
|
||||
@SuppressLint("CheckResult")
|
||||
fun delete(stagedDeletion: CallLogStagedDeletion): Maybe<CallLogDeletionResult> {
|
||||
return stagedDeletion.commit()
|
||||
.doOnSubscribe {
|
||||
clearSelected()
|
||||
}
|
||||
.map { failedRevocations ->
|
||||
if (failedRevocations == 0) {
|
||||
CallLogDeletionResult.Success
|
||||
} else {
|
||||
CallLogDeletionResult.FailedToRevoke(failedRevocations)
|
||||
}
|
||||
}
|
||||
.onErrorReturn { CallLogDeletionResult.UnknownFailure(it) }
|
||||
.toMaybe()
|
||||
}
|
||||
|
||||
fun clearSelected() {
|
||||
@@ -158,7 +185,6 @@ class CallLogViewModel(
|
||||
private data class CallLogState(
|
||||
val query: String? = null,
|
||||
val filter: CallLogFilter = CallLogFilter.ALL,
|
||||
val selectionState: CallLogSelectionState = CallLogSelectionState.empty(),
|
||||
val stagedDeletion: CallLogStagedDeletion? = null
|
||||
val selectionState: CallLogSelectionState = CallLogSelectionState.empty()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.Bundle;
|
||||
import android.text.Annotation;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.Selection;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -22,6 +23,7 @@ import android.view.MenuItem;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
@@ -71,6 +73,7 @@ public class ComposeText extends EmojiEditText {
|
||||
@Nullable private InputPanel.MediaListener mediaListener;
|
||||
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
|
||||
@Nullable private InlineQueryChangedListener inlineQueryChangedListener;
|
||||
@Nullable private StylingChangedListener stylingChangedListener;
|
||||
|
||||
public ComposeText(Context context) {
|
||||
super(context);
|
||||
@@ -191,6 +194,11 @@ public class ComposeText extends EmojiEditText {
|
||||
setHintWithChecks(hint);
|
||||
}
|
||||
|
||||
public void setDraftText(@Nullable CharSequence draftText) {
|
||||
setText("");
|
||||
append(draftText);
|
||||
}
|
||||
|
||||
public void appendInvite(String invite) {
|
||||
if (getText() == null) {
|
||||
return;
|
||||
@@ -216,6 +224,10 @@ public class ComposeText extends EmojiEditText {
|
||||
mentionValidatorWatcher.setMentionValidator(mentionValidator);
|
||||
}
|
||||
|
||||
public void setStylingChangedListener(@Nullable StylingChangedListener listener) {
|
||||
stylingChangedListener = listener;
|
||||
}
|
||||
|
||||
private boolean isLandscape() {
|
||||
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
@@ -279,15 +291,11 @@ public class ComposeText extends EmojiEditText {
|
||||
|
||||
public boolean hasStyling() {
|
||||
CharSequence trimmed = getTextTrimmed();
|
||||
return FeatureFlags.textFormatting() && (trimmed instanceof Spanned) && MessageStyler.hasStyling((Spanned) trimmed);
|
||||
return (trimmed instanceof Spanned) && MessageStyler.hasStyling((Spanned) trimmed);
|
||||
}
|
||||
|
||||
public @Nullable BodyRangeList getStyling() {
|
||||
if (FeatureFlags.textFormatting()) {
|
||||
return MessageStyler.getStyling(getTextTrimmed());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return MessageStyler.getStyling(getTextTrimmed());
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
@@ -301,87 +309,57 @@ public class ComposeText extends EmojiEditText {
|
||||
mentionValidatorWatcher = new MentionValidatorWatcher();
|
||||
addTextChangedListener(mentionValidatorWatcher);
|
||||
|
||||
if (FeatureFlags.textFormatting()) {
|
||||
spoilerRendererDelegate = new SpoilerRendererDelegate(this, true);
|
||||
spoilerRendererDelegate = new SpoilerRendererDelegate(this, true);
|
||||
|
||||
addTextChangedListener(new ComposeTextStyleWatcher());
|
||||
addTextChangedListener(new ComposeTextStyleWatcher());
|
||||
|
||||
setCustomSelectionActionModeCallback(new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
MenuItem copy = menu.findItem(android.R.id.copy);
|
||||
MenuItem cut = menu.findItem(android.R.id.cut);
|
||||
MenuItem paste = menu.findItem(android.R.id.paste);
|
||||
int copyOrder = copy != null ? copy.getOrder() : 0;
|
||||
int cutOrder = cut != null ? cut.getOrder() : 0;
|
||||
int pasteOrder = paste != null ? paste.getOrder() : 0;
|
||||
int largestOrder = Math.max(copyOrder, Math.max(cutOrder, pasteOrder));
|
||||
setCustomSelectionActionModeCallback(new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
MenuItem copy = menu.findItem(android.R.id.copy);
|
||||
MenuItem cut = menu.findItem(android.R.id.cut);
|
||||
MenuItem paste = menu.findItem(android.R.id.paste);
|
||||
int copyOrder = copy != null ? copy.getOrder() : 0;
|
||||
int cutOrder = cut != null ? cut.getOrder() : 0;
|
||||
int pasteOrder = paste != null ? paste.getOrder() : 0;
|
||||
int largestOrder = Math.max(copyOrder, Math.max(cutOrder, pasteOrder));
|
||||
|
||||
menu.add(0, R.id.edittext_bold, largestOrder, getContext().getString(R.string.TextFormatting_bold));
|
||||
menu.add(0, R.id.edittext_italic, largestOrder, getContext().getString(R.string.TextFormatting_italic));
|
||||
menu.add(0, R.id.edittext_strikethrough, largestOrder, getContext().getString(R.string.TextFormatting_strikethrough));
|
||||
menu.add(0, R.id.edittext_monospace, largestOrder, getContext().getString(R.string.TextFormatting_monospace));
|
||||
menu.add(0, R.id.edittext_spoiler, largestOrder, getContext().getString(R.string.TextFormatting_spoiler));
|
||||
menu.add(0, R.id.edittext_bold, largestOrder, getContext().getString(R.string.TextFormatting_bold));
|
||||
menu.add(0, R.id.edittext_italic, largestOrder, getContext().getString(R.string.TextFormatting_italic));
|
||||
menu.add(0, R.id.edittext_strikethrough, largestOrder, getContext().getString(R.string.TextFormatting_strikethrough));
|
||||
menu.add(0, R.id.edittext_monospace, largestOrder, getContext().getString(R.string.TextFormatting_monospace));
|
||||
menu.add(0, R.id.edittext_spoiler, largestOrder, getContext().getString(R.string.TextFormatting_spoiler));
|
||||
|
||||
return true;
|
||||
Editable text = getText();
|
||||
|
||||
if (text != null) {
|
||||
int start = getSelectionStart();
|
||||
int end = getSelectionEnd();
|
||||
if (MessageStyler.hasStyling(text, start, end)) {
|
||||
menu.add(0, R.id.edittext_clear_formatting, largestOrder, getContext().getString(R.string.TextFormatting_clear_formatting));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
Editable text = getText();
|
||||
|
||||
if (text == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.getItemId() != R.id.edittext_bold &&
|
||||
item.getItemId() != R.id.edittext_italic &&
|
||||
item.getItemId() != R.id.edittext_strikethrough &&
|
||||
item.getItemId() != R.id.edittext_monospace &&
|
||||
item.getItemId() != R.id.edittext_spoiler) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = getSelectionStart();
|
||||
int end = getSelectionEnd();
|
||||
|
||||
CharSequence charSequence = text.subSequence(start, end);
|
||||
SpannableString replacement = new SpannableString(charSequence);
|
||||
Object style = null;
|
||||
|
||||
if (item.getItemId() == R.id.edittext_bold) {
|
||||
style = MessageStyler.boldStyle();
|
||||
} else if (item.getItemId() == R.id.edittext_italic) {
|
||||
style = MessageStyler.italicStyle();
|
||||
} else if (item.getItemId() == R.id.edittext_strikethrough) {
|
||||
style = MessageStyler.strikethroughStyle();
|
||||
} else if (item.getItemId() == R.id.edittext_monospace) {
|
||||
style = MessageStyler.monoStyle();
|
||||
} else if (item.getItemId() == R.id.edittext_spoiler) {
|
||||
style = MessageStyler.spoilerStyle(MessageStyler.COMPOSE_ID, start, charSequence.length());
|
||||
}
|
||||
|
||||
if (style != null) {
|
||||
replacement.setSpan(style, 0, charSequence.length(), MessageStyler.SPAN_FLAGS);
|
||||
}
|
||||
|
||||
clearComposingText();
|
||||
|
||||
text.replace(start, end, replacement);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
boolean handled = handleFormatText(item.getItemId());
|
||||
if (handled) {
|
||||
mode.finish();
|
||||
return true;
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {}
|
||||
});
|
||||
}
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {}
|
||||
});
|
||||
}
|
||||
|
||||
private void setHintWithChecks(@Nullable CharSequence newHint) {
|
||||
@@ -559,6 +537,60 @@ public class ComposeText extends EmojiEditText {
|
||||
return TIME_PATTERN.matcher(text.subSequence(startOfToken, endOfToken)).find();
|
||||
}
|
||||
|
||||
public boolean isTextHighlighted() {
|
||||
return getText() != null && getSelectionStart() < getSelectionEnd();
|
||||
}
|
||||
|
||||
public boolean handleFormatText(@IdRes int id) {
|
||||
Editable text = getText();
|
||||
|
||||
if (text == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (id != R.id.edittext_bold &&
|
||||
id != R.id.edittext_italic &&
|
||||
id != R.id.edittext_strikethrough &&
|
||||
id != R.id.edittext_monospace &&
|
||||
id != R.id.edittext_spoiler &&
|
||||
id != R.id.edittext_clear_formatting)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = getSelectionStart();
|
||||
int end = getSelectionEnd();
|
||||
BodyRangeList.BodyRange.Style style = null;
|
||||
|
||||
if (id == R.id.edittext_bold) {
|
||||
style = BodyRangeList.BodyRange.Style.BOLD;
|
||||
} else if (id == R.id.edittext_italic) {
|
||||
style = BodyRangeList.BodyRange.Style.ITALIC;
|
||||
} else if (id == R.id.edittext_strikethrough) {
|
||||
style = BodyRangeList.BodyRange.Style.STRIKETHROUGH;
|
||||
} else if (id == R.id.edittext_monospace) {
|
||||
style = BodyRangeList.BodyRange.Style.MONOSPACE;
|
||||
} else if (id == R.id.edittext_spoiler) {
|
||||
style = BodyRangeList.BodyRange.Style.SPOILER;
|
||||
}
|
||||
|
||||
clearComposingText();
|
||||
|
||||
if (style != null) {
|
||||
MessageStyler.toggleStyle(style, text, start, end);
|
||||
} else {
|
||||
MessageStyler.clearStyling(text, start, end);
|
||||
}
|
||||
|
||||
Selection.setSelection(getText(), end);
|
||||
|
||||
if (stylingChangedListener != null) {
|
||||
stylingChangedListener.onStylingChanged();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
|
||||
|
||||
private static final String TAG = Log.tag(CommitContentListener.class);
|
||||
@@ -605,4 +637,7 @@ public class ComposeText extends EmojiEditText {
|
||||
void onCursorPositionChanged(int start, int end);
|
||||
}
|
||||
|
||||
public interface StylingChangedListener {
|
||||
void onStylingChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,17 @@ class ComposeTextStyleWatcher : TextWatcher {
|
||||
|
||||
try {
|
||||
if (editStart < 0 || editEnd < 0 || editStart >= editEnd || (editStart == 0 && editEnd == s.length)) {
|
||||
textSnapshotPriorToChange = null
|
||||
return
|
||||
}
|
||||
|
||||
val change = s.subSequence(editStart, editEnd)
|
||||
if (change.isEmpty() || textSnapshotPriorToChange == null || (editEnd - editStart == 1 && !StringUtil.isVisuallyEmpty(change[0])) || TextUtils.equals(textSnapshotPriorToChange, change)) {
|
||||
if (change.isEmpty() ||
|
||||
textSnapshotPriorToChange == null ||
|
||||
(editEnd - editStart == 1 && !StringUtil.isVisuallyEmpty(change[0])) ||
|
||||
TextUtils.equals(textSnapshotPriorToChange, change) ||
|
||||
editEnd - editStart > 1
|
||||
) {
|
||||
textSnapshotPriorToChange = null
|
||||
return
|
||||
}
|
||||
|
||||
@@ -320,8 +320,14 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
} else if (MessageRecordUtil.isScheduled(messageRecord)) {
|
||||
dateView.setText(DateUtils.getOnlyTimeString(getContext(), locale, ((MediaMmsMessageRecord) messageRecord).getScheduledDate()));
|
||||
} else {
|
||||
String date = DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp());
|
||||
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord instanceof MediaMmsMessageRecord && ((MediaMmsMessageRecord) messageRecord).isEditMessage()) {
|
||||
long timestamp = messageRecord.getTimestamp();
|
||||
if (messageRecord.isEditMessage()) {
|
||||
if (displayMode == ConversationItemDisplayMode.EDIT_HISTORY) {
|
||||
timestamp = messageRecord.getDateSent();
|
||||
}
|
||||
}
|
||||
String date = DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, timestamp);
|
||||
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord.isEditMessage()) {
|
||||
date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date);
|
||||
}
|
||||
dateView.setText(date);
|
||||
|
||||
@@ -2,26 +2,32 @@ package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.view.animation.RotateAnimation;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class DeliveryStatusView extends FrameLayout {
|
||||
public class DeliveryStatusView extends AppCompatImageView {
|
||||
|
||||
private static final String TAG = Log.tag(DeliveryStatusView.class);
|
||||
private static final String STATE_KEY = "DeliveryStatusView.STATE";
|
||||
private static final String ROOT_KEY = "DeliveryStatusView.ROOT";
|
||||
|
||||
private final RotateAnimation rotationAnimation;
|
||||
private final ImageView pendingIndicator;
|
||||
private final ImageView sentIndicator;
|
||||
private final ImageView deliveredIndicator;
|
||||
private final ImageView readIndicator;
|
||||
private final int horizontalPadding = (int) DimensionUnit.DP.toPixels(2);
|
||||
|
||||
private RotateAnimation rotationAnimation;
|
||||
|
||||
private State state = State.NONE;
|
||||
|
||||
public DeliveryStatusView(Context context) {
|
||||
this(context, null);
|
||||
@@ -34,75 +40,157 @@ public class DeliveryStatusView extends FrameLayout {
|
||||
public DeliveryStatusView(final Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
inflate(context, R.layout.delivery_status_view, this);
|
||||
|
||||
this.deliveredIndicator = findViewById(R.id.delivered_indicator);
|
||||
this.sentIndicator = findViewById(R.id.sent_indicator);
|
||||
this.pendingIndicator = findViewById(R.id.pending_indicator);
|
||||
this.readIndicator = findViewById(R.id.read_indicator);
|
||||
|
||||
rotationAnimation = new RotateAnimation(0, 360f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
rotationAnimation.setInterpolator(new LinearInterpolator());
|
||||
rotationAnimation.setDuration(1500);
|
||||
rotationAnimation.setRepeatCount(Animation.INFINITE);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DeliveryStatusView, 0, 0);
|
||||
setTint(typedArray.getColor(R.styleable.DeliveryStatusView_iconColor, getResources().getColor(R.color.core_white)));
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
setNone();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state) {
|
||||
if (state instanceof Bundle) {
|
||||
Bundle stateBundle = (Bundle) state;
|
||||
State s = State.fromCode(stateBundle.getInt(STATE_KEY, State.NONE.code));
|
||||
|
||||
switch (s) {
|
||||
case NONE:
|
||||
setNone();
|
||||
break;
|
||||
case PENDING:
|
||||
setPending();
|
||||
break;
|
||||
case SENT:
|
||||
setSent();
|
||||
break;
|
||||
case DELIVERED:
|
||||
setDelivered();
|
||||
break;
|
||||
case READ:
|
||||
setRead();
|
||||
break;
|
||||
}
|
||||
|
||||
Parcelable root = stateBundle.getParcelable(ROOT_KEY);
|
||||
super.onRestoreInstanceState(root);
|
||||
} else {
|
||||
super.onRestoreInstanceState(state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Parcelable onSaveInstanceState() {
|
||||
Parcelable root = super.onSaveInstanceState();
|
||||
Bundle stateBundle = new Bundle();
|
||||
|
||||
stateBundle.putParcelable(ROOT_KEY, root);
|
||||
stateBundle.putInt(STATE_KEY, state.code);
|
||||
|
||||
return stateBundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
if (state == State.PENDING && rotationAnimation == null) {
|
||||
final float pivotXValue;
|
||||
if (ViewUtil.isLtr(this)) {
|
||||
pivotXValue = (w - getPaddingEnd()) / 2f;
|
||||
} else {
|
||||
pivotXValue = ((w - getPaddingEnd()) / 2f) + getPaddingEnd();
|
||||
}
|
||||
|
||||
final float pivotYValue = (h - getPaddingTop() - getPaddingBottom()) / 2f;
|
||||
|
||||
rotationAnimation = new RotateAnimation(0, 360f,
|
||||
Animation.ABSOLUTE, pivotXValue,
|
||||
Animation.ABSOLUTE, pivotYValue);
|
||||
|
||||
rotationAnimation.setInterpolator(new LinearInterpolator());
|
||||
rotationAnimation.setDuration(1500);
|
||||
rotationAnimation.setRepeatCount(Animation.INFINITE);
|
||||
|
||||
startAnimation(rotationAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAnimation() {
|
||||
super.clearAnimation();
|
||||
rotationAnimation = null;
|
||||
}
|
||||
|
||||
public void setNone() {
|
||||
this.setVisibility(View.GONE);
|
||||
state = State.NONE;
|
||||
clearAnimation();
|
||||
setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return pendingIndicator.getVisibility() == View.VISIBLE;
|
||||
return state == State.PENDING;
|
||||
}
|
||||
|
||||
public void setPending() {
|
||||
this.setVisibility(View.VISIBLE);
|
||||
pendingIndicator.setVisibility(View.VISIBLE);
|
||||
pendingIndicator.startAnimation(rotationAnimation);
|
||||
sentIndicator.setVisibility(View.GONE);
|
||||
deliveredIndicator.setVisibility(View.GONE);
|
||||
readIndicator.setVisibility(View.GONE);
|
||||
state = State.PENDING;
|
||||
setVisibility(View.VISIBLE);
|
||||
ViewUtil.setPaddingStart(this, 0);
|
||||
ViewUtil.setPaddingEnd(this, horizontalPadding);
|
||||
setImageResource(R.drawable.ic_delivery_status_sending);
|
||||
}
|
||||
|
||||
public void setSent() {
|
||||
this.setVisibility(View.VISIBLE);
|
||||
pendingIndicator.setVisibility(View.GONE);
|
||||
pendingIndicator.clearAnimation();
|
||||
sentIndicator.setVisibility(View.VISIBLE);
|
||||
deliveredIndicator.setVisibility(View.GONE);
|
||||
readIndicator.setVisibility(View.GONE);
|
||||
state = State.SENT;
|
||||
setVisibility(View.VISIBLE);
|
||||
ViewUtil.setPaddingStart(this, horizontalPadding);
|
||||
ViewUtil.setPaddingEnd(this, 0);
|
||||
clearAnimation();
|
||||
setImageResource(R.drawable.ic_delivery_status_sent);
|
||||
}
|
||||
|
||||
public void setDelivered() {
|
||||
this.setVisibility(View.VISIBLE);
|
||||
pendingIndicator.setVisibility(View.GONE);
|
||||
pendingIndicator.clearAnimation();
|
||||
sentIndicator.setVisibility(View.GONE);
|
||||
deliveredIndicator.setVisibility(View.VISIBLE);
|
||||
readIndicator.setVisibility(View.GONE);
|
||||
state = State.DELIVERED;
|
||||
setVisibility(View.VISIBLE);
|
||||
ViewUtil.setPaddingStart(this, horizontalPadding);
|
||||
ViewUtil.setPaddingEnd(this, 0);
|
||||
clearAnimation();
|
||||
setImageResource(R.drawable.ic_delivery_status_delivered);
|
||||
}
|
||||
|
||||
public void setRead() {
|
||||
this.setVisibility(View.VISIBLE);
|
||||
pendingIndicator.setVisibility(View.GONE);
|
||||
pendingIndicator.clearAnimation();
|
||||
sentIndicator.setVisibility(View.GONE);
|
||||
deliveredIndicator.setVisibility(View.GONE);
|
||||
readIndicator.setVisibility(View.VISIBLE);
|
||||
state = State.READ;
|
||||
setVisibility(View.VISIBLE);
|
||||
ViewUtil.setPaddingStart(this, horizontalPadding);
|
||||
ViewUtil.setPaddingEnd(this, 0);
|
||||
clearAnimation();
|
||||
setImageResource(R.drawable.ic_delivery_status_read);
|
||||
}
|
||||
|
||||
public void setTint(int color) {
|
||||
pendingIndicator.setColorFilter(color);
|
||||
deliveredIndicator.setColorFilter(color);
|
||||
sentIndicator.setColorFilter(color);
|
||||
readIndicator.setColorFilter(color);
|
||||
setColorFilter(color);
|
||||
}
|
||||
|
||||
private enum State {
|
||||
NONE(0),
|
||||
PENDING(1),
|
||||
SENT(2),
|
||||
DELIVERED(3),
|
||||
READ(4);
|
||||
|
||||
final int code;
|
||||
|
||||
State(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
static State fromCode(int code) {
|
||||
for (State state : State.values()) {
|
||||
if (state.code == code) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
|
||||
private var inputId: Int? = null
|
||||
private var input: Fragment? = null
|
||||
|
||||
val isInputShowing: Boolean
|
||||
get() = input != null
|
||||
|
||||
lateinit var fragmentManager: FragmentManager
|
||||
var listener: Listener? = null
|
||||
|
||||
@@ -34,10 +37,13 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
|
||||
hideInput(resetKeyboardGuideline = false)
|
||||
}
|
||||
|
||||
fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, toggled: (Boolean) -> Unit = { }) {
|
||||
fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, showSoftKeyOnHide: Boolean = false) {
|
||||
if (fragmentCreator.id == inputId) {
|
||||
hideInput(resetKeyboardGuideline = true)
|
||||
toggled(false)
|
||||
if (showSoftKeyOnHide) {
|
||||
showSoftkey(imeTarget)
|
||||
} else {
|
||||
hideInput(resetKeyboardGuideline = true)
|
||||
}
|
||||
} else {
|
||||
hideInput(resetKeyboardGuideline = false)
|
||||
showInput(fragmentCreator, imeTarget)
|
||||
@@ -55,6 +61,7 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
|
||||
fragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.input_container, input!!)
|
||||
.runOnCommit { (input as? InputFragment)?.show() }
|
||||
.commit()
|
||||
|
||||
overrideKeyboardGuidelineWithPreviousHeight()
|
||||
@@ -66,6 +73,7 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
|
||||
private fun hideInput(resetKeyboardGuideline: Boolean) {
|
||||
val inputHidden = input != null
|
||||
input?.let {
|
||||
(input as? InputFragment)?.hide()
|
||||
fragmentManager
|
||||
.beginTransaction()
|
||||
.remove(it)
|
||||
@@ -94,4 +102,9 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
|
||||
fun onInputShown()
|
||||
fun onInputHidden()
|
||||
}
|
||||
|
||||
interface InputFragment {
|
||||
fun show()
|
||||
fun hide()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.conversation.VoiceNoteDraftView;
|
||||
import org.thoughtcrime.securesms.database.DraftTable;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
@@ -396,6 +397,7 @@ public class InputPanel extends LinearLayout
|
||||
public void enterEditMessageMode(@NonNull GlideRequests glideRequests, @NonNull ConversationMessage conversationMessageToEdit, boolean fromDraft) {
|
||||
SpannableString textToEdit = conversationMessageToEdit.getDisplayBody(getContext());
|
||||
if (!fromDraft) {
|
||||
MessageStyler.convertSpoilersToComposeMode(textToEdit);
|
||||
composeText.setText(textToEdit);
|
||||
composeText.setSelection(textToEdit.length());
|
||||
}
|
||||
|
||||
@@ -55,9 +55,11 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
private val parentEndGuideline: Guideline? by lazy { findViewById(R.id.parent_end_guideline) }
|
||||
private val keyboardGuideline: Guideline? by lazy { findViewById(R.id.keyboard_guideline) }
|
||||
|
||||
private val listeners: MutableList<KeyboardStateListener> = mutableListOf()
|
||||
private val keyboardAnimator = KeyboardInsetAnimator()
|
||||
private val displayMetrics = DisplayMetrics()
|
||||
private var overridingKeyboard: Boolean = false
|
||||
private var previousKeyboardHeight: Int = 0
|
||||
|
||||
init {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsetsCompat ->
|
||||
@@ -74,6 +76,14 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun addKeyboardStateListener(listener: KeyboardStateListener) {
|
||||
listeners += listener
|
||||
}
|
||||
|
||||
fun removeKeyboardStateListener(listener: KeyboardStateListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
private fun applyInsets(windowInsets: Insets, keyboardInsets: Insets) {
|
||||
val isLtr = ViewUtil.isLtr(this)
|
||||
|
||||
@@ -96,6 +106,18 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
keyboardAnimator.endingGuidelineEnd = windowInsets.bottom
|
||||
}
|
||||
}
|
||||
|
||||
if (previousKeyboardHeight != keyboardInsets.bottom) {
|
||||
listeners.forEach {
|
||||
if (previousKeyboardHeight <= 0) {
|
||||
it.onKeyboardShown()
|
||||
} else {
|
||||
it.onKeyboardHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousKeyboardHeight = keyboardInsets.bottom
|
||||
}
|
||||
|
||||
protected fun overrideKeyboardGuidelineWithPreviousHeight() {
|
||||
@@ -157,6 +179,11 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
private val Guideline?.guidelineEnd: Int
|
||||
get() = if (this == null) 0 else (layoutParams as LayoutParams).guideEnd
|
||||
|
||||
interface KeyboardStateListener {
|
||||
fun onKeyboardShown()
|
||||
fun onKeyboardHidden()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the [keyboardGuideline] to move with the IME keyboard opening or closing.
|
||||
*/
|
||||
|
||||
@@ -16,12 +16,16 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.signal.ringrtc.CallLinkRootKey;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.calls.links.CallLinks;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
@@ -166,9 +170,13 @@ public class LinkPreviewView extends FrameLayout {
|
||||
spinner.setVisibility(GONE);
|
||||
noPreview.setVisibility(GONE);
|
||||
|
||||
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl());
|
||||
if (!Util.isEmpty(linkPreview.getTitle())) {
|
||||
title.setText(linkPreview.getTitle());
|
||||
title.setVisibility(VISIBLE);
|
||||
} else if (callLinkRootKey != null) {
|
||||
title.setText(R.string.Recipient_signal_call);
|
||||
title.setVisibility(VISIBLE);
|
||||
} else {
|
||||
title.setVisibility(GONE);
|
||||
}
|
||||
@@ -176,6 +184,9 @@ public class LinkPreviewView extends FrameLayout {
|
||||
if (showDescription && !Util.isEmpty(linkPreview.getDescription())) {
|
||||
description.setText(linkPreview.getDescription());
|
||||
description.setVisibility(VISIBLE);
|
||||
} else if (callLinkRootKey != null) {
|
||||
description.setText(R.string.LinkPreviewView__use_this_link_to_join_a_signal_call);
|
||||
description.setVisibility(VISIBLE);
|
||||
} else {
|
||||
description.setVisibility(GONE);
|
||||
}
|
||||
@@ -205,7 +216,18 @@ public class LinkPreviewView extends FrameLayout {
|
||||
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
thumbnailState.applyState(thumbnail);
|
||||
thumbnail.get().setImageResource(glideRequests, new ImageSlide(getContext(), linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
|
||||
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
|
||||
thumbnail.get().showDownloadText(false);
|
||||
} else if (callLinkRootKey != null) {
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
thumbnailState.applyState(thumbnail);
|
||||
thumbnail.get().setImageDrawable(
|
||||
glideRequests,
|
||||
Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER
|
||||
.getPhotoForCallLink()
|
||||
.asDrawable(getContext(),
|
||||
AvatarColorHash.forCallLink(callLinkRootKey.getKeyBytes()))
|
||||
);
|
||||
thumbnail.get().showDownloadText(false);
|
||||
} else {
|
||||
thumbnail.setVisibility(GONE);
|
||||
|
||||
@@ -2,8 +2,11 @@ package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* A small card with a circular progress indicator in it. Usable in place
|
||||
@@ -16,7 +19,25 @@ class ProgressCard @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : MaterialCardView(context, attrs) {
|
||||
|
||||
private val title: TextView
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.progress_card, this)
|
||||
|
||||
title = findViewById(R.id.progress_card_text)
|
||||
|
||||
if (attrs != null) {
|
||||
context.withStyledAttributes(attrs, R.styleable.ProgressCard) {
|
||||
setTitleText(getString(R.styleable.ProgressCard_progressCardTitle))
|
||||
}
|
||||
} else {
|
||||
setTitleText(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun setTitleText(titleText: String?) {
|
||||
title.visible = !titleText.isNullOrEmpty()
|
||||
title.text = titleText
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,42 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.Discouraged
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Displays a small progress spinner in a card view, as a non-cancellable dialog fragment.
|
||||
*/
|
||||
class ProgressCardDialogFragment : DialogFragment(R.layout.progress_card_dialog) {
|
||||
class ProgressCardDialogFragment
|
||||
@Discouraged("Use create() instead.")
|
||||
constructor() : DialogFragment(R.layout.progress_card_dialog) {
|
||||
|
||||
companion object {
|
||||
@SuppressLint("DiscouragedApi")
|
||||
fun create(title: String? = null): ProgressCardDialogFragment {
|
||||
return ProgressCardDialogFragment().apply {
|
||||
arguments = ProgressCardDialogFragmentArgs.Builder(title).build().toBundle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val args: ProgressCardDialogFragmentArgs by navArgs()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
isCancelable = false
|
||||
return super.onCreateDialog(savedInstanceState).apply {
|
||||
this.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
view.findViewById<ProgressCard>(R.id.progress_card).setTitleText(args.title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
||||
.signature(signature)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.centerCrop()
|
||||
.into(viewHolder.imageView);
|
||||
|
||||
viewHolder.imageView.setOnClickListener(v -> {
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.glide.transforms.SignalDownsampleStrategy;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||
@@ -454,6 +455,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
|
||||
GlideRequest<Drawable> request = glideRequests.load(new DecryptableUri(uri))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
|
||||
.listener(listener);
|
||||
|
||||
if (animate) {
|
||||
@@ -486,6 +488,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
GlideRequest<Drawable> request = glideRequests.load(model)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.placeholder(model.getPlaceholder())
|
||||
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
|
||||
.transition(withCrossFade());
|
||||
|
||||
request = override(request, width, height);
|
||||
@@ -554,6 +557,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
private GlideRequest<Drawable> buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
GlideRequest<Drawable> request = applySizing(glideRequests.load(new DecryptableUri(Objects.requireNonNull(slide.getUri())))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
|
||||
.transition(withCrossFade()));
|
||||
|
||||
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
|
||||
|
||||
@@ -201,7 +201,7 @@ public final class TransferControlView extends FrameLayout {
|
||||
|
||||
private String getDownloadText(@NonNull List<Slide> slides) {
|
||||
if (slides.size() == 1) {
|
||||
return slides.get(0).getContentDescription();
|
||||
return slides.get(0).getContentDescription(getContext());
|
||||
} else {
|
||||
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_DONE ? count + 1 : count);
|
||||
return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount);
|
||||
|
||||
@@ -62,6 +62,7 @@ abstract class WrapperDialogFragment : DialogFragment(R.layout.fragment_containe
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
findListener<WrapperDialogFragmentCallback>()?.onWrapperDialogFragmentDismissed()
|
||||
}
|
||||
|
||||
|
||||
@@ -68,9 +68,10 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
private TextDirectionHeuristic textDirection;
|
||||
private boolean isJumbomoji;
|
||||
private boolean forceJumboEmoji;
|
||||
private boolean renderSpoilers;
|
||||
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private final SpoilerRendererDelegate spoilerRendererDelegate;
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private SpoilerRendererDelegate spoilerRendererDelegate;
|
||||
|
||||
public EmojiTextView(Context context) {
|
||||
this(context, null);
|
||||
@@ -90,6 +91,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
renderMentions = a.getBoolean(R.styleable.EmojiTextView_emoji_renderMentions, true);
|
||||
measureLastLine = a.getBoolean(R.styleable.EmojiTextView_measureLastLine, false);
|
||||
forceJumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
|
||||
renderSpoilers = a.getBoolean(R.styleable.EmojiTextView_emoji_renderSpoilers, false);
|
||||
a.recycle();
|
||||
|
||||
a = context.obtainStyledAttributes(attrs, new int[] { android.R.attr.textSize });
|
||||
@@ -99,7 +101,10 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
if (renderMentions) {
|
||||
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ContextCompat.getColor(getContext(), R.color.transparent_black_20));
|
||||
}
|
||||
spoilerRendererDelegate = new SpoilerRendererDelegate(this);
|
||||
|
||||
if (renderSpoilers) {
|
||||
spoilerRendererDelegate = new SpoilerRendererDelegate(this);
|
||||
}
|
||||
|
||||
textDirection = getLayoutDirection() == LAYOUT_DIRECTION_LTR ? TextDirectionHeuristics.FIRSTSTRONG_RTL : TextDirectionHeuristics.ANYRTL_LTR;
|
||||
|
||||
@@ -127,14 +132,16 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
}
|
||||
|
||||
private void drawSpecialRenderers(@NonNull Canvas canvas, @Nullable MentionRendererDelegate mentionDelegate, @NonNull SpoilerRendererDelegate spoilerDelegate) {
|
||||
private void drawSpecialRenderers(@NonNull Canvas canvas, @Nullable MentionRendererDelegate mentionDelegate, @Nullable SpoilerRendererDelegate spoilerDelegate) {
|
||||
int checkpoint = canvas.save();
|
||||
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
|
||||
try {
|
||||
if (mentionDelegate != null) {
|
||||
mentionDelegate.draw(canvas, (Spanned) getText(), getLayout());
|
||||
}
|
||||
spoilerDelegate.draw(canvas, (Spanned) getText(), getLayout());
|
||||
if (spoilerDelegate != null) {
|
||||
spoilerDelegate.draw(canvas, (Spanned) getText(), getLayout());
|
||||
}
|
||||
} finally {
|
||||
canvas.restoreToCount(checkpoint);
|
||||
}
|
||||
@@ -431,7 +438,9 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
@Override
|
||||
public void setTextColor(int color) {
|
||||
super.setTextColor(color);
|
||||
spoilerRendererDelegate.updateFromTextColor();
|
||||
if (spoilerRendererDelegate != null) {
|
||||
spoilerRendererDelegate.updateFromTextColor();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -105,7 +105,6 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
if (!isInitialised) initView();
|
||||
|
||||
setVisibility(VISIBLE);
|
||||
if (keyboardListener != null) keyboardListener.onShown();
|
||||
keyboardPagerFragment.show();
|
||||
}
|
||||
|
||||
@@ -113,7 +112,6 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
public void hide(boolean immediate) {
|
||||
setVisibility(GONE);
|
||||
onCloseEmojiSearchInternal(false);
|
||||
if (keyboardListener != null) keyboardListener.onHidden();
|
||||
Log.i(TAG, "hide()");
|
||||
keyboardPagerFragment.hide();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.identity;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -12,7 +11,6 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -24,9 +22,10 @@ public class UnverifiedBannerView extends LinearLayout {
|
||||
|
||||
private static final String TAG = Log.tag(UnverifiedBannerView.class);
|
||||
|
||||
private View container;
|
||||
private TextView text;
|
||||
private ImageView closeButton;
|
||||
private View container;
|
||||
private TextView text;
|
||||
private ImageView closeButton;
|
||||
private OnHideListener onHideListener;
|
||||
|
||||
public UnverifiedBannerView(Context context) {
|
||||
super(context);
|
||||
@@ -38,13 +37,11 @@ public class UnverifiedBannerView extends LinearLayout {
|
||||
initialize();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
|
||||
public UnverifiedBannerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public UnverifiedBannerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initialize();
|
||||
@@ -82,16 +79,27 @@ public class UnverifiedBannerView extends LinearLayout {
|
||||
});
|
||||
}
|
||||
|
||||
public void setOnHideListener(@Nullable OnHideListener onHideListener) {
|
||||
this.onHideListener = onHideListener;
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
if (onHideListener != null && onHideListener.onHide()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public interface DismissListener {
|
||||
public void onDismissed(List<IdentityRecord> unverifiedIdentities);
|
||||
void onDismissed(List<IdentityRecord> unverifiedIdentities);
|
||||
}
|
||||
|
||||
public interface ClickListener {
|
||||
public void onClicked(List<IdentityRecord> unverifiedIdentities);
|
||||
void onClicked(List<IdentityRecord> unverifiedIdentities);
|
||||
}
|
||||
|
||||
public interface OnHideListener {
|
||||
boolean onHide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.location;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -11,16 +9,20 @@ import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.gms.maps.CameraUpdateFactory;
|
||||
import com.google.android.gms.maps.GoogleMap;
|
||||
import com.google.android.gms.maps.MapView;
|
||||
import com.google.android.gms.maps.OnMapReadyCallback;
|
||||
import com.google.android.gms.maps.model.LatLng;
|
||||
import com.google.android.gms.maps.model.MarkerOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class SignalMapView extends LinearLayout {
|
||||
|
||||
private MapView mapView;
|
||||
@@ -53,42 +55,50 @@ public class SignalMapView extends LinearLayout {
|
||||
public ListenableFuture<Bitmap> display(final SignalPlace place) {
|
||||
final SettableFuture<Bitmap> future = new SettableFuture<>();
|
||||
|
||||
this.mapView.onCreate(null);
|
||||
this.mapView.onResume();
|
||||
|
||||
this.mapView.setVisibility(View.VISIBLE);
|
||||
this.imageView.setVisibility(View.GONE);
|
||||
|
||||
this.mapView.getMapAsync(new OnMapReadyCallback() {
|
||||
this.textView.setText(place.getDescription());
|
||||
snapshot(place, mapView).addListener(new ListenableFuture.Listener<Bitmap>() {
|
||||
@Override
|
||||
public void onMapReady(final GoogleMap googleMap) {
|
||||
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(place.getLatLong(), 13));
|
||||
googleMap.addMarker(new MarkerOptions().position(place.getLatLong()));
|
||||
googleMap.setBuildingsEnabled(true);
|
||||
googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
|
||||
googleMap.getUiSettings().setAllGesturesEnabled(false);
|
||||
googleMap.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {
|
||||
@Override
|
||||
public void onMapLoaded() {
|
||||
googleMap.snapshot(new GoogleMap.SnapshotReadyCallback() {
|
||||
@Override
|
||||
public void onSnapshotReady(Bitmap bitmap) {
|
||||
future.set(bitmap);
|
||||
imageView.setImageBitmap(bitmap);
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
mapView.setVisibility(View.GONE);
|
||||
mapView.onPause();
|
||||
mapView.onDestroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
public void onSuccess(Bitmap result) {
|
||||
future.set(result);
|
||||
imageView.setImageBitmap(result);
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
future.setException(e);
|
||||
}
|
||||
});
|
||||
|
||||
this.textView.setText(place.getDescription());
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public static ListenableFuture<Bitmap> snapshot(final LatLng place, @NonNull final MapView mapView) {
|
||||
final SettableFuture<Bitmap> future = new SettableFuture<>();
|
||||
mapView.onCreate(null);
|
||||
mapView.onResume();
|
||||
|
||||
mapView.setVisibility(View.VISIBLE);
|
||||
|
||||
mapView.getMapAsync(googleMap -> {
|
||||
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(place, 13));
|
||||
googleMap.addMarker(new MarkerOptions().position(place));
|
||||
googleMap.setBuildingsEnabled(true);
|
||||
googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
|
||||
googleMap.getUiSettings().setAllGesturesEnabled(false);
|
||||
googleMap.setOnMapLoadedCallback(() -> googleMap.snapshot(bitmap -> {
|
||||
future.set(bitmap);
|
||||
mapView.setVisibility(View.GONE);
|
||||
mapView.onPause();
|
||||
mapView.onDestroy();
|
||||
}));
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
public static ListenableFuture<Bitmap> snapshot(final SignalPlace place, @NonNull final MapView mapView) {
|
||||
return snapshot(place.getLatLong(), mapView);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,10 +12,14 @@ import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Encapsulates the logic for determining the type of mention rendering needed (single vs multi-line) and then
|
||||
* passing that information to the appropriate {@link MentionRenderer}.
|
||||
@@ -57,6 +61,12 @@ public class MentionRendererDelegate {
|
||||
if (MentionAnnotation.isMentionAnnotation(annotation)) {
|
||||
int spanStart = text.getSpanStart(annotation);
|
||||
int spanEnd = text.getSpanEnd(annotation);
|
||||
|
||||
List<Annotation> spoilerAnnotations = SpoilerAnnotation.getSpoilerAnnotations(text, spanStart, spanEnd, true);
|
||||
if (Util.hasItems(spoilerAnnotations)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int startLine = layout.getLineForOffset(spanStart);
|
||||
int endLine = layout.getLineForOffset(spanEnd);
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package org.thoughtcrime.securesms.components.reminder
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
class BubbleOptOutReminder(context: Context) : Reminder(null, context.getString(R.string.BubbleOptOutTooltip__description)) {
|
||||
class BubbleOptOutReminder : Reminder(R.string.BubbleOptOutTooltip__description) {
|
||||
|
||||
init {
|
||||
addAction(Action(context.getString(R.string.BubbleOptOutTooltip__turn_off), R.id.reminder_action_turn_off))
|
||||
addAction(Action(context.getString(R.string.BubbleOptOutTooltip__not_now), R.id.reminder_action_not_now))
|
||||
addAction(Action(R.string.BubbleOptOutTooltip__turn_off, R.id.reminder_action_bubble_turn_off))
|
||||
addAction(Action(R.string.BubbleOptOutTooltip__not_now, R.id.reminder_action_bubble_not_now))
|
||||
}
|
||||
|
||||
override fun isDismissable(): Boolean {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.components.reminder
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import kotlin.time.Duration.Companion.days
|
||||
@@ -8,12 +7,12 @@ import kotlin.time.Duration.Companion.days
|
||||
/**
|
||||
* Reminder shown when CDS is in a permanent error state, preventing us from doing a sync.
|
||||
*/
|
||||
class CdsPermanentErrorReminder(context: Context) : Reminder(null, context.getString(R.string.reminder_cds_permanent_error_body)) {
|
||||
class CdsPermanentErrorReminder : Reminder(R.string.reminder_cds_permanent_error_body) {
|
||||
|
||||
init {
|
||||
addAction(
|
||||
Action(
|
||||
context.getString(R.string.reminder_cds_permanent_error_learn_more),
|
||||
R.string.reminder_cds_permanent_error_learn_more,
|
||||
R.id.reminder_action_cds_permanent_error_learn_more
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package org.thoughtcrime.securesms.components.reminder
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
/**
|
||||
* Reminder shown when CDS is rate-limited, preventing us from temporarily doing a refresh.
|
||||
*/
|
||||
class CdsTemporyErrorReminder(context: Context) : Reminder(null, context.getString(R.string.reminder_cds_warning_body)) {
|
||||
class CdsTemporaryErrorReminder : Reminder(R.string.reminder_cds_warning_body) {
|
||||
|
||||
init {
|
||||
addAction(
|
||||
Action(
|
||||
context.getString(R.string.reminder_cds_warning_learn_more),
|
||||
R.string.reminder_cds_warning_learn_more,
|
||||
R.id.reminder_action_cds_temporary_error_learn_more
|
||||
)
|
||||
)
|
||||
@@ -19,10 +19,9 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@SuppressLint("BatteryLife")
|
||||
public class DozeReminder extends Reminder {
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
@RequiresApi(api = 23)
|
||||
public DozeReminder(@NonNull final Context context) {
|
||||
super(context.getString(R.string.DozeReminder_optimize_for_missing_play_services),
|
||||
context.getString(R.string.DozeReminder_this_device_does_not_support_play_services_tap_to_disable_system_battery));
|
||||
super(R.string.DozeReminder_optimize_for_missing_play_services, R.string.DozeReminder_this_device_does_not_support_play_services_tap_to_disable_system_battery);
|
||||
|
||||
setOkListener(v -> {
|
||||
TextSecurePreferences.setPromptedOptimizeDoze(context, true);
|
||||
@@ -40,5 +39,4 @@ public class DozeReminder extends Reminder {
|
||||
Build.VERSION.SDK_INT >= 23 &&
|
||||
!((PowerManager)context.getSystemService(Context.POWER_SERVICE)).isIgnoringBatteryOptimizations(context.getPackageName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||