mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-16 23:13:17 +01:00
Compare commits
461 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42ccd638bd | ||
|
|
e11577bd23 | ||
|
|
3c0b87bbca | ||
|
|
84a61b01ca | ||
|
|
c149e008fd | ||
|
|
c7a345eb0b | ||
|
|
348b6e9742 | ||
|
|
003b3e02e4 | ||
|
|
5b668d7931 | ||
|
|
87748fa80c | ||
|
|
ad0482fb5b | ||
|
|
12ceb1cb32 | ||
|
|
9ec2c5da52 | ||
|
|
f742d34588 | ||
|
|
4d8e058d33 | ||
|
|
77ba6e0f7b | ||
|
|
76a0e5c851 | ||
|
|
f3096cc24c | ||
|
|
49957e1d95 | ||
|
|
2f5cb5f090 | ||
|
|
ba394e1021 | ||
|
|
7611c64493 | ||
|
|
231248d20a | ||
|
|
a3a79fc58d | ||
|
|
6476e585c4 | ||
|
|
fd2961710d | ||
|
|
7f4ab67f98 | ||
|
|
b9ce38b85b | ||
|
|
50d5658add | ||
|
|
72777bc6cd | ||
|
|
f2046c3c05 | ||
|
|
745dfc3fbb | ||
|
|
cb77165b53 | ||
|
|
bde4700e87 | ||
|
|
e58cea9a26 | ||
|
|
556dc0d1ec | ||
|
|
8c1ddcf1c0 | ||
|
|
2549c1f97d | ||
|
|
5faa497821 | ||
|
|
d7a7e72c3a | ||
|
|
af1701e6fa | ||
|
|
32d1cc7d54 | ||
|
|
783a615c07 | ||
|
|
65bfee6eba | ||
|
|
8d4419705b | ||
|
|
6c3baf229c | ||
|
|
6e9a6283fc | ||
|
|
5b3899237b | ||
|
|
dddf830e47 | ||
|
|
fd930d0b1d | ||
|
|
2b5d65ae04 | ||
|
|
2ebaa04c2f | ||
|
|
1e316ea19f | ||
|
|
ac9257ec1c | ||
|
|
9b83c5e283 | ||
|
|
a7a4972013 | ||
|
|
f6f4e6fde7 | ||
|
|
e9160c2449 | ||
|
|
b0b1029d0f | ||
|
|
72b3a0555d | ||
|
|
135fde68c1 | ||
|
|
954e45ed97 | ||
|
|
2b3f16d3ad | ||
|
|
6820b84921 | ||
|
|
6a5f5f4ffa | ||
|
|
19381342b3 | ||
|
|
c2627dda8d | ||
|
|
db309b7930 | ||
|
|
403958fed3 | ||
|
|
3af53f2089 | ||
|
|
e472760d92 | ||
|
|
b04acd8ae0 | ||
|
|
6890973ce8 | ||
|
|
b3d9a85fa2 | ||
|
|
9f027ed584 | ||
|
|
8fb598e60a | ||
|
|
2edaba39a0 | ||
|
|
b0be7effe8 | ||
|
|
142979ce93 | ||
|
|
093dd7c62c | ||
|
|
4acafc3d77 | ||
|
|
65bf0aad79 | ||
|
|
ef6e846512 | ||
|
|
782464f664 | ||
|
|
c7352f62e5 | ||
|
|
90dd6b7cb3 | ||
|
|
ddfb4bf0a5 | ||
|
|
cdef21d6c0 | ||
|
|
c0f843061e | ||
|
|
5774771ea6 | ||
|
|
ab8d5474e0 | ||
|
|
6497ec8098 | ||
|
|
83c3b16b92 | ||
|
|
3c2bd032ba | ||
|
|
f798866619 | ||
|
|
ffad2c7386 | ||
|
|
7252e54593 | ||
|
|
cfab4dc658 | ||
|
|
7f5a8ce6bb | ||
|
|
d02a597451 | ||
|
|
8d92a1f195 | ||
|
|
74e630aacb | ||
|
|
65a12767f9 | ||
|
|
ab9d813636 | ||
|
|
007975e7da | ||
|
|
86ca1ebda0 | ||
|
|
57fb3e6377 | ||
|
|
7e6fcb80a3 | ||
|
|
5e46e1e3d9 | ||
|
|
21e370de9b | ||
|
|
77ef877c59 | ||
|
|
77caedb3bb | ||
|
|
3e77975c17 | ||
|
|
75e15c81e1 | ||
|
|
782a1ce301 | ||
|
|
be21b9e163 | ||
|
|
284140871e | ||
|
|
e6ac40a07c | ||
|
|
b8e98350c1 | ||
|
|
445ff263c6 | ||
|
|
e10c40d2b8 | ||
|
|
a41a2b3e64 | ||
|
|
e603391c35 | ||
|
|
7e0cd99f48 | ||
|
|
daedb8261d | ||
|
|
2c8744a319 | ||
|
|
a7c441225b | ||
|
|
e3c491860a | ||
|
|
43ad0b2294 | ||
|
|
bf897d10d2 | ||
|
|
0b1a93d3e6 | ||
|
|
7edef20f4f | ||
|
|
945c308cf5 | ||
|
|
f91494f813 | ||
|
|
9d28caac00 | ||
|
|
798f3a7b0e | ||
|
|
768e170ed4 | ||
|
|
a0ebb891de | ||
|
|
570b39f82e | ||
|
|
dc50899fe0 | ||
|
|
0f889e0259 | ||
|
|
cb906edd11 | ||
|
|
604f6709db | ||
|
|
0359f27cd9 | ||
|
|
0ca438ed25 | ||
|
|
6b6e9e92e8 | ||
|
|
b5e0991f5e | ||
|
|
f06f0e7ae0 | ||
|
|
0fcbb5ffda | ||
|
|
b1f7dbefd8 | ||
|
|
8fc2d5be37 | ||
|
|
40020728de | ||
|
|
4abb169568 | ||
|
|
da1ac5358f | ||
|
|
d504bd593a | ||
|
|
63e48efdfe | ||
|
|
8bb27b60fa | ||
|
|
437c1e2f21 | ||
|
|
7f4a12c179 | ||
|
|
19d3bbc70a | ||
|
|
559561bf72 | ||
|
|
c8c0589ac4 | ||
|
|
666218773c | ||
|
|
d3049a3433 | ||
|
|
130d5a8945 | ||
|
|
172751cd42 | ||
|
|
3ad7c96a3c | ||
|
|
6d41d1f6d2 | ||
|
|
cb74833dc2 | ||
|
|
8c7b6293fb | ||
|
|
9d1f46da9f | ||
|
|
216059b659 | ||
|
|
18392ed0a4 | ||
|
|
63a4d20ea9 | ||
|
|
057231b9c3 | ||
|
|
749bbf428d | ||
|
|
b0458f10a3 | ||
|
|
5b91c927b6 | ||
|
|
b45740884b | ||
|
|
87ad4be117 | ||
|
|
78de70881f | ||
|
|
e7a370a549 | ||
|
|
54eb579558 | ||
|
|
732b67d8cb | ||
|
|
eed45b57a1 | ||
|
|
3503c60fd1 | ||
|
|
c17ba30cfc | ||
|
|
5167c7235d | ||
|
|
803f94012a | ||
|
|
9281bcdd7d | ||
|
|
4dca554967 | ||
|
|
7c45fb6c17 | ||
|
|
8aa283488f | ||
|
|
604c65c7fb | ||
|
|
711148423d | ||
|
|
1f82ceecc6 | ||
|
|
1ac8701ada | ||
|
|
d61e33fdf3 | ||
|
|
e552b5160f | ||
|
|
7e063e8ad8 | ||
|
|
88a34936cd | ||
|
|
c1181478dd | ||
|
|
d13d8628b5 | ||
|
|
6048208c8c | ||
|
|
78214fb39b | ||
|
|
ff8d7fa6c2 | ||
|
|
3a2e8b9b19 | ||
|
|
bca4289c96 | ||
|
|
3fbd9baf0c | ||
|
|
e12c96f4b2 | ||
|
|
eec26aa481 | ||
|
|
865aeda6f2 | ||
|
|
2c4ebedda4 | ||
|
|
042bc8d79a | ||
|
|
4c7bd80f72 | ||
|
|
3a8591fdfb | ||
|
|
629aaa2093 | ||
|
|
5b5b118b7a | ||
|
|
c7016aa462 | ||
|
|
cf857e109a | ||
|
|
1c79840684 | ||
|
|
4ba7de9519 | ||
|
|
2eb8df347e | ||
|
|
9056371c41 | ||
|
|
1f57e1f366 | ||
|
|
aeb568bcf4 | ||
|
|
b7afe4411e | ||
|
|
cba784b8ec | ||
|
|
3aba15e88d | ||
|
|
fa384e93dc | ||
|
|
1f3e04da29 | ||
|
|
a484d48377 | ||
|
|
15f51ea26e | ||
|
|
80bfa103ab | ||
|
|
66f93e0d32 | ||
|
|
366780f6cb | ||
|
|
fb4c1fc268 | ||
|
|
e3bb7ccbd3 | ||
|
|
e3fb8a2137 | ||
|
|
ba4c0386ef | ||
|
|
644945825b | ||
|
|
7590c6dcbb | ||
|
|
b25cef86ee | ||
|
|
fdaaa560e7 | ||
|
|
4cd438b2db | ||
|
|
c0e1507ef4 | ||
|
|
8a75d78ce7 | ||
|
|
8176d25b4c | ||
|
|
21273bc165 | ||
|
|
213517f875 | ||
|
|
b1c006657a | ||
|
|
852dcd9711 | ||
|
|
427e73f7fd | ||
|
|
eae6a971e6 | ||
|
|
4b23e60dd6 | ||
|
|
f0988f37f3 | ||
|
|
e2e3617be9 | ||
|
|
3ac63cc59d | ||
|
|
d935d1deca | ||
|
|
3b1b00027b | ||
|
|
a1bc1aaa98 | ||
|
|
0ccaad1462 | ||
|
|
ad57e62680 | ||
|
|
4e57432dbb | ||
|
|
63412b0153 | ||
|
|
35199abf1f | ||
|
|
41b5813984 | ||
|
|
83215bb98f | ||
|
|
eb12395b8e | ||
|
|
4b07da4978 | ||
|
|
3fbc5423e5 | ||
|
|
9d9e6e2972 | ||
|
|
56a8451d07 | ||
|
|
2483a92975 | ||
|
|
34bbb98c96 | ||
|
|
155bdf6164 | ||
|
|
5358ed6eff | ||
|
|
4f3bb39e5c | ||
|
|
8a49534e2b | ||
|
|
2c3228d6df | ||
|
|
c82d518d4d | ||
|
|
35cd36e9fe | ||
|
|
ee176cbe3d | ||
|
|
bd915cdd7f | ||
|
|
c27f5787fe | ||
|
|
f6cdf459bb | ||
|
|
4e851f90df | ||
|
|
8d8a2a8eef | ||
|
|
277c17de83 | ||
|
|
d5fd424b95 | ||
|
|
e701e4bff0 | ||
|
|
0ddfb4456b | ||
|
|
69dc31681d | ||
|
|
2d7655a6bb | ||
|
|
fe088c39c7 | ||
|
|
731714d263 | ||
|
|
c165636180 | ||
|
|
372dd13eba | ||
|
|
b35ef0bb4d | ||
|
|
bd58c91d2c | ||
|
|
9a5fcdbe4d | ||
|
|
2452056cbe | ||
|
|
bdf7e5d367 | ||
|
|
aae683af41 | ||
|
|
174cd860a0 | ||
|
|
765185952e | ||
|
|
f4002850bb | ||
|
|
935dd7de45 | ||
|
|
d6b6884c69 | ||
|
|
2ed39e4448 | ||
|
|
2de5ea43fb | ||
|
|
88d2d4d9c7 | ||
|
|
aff0c43b39 | ||
|
|
bd18b731c8 | ||
|
|
7b499f96be | ||
|
|
63dab3f4b0 | ||
|
|
80598814bd | ||
|
|
b00abf1667 | ||
|
|
9594be8fcf | ||
|
|
acecd5f013 | ||
|
|
2d1efb604c | ||
|
|
a84c971cbe | ||
|
|
7564ef4811 | ||
|
|
01e75120a7 | ||
|
|
1314b04994 | ||
|
|
253cc5fec4 | ||
|
|
c296a28a4a | ||
|
|
ff95319559 | ||
|
|
3aa770ee08 | ||
|
|
653410cf27 | ||
|
|
ba08dbef5f | ||
|
|
c1df628079 | ||
|
|
e72cac7db5 | ||
|
|
cbfa573d3d | ||
|
|
1b404cef34 | ||
|
|
cb66996407 | ||
|
|
96f908b068 | ||
|
|
472c8a441f | ||
|
|
1f0c56546e | ||
|
|
97f8b5988d | ||
|
|
19dc90b68b | ||
|
|
67f0ba8624 | ||
|
|
a23c27b54b | ||
|
|
34dec1aec2 | ||
|
|
4f1aa34a46 | ||
|
|
a207bf965a | ||
|
|
33457acee2 | ||
|
|
80622147ab | ||
|
|
719f5e28d0 | ||
|
|
c2830163b8 | ||
|
|
bec9b3d88c | ||
|
|
8e25719b7b | ||
|
|
d80722dba7 | ||
|
|
aa0ab2134f | ||
|
|
7ca2420287 | ||
|
|
9f1deda220 | ||
|
|
265283fea5 | ||
|
|
fc847db389 | ||
|
|
975ec47adf | ||
|
|
ecc6a7b95e | ||
|
|
6f788ee3df | ||
|
|
5080567ca9 | ||
|
|
dec1902dc7 | ||
|
|
c2ca899a7c | ||
|
|
e8ad1e8ed1 | ||
|
|
db534cd376 | ||
|
|
9a1b8c9bb2 | ||
|
|
9389ee17b6 | ||
|
|
a1bcbe9c86 | ||
|
|
f2d994c772 | ||
|
|
6164152b15 | ||
|
|
874067909d | ||
|
|
4bdea886e3 | ||
|
|
fb1ba5a13e | ||
|
|
b3f4e0a7fe | ||
|
|
4db58a27a1 | ||
|
|
1692caeab7 | ||
|
|
2718dca6ea | ||
|
|
03fb266690 | ||
|
|
bf4d727a86 | ||
|
|
47c78e3d8a | ||
|
|
382edd7157 | ||
|
|
e01574c6b4 | ||
|
|
44800cf440 | ||
|
|
b71ee8f3bc | ||
|
|
267897b133 | ||
|
|
e2aec496c5 | ||
|
|
c9c18b91d7 | ||
|
|
9b837d3f02 | ||
|
|
5344850893 | ||
|
|
d2e09607fa | ||
|
|
590b4dec12 | ||
|
|
be211547f2 | ||
|
|
7cbf269b2a | ||
|
|
99d1671a50 | ||
|
|
6f5475fc94 | ||
|
|
a5954efc62 | ||
|
|
b59fee2f6e | ||
|
|
e4f4682357 | ||
|
|
889e17e4d5 | ||
|
|
e86c1515c8 | ||
|
|
aa6fa45949 | ||
|
|
ac3196bbb3 | ||
|
|
0b47c2ae93 | ||
|
|
84296a3860 | ||
|
|
90e6dd3d7d | ||
|
|
b56207d977 | ||
|
|
34f3ae38cc | ||
|
|
13a015fa13 | ||
|
|
233ba03f73 | ||
|
|
c547553770 | ||
|
|
0a5f852c09 | ||
|
|
ddf59fb45a | ||
|
|
5a6d77bae4 | ||
|
|
ae0d6b5926 | ||
|
|
9917b5d7b4 | ||
|
|
0558d5f0b3 | ||
|
|
597cf3f576 | ||
|
|
65af5f0849 | ||
|
|
cff5df4353 | ||
|
|
855bada9b8 | ||
|
|
9802724baa | ||
|
|
14db5ce349 | ||
|
|
bb1e6ffae0 | ||
|
|
210bb23aa4 | ||
|
|
de3a6a85c9 | ||
|
|
7ef41c0169 | ||
|
|
d08f1b65d0 | ||
|
|
5de05edaa1 | ||
|
|
b556967240 | ||
|
|
80a2e1e3cc | ||
|
|
b91a2e1450 | ||
|
|
45e406013a | ||
|
|
deb53e1751 | ||
|
|
601eb967de | ||
|
|
5c03608c8f | ||
|
|
0877d6a25e | ||
|
|
83ee4c0147 | ||
|
|
c09c6587b9 | ||
|
|
6617ecdf39 | ||
|
|
b36b34b1fd | ||
|
|
d8e0baa9ee | ||
|
|
3bb4cdf46b | ||
|
|
2181e34e6a | ||
|
|
d0ca769351 | ||
|
|
a090b07b1c | ||
|
|
178f5e80e3 | ||
|
|
d7bf4f178f | ||
|
|
dd9632da5b | ||
|
|
e235ec4129 | ||
|
|
988728be3e | ||
|
|
e2d86067cc | ||
|
|
b447f98f45 | ||
|
|
3e7f63af43 | ||
|
|
fdeed850b0 | ||
|
|
5c1d4d289f | ||
|
|
d19cba049d | ||
|
|
19ed3cb9ea | ||
|
|
cbb23b3d6c | ||
|
|
3c8c04d9e5 | ||
|
|
c3b792e4cf |
23
.github/stale.yml
vendored
23
.github/stale.yml
vendored
@@ -0,0 +1,23 @@
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 60
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 7
|
||||
|
||||
issues:
|
||||
exemptLabels:
|
||||
- acknowledged
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
closeComment: >
|
||||
This issue has been closed due to inactivity.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 1
|
||||
|
||||
7
.idea/codeStyles/Project.xml
generated
7
.idea/codeStyles/Project.xml
generated
@@ -51,6 +51,13 @@
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JAVA">
|
||||
<option name="BRACE_STYLE" value="5" />
|
||||
<option name="CLASS_BRACE_STYLE" value="5" />
|
||||
|
||||
@@ -59,7 +59,7 @@ The form and manner of this distribution makes it eligible for export under the
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2013-2021 Signal
|
||||
Copyright 2013-2022 Signal
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ try:
|
||||
print("\nTo include this in the distribution, copy it to the project's assets/databases/ directory.")
|
||||
print("If you support API 10 or lower, you must use the gzipped version to avoid corruption.")
|
||||
|
||||
except sqlite3.Error, e:
|
||||
except sqlite3.Error as e:
|
||||
if connection:
|
||||
connection.rollback()
|
||||
print("Error: %s" % e.args[0])
|
||||
|
||||
133
app/build.gradle
133
app/build.gradle
@@ -7,6 +7,7 @@ apply from: 'translations.gradle'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'app.cash.exhaustive'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply from: 'static-ips.gradle'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
@@ -62,8 +63,8 @@ ktlint {
|
||||
version = "0.43.2"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1001
|
||||
def canonicalVersionName = "5.31.4"
|
||||
def canonicalVersionCode = 1033
|
||||
def canonicalVersionName = "5.34.7"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -75,18 +76,18 @@ def abiPostFix = ['universal' : 0,
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
def selectableVariants = [
|
||||
'nightlyProdFlipper',
|
||||
'nightlyProdSpinner',
|
||||
'nightlyProdPerf',
|
||||
'nightlyProdRelease',
|
||||
'playProdDebug',
|
||||
'playProdFlipper',
|
||||
'playProdSpinner',
|
||||
'playProdPerf',
|
||||
'playProdRelease',
|
||||
'playStagingDebug',
|
||||
'playStagingFlipper',
|
||||
'playStagingSpinner',
|
||||
'playStagingPerf',
|
||||
'playStagingRelease',
|
||||
'websiteProdFlipper',
|
||||
'websiteProdSpinner',
|
||||
'websiteProdRelease',
|
||||
]
|
||||
|
||||
@@ -117,6 +118,48 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError true
|
||||
baseline file("lint-baseline.xml")
|
||||
disable "LintError"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
test {
|
||||
java.srcDirs += "$projectDir/src/testShared"
|
||||
}
|
||||
|
||||
androidTest {
|
||||
java.srcDirs += "$projectDir/src/testShared"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JAVA_VERSION
|
||||
targetCompatibility JAVA_VERSION
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'LICENSE.txt'
|
||||
exclude 'LICENSE'
|
||||
exclude 'NOTICE'
|
||||
exclude 'asm-license.txt'
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
}
|
||||
|
||||
|
||||
defaultConfig {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
@@ -144,9 +187,17 @@ android {
|
||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\"}"
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String[]", "SIGNAL_SERVICE_IPS", service_ips
|
||||
buildConfigField "String[]", "SIGNAL_STORAGE_IPS", storage_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDN_IPS", cdn_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDN2_IPS", cdn2_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDS_IPS", cds_ips
|
||||
buildConfigField "String[]", "SIGNAL_KBS_IPS", kbs_ips
|
||||
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
|
||||
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
|
||||
buildConfigField "String", "CDSH_CODE_HASH", "\"ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2\""
|
||||
buildConfigField "String", "CDSH_CODE_HASH", "\"2f79dc6c1599b71c70fc2d14f3ea2e3bc65134436eb87011c88845b137af673a\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
|
||||
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
|
||||
@@ -161,8 +212,8 @@ android {
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
||||
buildConfigField "int[]", "MOBILE_COIN_BLACKLIST", "new int[]{98,963,53,850,7}"
|
||||
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
||||
buildConfigField "String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
||||
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
|
||||
@@ -190,36 +241,6 @@ android {
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
test {
|
||||
java.srcDirs += "$projectDir/src/testShared"
|
||||
}
|
||||
|
||||
androidTest {
|
||||
java.srcDirs += "$projectDir/src/testShared"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JAVA_VERSION
|
||||
targetCompatibility JAVA_VERSION
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'LICENSE.txt'
|
||||
exclude 'LICENSE'
|
||||
exclude 'NOTICE'
|
||||
exclude 'asm-license.txt'
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
if (keystores['debug'] != null) {
|
||||
@@ -250,12 +271,12 @@ android {
|
||||
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
|
||||
}
|
||||
flipper {
|
||||
spinner {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Flipper\""
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Spinner\""
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
@@ -331,6 +352,7 @@ android {
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXQ==\""
|
||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
||||
buildConfigField "String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
|
||||
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
|
||||
@@ -344,6 +366,9 @@ android {
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
|
||||
def tag = getCurrentGitTag()
|
||||
if (tag != null && tag.length() > 0) {
|
||||
if (tag.startsWith("v")) {
|
||||
tag = tag.substring(1)
|
||||
}
|
||||
output.versionNameOverride = tag
|
||||
}
|
||||
} else {
|
||||
@@ -368,19 +393,6 @@ android {
|
||||
variant.setIgnore(true)
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError true
|
||||
baseline file("lint-baseline.xml")
|
||||
disable "LintError"
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -443,8 +455,9 @@ dependencies {
|
||||
implementation project(':device-transfer')
|
||||
implementation project(':image-editor')
|
||||
implementation project(':donations')
|
||||
implementation project(':contacts')
|
||||
|
||||
implementation libs.signal.client.android
|
||||
implementation libs.libsignal.android
|
||||
implementation libs.google.protobuf.javalite
|
||||
|
||||
implementation(libs.mobilecoin) {
|
||||
@@ -506,17 +519,13 @@ dependencies {
|
||||
}
|
||||
implementation libs.dnsjava
|
||||
|
||||
flipperImplementation libs.facebook.flipper
|
||||
flipperImplementation libs.facebook.soloader
|
||||
flipperImplementation libs.square.leakcanary
|
||||
spinnerImplementation project(":spinner")
|
||||
spinnerImplementation libs.square.leakcanary
|
||||
|
||||
testImplementation testLibs.junit.junit
|
||||
testImplementation testLibs.assertj.core
|
||||
testImplementation testLibs.mockito.core
|
||||
testImplementation testLibs.powermock.api.mockito
|
||||
testImplementation testLibs.powermock.module.junit4.core
|
||||
testImplementation testLibs.powermock.module.junit4.rule
|
||||
testImplementation testLibs.powermock.classloading.xstream
|
||||
testImplementation testLibs.mockito.kotlin
|
||||
|
||||
testImplementation testLibs.androidx.test.core
|
||||
testImplementation (testLibs.robolectric.robolectric) {
|
||||
@@ -543,7 +552,7 @@ dependencies {
|
||||
implementation libs.rxjava3.rxkotlin
|
||||
implementation libs.rxdogtag
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.0'
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||
}
|
||||
|
||||
def getLastCommitTimestamp() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep class org.whispersystems.** { *; }
|
||||
-keep class org.signal.libsignal.protocol.** { *; }
|
||||
-keep class org.thoughtcrime.securesms.** { *; }
|
||||
-keepclassmembers class ** {
|
||||
public void onEvent*(**);
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import java.util.UUID
|
||||
|
||||
class DistributionListDatabaseTest {
|
||||
|
||||
private lateinit var distributionDatabase: DistributionListDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
distributionDatabase = SignalDatabase.distributionLists
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createList_whenNoConflict_insertSuccessfully() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createList_whenNameConflict_failToInsert() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val id2: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNull(id2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getList_returnCorrectList() {
|
||||
createRecipients(3)
|
||||
val members: List<RecipientId> = recipientList(1, 2, 3)
|
||||
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", members)
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val record: DistributionListRecord? = distributionDatabase.getList(id!!)
|
||||
Assert.assertNotNull(record)
|
||||
Assert.assertEquals(id, record!!.id)
|
||||
Assert.assertEquals("test", record.name)
|
||||
Assert.assertEquals(members, record.members)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getMembers_returnsCorrectMembers() {
|
||||
createRecipients(3)
|
||||
val members: List<RecipientId> = recipientList(1, 2, 3)
|
||||
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", members)
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val foundMembers: List<RecipientId> = distributionDatabase.getMembers(id!!)
|
||||
Assert.assertEquals(members, foundMembers)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenStoryExists_getStoryType_returnsStoryWithReplies() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val storyType = distributionDatabase.getStoryType(id!!)
|
||||
Assert.assertEquals(StoryType.STORY_WITH_REPLIES, storyType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenStoryExistsAndMarkedNoReplies_getStoryType_returnsStoryWithoutReplies() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
distributionDatabase.setAllowsReplies(id!!, false)
|
||||
|
||||
val storyType = distributionDatabase.getStoryType(id)
|
||||
Assert.assertEquals(StoryType.STORY_WITHOUT_REPLIES, storyType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenStoryExistsAndMarkedNoReplies_getAllListsForContactSelectionUi_returnsStoryWithoutReplies() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
distributionDatabase.setAllowsReplies(id!!, false)
|
||||
|
||||
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
|
||||
Assert.assertFalse(records.first().allowsReplies)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenStoryExists_getAllListsForContactSelectionUi_returnsStoryWithReplies() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
|
||||
Assert.assertTrue(records.first().allowsReplies)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun givenStoryDoesNotExist_getStoryType_throwsIllegalStateException() {
|
||||
distributionDatabase.getStoryType(DistributionListId.from(12))
|
||||
Assert.fail("Expected an assertion error.")
|
||||
}
|
||||
|
||||
private fun createRecipients(count: Int) {
|
||||
for (i in 0 until count) {
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun recipientList(vararg ids: Long): List<RecipientId> {
|
||||
return ids.map { RecipientId.from(it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Helper methods for inserting an MMS message into the MMS table.
|
||||
*/
|
||||
object MmsHelper {
|
||||
|
||||
fun insert(
|
||||
recipient: Recipient = Recipient.UNKNOWN,
|
||||
body: String = "body",
|
||||
sentTimeMillis: Long = System.currentTimeMillis(),
|
||||
subscriptionId: Int = -1,
|
||||
expiresIn: Long = 0,
|
||||
viewOnce: Boolean = false,
|
||||
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
threadId: Long = 1,
|
||||
storyType: StoryType = StoryType.NONE
|
||||
): Long {
|
||||
val message = OutgoingMediaMessage(
|
||||
recipient,
|
||||
body,
|
||||
emptyList(),
|
||||
sentTimeMillis,
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
distributionType,
|
||||
storyType,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptySet(),
|
||||
emptySet()
|
||||
)
|
||||
|
||||
return insert(
|
||||
message = message,
|
||||
threadId = threadId,
|
||||
)
|
||||
}
|
||||
|
||||
fun insert(
|
||||
message: OutgoingMediaMessage,
|
||||
threadId: Long
|
||||
): Long {
|
||||
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -19,10 +18,9 @@ import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -30,10 +28,16 @@ class RecipientDatabaseTest {
|
||||
|
||||
private lateinit var recipientDatabase: RecipientDatabase
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
recipientDatabase = SignalDatabase.recipients
|
||||
ensureDbEmpty()
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
@@ -46,7 +50,7 @@ class RecipientDatabaseTest {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireAci())
|
||||
assertEquals(ACI_A, recipient.requireServiceId())
|
||||
assertFalse(recipient.hasE164())
|
||||
}
|
||||
|
||||
@@ -56,7 +60,7 @@ class RecipientDatabaseTest {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, false)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireAci())
|
||||
assertEquals(ACI_A, recipient.requireServiceId())
|
||||
assertFalse(recipient.hasE164())
|
||||
}
|
||||
|
||||
@@ -67,7 +71,7 @@ class RecipientDatabaseTest {
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(E164_A, recipient.requireE164())
|
||||
assertFalse(recipient.hasAci())
|
||||
assertFalse(recipient.hasServiceId())
|
||||
}
|
||||
|
||||
/** If all you have is an E164, you can just store that, regardless of trust level. */
|
||||
@@ -77,7 +81,7 @@ class RecipientDatabaseTest {
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(E164_A, recipient.requireE164())
|
||||
assertFalse(recipient.hasAci())
|
||||
assertFalse(recipient.hasServiceId())
|
||||
}
|
||||
|
||||
/** With high trust, you can associate an ACI-e164 pair. */
|
||||
@@ -86,7 +90,7 @@ class RecipientDatabaseTest {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireAci())
|
||||
assertEquals(ACI_A, recipient.requireServiceId())
|
||||
assertEquals(E164_A, recipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -96,7 +100,7 @@ class RecipientDatabaseTest {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireAci())
|
||||
assertEquals(ACI_A, recipient.requireServiceId())
|
||||
assertFalse(recipient.hasE164())
|
||||
}
|
||||
|
||||
@@ -107,26 +111,26 @@ class RecipientDatabaseTest {
|
||||
/** With high trust, you can associate an e164 with an existing ACI. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_highTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** With low trust, you cannot associate an ACI-e164 pair, and therefore cannot store the e164. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_lowTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
}
|
||||
|
||||
@@ -139,7 +143,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -152,7 +156,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -169,7 +173,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -182,12 +186,12 @@ class RecipientDatabaseTest {
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(E164_A, existingRecipient.requireE164())
|
||||
assertFalse(existingRecipient.hasAci())
|
||||
assertFalse(existingRecipient.hasServiceId())
|
||||
}
|
||||
|
||||
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. But high trust lets us take the e164 from the current holder. */
|
||||
@@ -201,14 +205,12 @@ class RecipientDatabaseTest {
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_B, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
assertEquals(PNI_A, retrievedRecipient.pni.get())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(ACI_A, existingRecipient.requireAci())
|
||||
assertEquals(ACI_A, existingRecipient.requireServiceId())
|
||||
assertFalse(existingRecipient.hasE164())
|
||||
assertNull(existingRecipient.pni.orNull())
|
||||
}
|
||||
|
||||
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. And low trust means we can’t take the e164. */
|
||||
@@ -220,11 +222,11 @@ class RecipientDatabaseTest {
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_B, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(ACI_A, existingRecipient.requireAci())
|
||||
assertEquals(ACI_A, existingRecipient.requireServiceId())
|
||||
assertEquals(E164_A, existingRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -243,11 +245,11 @@ class RecipientDatabaseTest {
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_B, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(ACI_A, existingRecipient.requireAci())
|
||||
assertEquals(ACI_A, existingRecipient.requireServiceId())
|
||||
assertEquals(E164_A, existingRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -264,7 +266,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -281,7 +283,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingAciId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||
@@ -304,7 +306,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingAciId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||
@@ -317,19 +319,19 @@ class RecipientDatabaseTest {
|
||||
/** Low trust means you can’t merge. If you’re retrieving a user from the table with this data, prefer the ACI one. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_lowTrust() {
|
||||
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
|
||||
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||
assertEquals(existingAciId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||
assertEquals(E164_A, existingE164Recipient.requireE164())
|
||||
assertFalse(existingE164Recipient.hasAci())
|
||||
assertFalse(existingE164Recipient.hasServiceId())
|
||||
}
|
||||
|
||||
/** Another high trust case. No new rules here, just a more complex scenario to show how different rules interact. */
|
||||
@@ -345,11 +347,11 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId1, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingRecipient2 = Recipient.resolved(existingId2)
|
||||
assertEquals(ACI_B, existingRecipient2.requireAci())
|
||||
assertEquals(ACI_B, existingRecipient2.requireServiceId())
|
||||
assertFalse(existingRecipient2.hasE164())
|
||||
|
||||
assert(changeNumberListener.numberChangeWasEnqueued)
|
||||
@@ -365,11 +367,11 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId1, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
|
||||
val existingRecipient2 = Recipient.resolved(existingId2)
|
||||
assertEquals(ACI_B, existingRecipient2.requireAci())
|
||||
assertEquals(ACI_B, existingRecipient2.requireServiceId())
|
||||
assertEquals(E164_A, existingRecipient2.requireE164())
|
||||
}
|
||||
|
||||
@@ -386,7 +388,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId1, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
assertFalse(recipientDatabase.getByE164(E164_B).isPresent)
|
||||
@@ -411,11 +413,11 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId2, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val recipientWithId1 = Recipient.resolved(existingId1)
|
||||
assertEquals(ACI_B, recipientWithId1.requireAci())
|
||||
assertEquals(ACI_B, recipientWithId1.requireServiceId())
|
||||
assertEquals(E164_A, recipientWithId1.requireE164())
|
||||
}
|
||||
|
||||
@@ -434,7 +436,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -453,7 +455,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -469,7 +471,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
|
||||
changeNumberListener.waitForJobManager()
|
||||
@@ -500,18 +502,18 @@ class RecipientDatabaseTest {
|
||||
@Test
|
||||
fun createByUuidSanityCheck() {
|
||||
// GIVEN one recipient
|
||||
val recipientId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
val recipientId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
|
||||
|
||||
// WHEN I retrieve one by UUID
|
||||
val possible: Optional<RecipientId> = recipientDatabase.getByAci(ACI_A)
|
||||
val possible: Optional<RecipientId> = recipientDatabase.getByServiceId(ACI_A)
|
||||
|
||||
// THEN I get it back, and it has the properties I expect
|
||||
assertTrue(possible.isPresent)
|
||||
assertEquals(recipientId, possible.get())
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertTrue(recipient.aci.isPresent)
|
||||
assertEquals(ACI_A, recipient.aci.get())
|
||||
assertTrue(recipient.serviceId.isPresent)
|
||||
assertEquals(ACI_A, recipient.serviceId.get())
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
@@ -520,7 +522,7 @@ class RecipientDatabaseTest {
|
||||
}
|
||||
|
||||
private fun ensureDbEmpty() {
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME} WHERE ${RecipientDatabase.DISTRIBUTION_LIST_ID} IS NULL ", null).use { cursor ->
|
||||
assertTrue(cursor.moveToFirst())
|
||||
assertEquals(0, cursor.getLong(0))
|
||||
}
|
||||
|
||||
@@ -9,31 +9,34 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.signal.libsignal.protocol.state.SessionRecord
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.signal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.thoughtcrime.securesms.util.CursorUtil
|
||||
import org.whispersystems.libsignal.IdentityKey
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress
|
||||
import org.whispersystems.libsignal.state.SessionRecord
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -50,6 +53,10 @@ class RecipientDatabaseTest_merges {
|
||||
private lateinit var mentionDatabase: MentionDatabase
|
||||
private lateinit var reactionDatabase: ReactionDatabase
|
||||
private lateinit var notificationProfileDatabase: NotificationProfileDatabase
|
||||
private lateinit var distributionListDatabase: DistributionListDatabase
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
@@ -64,17 +71,19 @@ class RecipientDatabaseTest_merges {
|
||||
mentionDatabase = SignalDatabase.mentions
|
||||
reactionDatabase = SignalDatabase.reactions
|
||||
notificationProfileDatabase = SignalDatabase.notificationProfiles
|
||||
distributionListDatabase = SignalDatabase.distributionLists
|
||||
|
||||
ensureDbEmpty()
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
}
|
||||
|
||||
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_general() {
|
||||
// Setup
|
||||
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
|
||||
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||
val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_B)
|
||||
val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_B)
|
||||
|
||||
val smsId1: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId
|
||||
val smsId2: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
|
||||
@@ -99,7 +108,7 @@ class RecipientDatabaseTest_merges {
|
||||
identityDatabase.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
|
||||
identityDatabase.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
|
||||
|
||||
sessionDatabase.store(SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord())
|
||||
sessionDatabase.store(localAci, SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord())
|
||||
|
||||
reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1))
|
||||
reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1))
|
||||
@@ -112,6 +121,8 @@ class RecipientDatabaseTest_merges {
|
||||
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdE164)
|
||||
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdAciB)
|
||||
|
||||
val distributionListId: DistributionListId = distributionListDatabase.createList("testlist", listOf(recipientIdE164, recipientIdAciB))!!
|
||||
|
||||
// Merge
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!!
|
||||
@@ -119,7 +130,7 @@ class RecipientDatabaseTest_merges {
|
||||
|
||||
// Recipient validation
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(recipientIdE164)
|
||||
@@ -175,7 +186,7 @@ class RecipientDatabaseTest_merges {
|
||||
assertNull(identityDatabase.getIdentityStoreRecord(E164_A))
|
||||
|
||||
// Session validation
|
||||
assertNotNull(sessionDatabase.load(SignalProtocolAddress(ACI_A.toString(), 1)))
|
||||
assertNotNull(sessionDatabase.load(localAci, SignalProtocolAddress(ACI_A.toString(), 1)))
|
||||
|
||||
// Reaction validation
|
||||
val reactionsSms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(smsId1, false))
|
||||
@@ -193,24 +204,22 @@ class RecipientDatabaseTest_merges {
|
||||
|
||||
assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci))
|
||||
assertThat("Notification Profile 2 should now contain ACI A ($recipientIdAci) and ACI B ($recipientIdAciB)", updatedProfile2.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
|
||||
|
||||
// Distribution List validation
|
||||
val updatedList: DistributionListRecord = distributionListDatabase.getList(distributionListId)!!
|
||||
|
||||
assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
|
||||
}
|
||||
|
||||
private val context: Application
|
||||
get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
|
||||
|
||||
private fun ensureDbEmpty() {
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
|
||||
assertTrue(cursor.moveToFirst())
|
||||
assertEquals(0, cursor.getLong(0))
|
||||
}
|
||||
}
|
||||
|
||||
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.absent()): IncomingTextMessage {
|
||||
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
|
||||
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
|
||||
}
|
||||
|
||||
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.absent()): IncomingMediaMessage {
|
||||
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.absent())
|
||||
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMediaMessage {
|
||||
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty())
|
||||
}
|
||||
|
||||
private fun identityKey(value: Byte): IdentityKey {
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.hamcrest.Matchers.notNullValue
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.groupChange
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.groupContext
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName", "TestFunctionName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
|
||||
private lateinit var recipients: RecipientDatabase
|
||||
private lateinit var sms: SmsDatabase
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
private var wallClock: Long = 1000
|
||||
|
||||
private lateinit var alice: RecipientId
|
||||
private lateinit var bob: RecipientId
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipients = SignalDatabase.recipients
|
||||
sms = SignalDatabase.sms
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
|
||||
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
|
||||
bob = recipients.getOrInsertFromServiceId(bobServiceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if no previous messages.
|
||||
*/
|
||||
@Test
|
||||
fun noPreviousMessage() {
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
1,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous message is text.
|
||||
*/
|
||||
@Test
|
||||
fun previousTextMesssage() {
|
||||
val threadId = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous is unrelated group change.
|
||||
*/
|
||||
@Test
|
||||
fun previousUnrelatedGroupChange() {
|
||||
val threadId = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addMember(bobServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing if previous join request is from a different recipient.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinRequestFromADifferentRecipient() {
|
||||
val threadId = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = bob,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = bobServiceId) {
|
||||
deleteRequestingMember(bobServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get().threadId
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse if previous is join request from same.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinRequestCollapse() {
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse if previous is join request from same, and leave second previous alone if text.
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinThenTextCollapse() {
|
||||
val secondLatestMessage = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get()
|
||||
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
assert(secondLatestMessage.threadId == latestMessage.threadId)
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse "twice" is previous is a join request and second previous is already collapsed join/delete from the same recipient.
|
||||
*/
|
||||
@Test
|
||||
fun previousCollapseAndJoinRequestDoubleCollapse() {
|
||||
val secondLatestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
addRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
).get()
|
||||
|
||||
assert(secondLatestMessage.threadId == latestMessage.threadId)
|
||||
|
||||
val result = sms.collapseJoinRequestEventsIfPossible(
|
||||
latestMessage.threadId,
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
change = groupChange(editor = aliceServiceId) {
|
||||
deleteRequestingMember(aliceServiceId)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
|
||||
assertThat("result message id should be same as second latest message", result.get().messageId, `is`(secondLatestMessage.messageId))
|
||||
assertThat("latest message should be deleted", sms.getMessageRecordOrNull(latestMessage.messageId), nullValue())
|
||||
}
|
||||
|
||||
private fun smsMessage(sender: RecipientId, body: String? = ""): IncomingTextMessage {
|
||||
wallClock++
|
||||
return IncomingTextMessage(sender, 1, wallClock, wallClock, wallClock, body, Optional.of(groupId), 0, true, null)
|
||||
}
|
||||
|
||||
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingGroupUpdateMessage {
|
||||
return IncomingGroupUpdateMessage(smsMessage(sender, null), groupContext)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val aliceServiceId: ServiceId = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
private val bobServiceId: ServiceId = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
private val masterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||
private val groupId = GroupId.v2(masterKey)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.containsInAnyOrder
|
||||
import org.hamcrest.Matchers.hasSize
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StorySendsDatabaseTest {
|
||||
|
||||
private lateinit var recipients1to10: List<RecipientId>
|
||||
private lateinit var recipients11to20: List<RecipientId>
|
||||
private lateinit var recipients6to15: List<RecipientId>
|
||||
private lateinit var recipients6to10: List<RecipientId>
|
||||
|
||||
private var messageId1: Long = 0
|
||||
private var messageId2: Long = 0
|
||||
private var messageId3: Long = 0
|
||||
|
||||
private lateinit var storySends: StorySendsDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
storySends = SignalDatabase.storySends
|
||||
|
||||
messageId1 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
|
||||
messageId2 = MmsHelper.insert(storyType = StoryType.STORY_WITH_REPLIES)
|
||||
messageId3 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
|
||||
|
||||
recipients1to10 = makeRecipients(10)
|
||||
recipients11to20 = makeRecipients(10)
|
||||
|
||||
recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5)
|
||||
recipients6to10 = recipients1to10.takeLast(5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_noOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients6to15, 100, true)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(5))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlapAll() {
|
||||
val recipient1 = recipients1to10.first()
|
||||
val recipient2 = recipients11to20.first()
|
||||
|
||||
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false)
|
||||
storySends.insert(messageId2, listOf(recipient1), 100, true)
|
||||
storySends.insert(messageId3, listOf(recipient2), 100, true)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||
val recipientIdsForMessage3 = storySends.getRecipientsToSendTo(messageId3, 100, true)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(0))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(1))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(recipient1))
|
||||
|
||||
assertThat(recipientIdsForMessage3, hasSize(1))
|
||||
assertThat(recipientIdsForMessage3, containsInAnyOrder(recipient2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlapWithEarlierMessage() {
|
||||
storySends.insert(messageId1, recipients6to15, 100, true)
|
||||
storySends.insert(messageId2, recipients1to10, 100, false)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(5))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_noOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100)
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(10))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
storySends.insert(messageId2, recipients6to15, 200, true)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200)
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage1, hasSize(5))
|
||||
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(5))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.takeLast(5).toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
SignalDatabase.mms.markAsRemoteDelete(messageId1)
|
||||
|
||||
storySends.insert(messageId2, recipients6to15, 200, true)
|
||||
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
assertThat(recipientIdsForMessage2, hasSize(10))
|
||||
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithReplies() {
|
||||
storySends.insert(messageId2, recipients1to10, 200, true)
|
||||
|
||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||
|
||||
assertThat(canReply, `is`(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithoutReplies() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
|
||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||
|
||||
assertThat(canReply, `is`(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canReply_storyWithAndWithoutRepliesOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
storySends.insert(messageId2, recipients6to10, 200, true)
|
||||
|
||||
val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200)
|
||||
val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200)
|
||||
|
||||
assertThat(message1OnlyRecipientCanReply, `is`(false))
|
||||
assertThat(message2RecipientCanReply, `is`(true))
|
||||
}
|
||||
|
||||
private fun makeRecipients(count: Int): List<RecipientId> {
|
||||
return (1..count).map {
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.signal.core.util.Hex;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.KbsData;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.thoughtcrime.securesms">
|
||||
|
||||
<application
|
||||
android:name=".FlipperApplicationContext"
|
||||
tools:replace="android:name">
|
||||
|
||||
<activity
|
||||
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||
android:exported="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,60 +0,0 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import com.facebook.flipper.android.AndroidFlipperClient
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
|
||||
import com.facebook.soloader.SoLoader
|
||||
import leakcanary.LeakCanary
|
||||
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter
|
||||
import shark.AndroidReferenceMatchers
|
||||
|
||||
class FlipperApplicationContext : ApplicationContext() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
SoLoader.init(this, false)
|
||||
|
||||
val client = AndroidFlipperClient.getInstance(this)
|
||||
client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
||||
client.addPlugin(DatabasesFlipperPlugin(FlipperSqlCipherAdapter(this)))
|
||||
client.addPlugin(SharedPreferencesFlipperPlugin(this))
|
||||
client.start()
|
||||
|
||||
LeakCanary.config = LeakCanary.config.copy(
|
||||
referenceMatchers = AndroidReferenceMatchers.appDefaults +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.service.media.MediaBrowserService\$ServiceBinder",
|
||||
fieldName = "this\$0"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "androidx.media.MediaBrowserServiceCompat\$MediaBrowserServiceImplApi26\$MediaBrowserServiceApi26",
|
||||
fieldName = "mBase"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.MediaBrowserCompat",
|
||||
fieldName = "mImpl"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.session.MediaControllerCompat",
|
||||
fieldName = "mToken"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.session.MediaControllerCompat",
|
||||
fieldName = "mImpl"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService",
|
||||
fieldName = "mApplication"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.service.GenericForegroundService\$LocalBinder",
|
||||
fieldName = "this\$0"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.contacts.ContactsSyncAdapter",
|
||||
fieldName = "mContext"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver;
|
||||
|
||||
import net.zetetic.database.DatabaseUtils;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A lot of this code is taken from {@link com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver}
|
||||
* and made to work with SqlCipher. Unfortunately I couldn't use it directly, nor subclass it.
|
||||
*/
|
||||
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
|
||||
|
||||
private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class);
|
||||
|
||||
public FlipperSqlCipherAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Descriptor> getDatabases() {
|
||||
try {
|
||||
SignalDatabaseOpenHelper mainOpenHelper = Objects.requireNonNull(SignalDatabase.getInstance());
|
||||
SignalDatabaseOpenHelper keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
|
||||
SignalDatabaseOpenHelper megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
|
||||
SignalDatabaseOpenHelper jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
|
||||
SignalDatabaseOpenHelper metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
|
||||
|
||||
return Arrays.asList(new Descriptor(mainOpenHelper),
|
||||
new Descriptor(keyValueOpenHelper),
|
||||
new Descriptor(megaphoneOpenHelper),
|
||||
new Descriptor(jobManagerOpenHelper),
|
||||
new Descriptor(metricsOpenHelper));
|
||||
} catch (Exception e) {
|
||||
Log.i(TAG, "Unable to use reflection to access raw database.", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTableNames(Descriptor descriptor) {
|
||||
SQLiteDatabase db = descriptor.getReadable();
|
||||
List<String> tableNames = new ArrayList<>();
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type IN (?, ?)", new String[] { "table", "view" })) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
tableNames.add(cursor.getString(0));
|
||||
}
|
||||
}
|
||||
|
||||
return tableNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseGetTableDataResponse getTableData(Descriptor descriptor, String table, String order, boolean reverse, int start, int count) {
|
||||
SQLiteDatabase db = descriptor.getReadable();
|
||||
|
||||
long total = DatabaseUtils.queryNumEntries(db, table);
|
||||
String orderBy = order != null ? order + (reverse ? " DESC" : " ASC") : null;
|
||||
String limitBy = start + ", " + count;
|
||||
|
||||
try (Cursor cursor = db.query(table, null, null, null, null, null, orderBy, limitBy)) {
|
||||
String[] columnNames = cursor.getColumnNames();
|
||||
List<List<Object>> rows = cursorToList(cursor);
|
||||
|
||||
return new DatabaseGetTableDataResponse(Arrays.asList(columnNames), rows, start, rows.size(), total);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseGetTableStructureResponse getTableStructure(Descriptor descriptor, String table) {
|
||||
SQLiteDatabase db = descriptor.getReadable();
|
||||
|
||||
Map<String, String> foreignKeyValues = new HashMap<>();
|
||||
|
||||
try(Cursor cursor = db.rawQuery("PRAGMA foreign_key_list(" + table + ")", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String from = cursor.getString(cursor.getColumnIndex("from"));
|
||||
String to = cursor.getString(cursor.getColumnIndex("to"));
|
||||
String tableName = cursor.getString(cursor.getColumnIndex("table")) + "(" + to + ")";
|
||||
|
||||
foreignKeyValues.put(from, tableName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
List<String> structureColumns = Arrays.asList("column_name", "data_type", "nullable", "default", "primary_key", "foreign_key");
|
||||
List<List<Object>> structureValues = new ArrayList<>();
|
||||
|
||||
try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String columnName = cursor.getString(cursor.getColumnIndex("name"));
|
||||
String foreignKey = foreignKeyValues.containsKey(columnName) ? foreignKeyValues.get(columnName) : null;
|
||||
|
||||
structureValues.add(Arrays.asList(columnName,
|
||||
cursor.getString(cursor.getColumnIndex("type")),
|
||||
cursor.getInt(cursor.getColumnIndex("notnull")) == 0,
|
||||
getObjectFromColumnIndex(cursor, cursor.getColumnIndex("dflt_value")),
|
||||
cursor.getInt(cursor.getColumnIndex("pk")) == 1,
|
||||
foreignKey));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
List<String> indexesColumns = Arrays.asList("index_name", "unique", "indexed_column_name");
|
||||
List<List<Object>> indexesValues = new ArrayList<>();
|
||||
|
||||
try (Cursor indexesCursor = db.rawQuery("PRAGMA index_list(" + table + ")", null)) {
|
||||
List<String> indexedColumnNames = new ArrayList<>();
|
||||
String indexName = indexesCursor.getString(indexesCursor.getColumnIndex("name"));
|
||||
|
||||
try(Cursor indexInfoCursor = db.rawQuery("PRAGMA index_info(" + indexName + ")", null)) {
|
||||
while (indexInfoCursor.moveToNext()) {
|
||||
indexedColumnNames.add(indexInfoCursor.getString(indexInfoCursor.getColumnIndex("name")));
|
||||
}
|
||||
}
|
||||
|
||||
indexesValues.add(Arrays.asList(indexName,
|
||||
indexesCursor.getInt(indexesCursor.getColumnIndex("unique")) == 1,
|
||||
TextUtils.join(",", indexedColumnNames)));
|
||||
|
||||
}
|
||||
|
||||
return new DatabaseGetTableStructureResponse(structureColumns, structureValues, indexesColumns, indexesValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseGetTableInfoResponse getTableInfo(Descriptor databaseDescriptor, String table) {
|
||||
SQLiteDatabase db = databaseDescriptor.getReadable();
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT sql FROM sqlite_master WHERE name = ?", new String[] { table })) {
|
||||
cursor.moveToFirst();
|
||||
return new DatabaseGetTableInfoResponse(cursor.getString(cursor.getColumnIndex("sql")));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseExecuteSqlResponse executeSQL(Descriptor descriptor, String query) {
|
||||
SQLiteDatabase db = descriptor.getWritable();
|
||||
|
||||
String firstWordUpperCase = getFirstWord(query).toUpperCase();
|
||||
|
||||
switch (firstWordUpperCase) {
|
||||
case "UPDATE":
|
||||
case "DELETE":
|
||||
return executeUpdateDelete(db, query);
|
||||
case "INSERT":
|
||||
return executeInsert(db, query);
|
||||
case "SELECT":
|
||||
case "PRAGMA":
|
||||
case "EXPLAIN":
|
||||
return executeSelect(db, query);
|
||||
default:
|
||||
return executeRawQuery(db, query);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getFirstWord(String s) {
|
||||
s = s.trim();
|
||||
int firstSpace = s.indexOf(' ');
|
||||
return firstSpace >= 0 ? s.substring(0, firstSpace) : s;
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeUpdateDelete(SQLiteDatabase database, String query) {
|
||||
SQLiteStatement statement = database.compileStatement(query);
|
||||
int count = statement.executeUpdateDelete();
|
||||
|
||||
return DatabaseExecuteSqlResponse.successfulUpdateDelete(count);
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeInsert(SQLiteDatabase database, String query) {
|
||||
SQLiteStatement statement = database.compileStatement(query);
|
||||
long insertedId = statement.executeInsert();
|
||||
|
||||
return DatabaseExecuteSqlResponse.successfulInsert(insertedId);
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeSelect(SQLiteDatabase database, String query) {
|
||||
try (Cursor cursor = database.rawQuery(query, null)) {
|
||||
String[] columnNames = cursor.getColumnNames();
|
||||
List<List<Object>> rows = cursorToList(cursor);
|
||||
|
||||
return DatabaseExecuteSqlResponse.successfulSelect(Arrays.asList(columnNames), rows);
|
||||
}
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeRawQuery(SQLiteDatabase database, String query) {
|
||||
database.execSQL(query);
|
||||
return DatabaseExecuteSqlResponse.successfulRawQuery();
|
||||
}
|
||||
|
||||
private static @NonNull List<List<Object>> cursorToList(Cursor cursor) {
|
||||
List<List<Object>> rows = new ArrayList<>();
|
||||
int numColumns = cursor.getColumnCount();
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
List<Object> values = new ArrayList<>(numColumns);
|
||||
|
||||
for (int column = 0; column < numColumns; column++) {
|
||||
values.add(getObjectFromColumnIndex(cursor, column));
|
||||
}
|
||||
|
||||
rows.add(values);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static @Nullable Object getObjectFromColumnIndex(Cursor cursor, int column) {
|
||||
switch (cursor.getType(column)) {
|
||||
case Cursor.FIELD_TYPE_NULL:
|
||||
return null;
|
||||
case Cursor.FIELD_TYPE_INTEGER:
|
||||
return cursor.getLong(column);
|
||||
case Cursor.FIELD_TYPE_FLOAT:
|
||||
return cursor.getDouble(column);
|
||||
case Cursor.FIELD_TYPE_BLOB:
|
||||
byte[] blob = cursor.getBlob(column);
|
||||
String bytes = blob != null ? "(blob) " + Hex.toStringCondensed(Arrays.copyOf(blob, Math.min(blob.length, 32))) : null;
|
||||
if (bytes != null && bytes.length() == 32 && blob.length > 32) {
|
||||
bytes += "...";
|
||||
}
|
||||
return bytes;
|
||||
case Cursor.FIELD_TYPE_STRING:
|
||||
default:
|
||||
return cursor.getString(column);
|
||||
}
|
||||
}
|
||||
|
||||
static class Descriptor implements DatabaseDescriptor {
|
||||
private final SignalDatabaseOpenHelper sqlCipherOpenHelper;
|
||||
|
||||
Descriptor(@NonNull SignalDatabaseOpenHelper sqlCipherOpenHelper) {
|
||||
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return sqlCipherOpenHelper.getDatabaseName();
|
||||
}
|
||||
|
||||
public @NonNull SQLiteDatabase getReadable() {
|
||||
return sqlCipherOpenHelper.getSqlCipherDatabase();
|
||||
}
|
||||
|
||||
public @NonNull SQLiteDatabase getWritable() {
|
||||
return sqlCipherOpenHelper.getSqlCipherDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,8 @@
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
|
||||
|
||||
<application android:name=".ApplicationContext"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -308,8 +310,6 @@
|
||||
android:allowEmbedded="true"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity android:name=".longmessage.LongMessageActivity" />
|
||||
|
||||
<activity android:name=".conversation.ConversationPopupActivity"
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
android:launchMode="singleTask"
|
||||
@@ -318,12 +318,6 @@
|
||||
android:theme="@style/TextSecure.LightTheme.Popup"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".messagedetails.MessageDetailsActivity"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
@@ -394,6 +388,24 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".stories.my.MyStoriesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
|
||||
<activity
|
||||
android:name=".stories.settings.StorySettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name=".stories.viewer.StoryViewerActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar.StoryViewer"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
|
||||
|
||||
<activity android:name=".components.settings.app.changenumber.ChangeNumberLockActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
@@ -636,6 +648,13 @@
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
|
||||
<service android:name=".service.webrtc.AndroidCallConnectionService"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name=".components.voice.VoiceNotePlaybackService">
|
||||
<intent-filter>
|
||||
@@ -738,6 +757,8 @@
|
||||
|
||||
<receiver android:name=".service.ExpirationListener" />
|
||||
|
||||
<receiver android:name=".service.ExpiringStoriesManager$ExpireStoriesAlarm" />
|
||||
|
||||
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
||||
|
||||
<receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
|
||||
public final class AppCapabilities {
|
||||
@@ -19,6 +20,6 @@ public final class AppCapabilities {
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER);
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, FeatureFlags.stories());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,9 +52,11 @@ public final class AppInitialization {
|
||||
Log.i(TAG, "onPostBackupRestore()");
|
||||
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.onPostBackupRestore();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
SignalStore.onboarding().clearAll();
|
||||
TextSecurePreferences.onPostBackupRestore(context);
|
||||
TextSecurePreferences.setPasswordDisabled(context, true);
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
|
||||
|
||||
@@ -35,30 +35,32 @@ import org.signal.core.util.logging.AndroidLogger;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveReleaseChannelJob;
|
||||
import org.thoughtcrime.securesms.mms.SignalGlideModule;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
||||
import org.thoughtcrime.securesms.database.LogDatabase;
|
||||
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
|
||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
||||
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveReleaseChannelJob;
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
@@ -66,6 +68,7 @@ import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
|
||||
import org.thoughtcrime.securesms.mms.SignalGlideModule;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||
@@ -78,6 +81,7 @@ import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
@@ -89,9 +93,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.security.Security;
|
||||
@@ -176,7 +178,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
.addNonBlocking(this::initializeFcmCheck)
|
||||
.addNonBlocking(this::initializeSignedPreKeyCheck)
|
||||
.addNonBlocking(CreateSignedPreKeyJob::enqueueIfNeeded)
|
||||
.addNonBlocking(this::initializePeriodicTasks)
|
||||
.addNonBlocking(this::initializeCircumvention)
|
||||
.addNonBlocking(this::initializePendingMessages)
|
||||
@@ -188,6 +190,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(EmojiSource::refresh)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||
.addNonBlocking(this::ensureProfileUploaded)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
@@ -196,6 +199,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
|
||||
.addPostRender(RetrieveReleaseChannelJob::enqueue)
|
||||
.addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount())
|
||||
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
|
||||
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
|
||||
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
@@ -352,12 +359,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSignedPreKeyCheck() {
|
||||
if (!TextSecurePreferences.isSignedPreKeyRegistered(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new CreateSignedPreKeyJob(this));
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeExpiringMessageManager() {
|
||||
ApplicationDependencies.getExpiringMessageManager().checkSchedule();
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable, Multiselectable {
|
||||
@@ -94,6 +94,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onChangeNumberUpdateContact(@NonNull Recipient recipient);
|
||||
void onCallToAction(@NonNull String action);
|
||||
void onDonateClicked();
|
||||
void onBlockJoinRequest(@NonNull Recipient recipient);
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
@@ -20,22 +20,23 @@ import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
@@ -123,12 +124,12 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
|
||||
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
|
||||
callback.accept(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {}
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {}
|
||||
|
||||
@Override
|
||||
public void onBeginScroll() {
|
||||
@@ -152,7 +153,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
@Override
|
||||
protected Void doInBackground(Context... params) {
|
||||
try {
|
||||
DirectoryHelper.refreshDirectory(params[0], true);
|
||||
ContactDiscovery.refreshAll(params[0], true);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Rect;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
@@ -66,9 +67,10 @@ import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction;
|
||||
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
@@ -77,6 +79,7 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sharing.ShareContact;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -84,11 +87,11 @@ import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -96,10 +99,9 @@ import java.util.function.Consumer;
|
||||
* Fragment for selecting a one or more contacts from a list.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public final class ContactSelectionListFragment extends LoggingFragment
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||
@@ -138,18 +140,19 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
|
||||
private View shadowView;
|
||||
private ToolbarShadowAnimationHelper toolbarShadowAnimationHelper;
|
||||
|
||||
private HeaderActionProvider headerActionProvider;
|
||||
private TextView headerActionView;
|
||||
|
||||
@Nullable private FixedViewsAdapter headerAdapter;
|
||||
@Nullable private FixedViewsAdapter footerAdapter;
|
||||
@Nullable private ListCallback listCallback;
|
||||
@Nullable private ScrollCallback scrollCallback;
|
||||
private GlideRequests glideRequests;
|
||||
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
||||
private Set<RecipientId> currentSelection;
|
||||
private boolean isMulti;
|
||||
private boolean hideCount;
|
||||
private boolean canSelectSelf;
|
||||
private GlideRequests glideRequests;
|
||||
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
||||
private Set<RecipientId> currentSelection;
|
||||
private boolean isMulti;
|
||||
private boolean hideCount;
|
||||
private boolean canSelectSelf;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
@@ -190,6 +193,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) {
|
||||
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) getParentFragment();
|
||||
}
|
||||
|
||||
if (context instanceof HeaderActionProvider) {
|
||||
headerActionProvider = (HeaderActionProvider) context;
|
||||
}
|
||||
|
||||
if (getParentFragment() instanceof HeaderActionProvider) {
|
||||
headerActionProvider = (HeaderActionProvider) getParentFragment();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -243,11 +254,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
|
||||
constraintLayout = view.findViewById(R.id.container);
|
||||
shadowView = view.findViewById(R.id.toolbar_shadow);
|
||||
headerActionView = view.findViewById(R.id.header_action);
|
||||
|
||||
toolbarShadowAnimationHelper = new ToolbarShadowAnimationHelper(shadowView);
|
||||
|
||||
final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
|
||||
|
||||
recyclerView.addOnScrollListener(toolbarShadowAnimationHelper);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setItemAnimator(new DefaultItemAnimator() {
|
||||
@Override
|
||||
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
@@ -285,6 +299,40 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
currentSelection = getCurrentSelection();
|
||||
|
||||
final HeaderAction headerAction;
|
||||
if (headerActionProvider != null) {
|
||||
headerAction = headerActionProvider.getHeaderAction();
|
||||
|
||||
headerActionView.setEnabled(true);
|
||||
headerActionView.setText(headerAction.getLabel());
|
||||
headerActionView.setCompoundDrawablesRelativeWithIntrinsicBounds(headerAction.getIcon(), 0, 0, 0);
|
||||
headerActionView.setOnClickListener(v -> headerAction.getAction().run());
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
|
||||
private final Rect bounds = new Rect();
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (hideLetterHeaders()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int firstPosition = layoutManager.findFirstVisibleItemPosition();
|
||||
if (firstPosition == 0) {
|
||||
View firstChild = recyclerView.getChildAt(0);
|
||||
recyclerView.getDecoratedBoundsWithMargins(firstChild, bounds);
|
||||
headerActionView.setTranslationY(bounds.top);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
headerActionView.setEnabled(false);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -491,12 +539,19 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
fastScroller.setRecyclerView(null);
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (headerActionView.isEnabled() && !hasQueryFilter()) {
|
||||
headerActionView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
headerActionView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
cursorRecyclerViewAdapter.changeCursor(null);
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
headerActionView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private boolean shouldDisplayRecents() {
|
||||
@@ -521,7 +576,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
DirectoryHelper.refreshDirectory(context, false);
|
||||
ContactDiscovery.refreshAll(context, false);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
@@ -546,11 +601,44 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the caller to submit a list of recipients to be marked selected. Useful for when a screen needs to load preselected
|
||||
* entries in the background before setting them in the adapter.
|
||||
*
|
||||
* @param contacts List of the contacts to select. This will not overwrite the current selection, but append to it.
|
||||
*/
|
||||
public void markSelected(@NonNull Set<ShareContact> contacts) {
|
||||
if (contacts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<SelectedContact> toMarkSelected = contacts.stream()
|
||||
.map(contact -> {
|
||||
if (contact.getRecipientId().isPresent()) {
|
||||
return SelectedContact.forRecipientId(contact.getRecipientId().get());
|
||||
} else {
|
||||
return SelectedContact.forPhone(null, contact.getNumber());
|
||||
}
|
||||
})
|
||||
.filter(c -> !cursorRecyclerViewAdapter.isSelectedContact(c))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
if (toMarkSelected.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final SelectedContact selectedContact : toMarkSelected) {
|
||||
markContactSelected(selectedContact);
|
||||
}
|
||||
|
||||
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount());
|
||||
}
|
||||
|
||||
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
|
||||
@Override
|
||||
public void onItemClick(ContactSelectionListItem contact) {
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orElse(null), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orElse(null), contact.getNumber());
|
||||
|
||||
if (!canSelectSelf && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
||||
@@ -571,12 +659,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
return UsernameUtil.fetchAciForUsername(requireContext(), contact.getNumber());
|
||||
return UsernameUtil.fetchAciForUsername(contact.getNumber());
|
||||
}, uuid -> {
|
||||
loadingDialog.dismiss();
|
||||
if (uuid.isPresent()) {
|
||||
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||
Recipient recipient = Recipient.externalUsername(uuid.get(), contact.getNumber());
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
|
||||
@@ -668,7 +756,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
|
||||
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
|
||||
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
|
||||
resolved -> addChipForRecipient(resolved, selectedContact));
|
||||
}
|
||||
|
||||
@@ -686,7 +774,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
markContactUnselected(selectedContact);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orNull());
|
||||
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orElse(null));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -768,19 +856,25 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
/** Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it. */
|
||||
void onBeforeContactSelected(Optional<RecipientId> recipientId, @Nullable String number, Consumer<Boolean> callback);
|
||||
void onContactDeselected(Optional<RecipientId> recipientId, @Nullable String number);
|
||||
/**
|
||||
* Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it.
|
||||
*/
|
||||
void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, @Nullable String number, @NonNull Consumer<Boolean> callback);
|
||||
|
||||
void onContactDeselected(@NonNull Optional<RecipientId> recipientId, @Nullable String number);
|
||||
|
||||
void onSelectionChanged();
|
||||
}
|
||||
|
||||
public interface OnSelectionLimitReachedListener {
|
||||
void onSuggestedLimitReached(int limit);
|
||||
|
||||
void onHardLimitReached(int limit);
|
||||
}
|
||||
|
||||
public interface ListCallback {
|
||||
void onInvite();
|
||||
|
||||
void onNewGroup(boolean forceV1);
|
||||
}
|
||||
|
||||
@@ -788,6 +882,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
void onBeginScroll();
|
||||
}
|
||||
|
||||
public interface HeaderActionProvider {
|
||||
@NonNull HeaderAction getHeaderAction();
|
||||
}
|
||||
|
||||
public interface AbstractContactsCursorLoaderFactoryProvider {
|
||||
@NonNull AbstractContactsCursorLoader.Factory get();
|
||||
}
|
||||
|
||||
@@ -17,13 +17,17 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
@@ -32,11 +36,6 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
|
||||
@@ -186,12 +185,13 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
return BAD_CODE;
|
||||
}
|
||||
|
||||
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
|
||||
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context);
|
||||
Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext()));
|
||||
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
|
||||
IdentityKeyPair aciIdentityKeyPair = SignalStore.account().getAciIdentityKey();
|
||||
IdentityKeyPair pniIdentityKeyPair = SignalStore.account().getPniIdentityKey();
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
|
||||
TextSecurePreferences.setMultiDevice(DeviceActivity.this, true);
|
||||
accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, profileKey, verificationCode);
|
||||
accountManager.addDevice(ephemeralId, publicKey, aciIdentityKeyPair, pniIdentityKeyPair, profileKey, verificationCode);
|
||||
|
||||
return SUCCESS;
|
||||
} catch (NotFoundException e) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
@@ -16,6 +16,7 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AnimRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
@@ -37,8 +38,8 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -134,13 +135,13 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
|
||||
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
|
||||
callback.accept(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
}
|
||||
|
||||
@@ -250,7 +251,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
for (SelectedContact contact : contacts) {
|
||||
RecipientId recipientId = contact.getOrCreateRecipientId(context);
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().orElse(-1);
|
||||
|
||||
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
|
||||
|
||||
|
||||
@@ -5,19 +5,26 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLockedDialog;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
|
||||
public class MainActivity extends PassphraseRequiredActivity implements VoiceNoteMediaControllerOwner {
|
||||
|
||||
@@ -26,13 +33,14 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final MainNavigator navigator = new MainNavigator(this);
|
||||
|
||||
private VoiceNoteMediaController mediaController;
|
||||
private VoiceNoteMediaController mediaController;
|
||||
private ConversationListTabsViewModel conversationListTabsViewModel;
|
||||
|
||||
public static @NonNull Intent clearTop(@NonNull Context context) {
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK |
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK |
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
|
||||
return intent;
|
||||
@@ -42,22 +50,28 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
AppStartup.getInstance().onCriticalRenderEventStart();
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
mediaController = new VoiceNoteMediaController(this);
|
||||
navigator.onCreate(savedInstanceState);
|
||||
|
||||
ConversationListTabRepository repository = new ConversationListTabRepository();
|
||||
ConversationListTabsViewModel.Factory factory = new ConversationListTabsViewModel.Factory(repository);
|
||||
|
||||
handleGroupLinkInIntent(getIntent());
|
||||
handleProxyInIntent(getIntent());
|
||||
handleSignalMeIntent(getIntent());
|
||||
|
||||
CachedInflater.from(this).clear();
|
||||
|
||||
conversationListTabsViewModel = new ViewModelProvider(this, factory).get(ConversationListTabsViewModel.class);
|
||||
updateTabVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent getIntent() {
|
||||
return super.getIntent().setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK |
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK |
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
}
|
||||
|
||||
@@ -82,6 +96,8 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
if (SignalStore.misc().isOldDeviceTransferLocked()) {
|
||||
OldDeviceTransferLockedDialog.show(getSupportFragmentManager());
|
||||
}
|
||||
|
||||
updateTabVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,6 +115,17 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTabVisibility() {
|
||||
if (Stories.isFeatureEnabled()) {
|
||||
findViewById(R.id.conversation_list_tabs).setVisibility(View.VISIBLE);
|
||||
WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_colorSecondaryContainer));
|
||||
} else {
|
||||
findViewById(R.id.conversation_list_tabs).setVisibility(View.GONE);
|
||||
WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_background_primary));
|
||||
conversationListTabsViewModel.onChatsSelected();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull MainNavigator getNavigator() {
|
||||
return navigator;
|
||||
}
|
||||
|
||||
@@ -2,18 +2,13 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity;
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -36,16 +31,6 @@ public class MainNavigator {
|
||||
return ((MainActivity) activity).getNavigator();
|
||||
}
|
||||
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
if (savedInstanceState != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.add(R.id.fragment_container, ConversationListFragment.newInstance())
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the back pressed was handled in our own custom way, false if it should be given
|
||||
* to the system to do the default behavior.
|
||||
@@ -74,14 +59,6 @@ public class MainNavigator {
|
||||
activity.startActivityForResult(AppSettingsActivity.home(activity), REQUEST_CONFIG_CHANGES);
|
||||
}
|
||||
|
||||
public void goToArchiveList() {
|
||||
getFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||
.replace(R.id.fragment_container, ConversationListArchiveFragment.newInstance())
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public void goToGroupCreation() {
|
||||
activity.startActivity(CreateGroupActivity.newIntent(activity));
|
||||
}
|
||||
|
||||
@@ -21,13 +21,11 @@ import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -205,23 +203,25 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
else from = "";
|
||||
|
||||
if (showThread) {
|
||||
String to = null;
|
||||
String titleText = null;
|
||||
Recipient threadRecipient = mediaItem.threadRecipient;
|
||||
|
||||
if (threadRecipient != null) {
|
||||
if (mediaItem.outgoing || threadRecipient.isGroup()) {
|
||||
if (mediaItem.outgoing) {
|
||||
if (threadRecipient.isSelf()) {
|
||||
from = getString(R.string.note_to_self);
|
||||
titleText = getString(R.string.note_to_self);
|
||||
} else {
|
||||
to = threadRecipient.getDisplayName(this);
|
||||
titleText = getString(R.string.MediaPreviewActivity_you_to_s, threadRecipient.getDisplayName(this));
|
||||
}
|
||||
} else {
|
||||
to = getString(R.string.MediaPreviewActivity_you);
|
||||
if (threadRecipient.isGroup()) {
|
||||
titleText = getString(R.string.MediaPreviewActivity_s_to_s, from, threadRecipient.getDisplayName(this));
|
||||
} else {
|
||||
titleText = getString(R.string.MediaPreviewActivity_s_to_you, from);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return to != null ? getString(R.string.MediaPreviewActivity_s_to_s, from, to)
|
||||
: from;
|
||||
return titleText != null ? titleText : from;
|
||||
} else {
|
||||
return from;
|
||||
}
|
||||
@@ -555,19 +555,27 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
cursor = Objects.requireNonNull(data.first);
|
||||
|
||||
int mediaPosition = Objects.requireNonNull(data.second);
|
||||
|
||||
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(),this, cursor, mediaPosition, leftIsRecent);
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
|
||||
viewModel.setCursor(this, cursor, leftIsRecent);
|
||||
|
||||
int item = restartItem >= 0 ? restartItem : mediaPosition;
|
||||
mediaPager.setCurrentItem(item);
|
||||
int mediaPosition = Objects.requireNonNull(data.second);
|
||||
|
||||
if (item == 0) {
|
||||
viewPagerListener.onPageSelected(0);
|
||||
CursorPagerAdapter oldAdapter = (CursorPagerAdapter) mediaPager.getAdapter();
|
||||
if (oldAdapter == null) {
|
||||
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(), this, cursor, mediaPosition, leftIsRecent);
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
} else {
|
||||
oldAdapter.setCursor(cursor, mediaPosition);
|
||||
oldAdapter.setActive(true);
|
||||
}
|
||||
|
||||
if (oldAdapter == null || restartItem >= 0) {
|
||||
int item = restartItem >= 0 ? restartItem : mediaPosition;
|
||||
mediaPager.setCurrentItem(item);
|
||||
|
||||
if (item == 0) {
|
||||
viewPagerListener.onPageSelected(0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mediaNotAvailable();
|
||||
@@ -715,10 +723,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
private final Map<Integer, MediaPreviewFragment> mediaFragments = new HashMap<>();
|
||||
|
||||
private final Context context;
|
||||
private final Cursor cursor;
|
||||
private final boolean leftIsRecent;
|
||||
|
||||
private boolean active;
|
||||
private Cursor cursor;
|
||||
private int autoPlayPosition;
|
||||
|
||||
CursorPagerAdapter(@NonNull FragmentManager fragmentManager,
|
||||
@@ -739,6 +747,11 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setCursor(@NonNull Cursor cursor, int autoPlayPosition) {
|
||||
this.cursor = cursor;
|
||||
this.autoPlayPosition = autoPlayPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
if (!active) return 0;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -21,10 +21,11 @@ import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
@@ -34,9 +35,9 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
@@ -61,7 +62,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
|
||||
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
|
||||
if (recipientId.isPresent()) {
|
||||
launch(Recipient.resolved(recipientId.get()));
|
||||
} else {
|
||||
@@ -75,10 +76,10 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
Recipient resolved = Recipient.external(this, number);
|
||||
|
||||
if (!resolved.isRegistered() || !resolved.hasAci()) {
|
||||
if (!resolved.isRegistered() || !resolved.hasServiceId()) {
|
||||
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
||||
try {
|
||||
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
|
||||
ContactDiscovery.refresh(this, resolved, false);
|
||||
resolved = Recipient.resolved(resolved.getId());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
|
||||
|
||||
@@ -19,9 +19,9 @@ package org.thoughtcrime.securesms;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||
|
||||
/**
|
||||
@@ -61,7 +61,8 @@ public class PassphraseCreateActivity extends PassphraseActivity {
|
||||
passphrase);
|
||||
|
||||
MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret);
|
||||
IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this);
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary();
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary();
|
||||
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -9,7 +9,9 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public class TransportOption implements Parcelable {
|
||||
|
||||
@@ -18,8 +20,8 @@ public class TransportOption implements Parcelable {
|
||||
TEXTSECURE
|
||||
}
|
||||
|
||||
private final int drawable;
|
||||
private final int backgroundColor;
|
||||
private final int drawable;
|
||||
private final int backgroundColor;
|
||||
private final @NonNull String text;
|
||||
private final @NonNull Type type;
|
||||
private final @NonNull String composeHint;
|
||||
@@ -35,7 +37,7 @@ public class TransportOption implements Parcelable {
|
||||
@NonNull CharacterCalculator characterCalculator)
|
||||
{
|
||||
this(type, drawable, backgroundColor, text, composeHint, characterCalculator,
|
||||
Optional.<CharSequence>absent(), Optional.<Integer>absent());
|
||||
Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
public TransportOption(@NonNull Type type,
|
||||
@@ -64,8 +66,8 @@ public class TransportOption implements Parcelable {
|
||||
in.readString(),
|
||||
in.readString(),
|
||||
CharacterCalculator.readFromParcel(in),
|
||||
Optional.fromNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
|
||||
in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.absent());
|
||||
Optional.ofNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
|
||||
in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.empty());
|
||||
}
|
||||
|
||||
public @NonNull Type getType() {
|
||||
@@ -123,7 +125,7 @@ public class TransportOption implements Parcelable {
|
||||
dest.writeString(text);
|
||||
dest.writeString(composeHint);
|
||||
CharacterCalculator.writeToParcel(dest, characterCalculator);
|
||||
TextUtils.writeToParcel(simName.orNull(), dest, flags);
|
||||
TextUtils.writeToParcel(simName.orElse(null), dest, flags);
|
||||
|
||||
if (simSubscriptionId.isPresent()) {
|
||||
dest.writeInt(1);
|
||||
|
||||
@@ -14,13 +14,14 @@ import org.thoughtcrime.securesms.util.PushCharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.SmsCharacterCalculator;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.thoughtcrime.securesms.TransportOption.Type;
|
||||
|
||||
@@ -33,8 +34,8 @@ public class TransportOptions {
|
||||
private final List<TransportOption> enabledTransports;
|
||||
|
||||
private Type defaultTransportType = Type.SMS;
|
||||
private Optional<Integer> defaultSubscriptionId = Optional.absent();
|
||||
private Optional<TransportOption> selectedOption = Optional.absent();
|
||||
private Optional<Integer> defaultSubscriptionId = Optional.empty();
|
||||
private Optional<TransportOption> selectedOption = Optional.empty();
|
||||
|
||||
private final Optional<Integer> systemSubscriptionId;
|
||||
|
||||
@@ -54,7 +55,7 @@ public class TransportOptions {
|
||||
setSelectedTransport(null);
|
||||
} else {
|
||||
this.defaultTransportType = Type.SMS;
|
||||
this.defaultSubscriptionId = Optional.absent();
|
||||
this.defaultSubscriptionId = Optional.empty();
|
||||
|
||||
notifyTransportChangeListeners();
|
||||
}
|
||||
@@ -81,7 +82,7 @@ public class TransportOptions {
|
||||
}
|
||||
|
||||
public void setSelectedTransport(@Nullable TransportOption transportOption) {
|
||||
this.selectedOption = Optional.fromNullable(transportOption);
|
||||
this.selectedOption = Optional.ofNullable(transportOption);
|
||||
notifyTransportChangeListeners();
|
||||
}
|
||||
|
||||
@@ -93,7 +94,7 @@ public class TransportOptions {
|
||||
if (selectedOption.isPresent()) return selectedOption.get();
|
||||
|
||||
if (defaultTransportType == Type.SMS) {
|
||||
TransportOption transportOption = findEnabledSmsTransportOption(defaultSubscriptionId.or(systemSubscriptionId));
|
||||
TransportOption transportOption = findEnabledSmsTransportOption(OptionalUtil.or(defaultSubscriptionId, systemSubscriptionId));
|
||||
if (transportOption != null) {
|
||||
return transportOption;
|
||||
}
|
||||
@@ -124,7 +125,7 @@ public class TransportOptions {
|
||||
|
||||
for (TransportOption transportOption : enabledTransports) {
|
||||
if (transportOption.getType() == Type.SMS &&
|
||||
subId == transportOption.getSimSubscriptionId().or(-1)) {
|
||||
subId == transportOption.getSimSubscriptionId().orElse(-1)) {
|
||||
return transportOption;
|
||||
}
|
||||
}
|
||||
@@ -133,7 +134,7 @@ public class TransportOptions {
|
||||
}
|
||||
|
||||
public void disableTransport(Type type) {
|
||||
TransportOption selected = selectedOption.orNull();
|
||||
TransportOption selected = selectedOption.orElse(null);
|
||||
|
||||
Iterator<TransportOption> iterator = enabledTransports.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
|
||||
@@ -49,6 +49,7 @@ import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
|
||||
@@ -75,10 +76,10 @@ import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VibrateUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
import java.util.List;
|
||||
@@ -92,6 +93,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private static final String TAG = Log.tag(WebRtcCallActivity.class);
|
||||
|
||||
private static final int STANDARD_DELAY_FINISH = 1000;
|
||||
private static final int VIBRATE_DURATION = 50;
|
||||
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
|
||||
@@ -501,6 +503,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCallReconnecting() {
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__reconnecting));
|
||||
VibrateUtil.vibrate(this, VIBRATE_DURATION);
|
||||
}
|
||||
|
||||
private void handleRecipientUnavailable() {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
|
||||
@@ -623,6 +630,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
handleCallPreJoin(event); break;
|
||||
case CALL_CONNECTED:
|
||||
handleCallConnected(event); break;
|
||||
case CALL_RECONNECTING:
|
||||
handleCallReconnecting(); break;
|
||||
case NETWORK_FAILURE:
|
||||
handleServerFailure(); break;
|
||||
case CALL_RINGING:
|
||||
|
||||
@@ -9,12 +9,12 @@ import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class PointerAttachment extends Attachment {
|
||||
|
||||
@@ -93,7 +93,7 @@ public class PointerAttachment extends Attachment {
|
||||
}
|
||||
|
||||
public static Optional<Attachment> forPointer(Optional<SignalServiceAttachment> pointer, @Nullable StickerLocator stickerLocator, @Nullable String fastPreflightId) {
|
||||
if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.absent();
|
||||
if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.empty();
|
||||
|
||||
String encodedKey = null;
|
||||
|
||||
@@ -103,12 +103,12 @@ public class PointerAttachment extends Attachment {
|
||||
|
||||
return Optional.of(new PointerAttachment(pointer.get().getContentType(),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||
pointer.get().asPointer().getSize().or(0),
|
||||
pointer.get().asPointer().getFileName().orNull(),
|
||||
pointer.get().asPointer().getSize().orElse(0),
|
||||
pointer.get().asPointer().getFileName().orElse(null),
|
||||
pointer.get().asPointer().getCdnNumber(),
|
||||
pointer.get().asPointer().getRemoteId().toString(),
|
||||
encodedKey, null,
|
||||
pointer.get().asPointer().getDigest().orNull(),
|
||||
pointer.get().asPointer().getDigest().orElse(null),
|
||||
fastPreflightId,
|
||||
pointer.get().asPointer().getVoiceNote(),
|
||||
pointer.get().asPointer().isBorderless(),
|
||||
@@ -116,9 +116,9 @@ public class PointerAttachment extends Attachment {
|
||||
pointer.get().asPointer().getWidth(),
|
||||
pointer.get().asPointer().getHeight(),
|
||||
pointer.get().asPointer().getUploadTimestamp(),
|
||||
pointer.get().asPointer().getCaption().orNull(),
|
||||
pointer.get().asPointer().getCaption().orElse(null),
|
||||
stickerLocator,
|
||||
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
|
||||
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orElse(null))));
|
||||
|
||||
}
|
||||
|
||||
@@ -127,13 +127,13 @@ public class PointerAttachment extends Attachment {
|
||||
|
||||
return Optional.of(new PointerAttachment(pointer.getContentType(),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||
thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0,
|
||||
pointer.getFileName(),
|
||||
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
|
||||
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
|
||||
null,
|
||||
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
|
||||
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
@@ -141,7 +141,7 @@ public class PointerAttachment extends Attachment {
|
||||
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null,
|
||||
thumbnail != null ? thumbnail.asPointer().getCaption().orElse(null) : null,
|
||||
null,
|
||||
null));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
@@ -52,7 +51,7 @@ public class AudioRecorder {
|
||||
.withMimeType(MediaUtil.AUDIO_AAC)
|
||||
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
|
||||
|
||||
recorder = Build.VERSION.SDK_INT >= 26 && FeatureFlags.voiceNoteRecordingV2() ? new MediaRecorderWrapper() : new AudioCodec();
|
||||
recorder = Build.VERSION.SDK_INT >= 26 ? new MediaRecorderWrapper() : new AudioCodec();
|
||||
recorder.start(fds[1]);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
|
||||
@@ -14,10 +14,10 @@ import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
import javax.annotation.meta.Exhaustive
|
||||
|
||||
/**
|
||||
@@ -128,6 +128,6 @@ object AvatarRenderer {
|
||||
}
|
||||
|
||||
private fun createMedia(uri: Uri, size: Long): Media {
|
||||
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent())
|
||||
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.empty(), Optional.empty(), Optional.empty())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ class TextAvatarDrawable(
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val textSize = Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f)
|
||||
val width = bounds.width()
|
||||
val textSize = Avatars.getTextSizeForLength(context, avatar.text, width * 0.8f, width * 0.45f)
|
||||
val candidates = EmojiProvider.getCandidates(avatar.text)
|
||||
|
||||
textPaint.textSize = textSize
|
||||
|
||||
@@ -244,4 +244,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
}
|
||||
.execute()
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.thoughtcrime.securesms.avatar.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* AvatarView encapsulating the AvatarImageView and decorations.
|
||||
*/
|
||||
class AvatarView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : FrameLayout(context, attrs) {
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.avatar_view, this)
|
||||
|
||||
isClickable = false
|
||||
}
|
||||
|
||||
private val avatar: AvatarImageView = findViewById<AvatarImageView>(R.id.avatar_image_view).apply {
|
||||
initialize(context, attrs)
|
||||
}
|
||||
|
||||
private val storyRing: View = findViewById(R.id.avatar_story_ring)
|
||||
|
||||
private fun showStoryRing(hasUnreadStory: Boolean) {
|
||||
if (!Stories.isFeatureEnabled()) {
|
||||
return
|
||||
}
|
||||
|
||||
storyRing.visible = true
|
||||
storyRing.isActivated = hasUnreadStory
|
||||
|
||||
avatar.scaleX = 0.82f
|
||||
avatar.scaleY = 0.82f
|
||||
}
|
||||
|
||||
private fun hideStoryRing() {
|
||||
storyRing.visible = false
|
||||
|
||||
avatar.scaleX = 1f
|
||||
avatar.scaleY = 1f
|
||||
}
|
||||
|
||||
fun setStoryRingFromState(storyViewState: StoryViewState) {
|
||||
when (storyViewState) {
|
||||
StoryViewState.NONE -> hideStoryRing()
|
||||
StoryViewState.UNVIEWED -> showStoryRing(true)
|
||||
StoryViewState.VIEWED -> showStoryRing(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays Note-to-Self
|
||||
*/
|
||||
fun displayChatAvatar(recipient: Recipient) {
|
||||
avatar.setAvatar(recipient)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays Note-to-Self
|
||||
*/
|
||||
fun displayChatAvatar(requestManager: GlideRequests, recipient: Recipient, isQuickContactEnabled: Boolean) {
|
||||
avatar.setAvatar(requestManager, recipient, isQuickContactEnabled)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays Profile image
|
||||
*/
|
||||
fun displayProfileAvatar(recipient: Recipient) {
|
||||
avatar.setRecipient(recipient)
|
||||
}
|
||||
|
||||
fun setFallbackPhotoProvider(fallbackPhotoProvider: Recipient.FallbackPhotoProvider) {
|
||||
avatar.setFallbackPhotoProvider(fallbackPhotoProvider)
|
||||
}
|
||||
|
||||
fun disableQuickContact() {
|
||||
avatar.disableQuickContact()
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
@@ -18,10 +18,11 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.Conversions;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.kdf.HKDFv3;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
|
||||
@@ -47,13 +48,11 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.kdf.HKDFv3;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -182,12 +181,6 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
stopwatch.split("table::" + table);
|
||||
}
|
||||
|
||||
for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) {
|
||||
throwIfCanceled(cancellationSignal);
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
|
||||
outputStream.write(preference);
|
||||
}
|
||||
|
||||
for (BackupProtos.SharedPreference preference : TextSecurePreferences.getPreferencesToSaveToBackup(context)) {
|
||||
throwIfCanceled(cancellationSignal);
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
|
||||
@@ -448,7 +441,12 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
|
||||
Class<?> type = dataSet.getType(key);
|
||||
if (type == byte[].class) {
|
||||
builder.setBlobValue(ByteString.copyFrom(dataSet.getBlob(key, null)));
|
||||
byte[] data = dataSet.getBlob(key, null);
|
||||
if (data != null) {
|
||||
builder.setBlobValue(ByteString.copyFrom(dataSet.getBlob(key, null)));
|
||||
} else {
|
||||
Log.w(TAG, "Skipping storing null blob for key: " + key);
|
||||
}
|
||||
} else if (type == Boolean.class) {
|
||||
builder.setBooleanValue(dataSet.getBoolean(key, false));
|
||||
} else if (type == Float.class) {
|
||||
@@ -458,7 +456,12 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
} else if (type == Long.class) {
|
||||
builder.setLongValue(dataSet.getLong(key, 0));
|
||||
} else if (type == String.class) {
|
||||
builder.setStringValue(dataSet.getString(key, null));
|
||||
String data = dataSet.getString(key, null);
|
||||
if (data != null) {
|
||||
builder.setStringValue(dataSet.getString(key, null));
|
||||
} else {
|
||||
Log.w(TAG, "Skipping storing null string for key: " + key);
|
||||
}
|
||||
} else {
|
||||
throw new AssertionError("Unknown type: " + type);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.Conversions;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.kdf.HKDFv3;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion;
|
||||
@@ -32,12 +34,11 @@ import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.whispersystems.libsignal.kdf.HKDFv3;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.signal.core.util.SqlUtil;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
@@ -68,6 +69,18 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(FullBackupImporter.class);
|
||||
|
||||
private static final String[] TABLES_TO_DROP_FIRST = {
|
||||
"distribution_list_member",
|
||||
"distribution_list",
|
||||
"message_send_log_recipients",
|
||||
"msl_recipient",
|
||||
"msl_message",
|
||||
"reaction",
|
||||
"notification_profile_schedule",
|
||||
"notification_profile_allowed_members",
|
||||
"story_sends"
|
||||
};
|
||||
|
||||
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
|
||||
throws IOException
|
||||
@@ -250,6 +263,17 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static void processPreference(@NonNull Context context, SharedPreference preference) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(preference.getFile(), 0);
|
||||
|
||||
// Identity keys were moved from shared prefs into SignalStore. Need to handle importing backups made before the migration.
|
||||
if ("SecureSMS-Preferences".equals(preference.getFile())) {
|
||||
if ("pref_identity_public_v3".equals(preference.getKey()) && preference.hasValue()) {
|
||||
SignalStore.account().restoreLegacyIdentityPublicKeyFromBackup(preference.getValue());
|
||||
} else if ("pref_identity_private_v3".equals(preference.getKey()) && preference.hasValue()) {
|
||||
SignalStore.account().restoreLegacyIdentityPrivateKeyFromBackup(preference.getValue());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (preference.hasValue()) {
|
||||
preferences.edit().putString(preference.getKey(), preference.getValue()).commit();
|
||||
} else if (preference.hasBooleanValue()) {
|
||||
@@ -260,12 +284,17 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
}
|
||||
|
||||
private static void dropAllTables(@NonNull SQLiteDatabase db) {
|
||||
for (String name : TABLES_TO_DROP_FIRST) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + name);
|
||||
}
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT name, type FROM sqlite_master", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String name = cursor.getString(0);
|
||||
String type = cursor.getString(1);
|
||||
|
||||
if ("table".equals(type) && !name.startsWith("sqlite_")) {
|
||||
Log.i(TAG, "Dropping table: " + name);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
class BadgeImageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
|
||||
@@ -9,13 +9,13 @@ import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.util.Pair
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||
import org.thoughtcrime.securesms.util.ScreenDensity
|
||||
import org.whispersystems.libsignal.util.Pair
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import java.math.BigDecimal
|
||||
import java.sql.Timestamp
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.badges.self.expired
|
||||
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.SplashImage
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
|
||||
class CantProcessSubscriptionPaymentBottomSheetDialogFragment : DSLSettingsBottomSheetFragment() {
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
SplashImage.register(adapter)
|
||||
adapter.submitList(getConfiguration().toMappingModelList())
|
||||
}
|
||||
|
||||
private fun getConfiguration(): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(SplashImage.Model(R.drawable.ic_card_process))
|
||||
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__cant_process_subscription_payment, DSLSettingsText.CenterModifier)
|
||||
)
|
||||
|
||||
textPref(
|
||||
summary = DSLSettingsText.from(
|
||||
requireContext().getString(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__were_having_trouble),
|
||||
DSLSettingsText.LearnMoreModifier(ContextCompat.getColor(requireContext(), R.color.signal_accent_primary)) {
|
||||
CommunicationActions.openBrowserLink(requireContext(), requireContext().getString(R.string.donation_decline_code_error_url))
|
||||
},
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(android.R.string.ok)
|
||||
) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
secondaryButtonNoOutline(
|
||||
text = DSLSettingsText.from(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__dont_show_this_again)
|
||||
) {
|
||||
SignalStore.donationsValues().showCantProcessDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
@@ -27,9 +28,13 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
}
|
||||
|
||||
private fun getConfiguration(): DSLConfiguration {
|
||||
val badge: Badge = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments()).badge
|
||||
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments())
|
||||
val badge: Badge = args.badge
|
||||
val cancellationReason: UnexpectedSubscriptionCancellation? = UnexpectedSubscriptionCancellation.fromStatus(args.cancelationReason)
|
||||
val isLikelyASustainer = SignalStore.donationsValues().isLikelyASustainer()
|
||||
|
||||
val inactive = cancellationReason == UnexpectedSubscriptionCancellation.INACTIVE
|
||||
|
||||
return configure {
|
||||
customPref(ExpiredBadge.Model(badge))
|
||||
|
||||
@@ -50,8 +55,10 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
DSLSettingsText.from(
|
||||
if (badge.isBoost()) {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_boost_badge_has_expired)
|
||||
} else if (inactive) {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer_subscription_was_automatically, badge.name)
|
||||
} else {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer, badge.name)
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer_subscription_was_canceled)
|
||||
},
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
@@ -109,8 +116,8 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun show(badge: Badge, fragmentManager: FragmentManager) {
|
||||
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge).build()
|
||||
fun show(badge: Badge, cancellationReason: UnexpectedSubscriptionCancellation?, fragmentManager: FragmentManager) {
|
||||
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge, cancellationReason?.status).build()
|
||||
val fragment = ExpiredBadgeBottomSheetDialogFragment()
|
||||
fragment.arguments = args.toBundle()
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
Badge.register(adapter) { badge, _, isFaded ->
|
||||
if (badge.isExpired() || isFaded) {
|
||||
findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge))
|
||||
findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge, null))
|
||||
} else {
|
||||
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, Recipient.self().id, badge)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.util.Optional
|
||||
|
||||
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
|
||||
|
||||
@@ -54,13 +54,13 @@ class BadgesOverviewViewModel(
|
||||
subscriptionsRepository.getSubscriptions()
|
||||
) { active, all ->
|
||||
if (!active.isActive && active.activeSubscription?.willCancelAtPeriodEnd() == true) {
|
||||
Optional.fromNullable<String>(all.firstOrNull { it.level == active.activeSubscription?.level }?.badge?.id)
|
||||
Optional.ofNullable<String>(all.firstOrNull { it.level == active.activeSubscription?.level }?.badge?.id)
|
||||
} else {
|
||||
Optional.absent()
|
||||
Optional.empty()
|
||||
}
|
||||
}.subscribeBy(
|
||||
onSuccess = { badgeId ->
|
||||
store.update { it.copy(fadedBadgeId = badgeId.orNull()) }
|
||||
store.update { it.copy(fadedBadgeId = badgeId.orElse(null)) }
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Could not retrieve data from server", throwable)
|
||||
|
||||
@@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
@@ -24,8 +24,8 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class BlockedUsersActivity extends PassphraseRequiredActivity implements BlockedUsersFragment.Listener, ContactSelectionListFragment.OnContactSelectedListener {
|
||||
@@ -87,8 +87,8 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
|
||||
final String displayName = recipientId.transform(id -> Recipient.resolved(id).getDisplayName(this)).or(number);
|
||||
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
|
||||
final String displayName = recipientId.map(id -> Recipient.resolved(id).getDisplayName(this)).orElse(number);
|
||||
|
||||
AlertDialog confirmationDialog = new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.BlockedUsersActivity__block_user)
|
||||
@@ -116,7 +116,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -64,7 +65,9 @@ final class BlockedUsersAdapter extends ListAdapter<Recipient, BlockedUsersAdapt
|
||||
displayName.setText(recipient.getDisplayName(itemView.getContext()));
|
||||
|
||||
if (recipient.hasAUserSetDisplayName(itemView.getContext())) {
|
||||
String identifier = recipient.getE164().transform(PhoneNumberFormatter::prettyPrint).or(recipient.getUsername()).orNull();
|
||||
String identifier = OptionalUtil.or(recipient.getE164().map(PhoneNumberFormatter::prettyPrint),
|
||||
recipient.getUsername())
|
||||
.orElse(null);
|
||||
|
||||
if (identifier != null) {
|
||||
numberOrUsername.setText(identifier);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package org.thoughtcrime.securesms.blocked;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -15,6 +12,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
@@ -74,24 +72,9 @@ public class BlockedUsersFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void handleRecipientClicked(@NonNull Recipient recipient) {
|
||||
AlertDialog confirmationDialog = new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.BlockedUsersActivity__unblock_user)
|
||||
.setMessage(getString(R.string.BlockedUsersActivity__do_you_want_to_unblock_s, recipient.getDisplayName(requireContext())))
|
||||
.setPositiveButton(R.string.BlockedUsersActivity__unblock, (dialog, which) -> {
|
||||
viewModel.unblock(recipient.getId());
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setCancelable(true)
|
||||
.create();
|
||||
|
||||
confirmationDialog.setOnShowListener(dialog -> {
|
||||
confirmationDialog.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(Color.RED);
|
||||
BlockUnblockDialog.showUnblockFor(requireContext(), getViewLifecycleOwner().getLifecycle(), recipient, () -> {
|
||||
viewModel.unblock(recipient.getId());
|
||||
});
|
||||
|
||||
confirmationDialog.show();
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
|
||||
@@ -95,7 +95,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
initialize(context, attrs);
|
||||
}
|
||||
|
||||
private void initialize(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
public void initialize(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
setScaleType(ScaleType.CENTER_CROP);
|
||||
|
||||
if (attrs != null) {
|
||||
|
||||
@@ -41,10 +41,10 @@ import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ConversationItemFooter extends ConstraintLayout {
|
||||
@@ -246,7 +246,7 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
});
|
||||
|
||||
if (isOutgoing) {
|
||||
dateView.setMaxWidth(ViewUtil.dpToPx(28));
|
||||
dateView.setMaxWidth(ViewUtil.dpToPx(32));
|
||||
} else {
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(this);
|
||||
|
||||
@@ -11,15 +11,11 @@ import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class ConversationTypingView extends ConstraintLayout {
|
||||
|
||||
@@ -7,11 +7,9 @@ import android.graphics.Path;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class CornerMask {
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
public class DocumentView extends FrameLayout {
|
||||
|
||||
@@ -105,11 +106,11 @@ public class DocumentView extends FrameLayout {
|
||||
|
||||
this.documentSlide = documentSlide;
|
||||
|
||||
this.fileName.setText(documentSlide.getFileName()
|
||||
.or(documentSlide.getCaption())
|
||||
.or(getContext().getString(R.string.DocumentView_unnamed_file)));
|
||||
this.fileName.setText(OptionalUtil.or(documentSlide.getFileName(),
|
||||
documentSlide.getCaption())
|
||||
.orElse(getContext().getString(R.string.DocumentView_unnamed_file)));
|
||||
this.fileSize.setText(Util.getPrettyFileSize(documentSlide.getFileSize()));
|
||||
this.document.setText(documentSlide.getFileType(getContext()).or("").toLowerCase());
|
||||
this.document.setText(documentSlide.getFileType(getContext()).orElse("").toLowerCase());
|
||||
this.setOnClickListener(new OpenClickedListener(documentSlide));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
@@ -12,6 +13,7 @@ import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
/**
|
||||
@@ -21,9 +23,12 @@ abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFr
|
||||
|
||||
protected open val peekHeightPercentage: Float = 0.5f
|
||||
|
||||
@StyleRes
|
||||
protected open val themeResId: Int = R.style.Widget_Signal_FixedRoundedCorners
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NORMAL, R.style.Widget_Signal_FixedRoundedCorners)
|
||||
setStyle(STYLE_NORMAL, themeResId)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
@@ -38,7 +43,8 @@ abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFr
|
||||
|
||||
val dialogBackground = MaterialShapeDrawable(shapeAppearanceModel)
|
||||
|
||||
dialogBackground.setTint(ContextCompat.getColor(requireContext(), R.color.signal_background_dialog))
|
||||
val bottomSheetStyle = ThemeUtil.getThemedResourceId(ContextThemeWrapper(requireContext(), themeResId), R.attr.bottomSheetStyle)
|
||||
dialogBackground.setTint(ThemeUtil.getThemedColor(ContextThemeWrapper(requireContext(), bottomSheetStyle), R.attr.backgroundTint))
|
||||
|
||||
dialog.behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
/**
|
||||
* Activity that wraps a given fragment
|
||||
*/
|
||||
abstract class FragmentWrapperActivity : PassphraseRequiredActivity() {
|
||||
|
||||
protected open val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
setContentView(R.layout.fragment_container)
|
||||
dynamicTheme.onCreate(this)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, getFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getFragment(): Fragment
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
dynamicTheme.onResume(this)
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
builder.append(suffix);
|
||||
}
|
||||
|
||||
if (recipient.isReleaseNotes()) {
|
||||
if (recipient.showVerified()) {
|
||||
Drawable official = ContextUtil.requireDrawable(getContext(), R.drawable.ic_official_20);
|
||||
official.setBounds(0, 0, ViewUtil.dpToPx(20), ViewUtil.dpToPx(20));
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
/**
|
||||
* Base dialog fragment for rendering as a full screen dialog with animation
|
||||
@@ -35,7 +34,11 @@ public abstract class FullScreenDialogFragment extends DialogFragment {
|
||||
View view = inflater.inflate(R.layout.full_screen_dialog_fragment, container, false);
|
||||
inflater.inflate(getDialogLayoutResource(), view.findViewById(R.id.full_screen_dialog_content), true);
|
||||
toolbar = view.findViewById(R.id.full_screen_dialog_toolbar);
|
||||
toolbar.setTitle(getTitle());
|
||||
|
||||
if (getTitle() != -1) {
|
||||
toolbar.setTitle(getTitle());
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener(v -> onNavigateUp());
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -53,11 +53,11 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class InputPanel extends LinearLayout
|
||||
@@ -79,7 +79,8 @@ public class InputPanel extends LinearLayout
|
||||
private ComposeText composeText;
|
||||
private View quickCameraToggle;
|
||||
private View quickAudioToggle;
|
||||
private View buttonToggle;
|
||||
private AnimatingToggle buttonToggle;
|
||||
private SendButton sendButton;
|
||||
private View recordingContainer;
|
||||
private View recordLockCancel;
|
||||
private ViewGroup composeContainer;
|
||||
@@ -127,6 +128,7 @@ public class InputPanel extends LinearLayout
|
||||
this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
|
||||
this.quickAudioToggle = findViewById(R.id.quick_audio_toggle);
|
||||
this.buttonToggle = findViewById(R.id.button_toggle);
|
||||
this.sendButton = findViewById(R.id.send_button);
|
||||
this.recordingContainer = findViewById(R.id.recording_container);
|
||||
this.recordLockCancel = findViewById(R.id.record_cancel);
|
||||
this.voiceNoteDraftView = findViewById(R.id.voice_note_draft_view);
|
||||
@@ -185,7 +187,13 @@ public class InputPanel extends LinearLayout
|
||||
: 0;
|
||||
|
||||
this.quoteView.setVisibility(VISIBLE);
|
||||
this.quoteView.measure(0, 0);
|
||||
|
||||
int maxWidth = composeContainer.getWidth();
|
||||
if (quoteView.getLayoutParams() instanceof MarginLayoutParams) {
|
||||
MarginLayoutParams layoutParams = (MarginLayoutParams) quoteView.getLayoutParams();
|
||||
maxWidth -= layoutParams.leftMargin + layoutParams.rightMargin;
|
||||
}
|
||||
this.quoteView.measure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), 0);
|
||||
|
||||
if (quoteAnimator != null) {
|
||||
quoteAnimator.cancel();
|
||||
@@ -250,7 +258,7 @@ public class InputPanel extends LinearLayout
|
||||
if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) {
|
||||
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody().toString(), false, quoteView.getAttachments(), quoteView.getMentions()));
|
||||
} else {
|
||||
return Optional.absent();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,13 +362,13 @@ public class InputPanel extends LinearLayout
|
||||
slideToCancel.display();
|
||||
|
||||
if (emojiVisible) {
|
||||
ViewUtil.fadeOut(mediaKeyboard, FADE_TIME, View.INVISIBLE);
|
||||
fadeOut(mediaKeyboard);
|
||||
}
|
||||
|
||||
ViewUtil.fadeOut(composeText, FADE_TIME, View.INVISIBLE);
|
||||
ViewUtil.fadeOut(quickCameraToggle, FADE_TIME, View.INVISIBLE);
|
||||
ViewUtil.fadeOut(quickAudioToggle, FADE_TIME, View.INVISIBLE);
|
||||
buttonToggle.animate().alpha(0).setDuration(FADE_TIME).start();
|
||||
fadeOut(composeText);
|
||||
fadeOut(quickCameraToggle);
|
||||
fadeOut(quickAudioToggle);
|
||||
fadeOut(buttonToggle);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -401,7 +409,7 @@ public class InputPanel extends LinearLayout
|
||||
public void onRecordLocked() {
|
||||
slideToCancel.hide();
|
||||
recordLockCancel.setVisibility(View.VISIBLE);
|
||||
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
|
||||
fadeIn(buttonToggle);
|
||||
if (listener != null) listener.onRecorderLocked();
|
||||
}
|
||||
|
||||
@@ -475,6 +483,7 @@ public class InputPanel extends LinearLayout
|
||||
voiceNoteDraftView.setDraft(voiceNoteDraft);
|
||||
voiceNoteDraftView.setVisibility(VISIBLE);
|
||||
hideNormalComposeViews();
|
||||
buttonToggle.displayQuick(sendButton);
|
||||
} else {
|
||||
voiceNoteDraftView.clearDraft();
|
||||
ViewUtil.fadeOut(voiceNoteDraftView, FADE_TIME);
|
||||
@@ -488,36 +497,33 @@ public class InputPanel extends LinearLayout
|
||||
|
||||
private void hideNormalComposeViews() {
|
||||
if (emojiVisible) {
|
||||
Animation animation = mediaKeyboard.getAnimation();
|
||||
if (animation != null) {
|
||||
animation.cancel();
|
||||
}
|
||||
|
||||
mediaKeyboard.setVisibility(View.INVISIBLE);
|
||||
mediaKeyboard.animate().cancel();
|
||||
mediaKeyboard.setAlpha(0f);
|
||||
}
|
||||
|
||||
for (Animation animation : Arrays.asList(composeText.getAnimation(), quickCameraToggle.getAnimation(), quickAudioToggle.getAnimation())) {
|
||||
if (animation != null) {
|
||||
animation.cancel();
|
||||
}
|
||||
for (View view : Arrays.asList(composeText, quickCameraToggle, quickAudioToggle)) {
|
||||
view.animate().cancel();
|
||||
view.setAlpha(0f);
|
||||
}
|
||||
|
||||
buttonToggle.animate().cancel();
|
||||
|
||||
composeText.setVisibility(View.INVISIBLE);
|
||||
quickCameraToggle.setVisibility(View.INVISIBLE);
|
||||
quickAudioToggle.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
private void fadeInNormalComposeViews() {
|
||||
if (emojiVisible) {
|
||||
ViewUtil.fadeIn(mediaKeyboard, FADE_TIME);
|
||||
fadeIn(mediaKeyboard);
|
||||
}
|
||||
|
||||
ViewUtil.fadeIn(composeText, FADE_TIME);
|
||||
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
|
||||
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
|
||||
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
|
||||
fadeIn(composeText);
|
||||
fadeIn(quickCameraToggle);
|
||||
fadeIn(quickAudioToggle);
|
||||
fadeIn(buttonToggle);
|
||||
}
|
||||
|
||||
private void fadeIn(@NonNull View v) {
|
||||
v.animate().alpha(1).setDuration(FADE_TIME).start();
|
||||
}
|
||||
|
||||
private void fadeOut(@NonNull View v) {
|
||||
v.animate().alpha(0).setDuration(FADE_TIME).start();
|
||||
}
|
||||
|
||||
private void updateVisibility() {
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.Guideline;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
|
||||
|
||||
private var hasShown = false
|
||||
|
||||
protected open val withDim: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setStyle(STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet)
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -29,7 +31,10 @@ abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
|
||||
dialog.window?.setDimAmount(0f)
|
||||
if (!withDim) {
|
||||
dialog.window?.setDimAmount(0f)
|
||||
}
|
||||
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
|
||||
return dialog
|
||||
|
||||
@@ -22,13 +22,13 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
/**
|
||||
* The view shown in the compose box or conversation that represents the state of the link preview.
|
||||
|
||||
@@ -19,7 +19,6 @@ import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@@ -101,7 +100,7 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
if (!Permissions.hasAll(getContext(), Manifest.permission.RECORD_AUDIO)) {
|
||||
if (listener != null) listener.onRecordPermissionRequired();
|
||||
} else {
|
||||
} else if (state == State.NOT_RUNNING) {
|
||||
state = State.RUNNING_HELD;
|
||||
floatingRecordButton.display(event.getX(), event.getY());
|
||||
lockDropTarget.display();
|
||||
|
||||
@@ -29,13 +29,17 @@ public class OutlinedThumbnailView extends ThumbnailView {
|
||||
cornerMask = new CornerMask(this);
|
||||
outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
|
||||
int defaultOutlinerColor = ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20);
|
||||
outliner.setColor(defaultOutlinerColor);
|
||||
|
||||
int radius = 0;
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.OutlinedThumbnailView, 0, 0);
|
||||
radius = typedArray.getDimensionPixelOffset(R.styleable.OutlinedThumbnailView_otv_cornerRadius, 0);
|
||||
|
||||
outliner.setStrokeWidth(typedArray.getDimensionPixelSize(R.styleable.OutlinedThumbnailView_otv_strokeWidth, 1));
|
||||
outliner.setColor(typedArray.getColor(R.styleable.OutlinedThumbnailView_otv_strokeColor, defaultOutlinerColor));
|
||||
}
|
||||
|
||||
setRadius(radius);
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contacts.RecipientsAdapter;
|
||||
import org.thoughtcrime.securesms.contacts.RecipientsEditor;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
/**
|
||||
* Panel component combining both an editable field with a button for
|
||||
* a list-based contact selector.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class PushRecipientsPanel extends RelativeLayout implements RecipientForeverObserver {
|
||||
private final String TAG = Log.tag(PushRecipientsPanel.class);
|
||||
private RecipientsPanelChangedListener panelChangeListener;
|
||||
|
||||
private RecipientsEditor recipientsText;
|
||||
private View panel;
|
||||
|
||||
private static final int RECIPIENTS_MAX_LENGTH = 312;
|
||||
|
||||
public PushRecipientsPanel(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public PushRecipientsPanel(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public PushRecipientsPanel(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
Stream.of(getRecipients()).map(Recipient::live).forEach(r -> r.removeForeverObserver(this));
|
||||
}
|
||||
|
||||
public List<Recipient> getRecipients() {
|
||||
String rawText = recipientsText.getText().toString();
|
||||
return getRecipientsFromString(getContext(), rawText);
|
||||
}
|
||||
|
||||
public void disable() {
|
||||
recipientsText.setText("");
|
||||
panel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setPanelChangeListener(RecipientsPanelChangedListener panelChangeListener) {
|
||||
this.panelChangeListener = panelChangeListener;
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.push_recipients_panel, this, true);
|
||||
|
||||
View imageButton = findViewById(R.id.contacts_button);
|
||||
((MarginLayoutParams) imageButton.getLayoutParams()).topMargin = 0;
|
||||
|
||||
panel = findViewById(R.id.recipients_panel);
|
||||
initRecipientsEditor();
|
||||
}
|
||||
|
||||
private void initRecipientsEditor() {
|
||||
|
||||
this.recipientsText = (RecipientsEditor)findViewById(R.id.recipients_text);
|
||||
|
||||
List<Recipient> recipients = getRecipients();
|
||||
|
||||
Stream.of(recipients).map(Recipient::live).forEach(r -> r.observeForever(this));
|
||||
|
||||
recipientsText.setAdapter(new RecipientsAdapter(this.getContext()));
|
||||
recipientsText.populate(recipients);
|
||||
|
||||
recipientsText.setOnFocusChangeListener(new FocusChangedListener());
|
||||
recipientsText.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
if (panelChangeListener != null) {
|
||||
panelChangeListener.onRecipientsPanelUpdate(getRecipients());
|
||||
}
|
||||
recipientsText.setText("");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull List<Recipient> getRecipientsFromString(Context context, @NonNull String rawText) {
|
||||
StringTokenizer tokenizer = new StringTokenizer(rawText, ",");
|
||||
List<Recipient> recipients = new LinkedList<>();
|
||||
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
String token = tokenizer.nextToken().trim();
|
||||
|
||||
if (!TextUtils.isEmpty(token)) {
|
||||
if (hasBracketedNumber(token)) recipients.add(Recipient.external(context, parseBracketedNumber(token)));
|
||||
else recipients.add(Recipient.external(context, token));
|
||||
}
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
private boolean hasBracketedNumber(String recipient) {
|
||||
int openBracketIndex = recipient.indexOf('<');
|
||||
|
||||
return (openBracketIndex != -1) &&
|
||||
(recipient.indexOf('>', openBracketIndex) != -1);
|
||||
}
|
||||
|
||||
private String parseBracketedNumber(String recipient) {
|
||||
int begin = recipient.indexOf('<');
|
||||
int end = recipient.indexOf('>', begin);
|
||||
String value = recipient.substring(begin + 1, end);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
recipientsText.populate(getRecipients());
|
||||
}
|
||||
|
||||
private class FocusChangedListener implements View.OnFocusChangeListener {
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (!hasFocus && (panelChangeListener != null)) {
|
||||
panelChangeListener.onRecipientsPanelUpdate(getRecipients());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface RecipientsPanelChangedListener {
|
||||
public void onRecipientsPanelUpdate(List<Recipient> recipients);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -35,6 +34,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
@@ -45,9 +45,31 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
|
||||
private static final String TAG = Log.tag(QuoteView.class);
|
||||
|
||||
private static final int MESSAGE_TYPE_PREVIEW = 0;
|
||||
private static final int MESSAGE_TYPE_OUTGOING = 1;
|
||||
private static final int MESSAGE_TYPE_INCOMING = 2;
|
||||
public enum MessageType {
|
||||
// These codes must match the values for the QuoteView_message_type XML attribute.
|
||||
PREVIEW(0),
|
||||
OUTGOING(1),
|
||||
INCOMING(2),
|
||||
STORY_REPLY_OUTGOING(3),
|
||||
STORY_REPLY_INCOMING(4),
|
||||
STORY_REPLY_PREVIEW(5);
|
||||
|
||||
private final int code;
|
||||
|
||||
MessageType(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
private static @NonNull MessageType fromCode(int code) {
|
||||
for (MessageType value : values()) {
|
||||
if (value.code == code) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unsupported code " + code);
|
||||
}
|
||||
}
|
||||
|
||||
private ViewGroup mainView;
|
||||
private ViewGroup footerView;
|
||||
@@ -66,11 +88,13 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
private TextView mediaDescriptionText;
|
||||
private TextView missingLinkText;
|
||||
private SlideDeck attachments;
|
||||
private int messageType;
|
||||
private MessageType messageType;
|
||||
private int largeCornerRadius;
|
||||
private int smallCornerRadius;
|
||||
private CornerMask cornerMask;
|
||||
|
||||
private int thumbHeight;
|
||||
private int thumbWidth;
|
||||
|
||||
public QuoteView(Context context) {
|
||||
super(context);
|
||||
@@ -112,30 +136,25 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
this.smallCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_bottom);
|
||||
|
||||
cornerMask = new CornerMask(this);
|
||||
cornerMask.setRadii(largeCornerRadius, largeCornerRadius, smallCornerRadius, smallCornerRadius);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0);
|
||||
int primaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorPrimary, Color.BLACK);
|
||||
int secondaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorSecondary, Color.BLACK);
|
||||
messageType = typedArray.getInt(R.styleable.QuoteView_message_type, 0);
|
||||
messageType = MessageType.fromCode(typedArray.getInt(R.styleable.QuoteView_message_type, 0));
|
||||
typedArray.recycle();
|
||||
|
||||
dismissView.setVisibility(messageType == MESSAGE_TYPE_PREVIEW ? VISIBLE : GONE);
|
||||
dismissView.setVisibility(messageType == MessageType.PREVIEW ? VISIBLE : GONE);
|
||||
|
||||
authorView.setTextColor(primaryColor);
|
||||
bodyView.setTextColor(primaryColor);
|
||||
attachmentNameView.setTextColor(primaryColor);
|
||||
mediaDescriptionText.setTextColor(secondaryColor);
|
||||
missingLinkText.setTextColor(primaryColor);
|
||||
|
||||
if (messageType == MESSAGE_TYPE_PREVIEW) {
|
||||
int radius = getResources().getDimensionPixelOffset(R.dimen.quote_corner_radius_preview);
|
||||
cornerMask.setTopLeftRadius(radius);
|
||||
cornerMask.setTopRightRadius(radius);
|
||||
}
|
||||
}
|
||||
|
||||
setMessageType(messageType);
|
||||
|
||||
dismissView.setOnClickListener(view -> setVisibility(GONE));
|
||||
}
|
||||
|
||||
@@ -151,6 +170,28 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
if (author != null) author.removeForeverObserver(this);
|
||||
}
|
||||
|
||||
public void setMessageType(@NonNull MessageType messageType) {
|
||||
this.messageType = messageType;
|
||||
|
||||
cornerMask.setRadii(largeCornerRadius, largeCornerRadius, smallCornerRadius, smallCornerRadius);
|
||||
thumbWidth = thumbHeight = getResources().getDimensionPixelSize(R.dimen.quote_thumb_size);
|
||||
|
||||
if (messageType == MessageType.PREVIEW) {
|
||||
int radius = getResources().getDimensionPixelOffset(R.dimen.quote_corner_radius_preview);
|
||||
cornerMask.setTopLeftRadius(radius);
|
||||
cornerMask.setTopRightRadius(radius);
|
||||
} else if (isStoryReply()) {
|
||||
thumbWidth = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_width);
|
||||
thumbHeight = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_height);
|
||||
}
|
||||
|
||||
ViewGroup.LayoutParams params = thumbnailView.getLayoutParams();
|
||||
params.height = thumbHeight;
|
||||
params.width = thumbWidth;
|
||||
|
||||
thumbnailView.setLayoutParams(params);
|
||||
}
|
||||
|
||||
public void setQuote(GlideRequests glideRequests,
|
||||
long id,
|
||||
@NonNull Recipient author,
|
||||
@@ -168,11 +209,11 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
|
||||
this.author.observeForever(this);
|
||||
setQuoteAuthor(author);
|
||||
setQuoteText(body, attachments);
|
||||
setQuoteAttachment(glideRequests, attachments);
|
||||
setQuoteText(body, attachments, originalMissing);
|
||||
setQuoteAttachment(glideRequests, body, attachments, originalMissing);
|
||||
setQuoteMissingFooter(originalMissing);
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21 && messageType == MESSAGE_TYPE_INCOMING && chatColors != null) {
|
||||
if (Build.VERSION.SDK_INT < 21 && messageType == MessageType.INCOMING && chatColors != null) {
|
||||
this.setBackgroundColor(chatColors.asSingleColor());
|
||||
} else {
|
||||
this.setBackground(null);
|
||||
@@ -208,20 +249,51 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
}
|
||||
|
||||
private void setQuoteAuthor(@NonNull Recipient author) {
|
||||
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
|
||||
boolean preview = messageType == MESSAGE_TYPE_PREVIEW;
|
||||
boolean outgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING;
|
||||
boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW;
|
||||
|
||||
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
|
||||
: author.getDisplayName(getContext()));
|
||||
if (isStoryReply()) {
|
||||
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_your_story)
|
||||
: getContext().getString(R.string.QuoteView_s_story, author.getDisplayName(getContext())));
|
||||
} else {
|
||||
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
|
||||
: author.getDisplayName(getContext()));
|
||||
}
|
||||
|
||||
quoteBarView.setBackgroundColor(ContextCompat.getColor(getContext(), outgoing ? R.color.core_white : android.R.color.transparent));
|
||||
mainView.setBackgroundColor(ContextCompat.getColor(getContext(), preview ? R.color.quote_preview_background : R.color.quote_view_background));
|
||||
quoteBarView.setBackgroundColor(ContextCompat.getColor(getContext(), outgoing || isStoryReply() ? R.color.core_white : android.R.color.transparent));
|
||||
mainView.setBackgroundColor(ContextCompat.getColor(getContext(), preview || (!outgoing && isStoryReply()) ? R.color.quote_preview_background : R.color.quote_view_background));
|
||||
}
|
||||
|
||||
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) {
|
||||
private boolean isStoryReply() {
|
||||
return messageType == MessageType.STORY_REPLY_OUTGOING ||
|
||||
messageType == MessageType.STORY_REPLY_INCOMING ||
|
||||
messageType == MessageType.STORY_REPLY_PREVIEW;
|
||||
}
|
||||
|
||||
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments, boolean originalMissing) {
|
||||
if (originalMissing && isStoryReply()) {
|
||||
bodyView.setVisibility(GONE);
|
||||
mediaDescriptionText.setVisibility(VISIBLE);
|
||||
|
||||
mediaDescriptionText.setText(R.string.QuoteView_no_longer_available);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isTextStory = !attachments.containsMediaSlide() && isStoryReply();
|
||||
|
||||
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
|
||||
if (isTextStory && body != null) {
|
||||
try {
|
||||
bodyView.setText(StoryTextPostModel.parseFrom(body.toString(), id, author.getId()).getText());
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Could not parse body of text post.", e);
|
||||
bodyView.setText("");
|
||||
}
|
||||
} else {
|
||||
bodyView.setText(body == null ? "" : body);
|
||||
}
|
||||
|
||||
bodyView.setVisibility(VISIBLE);
|
||||
bodyView.setText(body == null ? "" : body);
|
||||
mediaDescriptionText.setVisibility(GONE);
|
||||
return;
|
||||
}
|
||||
@@ -260,7 +332,22 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
}
|
||||
}
|
||||
|
||||
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
|
||||
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull CharSequence body, @NonNull SlideDeck slideDeck, boolean originalMissing) {
|
||||
mainView.setMinimumHeight(isStoryReply() && originalMissing ? 0 : thumbHeight);
|
||||
|
||||
if (!attachments.containsMediaSlide() && isStoryReply()) {
|
||||
StoryTextPostModel model = StoryTextPostModel.parseFrom(body.toString(), id, author.getId());
|
||||
attachmentVideoOverlayView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
thumbnailView.setVisibility(VISIBLE);
|
||||
glideRequests.load(model)
|
||||
.centerCrop()
|
||||
.override(thumbWidth, thumbHeight)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.into(thumbnailView);
|
||||
return;
|
||||
}
|
||||
|
||||
Slide imageVideoSlide = slideDeck.getSlides().stream().filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).findFirst().orElse(null);
|
||||
Slide documentSlide = slideDeck.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
|
||||
Slide viewOnceSlide = slideDeck.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
|
||||
@@ -279,13 +366,13 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
}
|
||||
glideRequests.load(new DecryptableUri(imageVideoSlide.getUri()))
|
||||
.centerCrop()
|
||||
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
|
||||
.override(thumbWidth, thumbHeight)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.into(thumbnailView);
|
||||
} else if (documentSlide != null){
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(VISIBLE);
|
||||
attachmentNameView.setText(documentSlide.getFileName().or(""));
|
||||
attachmentNameView.setText(documentSlide.getFileName().orElse(""));
|
||||
} else {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
@@ -298,7 +385,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
}
|
||||
|
||||
private void setQuoteMissingFooter(boolean missing) {
|
||||
footerView.setVisibility(missing ? VISIBLE : GONE);
|
||||
footerView.setVisibility(missing && !isStoryReply() ? VISIBLE : GONE);
|
||||
footerView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.quote_view_background));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import kotlin.jvm.functions.Function2;
|
||||
|
||||
/**
|
||||
@@ -121,7 +120,11 @@ public final class RotatableGradientDrawable extends Drawable {
|
||||
public void draw(Canvas canvas) {
|
||||
int save = canvas.save();
|
||||
canvas.rotate(degrees, getBounds().width() / 2f, getBounds().height() / 2f);
|
||||
canvas.drawRect(fillRect, fillPaint);
|
||||
|
||||
int height = fillRect.height();
|
||||
int width = fillRect.width();
|
||||
canvas.drawRect(fillRect.left - width, fillRect.top - height, fillRect.right + width, fillRect.bottom + height, fillPaint);
|
||||
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ public class SearchToolbar extends LinearLayout {
|
||||
EditText searchText = searchView.findViewById(R.id.search_src_text);
|
||||
|
||||
searchView.setSubmitButtonEnabled(false);
|
||||
searchView.setMaxWidth(Integer.MAX_VALUE);
|
||||
|
||||
if (searchText != null) searchText.setHint(R.string.SearchToolbar_search);
|
||||
else searchView.setQueryHint(getResources().getString(R.string.SearchToolbar_search));
|
||||
|
||||
@@ -12,7 +12,9 @@ import org.thoughtcrime.securesms.TransportOptions;
|
||||
import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener;
|
||||
import org.thoughtcrime.securesms.TransportOptionsPopup;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public class SendButton extends AppCompatImageButton
|
||||
implements TransportOptions.OnTransportChangedListener,
|
||||
@@ -22,7 +24,7 @@ public class SendButton extends AppCompatImageButton
|
||||
|
||||
private final TransportOptions transportOptions;
|
||||
|
||||
private Optional<TransportOptionsPopup> transportOptionsPopup = Optional.absent();
|
||||
private Optional<TransportOptionsPopup> transportOptionsPopup = Optional.empty();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public SendButton(Context context) {
|
||||
|
||||
@@ -5,10 +5,6 @@ import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.graphics.drawable.shapes.Shape;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
@@ -19,7 +15,6 @@ import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
@@ -33,26 +28,23 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.video.VideoPlayer;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
|
||||
@@ -77,7 +69,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
private final int[] bounds = new int[4];
|
||||
private final int[] measureDimens = new int[2];
|
||||
|
||||
private Optional<TransferControlView> transferControls = Optional.absent();
|
||||
private Optional<TransferControlView> transferControls = Optional.empty();
|
||||
private SlideClickListener thumbnailClickListener = null;
|
||||
private SlidesClickedListener downloadClickListener = null;
|
||||
private Slide slide = null;
|
||||
@@ -400,6 +392,32 @@ public class ThumbnailView extends FrameLayout {
|
||||
return future;
|
||||
}
|
||||
|
||||
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull StoryTextPostModel model, int width, int height) {
|
||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
|
||||
|
||||
GlideRequest request = glideRequests.load(model)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.placeholder(model.getPlaceholder())
|
||||
.transition(withCrossFade());
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
request = request.override(width, height);
|
||||
}
|
||||
|
||||
if (radius > 0) {
|
||||
request = request.transforms(new CenterCrop(), new RoundedCorners(radius));
|
||||
} else {
|
||||
request = request.transforms(new CenterCrop());
|
||||
}
|
||||
|
||||
request.into(new GlideDrawableListeningTarget(image, future));
|
||||
blurhash.setImageDrawable(null);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public void setThumbnailClickListener(SlideClickListener listener) {
|
||||
this.thumbnailClickListener = listener;
|
||||
}
|
||||
@@ -410,11 +428,15 @@ public class ThumbnailView extends FrameLayout {
|
||||
|
||||
public void clear(GlideRequests glideRequests) {
|
||||
glideRequests.clear(image);
|
||||
image.setImageDrawable(null);
|
||||
|
||||
if (transferControls.isPresent()) {
|
||||
getTransferControls().clear();
|
||||
}
|
||||
|
||||
glideRequests.clear(blurhash);
|
||||
blurhash.setImageDrawable(null);
|
||||
|
||||
slide = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,12 +42,12 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class CameraView extends ViewGroup {
|
||||
@@ -56,8 +56,8 @@ public class CameraView extends ViewGroup {
|
||||
private final CameraSurfaceView surface;
|
||||
private final OnOrientationChange onOrientationChange;
|
||||
|
||||
private volatile Optional<Camera> camera = Optional.absent();
|
||||
private volatile int cameraId = CameraInfo.CAMERA_FACING_BACK;
|
||||
private volatile Optional<Camera> camera = Optional.empty();
|
||||
private volatile int cameraId = CameraInfo.CAMERA_FACING_BACK;
|
||||
private volatile int displayOrientation = -1;
|
||||
|
||||
private @NonNull State state = State.PAUSED;
|
||||
@@ -104,7 +104,7 @@ public class CameraView extends ViewGroup {
|
||||
Void onRunBackground() {
|
||||
try {
|
||||
long openStartMillis = System.currentTimeMillis();
|
||||
camera = Optional.fromNullable(Camera.open(cameraId));
|
||||
camera = Optional.ofNullable(Camera.open(cameraId));
|
||||
Log.i(TAG, "camera.open() -> " + (System.currentTimeMillis() - openStartMillis) + "ms");
|
||||
synchronized (CameraView.this) {
|
||||
CameraView.this.notifyAll();
|
||||
@@ -145,7 +145,7 @@ public class CameraView extends ViewGroup {
|
||||
@Override
|
||||
protected void onPreMain() {
|
||||
cameraToDestroy = camera;
|
||||
camera = Optional.absent();
|
||||
camera = Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
|
||||
public class EmojiEditText extends AppCompatEditText {
|
||||
|
||||
@@ -1,26 +1,39 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class EmojiImageView extends AppCompatImageView {
|
||||
|
||||
private final boolean forceJumboEmoji;
|
||||
|
||||
public EmojiImageView(Context context) {
|
||||
super(context);
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public EmojiImageView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public EmojiImageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiImageView, 0, 0);
|
||||
forceJumboEmoji = a.getBoolean(R.styleable.EmojiImageView_forceJumbo, false);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
public void setImageEmoji(CharSequence emoji) {
|
||||
if (isInEditMode()) {
|
||||
setImageResource(R.drawable.ic_emoji);
|
||||
} else {
|
||||
setImageDrawable(EmojiProvider.getEmojiDrawable(getContext(), emoji));
|
||||
setImageDrawable(EmojiProvider.getEmojiDrawable(getContext(), emoji, forceJumboEmoji));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.Emoj
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo;
|
||||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiFiles;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiPageCache;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
|
||||
@@ -13,7 +13,7 @@ import android.text.TextDirectionHeuristic;
|
||||
import android.text.TextDirectionHeuristics;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.TransformationMethod;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ViewGroup;
|
||||
@@ -33,9 +33,9 @@ import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
@@ -141,7 +141,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
previousTransformationMethod = getTransformationMethod();
|
||||
|
||||
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")), BufferType.SPANNABLE);
|
||||
super.setText(new SpannableStringBuilder(Optional.ofNullable(text).orElse("")), BufferType.SPANNABLE);
|
||||
} else {
|
||||
CharSequence emojified = EmojiProvider.emojify(candidates, text, this, isJumbomoji || forceJumboEmoji);
|
||||
super.setText(new SpannableStringBuilder(emojified), BufferType.SPANNABLE);
|
||||
@@ -149,7 +149,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
|
||||
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
|
||||
// We ellipsize them ourselves by manually truncating the appropriate section.
|
||||
if (getText() != null && getText().length() > 0 && getEllipsize() == TextUtils.TruncateAt.END) {
|
||||
if (getText() != null && getText().length() > 0 && isEllipsizedAtEnd()) {
|
||||
if (maxLength > 0) {
|
||||
ellipsizeAnyTextForMaxLength();
|
||||
} else if (getMaxLines() > 0) {
|
||||
@@ -162,6 +162,17 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to determine whether to apply custom ellipsizing logic without necessarily having the
|
||||
* ellipsize property set. This allows us to work around implementations of Layout which apply an
|
||||
* ellipsis even when maxLines is not set.
|
||||
*/
|
||||
private boolean isEllipsizedAtEnd() {
|
||||
return getEllipsize() == TextUtils.TruncateAt.END ||
|
||||
(getMaxLines() > 0 && getMaxLines() < Integer.MAX_VALUE) ||
|
||||
maxLength > 0;
|
||||
}
|
||||
|
||||
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
widthMeasureSpec = applyWidthMeasureRoundingFix(widthMeasureSpec);
|
||||
|
||||
@@ -186,16 +197,16 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting from API 30, there can be a rounding error in text layout when a non-default font
|
||||
* scale is used. This causes a line break to be inserted where there shouldn't be one. Force the
|
||||
* width to be larger to work around this problem.
|
||||
* Starting from API 30, there can be a rounding error in text layout when a non-zero letter
|
||||
* spacing is used. This causes a line break to be inserted where there shouldn't be one. Force
|
||||
* the width to be larger to work around this problem.
|
||||
* https://issuetracker.google.com/issues/173574230
|
||||
*
|
||||
* @param widthMeasureSpec the original measure spec passed to {@link #onMeasure(int, int)}
|
||||
* @return the measure spec with the workaround, or the original one.
|
||||
*/
|
||||
private int applyWidthMeasureRoundingFix(int widthMeasureSpec) {
|
||||
if (Build.VERSION.SDK_INT >= 30 && Math.abs(getResources().getConfiguration().fontScale - 1f) > 0.01f) {
|
||||
if (Build.VERSION.SDK_INT >= 30 && getLetterSpacing() > 0) {
|
||||
CharSequence text = getText();
|
||||
if (text != null) {
|
||||
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
|
||||
@@ -218,7 +229,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((Spanned) text).nextSpanTransition(-1, text.length(), MetricAffectingSpan.class) != text.length();
|
||||
return ((Spanned) text).nextSpanTransition(-1, text.length(), CharacterStyle.class) != text.length();
|
||||
}
|
||||
|
||||
public int getLastLineWidth() {
|
||||
@@ -283,14 +294,15 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
int lineCount = getLineCount();
|
||||
if (lineCount > maxLines) {
|
||||
int overflowStart = getLayout().getLineStart(maxLines - 1);
|
||||
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
|
||||
int overflowEnd = getLayout().getLineEnd(maxLines - 1);
|
||||
CharSequence overflow = getText().subSequence(overflowStart, overflowEnd);
|
||||
float adjust = overflowText != null ? getPaint().measureText(overflowText, 0, overflowText.length()) : 0f;
|
||||
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END);
|
||||
|
||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||
newContent.append(getText().subSequence(0, overflowStart))
|
||||
.append(ellipsized.subSequence(0, ellipsized.length()))
|
||||
.append(Optional.fromNullable(overflowText).or(""));
|
||||
.append(Optional.ofNullable(overflowText).orElse(""));
|
||||
|
||||
EmojiParser.CandidateList newCandidates = isInEditMode() ? null : EmojiProvider.getCandidates(newContent);
|
||||
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, isJumbomoji || forceJumboEmoji);
|
||||
@@ -302,7 +314,7 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
if (getLayout() != null) {
|
||||
ellipsize.run();
|
||||
} else {
|
||||
ViewKt.doOnNextLayout(this, view -> {
|
||||
ViewKt.doOnPreDraw(this, view -> {
|
||||
ellipsize.run();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -20,8 +21,7 @@ import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment;
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.thoughtcrime.securesms.util.ThemedFragment;
|
||||
|
||||
public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
|
||||
@@ -34,6 +34,7 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
private State keyboardState;
|
||||
private KeyboardPagerFragment keyboardPagerFragment;
|
||||
private FragmentManager fragmentManager;
|
||||
private int mediaKeyboardTheme;
|
||||
|
||||
public MediaKeyboard(Context context) {
|
||||
this(context, null);
|
||||
@@ -41,6 +42,12 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
|
||||
public MediaKeyboard(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MediaKeyboard);
|
||||
mediaKeyboardTheme = array.getResourceId(R.styleable.MediaKeyboard_media_keyboard_theme, -1);
|
||||
array.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFragmentManager(@NonNull FragmentManager fragmentManager) {
|
||||
@@ -70,6 +77,10 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
show();
|
||||
}
|
||||
|
||||
public boolean isInitialised() {
|
||||
return isInitialised;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (!isInitialised) initView();
|
||||
|
||||
@@ -122,9 +133,14 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
|
||||
keyboardState = State.EMOJI_SEARCH;
|
||||
|
||||
EmojiSearchFragment emojiSearchFragment = new EmojiSearchFragment();
|
||||
if (mediaKeyboardTheme != -1) {
|
||||
ThemedFragment.withTheme(emojiSearchFragment, mediaKeyboardTheme);
|
||||
}
|
||||
|
||||
fragmentManager.beginTransaction()
|
||||
.hide(keyboardPagerFragment)
|
||||
.add(R.id.media_keyboard_fragment_container, new EmojiSearchFragment(), EMOJI_SEARCH)
|
||||
.add(R.id.media_keyboard_fragment_container, emojiSearchFragment, EMOJI_SEARCH)
|
||||
.runOnCommit(() -> show(latestKeyboardHeight, true))
|
||||
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
|
||||
.commitAllowingStateLoss();
|
||||
@@ -141,6 +157,10 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
}
|
||||
|
||||
keyboardPagerFragment = new KeyboardPagerFragment();
|
||||
if (mediaKeyboardTheme != -1) {
|
||||
ThemedFragment.withTheme(keyboardPagerFragment, mediaKeyboardTheme);
|
||||
}
|
||||
|
||||
fragmentManager.beginTransaction()
|
||||
.replace(R.id.media_keyboard_fragment_container, keyboardPagerFragment)
|
||||
.commitNowAllowingStateLoss();
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.util.Optional
|
||||
|
||||
open class SimpleEmojiTextView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@@ -21,7 +21,7 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
|
||||
bufferType = type
|
||||
val candidates = if (isInEditMode) null else EmojiProvider.getCandidates(text)
|
||||
if (SignalStore.settings().isPreferSystemEmoji || candidates == null || candidates.size() == 0) {
|
||||
super.setText(Optional.fromNullable(text).or(""), type)
|
||||
super.setText(Optional.ofNullable(text).orElse(""), type)
|
||||
} else {
|
||||
val startDrawableSize: Int = compoundDrawables[0]?.let { it.intrinsicWidth + compoundDrawablePadding } ?: 0
|
||||
val endDrawableSize: Int = compoundDrawables[1]?.let { it.intrinsicWidth + compoundDrawablePadding } ?: 0
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
@@ -40,7 +40,7 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities();
|
||||
final SignalIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities();
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.identity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Context;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class PushRegistrationReminder extends Reminder {
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Tiago Ornelas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components.segmentedprogressbar
|
||||
|
||||
/**
|
||||
* Created by Tiago Ornelas on 18/04/2020.
|
||||
* Model that holds the segment state
|
||||
*/
|
||||
class Segment(val animationDurationMillis: Long) {
|
||||
|
||||
var animationProgressPercentage: Float = 0f
|
||||
|
||||
var animationState: AnimationState = AnimationState.IDLE
|
||||
set(value) {
|
||||
animationProgressPercentage = when (value) {
|
||||
AnimationState.ANIMATED -> 1f
|
||||
AnimationState.IDLE -> 0f
|
||||
else -> animationProgressPercentage
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents possible drawing states of the segment
|
||||
*/
|
||||
enum class AnimationState {
|
||||
ANIMATED,
|
||||
ANIMATING,
|
||||
IDLE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.segmentedprogressbar
|
||||
|
||||
data class SegmentState(
|
||||
val position: Long,
|
||||
val duration: Long
|
||||
)
|
||||
@@ -0,0 +1,416 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Tiago Ornelas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components.segmentedprogressbar
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Path
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import org.thoughtcrime.securesms.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Created by Tiago Ornelas on 18/04/2020.
|
||||
* Represents a segmented progress bar on which, the progress is set by segments
|
||||
* @see Segment
|
||||
* And the progress of each segment is animated based on a set speed
|
||||
*/
|
||||
class SegmentedProgressBar : View, ViewPager.OnPageChangeListener, View.OnTouchListener {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* It is common now for devices to run at 60FPS
|
||||
*/
|
||||
val MILLIS_PER_FRAME = TimeUnit.MILLISECONDS.toMillis(17)
|
||||
}
|
||||
|
||||
private val path = Path()
|
||||
private val corners = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
|
||||
|
||||
/**
|
||||
* Number of total segments to draw
|
||||
*/
|
||||
var segmentCount: Int = resources.getInteger(R.integer.segmentedprogressbar_default_segments_count)
|
||||
set(value) {
|
||||
field = value
|
||||
this.initSegments()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping of segment index -> duration in millis. Negative durations
|
||||
* ARE valid but they'll result in a call to SegmentedProgressBarListener#onRequestSegmentProgressPercentage
|
||||
* which should return the current % position for the currently playing item. This helps
|
||||
* to avoid synchronizing the seek bar to playback.
|
||||
*/
|
||||
var segmentDurations: Map<Int, Long> = mapOf()
|
||||
set(value) {
|
||||
field = value
|
||||
this.initSegments()
|
||||
}
|
||||
|
||||
var margin: Int = resources.getDimensionPixelSize(R.dimen.segmentedprogressbar_default_segment_margin)
|
||||
private set
|
||||
var radius: Int = resources.getDimensionPixelSize(R.dimen.segmentedprogressbar_default_corner_radius)
|
||||
private set
|
||||
var segmentStrokeWidth: Int =
|
||||
resources.getDimensionPixelSize(R.dimen.segmentedprogressbar_default_segment_stroke_width)
|
||||
private set
|
||||
|
||||
var segmentBackgroundColor: Int = Color.WHITE
|
||||
private set
|
||||
var segmentSelectedBackgroundColor: Int =
|
||||
context.getThemeColor(R.attr.colorAccent)
|
||||
private set
|
||||
var segmentStrokeColor: Int = Color.BLACK
|
||||
private set
|
||||
var segmentSelectedStrokeColor: Int = Color.BLACK
|
||||
private set
|
||||
|
||||
var timePerSegmentMs: Long =
|
||||
resources.getInteger(R.integer.segmentedprogressbar_default_time_per_segment_ms).toLong()
|
||||
private set
|
||||
|
||||
private var segments = mutableListOf<Segment>()
|
||||
private val selectedSegment: Segment?
|
||||
get() = segments.firstOrNull { it.animationState == Segment.AnimationState.ANIMATING }
|
||||
private val selectedSegmentIndex: Int
|
||||
get() = segments.indexOf(this.selectedSegment)
|
||||
|
||||
// Drawing
|
||||
val strokeApplicable: Boolean
|
||||
get() = segmentStrokeWidth * 4 <= measuredHeight
|
||||
|
||||
val segmentWidth: Float
|
||||
get() = (measuredWidth - margin * (segmentCount - 1)).toFloat() / segmentCount
|
||||
|
||||
var viewPager: ViewPager? = null
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
set(value) {
|
||||
field = value
|
||||
if (value == null) {
|
||||
viewPager?.removeOnPageChangeListener(this)
|
||||
viewPager?.setOnTouchListener(null)
|
||||
} else {
|
||||
viewPager?.addOnPageChangeListener(this)
|
||||
viewPager?.setOnTouchListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets callbacks for progress bar state changes
|
||||
* @see SegmentedProgressBarListener
|
||||
*/
|
||||
var listener: SegmentedProgressBarListener? = null
|
||||
|
||||
private var lastFrameTimeMillis: Long = 0L
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
|
||||
val typedArray =
|
||||
context.theme.obtainStyledAttributes(attrs, R.styleable.SegmentedProgressBar, 0, 0)
|
||||
|
||||
segmentCount =
|
||||
typedArray.getInt(R.styleable.SegmentedProgressBar_totalSegments, segmentCount)
|
||||
|
||||
margin =
|
||||
typedArray.getDimensionPixelSize(
|
||||
R.styleable.SegmentedProgressBar_segmentMargins,
|
||||
margin
|
||||
)
|
||||
radius =
|
||||
typedArray.getDimensionPixelSize(
|
||||
R.styleable.SegmentedProgressBar_segmentCornerRadius,
|
||||
radius
|
||||
)
|
||||
segmentStrokeWidth =
|
||||
typedArray.getDimensionPixelSize(
|
||||
R.styleable.SegmentedProgressBar_segmentStrokeWidth,
|
||||
segmentStrokeWidth
|
||||
)
|
||||
|
||||
segmentBackgroundColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.SegmentedProgressBar_segmentBackgroundColor,
|
||||
segmentBackgroundColor
|
||||
)
|
||||
segmentSelectedBackgroundColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.SegmentedProgressBar_segmentSelectedBackgroundColor,
|
||||
segmentSelectedBackgroundColor
|
||||
)
|
||||
|
||||
segmentStrokeColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.SegmentedProgressBar_segmentStrokeColor,
|
||||
segmentStrokeColor
|
||||
)
|
||||
segmentSelectedStrokeColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.SegmentedProgressBar_segmentSelectedStrokeColor,
|
||||
segmentSelectedStrokeColor
|
||||
)
|
||||
|
||||
timePerSegmentMs =
|
||||
typedArray.getInt(
|
||||
R.styleable.SegmentedProgressBar_timePerSegment,
|
||||
timePerSegmentMs.toInt()
|
||||
).toLong()
|
||||
|
||||
typedArray.recycle()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
init {
|
||||
setLayerType(LAYER_TYPE_SOFTWARE, null)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
segments.forEachIndexed { index, segment ->
|
||||
val drawingComponents = getDrawingComponents(segment, index)
|
||||
|
||||
when (index) {
|
||||
0 -> {
|
||||
corners.indices.forEach { corners[it] = 0f }
|
||||
corners[0] = radius.toFloat()
|
||||
corners[1] = radius.toFloat()
|
||||
corners[6] = radius.toFloat()
|
||||
corners[7] = radius.toFloat()
|
||||
}
|
||||
segments.lastIndex -> {
|
||||
corners.indices.forEach { corners[it] = 0f }
|
||||
corners[2] = radius.toFloat()
|
||||
corners[3] = radius.toFloat()
|
||||
corners[4] = radius.toFloat()
|
||||
corners[5] = radius.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
drawingComponents.first.forEachIndexed { drawingIndex, rectangle ->
|
||||
when (index) {
|
||||
0, segments.lastIndex -> {
|
||||
path.reset()
|
||||
path.addRoundRect(rectangle, corners, Path.Direction.CW)
|
||||
canvas?.drawPath(path, drawingComponents.second[drawingIndex])
|
||||
}
|
||||
else -> canvas?.drawRect(
|
||||
rectangle,
|
||||
drawingComponents.second[drawingIndex]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFrame(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
/**
|
||||
* Start/Resume progress animation
|
||||
*/
|
||||
fun start() {
|
||||
pause()
|
||||
val segment = selectedSegment
|
||||
if (segment == null) {
|
||||
next()
|
||||
} else {
|
||||
isPaused = false
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the animation process
|
||||
*/
|
||||
fun pause() {
|
||||
isPaused = true
|
||||
lastFrameTimeMillis = 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the whole animation state and selected segments
|
||||
* !Doesn't restart it!
|
||||
* To restart, call the start() method
|
||||
*/
|
||||
fun reset() {
|
||||
this.segments.map { it.animationState = Segment.AnimationState.IDLE }
|
||||
this.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts animation for the following segment
|
||||
*/
|
||||
fun next() {
|
||||
loadSegment(offset = 1, userAction = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts animation for the previous segment
|
||||
*/
|
||||
fun previous() {
|
||||
loadSegment(offset = -1, userAction = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts animation for the current segment
|
||||
*/
|
||||
fun restartSegment() {
|
||||
loadSegment(offset = 0, userAction = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips a number of segments
|
||||
* @param offset number o segments fo skip
|
||||
*/
|
||||
fun skip(offset: Int) {
|
||||
loadSegment(offset = offset, userAction = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current segment to the
|
||||
* @param position index
|
||||
*/
|
||||
fun setPosition(position: Int) {
|
||||
loadSegment(offset = position - this.selectedSegmentIndex, userAction = true)
|
||||
}
|
||||
|
||||
// Private methods
|
||||
private fun loadSegment(offset: Int, userAction: Boolean) {
|
||||
val oldSegmentIndex = this.segments.indexOf(this.selectedSegment)
|
||||
|
||||
val nextSegmentIndex = oldSegmentIndex + offset
|
||||
|
||||
// Index out of bounds, ignore operation
|
||||
if (userAction && nextSegmentIndex !in 0 until segmentCount) {
|
||||
if (nextSegmentIndex >= segmentCount) {
|
||||
this.listener?.onFinished()
|
||||
} else {
|
||||
restartSegment()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
segments.mapIndexed { index, segment ->
|
||||
if (offset > 0) {
|
||||
if (index < nextSegmentIndex) segment.animationState =
|
||||
Segment.AnimationState.ANIMATED
|
||||
} else if (offset < 0) {
|
||||
if (index > nextSegmentIndex - 1) segment.animationState =
|
||||
Segment.AnimationState.IDLE
|
||||
} else if (offset == 0) {
|
||||
if (index == nextSegmentIndex) segment.animationState = Segment.AnimationState.IDLE
|
||||
}
|
||||
}
|
||||
|
||||
val nextSegment = this.segments.getOrNull(nextSegmentIndex)
|
||||
|
||||
// Handle next segment transition/ending
|
||||
if (nextSegment != null) {
|
||||
pause()
|
||||
nextSegment.animationState = Segment.AnimationState.ANIMATING
|
||||
isPaused = false
|
||||
invalidate()
|
||||
this.listener?.onPage(oldSegmentIndex, this.selectedSegmentIndex)
|
||||
viewPager?.currentItem = this.selectedSegmentIndex
|
||||
} else {
|
||||
pause()
|
||||
this.listener?.onFinished()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSegmentProgressPercentage(segment: Segment, timeSinceLastFrameMillis: Long): Float {
|
||||
return if (segment.animationDurationMillis > 0) {
|
||||
segment.animationProgressPercentage + timeSinceLastFrameMillis.toFloat() / segment.animationDurationMillis
|
||||
} else {
|
||||
listener?.onRequestSegmentProgressPercentage() ?: 0f
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSegments() {
|
||||
this.segments.clear()
|
||||
segments.addAll(
|
||||
List(segmentCount) {
|
||||
val duration = segmentDurations[it] ?: timePerSegmentMs
|
||||
Segment(duration)
|
||||
}
|
||||
)
|
||||
this.invalidate()
|
||||
reset()
|
||||
}
|
||||
|
||||
private var isPaused = true
|
||||
|
||||
private fun onFrame(frameTimeMillis: Long) {
|
||||
if (isPaused) {
|
||||
return
|
||||
}
|
||||
|
||||
val lastFrameTimeMillis = this.lastFrameTimeMillis
|
||||
|
||||
this.lastFrameTimeMillis = frameTimeMillis
|
||||
|
||||
val selectedSegment = this.selectedSegment
|
||||
if (selectedSegment == null) {
|
||||
loadSegment(offset = 1, userAction = false)
|
||||
} else if (lastFrameTimeMillis > 0L) {
|
||||
val segmentProgressPercentage = getSegmentProgressPercentage(selectedSegment, frameTimeMillis - lastFrameTimeMillis)
|
||||
selectedSegment.animationProgressPercentage = segmentProgressPercentage
|
||||
if (selectedSegment.animationProgressPercentage >= 1f) {
|
||||
loadSegment(offset = 1, userAction = false)
|
||||
} else {
|
||||
this.invalidate()
|
||||
}
|
||||
} else {
|
||||
this.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(state: Int) {}
|
||||
|
||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
this.setPosition(position)
|
||||
}
|
||||
|
||||
override fun onTouch(p0: View?, p1: MotionEvent?): Boolean {
|
||||
when (p1?.action) {
|
||||
MotionEvent.ACTION_DOWN -> pause()
|
||||
MotionEvent.ACTION_UP -> start()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Tiago Ornelas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components.segmentedprogressbar
|
||||
|
||||
/**
|
||||
* Created by Tiago Ornelas on 18/04/2020.
|
||||
* Interface to communicate progress events
|
||||
*/
|
||||
interface SegmentedProgressBarListener {
|
||||
/**
|
||||
* Notifies when selected segment changed
|
||||
*/
|
||||
fun onPage(oldPageIndex: Int, newPageIndex: Int)
|
||||
|
||||
/**
|
||||
* Notifies when last segment finished animating
|
||||
*/
|
||||
fun onFinished()
|
||||
|
||||
fun onRequestSegmentProgressPercentage(): Float?
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Tiago Ornelas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components.segmentedprogressbar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.util.TypedValue
|
||||
|
||||
fun Context.getThemeColor(attributeColor: Int): Int {
|
||||
val typedValue = TypedValue()
|
||||
this.theme.resolveAttribute(attributeColor, typedValue, true)
|
||||
return typedValue.data
|
||||
}
|
||||
|
||||
fun SegmentedProgressBar.getDrawingComponents(
|
||||
segment: Segment,
|
||||
segmentIndex: Int
|
||||
): Pair<MutableList<RectF>, MutableList<Paint>> {
|
||||
|
||||
val rectangles = mutableListOf<RectF>()
|
||||
val paints = mutableListOf<Paint>()
|
||||
val segmentWidth = segmentWidth
|
||||
val startBound = segmentIndex * segmentWidth + ((segmentIndex) * margin)
|
||||
val endBound = startBound + segmentWidth
|
||||
val stroke = if (!strokeApplicable) 0f else this.segmentStrokeWidth.toFloat()
|
||||
|
||||
val backgroundPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
color = segmentBackgroundColor
|
||||
}
|
||||
|
||||
val selectedBackgroundPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
color = segmentSelectedBackgroundColor
|
||||
}
|
||||
|
||||
val strokePaint = Paint().apply {
|
||||
color =
|
||||
if (segment.animationState == Segment.AnimationState.IDLE) segmentStrokeColor else segmentSelectedStrokeColor
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = stroke
|
||||
}
|
||||
|
||||
// Background component
|
||||
if (segment.animationState == Segment.AnimationState.ANIMATED) {
|
||||
rectangles.add(RectF(startBound + stroke, height - stroke, endBound - stroke, stroke))
|
||||
paints.add(selectedBackgroundPaint)
|
||||
} else {
|
||||
rectangles.add(RectF(startBound + stroke, height - stroke, endBound - stroke, stroke))
|
||||
paints.add(backgroundPaint)
|
||||
}
|
||||
|
||||
// Progress component
|
||||
if (segment.animationState == Segment.AnimationState.ANIMATING) {
|
||||
rectangles.add(
|
||||
RectF(
|
||||
startBound + stroke,
|
||||
height - stroke,
|
||||
startBound + segment.animationProgressPercentage * segmentWidth,
|
||||
stroke
|
||||
)
|
||||
)
|
||||
paints.add(selectedBackgroundPaint)
|
||||
}
|
||||
|
||||
// Stroke component
|
||||
if (stroke > 0) {
|
||||
rectangles.add(RectF(startBound + stroke, height - stroke, endBound - stroke, stroke))
|
||||
paints.add(strokePaint)
|
||||
}
|
||||
|
||||
return Pair(rectangles, paints)
|
||||
}
|
||||
@@ -22,38 +22,47 @@ abstract class DSLSettingsFragment(
|
||||
@StringRes private val titleId: Int = -1,
|
||||
@MenuRes private val menuId: Int = -1,
|
||||
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment,
|
||||
val layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
|
||||
protected var layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
|
||||
) : Fragment(layoutId) {
|
||||
|
||||
private var recyclerView: RecyclerView? = null
|
||||
protected var recyclerView: RecyclerView? = null
|
||||
private set
|
||||
|
||||
private var scrollAnimationHelper: OnScrollAnimationHelper? = null
|
||||
|
||||
@CallSuper
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
val toolbarShadow: View = view.findViewById(R.id.toolbar_shadow)
|
||||
val toolbar: Toolbar? = view.findViewById(R.id.toolbar)
|
||||
val toolbarShadow: View? = view.findViewById(R.id.toolbar_shadow)
|
||||
|
||||
if (titleId != -1) {
|
||||
toolbar.setTitle(titleId)
|
||||
toolbar?.setTitle(titleId)
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
toolbar?.setNavigationOnClickListener {
|
||||
requireActivity().onBackPressed()
|
||||
}
|
||||
|
||||
if (menuId != -1) {
|
||||
toolbar.inflateMenu(menuId)
|
||||
toolbar.setOnMenuItemClickListener { onOptionsItemSelected(it) }
|
||||
toolbar?.inflateMenu(menuId)
|
||||
toolbar?.setOnMenuItemClickListener { onOptionsItemSelected(it) }
|
||||
}
|
||||
|
||||
if (toolbarShadow != null) {
|
||||
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
|
||||
}
|
||||
|
||||
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
|
||||
val settingsAdapter = DSLSettingsAdapter()
|
||||
|
||||
recyclerView = view.findViewById<RecyclerView>(R.id.recycler).apply {
|
||||
edgeEffectFactory = EdgeEffectFactory()
|
||||
layoutManager = layoutManagerProducer(requireContext())
|
||||
adapter = settingsAdapter
|
||||
addOnScrollListener(scrollAnimationHelper!!)
|
||||
|
||||
val helper = scrollAnimationHelper
|
||||
if (helper != null) {
|
||||
addOnScrollListener(helper)
|
||||
}
|
||||
}
|
||||
|
||||
bindAdapter(settingsAdapter)
|
||||
|
||||
@@ -4,8 +4,11 @@ import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.InsetDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.Px
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
@@ -24,6 +27,23 @@ sealed class DSLSettingsIcon {
|
||||
}
|
||||
}
|
||||
|
||||
private data class FromResourceWithBackground(
|
||||
@DrawableRes private val iconId: Int,
|
||||
@ColorRes private val iconTintId: Int,
|
||||
@DrawableRes private val backgroundId: Int,
|
||||
@ColorRes private val backgroundTint: Int,
|
||||
@Px private val insetPx: Int,
|
||||
) : DSLSettingsIcon() {
|
||||
override fun resolve(context: Context): Drawable {
|
||||
return LayerDrawable(
|
||||
arrayOf(
|
||||
FromResource(backgroundId, backgroundTint).resolve(context),
|
||||
InsetDrawable(FromResource(iconId, iconTintId).resolve(context), insetPx, insetPx, insetPx, insetPx)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class FromDrawable(
|
||||
private val drawable: Drawable
|
||||
) : DSLSettingsIcon() {
|
||||
@@ -33,6 +53,17 @@ sealed class DSLSettingsIcon {
|
||||
abstract fun resolve(context: Context): Drawable
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(
|
||||
@DrawableRes iconId: Int,
|
||||
@ColorRes iconTintId: Int,
|
||||
@DrawableRes backgroundId: Int,
|
||||
@ColorRes backgroundTint: Int,
|
||||
@Px insetPx: Int = 0
|
||||
): DSLSettingsIcon {
|
||||
return FromResourceWithBackground(iconId, iconTintId, backgroundId, backgroundTint, insetPx)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun from(@DrawableRes iconId: Int, @ColorRes iconTintId: Int = R.color.signal_icon_tint_primary): DSLSettingsIcon = FromResource(iconId, iconTintId)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.StyleRes
|
||||
@@ -81,4 +82,17 @@ sealed class DSLSettingsText {
|
||||
return SpanUtil.bold(charSequence)
|
||||
}
|
||||
}
|
||||
|
||||
class LearnMoreModifier(
|
||||
@ColorInt private val learnMoreColor: Int,
|
||||
val onClick: () -> Unit
|
||||
) : Modifier {
|
||||
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
||||
return SpannableStringBuilder(charSequence).append(" ").append(
|
||||
SpanUtil.learnMore(context, learnMoreColor) {
|
||||
onClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRep
|
||||
val state: LiveData<AppSettingsState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
store.update(unreadPaymentsLiveData) { payments, state -> state.copy(unreadPaymentsCount = payments.transform { it.unreadCount }.or(0)) }
|
||||
store.update(unreadPaymentsLiveData) { payments, state -> state.copy(unreadPaymentsCount = payments.map { it.unreadCount }.orElse(0)) }
|
||||
store.update(selfLiveData) { self, state -> state.copy(self = self) }
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.thoughtcrime.securesms.lock.v2.KbsConstants
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
|
||||
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -107,7 +106,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
|
||||
|
||||
sectionHeaderPref(R.string.AccountSettingsFragment__account)
|
||||
|
||||
if (FeatureFlags.changeNumber() && Recipient.self().changeNumberCapability == Recipient.Capability.SUPPORTED) {
|
||||
if (Recipient.self().changeNumberCapability == Recipient.Capability.SUPPORTED && SignalStore.account().isRegistered) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.AccountSettingsFragment__change_phone_number),
|
||||
onClick = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
@@ -30,4 +31,12 @@ class ChatsSettingsRepository {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun syncPreferSystemContactPhotos() {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceContactUpdateJob(true))
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user