mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-17 15:33:30 +01:00
Compare commits
407 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8771dbf49f | ||
|
|
4615b0d32d | ||
|
|
f9a2208832 | ||
|
|
b981ac4fe4 | ||
|
|
a3219348b6 | ||
|
|
a35a35cee8 | ||
|
|
85f1f27b13 | ||
|
|
388c91410b | ||
|
|
3c0afe4b24 | ||
|
|
ae1f834619 | ||
|
|
9f9bf3c604 | ||
|
|
f9c4fe736a | ||
|
|
638bae6de3 | ||
|
|
e363bac1a3 | ||
|
|
e690e9bd69 | ||
|
|
c0ac2176c1 | ||
|
|
dccfafa9e8 | ||
|
|
0edfb0bd68 | ||
|
|
31a815013e | ||
|
|
4364e9513f | ||
|
|
1621c060b5 | ||
|
|
b1c32476b0 | ||
|
|
ba96db2ae0 | ||
|
|
182a112cdd | ||
|
|
a45e26ab6b | ||
|
|
b6022be41f | ||
|
|
0801a0e329 | ||
|
|
89cbfd3299 | ||
|
|
5b2ca6a1d3 | ||
|
|
c5d7188dcb | ||
|
|
818eb81f87 | ||
|
|
510a295198 | ||
|
|
98fab95683 | ||
|
|
79d45bb497 | ||
|
|
fc3d77ed9a | ||
|
|
ee05cf87aa | ||
|
|
ae18aed15b | ||
|
|
a5aa079216 | ||
|
|
ae7a03bc8f | ||
|
|
6ed797c031 | ||
|
|
ef4015aec9 | ||
|
|
ffedc3fa7d | ||
|
|
20285e7e5b | ||
|
|
89e55a7133 | ||
|
|
11aa168a6b | ||
|
|
0fc6e642fe | ||
|
|
8e0553c849 | ||
|
|
75b4ffc16e | ||
|
|
643b07d564 | ||
|
|
637a44379c | ||
|
|
a2d42b0415 | ||
|
|
a76983ca0a | ||
|
|
22e79a045c | ||
|
|
061b87ead0 | ||
|
|
511abd67c6 | ||
|
|
1627d92009 | ||
|
|
2cb67f6ee3 | ||
|
|
13e0b8dec0 | ||
|
|
7626070c28 | ||
|
|
ca5140d3ec | ||
|
|
3694431503 | ||
|
|
cd1f0632fa | ||
|
|
1508b1d401 | ||
|
|
bf874e17e5 | ||
|
|
d2b8a17723 | ||
|
|
67cfdf101d | ||
|
|
125840e5fc | ||
|
|
f5ab4bec7a | ||
|
|
ef7d5d55cb | ||
|
|
1a9d785cbb | ||
|
|
cad0bab435 | ||
|
|
bdc3435fc1 | ||
|
|
f260633c9d | ||
|
|
8a00caabd7 | ||
|
|
b4fe5bdcc6 | ||
|
|
1f649057d6 | ||
|
|
41059a2b67 | ||
|
|
3d65a957f4 | ||
|
|
ff038e3ade | ||
|
|
44fa42fca4 | ||
|
|
73d8c74718 | ||
|
|
db4a0deccc | ||
|
|
8b23a409ef | ||
|
|
ec7e73bb7c | ||
|
|
321b85d5d0 | ||
|
|
98c9638bc4 | ||
|
|
de1c9f2581 | ||
|
|
1af6af5045 | ||
|
|
0121811195 | ||
|
|
18cf55b156 | ||
|
|
0d4e109c72 | ||
|
|
3e358da83a | ||
|
|
85453ca442 | ||
|
|
a5e5a73580 | ||
|
|
95f7b8d79f | ||
|
|
42d0d84ae0 | ||
|
|
686219d473 | ||
|
|
843ed24bbb | ||
|
|
e17c49505c | ||
|
|
473747ee03 | ||
|
|
9ea97aabbb | ||
|
|
811d79c873 | ||
|
|
018782e63d | ||
|
|
01070a9cc0 | ||
|
|
14aecc4684 | ||
|
|
8aea20f147 | ||
|
|
87f175a96b | ||
|
|
6b5117a609 | ||
|
|
0ab66f81be | ||
|
|
12ec0ca84c | ||
|
|
915d56ac15 | ||
|
|
ecc43f1dea | ||
|
|
d8a4678b8f | ||
|
|
306875478e | ||
|
|
2df303cde7 | ||
|
|
4309127b8c | ||
|
|
39155b55a0 | ||
|
|
02dc457636 | ||
|
|
732a6324d6 | ||
|
|
54614e67aa | ||
|
|
15362c04fb | ||
|
|
658de3b6e7 | ||
|
|
ab55fec6bd | ||
|
|
3a1f06f510 | ||
|
|
1ad0b0e6ae | ||
|
|
7ccc7ec856 | ||
|
|
f0ab919ca5 | ||
|
|
8a05626791 | ||
|
|
c0a468e42b | ||
|
|
8bee95eb02 | ||
|
|
dedb78e454 | ||
|
|
2c1f30db1d | ||
|
|
6d3319bfb1 | ||
|
|
e4b9832045 | ||
|
|
99aa4cbc98 | ||
|
|
1f952bd31e | ||
|
|
882bdcc726 | ||
|
|
b0f43535c6 | ||
|
|
16ae2c870f | ||
|
|
18bb876d1b | ||
|
|
dce8fde195 | ||
|
|
270ab34c6a | ||
|
|
aa872d29bc | ||
|
|
6315d4b96c | ||
|
|
0cb53f40f4 | ||
|
|
51c86cab10 | ||
|
|
1f860d41b5 | ||
|
|
573de99840 | ||
|
|
68e0a30c92 | ||
|
|
6fc9db0aff | ||
|
|
737d893c87 | ||
|
|
f06e1d9b98 | ||
|
|
4cff0a3369 | ||
|
|
cc64a922d7 | ||
|
|
e8c769bd1d | ||
|
|
deba07d6cb | ||
|
|
bacad359b2 | ||
|
|
a9d7417597 | ||
|
|
6b94fc82eb | ||
|
|
b9f060b442 | ||
|
|
ca24682366 | ||
|
|
5047fc54f2 | ||
|
|
48c115eba1 | ||
|
|
fd2677e8fe | ||
|
|
f6bd27eff9 | ||
|
|
ff41816fef | ||
|
|
1e6a17adc3 | ||
|
|
55aff18b1f | ||
|
|
5d6b3a8a75 | ||
|
|
31b98ec612 | ||
|
|
320bf45518 | ||
|
|
1893896254 | ||
|
|
19a95f479e | ||
|
|
5bcb7cece4 | ||
|
|
f4f5fe2789 | ||
|
|
e947212862 | ||
|
|
57f86b14fc | ||
|
|
e2dc7fb5bf | ||
|
|
6499ed4637 | ||
|
|
8c45600365 | ||
|
|
f8ef850fba | ||
|
|
151e2e5203 | ||
|
|
5dd3d8515f | ||
|
|
0f6c16c373 | ||
|
|
75bf3a7c7e | ||
|
|
48e47c9d92 | ||
|
|
3d45ab1b36 | ||
|
|
4d5d42157a | ||
|
|
a6dfee16e9 | ||
|
|
0e8550748d | ||
|
|
b82604953c | ||
|
|
100796b3b9 | ||
|
|
f5af964286 | ||
|
|
2836a6060d | ||
|
|
80e31051e6 | ||
|
|
1fb0573fec | ||
|
|
5ba04936b1 | ||
|
|
011f6e6cf4 | ||
|
|
ed3f992b83 | ||
|
|
782217a73d | ||
|
|
a37b89feaf | ||
|
|
e5b628b467 | ||
|
|
482a10de02 | ||
|
|
c4164b17a2 | ||
|
|
b8dc541fc5 | ||
|
|
2b6190bf34 | ||
|
|
2a70423a22 | ||
|
|
35c74573e7 | ||
|
|
c26c455b3c | ||
|
|
4e2e525509 | ||
|
|
ec83327eec | ||
|
|
bafb62f214 | ||
|
|
38f5e8b4eb | ||
|
|
9827deffd3 | ||
|
|
65105fd3cb | ||
|
|
5d604c4e55 | ||
|
|
22221222bd | ||
|
|
bad2f99968 | ||
|
|
392d582865 | ||
|
|
33dbf316a9 | ||
|
|
00a8565e91 | ||
|
|
0bac08dcc4 | ||
|
|
3b2dfb6ede | ||
|
|
997f6ef534 | ||
|
|
fb0b1af056 | ||
|
|
3037a33267 | ||
|
|
ff633ddd59 | ||
|
|
cae5dad5d8 | ||
|
|
1a03b8fc1d | ||
|
|
049ba6a706 | ||
|
|
f52364f75c | ||
|
|
87b699f3d8 | ||
|
|
f73b8a7fd2 | ||
|
|
8af8468f4d | ||
|
|
49270e677e | ||
|
|
09dd2583b9 | ||
|
|
dc22b27cd8 | ||
|
|
2a9eb1bae0 | ||
|
|
c06fb81490 | ||
|
|
af1b9579b4 | ||
|
|
7bbfc2d34c | ||
|
|
70355aa70e | ||
|
|
56c502c9bf | ||
|
|
a05793c882 | ||
|
|
53f60f5a4c | ||
|
|
43d969f6b5 | ||
|
|
a51bb8e23f | ||
|
|
5ceb3db0c4 | ||
|
|
9a65328c1b | ||
|
|
35eef0150d | ||
|
|
8511d3576f | ||
|
|
f6542440c7 | ||
|
|
35393fc331 | ||
|
|
f31e12572a | ||
|
|
cef7878b47 | ||
|
|
3574be913a | ||
|
|
b8cf0cc1be | ||
|
|
b0788f7307 | ||
|
|
cf9b91ebd4 | ||
|
|
1af15842cc | ||
|
|
17517cfc88 | ||
|
|
4615f246ac | ||
|
|
62ee60df82 | ||
|
|
4f3c545eda | ||
|
|
b92a41ab70 | ||
|
|
6673da0b04 | ||
|
|
102f9de06f | ||
|
|
614d6ce04b | ||
|
|
5bb48caafd | ||
|
|
6c7d837964 | ||
|
|
755ec672c0 | ||
|
|
186bd9db48 | ||
|
|
48a81da883 | ||
|
|
2980e547cb | ||
|
|
c9c2bbcf80 | ||
|
|
33da599ee0 | ||
|
|
113bcca277 | ||
|
|
deca8e3feb | ||
|
|
e02c8b9db7 | ||
|
|
314ea98393 | ||
|
|
0840cfc6e7 | ||
|
|
de4cb931f3 | ||
|
|
abde740ff7 | ||
|
|
9efe216070 | ||
|
|
2427c226a8 | ||
|
|
ae73601f52 | ||
|
|
85551ca824 | ||
|
|
12565d28ae | ||
|
|
f0a4956cdd | ||
|
|
ba0befde20 | ||
|
|
dd7652ad44 | ||
|
|
b41303ba0d | ||
|
|
a70ab94d24 | ||
|
|
10dd39abea | ||
|
|
5113f8b203 | ||
|
|
8f007a23cd | ||
|
|
b34bb2e7d7 | ||
|
|
98fce53cf1 | ||
|
|
ced05fe579 | ||
|
|
fae21e4dbb | ||
|
|
5e3a3e1da9 | ||
|
|
03ad5073d2 | ||
|
|
3bd354289d | ||
|
|
8808526d0b | ||
|
|
0a19440ffc | ||
|
|
9815851bb9 | ||
|
|
1581a6e1cc | ||
|
|
e3aa244f31 | ||
|
|
8fcce9fba5 | ||
|
|
7d49c77d1a | ||
|
|
947f59e81b | ||
|
|
7cac62f3f2 | ||
|
|
4578c33968 | ||
|
|
0160303d19 | ||
|
|
31aabd9851 | ||
|
|
7f39b9b50f | ||
|
|
69a2664668 | ||
|
|
acebf5964c | ||
|
|
ec2e3e29c3 | ||
|
|
0fc144d4a7 | ||
|
|
73025ec6de | ||
|
|
1d0e00648f | ||
|
|
42b5654a99 | ||
|
|
2eb787d78b | ||
|
|
1249cced2d | ||
|
|
0be1a30766 | ||
|
|
ea253a2e67 | ||
|
|
c4fadccf72 | ||
|
|
fcf62512a7 | ||
|
|
16ab27084c | ||
|
|
c1820459b7 | ||
|
|
d88999d6d4 | ||
|
|
68655194a6 | ||
|
|
f533a898f5 | ||
|
|
2167522f7d | ||
|
|
f198b890fa | ||
|
|
85cb41050e | ||
|
|
00c131355f | ||
|
|
13ef53372e | ||
|
|
f2cf77339e | ||
|
|
3e5be2cfe2 | ||
|
|
c0a68202a7 | ||
|
|
07a6942ea8 | ||
|
|
41585699d2 | ||
|
|
2fcb240c2b | ||
|
|
566e981473 | ||
|
|
26e04ce6d2 | ||
|
|
2e2b4e1406 | ||
|
|
b89e08dad7 | ||
|
|
5711b8a0fa | ||
|
|
62f9f19540 | ||
|
|
731683ae09 | ||
|
|
343aadcd9a | ||
|
|
c4ad6c2992 | ||
|
|
97dd756136 | ||
|
|
7989c40f52 | ||
|
|
0749905909 | ||
|
|
168481fee5 | ||
|
|
7866e2e29c | ||
|
|
4eb0dca8f6 | ||
|
|
bc54f6ca07 | ||
|
|
223c0c4bce | ||
|
|
b39099b84e | ||
|
|
22d6546704 | ||
|
|
a7af687f8e | ||
|
|
ce9cd132ec | ||
|
|
62fa99e0ee | ||
|
|
43e4cba3d7 | ||
|
|
6cbc2f684d | ||
|
|
ffc9e8caff | ||
|
|
49c9b0acde | ||
|
|
9c6908873c | ||
|
|
528fe67db9 | ||
|
|
39e14e922b | ||
|
|
0c8b6f8ef8 | ||
|
|
f65de84c19 | ||
|
|
88074134af | ||
|
|
b5cc570363 | ||
|
|
3cbf0933ff | ||
|
|
7f9c89483f | ||
|
|
8ef3d3fbbf | ||
|
|
c225c2b37d | ||
|
|
ff76c5fca5 | ||
|
|
5b99f590f8 | ||
|
|
2d0feca278 | ||
|
|
92e506b117 | ||
|
|
cac841d8e6 | ||
|
|
77cb9bc174 | ||
|
|
309e33016a | ||
|
|
938b24f623 | ||
|
|
82c637ef4b | ||
|
|
d9e8480a12 | ||
|
|
5115717f67 | ||
|
|
33ac48e771 | ||
|
|
c53f1fcecf | ||
|
|
78704dce8a | ||
|
|
7f3ba1978d | ||
|
|
891dfc1b68 | ||
|
|
b0ccb543d1 | ||
|
|
7752b3aba3 | ||
|
|
0fa13eb097 | ||
|
|
641db1cbe2 | ||
|
|
8d53c2392a | ||
|
|
8d0acb277c | ||
|
|
6e00920c95 | ||
|
|
13638dc1c9 | ||
|
|
1222d020ad |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -50,5 +50,5 @@ Describe here the issue that you are experiencing.
|
|||||||
**Signal version:** 0.0.0
|
**Signal version:** 0.0.0
|
||||||
|
|
||||||
### Link to debug log
|
### Link to debug log
|
||||||
<!-- immediately after the bug has happened capture a debug log via Signal's advanced settings and paste the link below -->
|
<!-- immediately after the bug has happened capture a debug log via Signal's settings (Help -> Debug log) and paste the link below -->
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ apply from: 'translations.gradle'
|
|||||||
apply from: 'witness-verifications.gradle'
|
apply from: 'witness-verifications.gradle'
|
||||||
apply plugin: 'org.jetbrains.kotlin.android'
|
apply plugin: 'org.jetbrains.kotlin.android'
|
||||||
apply plugin: 'app.cash.exhaustive'
|
apply plugin: 'app.cash.exhaustive'
|
||||||
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
@@ -23,12 +24,6 @@ repositories {
|
|||||||
includeGroupByRegex "org\\.signal.*"
|
includeGroupByRegex "org\\.signal.*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
maven { // textdrawable
|
|
||||||
url 'https://dl.bintray.com/amulyakhare/maven'
|
|
||||||
content {
|
|
||||||
includeGroupByRegex "com\\.amulyakhare.*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
maven {
|
maven {
|
||||||
url "https://www.jitpack.io"
|
url "https://www.jitpack.io"
|
||||||
}
|
}
|
||||||
@@ -45,7 +40,6 @@ repositories {
|
|||||||
includeVersion "cn.carbswang.android", "NumberPickerView", "1.0.9"
|
includeVersion "cn.carbswang.android", "NumberPickerView", "1.0.9"
|
||||||
includeVersion "com.takisoft.fix", "colorpicker", "0.9.1"
|
includeVersion "com.takisoft.fix", "colorpicker", "0.9.1"
|
||||||
includeVersion "com.codewaves.stickyheadergrid", "stickyheadergrid", "0.9.4"
|
includeVersion "com.codewaves.stickyheadergrid", "stickyheadergrid", "0.9.4"
|
||||||
includeVersion "com.amulyakhare", "com.amulyakhare.textdrawable", "1.0.1"
|
|
||||||
includeVersion "com.google.android", "flexbox", "0.3.0"
|
includeVersion "com.google.android", "flexbox", "0.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +47,7 @@ repositories {
|
|||||||
|
|
||||||
protobuf {
|
protobuf {
|
||||||
protoc {
|
protoc {
|
||||||
artifact = 'com.google.protobuf:protoc:3.10.0'
|
artifact = 'com.google.protobuf:protoc:3.11.4'
|
||||||
}
|
}
|
||||||
generateProtoTasks {
|
generateProtoTasks {
|
||||||
all().each { task ->
|
all().each { task ->
|
||||||
@@ -66,8 +60,8 @@ protobuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 933
|
def canonicalVersionCode = 968
|
||||||
def canonicalVersionName = "5.24.13"
|
def canonicalVersionName = "5.27.9"
|
||||||
|
|
||||||
def postFixSize = 100
|
def postFixSize = 100
|
||||||
def abiPostFix = ['universal' : 0,
|
def abiPostFix = ['universal' : 0,
|
||||||
@@ -79,16 +73,9 @@ def abiPostFix = ['universal' : 0,
|
|||||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||||
|
|
||||||
def selectableVariants = [
|
def selectableVariants = [
|
||||||
'internalProdFlipper',
|
|
||||||
'internalProdPerf',
|
|
||||||
'internalProdRelease',
|
|
||||||
'internalStagingFlipper',
|
|
||||||
'internalStagingPerf',
|
|
||||||
'internalStagingRelease',
|
|
||||||
'nightlyProdFlipper',
|
'nightlyProdFlipper',
|
||||||
'nightlyProdPerf',
|
'nightlyProdPerf',
|
||||||
'nightlyProdRelease',
|
'nightlyProdRelease',
|
||||||
'nightlyStagingPerf',
|
|
||||||
'playProdDebug',
|
'playProdDebug',
|
||||||
'playProdFlipper',
|
'playProdFlipper',
|
||||||
'playProdPerf',
|
'playProdPerf',
|
||||||
@@ -159,24 +146,26 @@ android {
|
|||||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||||
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
|
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
|
||||||
buildConfigField "String", "CDSH_CODE_HASH", "\"ec31a51880d19a5e9e0fed404740c1a3ff53a553125564b745acce475f0fded8\""
|
buildConfigField "String", "CDSH_CODE_HASH", "\"ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2\""
|
||||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
||||||
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
|
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
|
||||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
|
||||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
|
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY\""
|
||||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||||
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
||||||
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44,49,33,41}"
|
buildConfigField "int[]", "MOBILE_COIN_BLACKLIST", "new int[]{98,963,53,850,7}"
|
||||||
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
||||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
||||||
|
|
||||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
|
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
|
||||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"unset\""
|
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"unset\""
|
||||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"unset\""
|
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"unset\""
|
||||||
|
buildConfigField "String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\""
|
||||||
|
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\""
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||||
@@ -194,6 +183,11 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -291,14 +285,6 @@ android {
|
|||||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"website\""
|
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"website\""
|
||||||
}
|
}
|
||||||
|
|
||||||
internal {
|
|
||||||
dimension 'distribution'
|
|
||||||
ext.websiteUpdateUrl = "null"
|
|
||||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
|
||||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
|
||||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"internal\""
|
|
||||||
}
|
|
||||||
|
|
||||||
nightly {
|
nightly {
|
||||||
dimension 'distribution'
|
dimension 'distribution'
|
||||||
versionNameSuffix "-nightly-untagged-${getDateSuffix()}"
|
versionNameSuffix "-nightly-untagged-${getDateSuffix()}"
|
||||||
@@ -339,16 +325,17 @@ android {
|
|||||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
||||||
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
|
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
|
||||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
||||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
|
||||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARB\""
|
||||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
||||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
|
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
|
||||||
|
|
||||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
|
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
|
||||||
|
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,8 +441,8 @@ dependencies {
|
|||||||
implementation project(':video')
|
implementation project(':video')
|
||||||
implementation project(':device-transfer')
|
implementation project(':device-transfer')
|
||||||
implementation project(':image-editor')
|
implementation project(':image-editor')
|
||||||
|
implementation project(':donations')
|
||||||
|
|
||||||
implementation libs.signal.zkgroup.android
|
|
||||||
implementation libs.signal.client.android
|
implementation libs.signal.client.android
|
||||||
implementation libs.google.protobuf.javalite
|
implementation libs.google.protobuf.javalite
|
||||||
|
|
||||||
@@ -486,7 +473,6 @@ dependencies {
|
|||||||
implementation libs.floatingactionbutton
|
implementation libs.floatingactionbutton
|
||||||
implementation libs.google.zxing.android.integration
|
implementation libs.google.zxing.android.integration
|
||||||
implementation libs.time.duration.picker
|
implementation libs.time.duration.picker
|
||||||
implementation libs.textdrawable
|
|
||||||
implementation libs.google.zxing.core
|
implementation libs.google.zxing.core
|
||||||
implementation (libs.subsampling.scale.image.view) {
|
implementation (libs.subsampling.scale.image.view) {
|
||||||
exclude group: 'com.android.support', module: 'support-annotations'
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
@@ -523,6 +509,7 @@ dependencies {
|
|||||||
|
|
||||||
flipperImplementation libs.facebook.flipper
|
flipperImplementation libs.facebook.flipper
|
||||||
flipperImplementation libs.facebook.soloader
|
flipperImplementation libs.facebook.soloader
|
||||||
|
flipperImplementation libs.square.leakcanary
|
||||||
|
|
||||||
testImplementation testLibs.junit.junit
|
testImplementation testLibs.junit.junit
|
||||||
testImplementation testLibs.assertj.core
|
testImplementation testLibs.assertj.core
|
||||||
@@ -544,12 +531,16 @@ dependencies {
|
|||||||
androidTestImplementation testLibs.androidx.test.ext.junit
|
androidTestImplementation testLibs.androidx.test.ext.junit
|
||||||
androidTestImplementation testLibs.espresso.core
|
androidTestImplementation testLibs.espresso.core
|
||||||
|
|
||||||
|
testImplementation testLibs.espresso.core
|
||||||
|
|
||||||
implementation libs.kotlin.stdlib.jdk8
|
implementation libs.kotlin.stdlib.jdk8
|
||||||
implementation libs.kotlin.reflect
|
implementation libs.kotlin.reflect
|
||||||
implementation libs.jackson.module.kotlin
|
implementation libs.jackson.module.kotlin
|
||||||
|
|
||||||
implementation libs.rxjava3.rxandroid
|
implementation libs.rxjava3.rxandroid
|
||||||
implementation libs.rxjava3.rxkotlin
|
implementation libs.rxjava3.rxkotlin
|
||||||
|
|
||||||
|
androidTestUtil 'androidx.test:orchestrator:1.4.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencyVerification {
|
dependencyVerification {
|
||||||
@@ -599,7 +590,8 @@ def getCurrentGitTag() {
|
|||||||
def output = stdout.toString().trim()
|
def output = stdout.toString().trim()
|
||||||
|
|
||||||
if (output != null && output.size() > 0) {
|
if (output != null && output.size() > 0) {
|
||||||
return output.split('\n')[0];
|
def tags = output.split('\n').toList()
|
||||||
|
return tags.stream().filter(t -> t.contains('nightly')).findFirst().orElse(tags.get(0))
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,384 @@
|
|||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
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 java.lang.IllegalArgumentException
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class RecipientDatabaseTest {
|
||||||
|
|
||||||
|
private lateinit var recipientDatabase: RecipientDatabase
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
recipientDatabase = SignalDatabase.recipients
|
||||||
|
ensureDbEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// If both the ACI and E164 map to no one
|
||||||
|
// ==============================================================
|
||||||
|
|
||||||
|
/** If all you have is an ACI, you can just store that, regardless of trust level. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly_highTrust() {
|
||||||
|
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
|
||||||
|
|
||||||
|
val recipient = Recipient.resolved(recipientId)
|
||||||
|
assertEquals(ACI_A, recipient.requireAci())
|
||||||
|
assertFalse(recipient.hasE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** If all you have is an ACI, you can just store that, regardless of trust level. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly_lowTrust() {
|
||||||
|
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, false)
|
||||||
|
|
||||||
|
val recipient = Recipient.resolved(recipientId)
|
||||||
|
assertEquals(ACI_A, recipient.requireAci())
|
||||||
|
assertFalse(recipient.hasE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** If all you have is an E164, you can just store that, regardless of trust level. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only_highTrust() {
|
||||||
|
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
|
||||||
|
|
||||||
|
val recipient = Recipient.resolved(recipientId)
|
||||||
|
assertEquals(E164_A, recipient.requireE164())
|
||||||
|
assertFalse(recipient.hasAci())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** If all you have is an E164, you can just store that, regardless of trust level. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only_lowTrust() {
|
||||||
|
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, false)
|
||||||
|
|
||||||
|
val recipient = Recipient.resolved(recipientId)
|
||||||
|
assertEquals(E164_A, recipient.requireE164())
|
||||||
|
assertFalse(recipient.hasAci())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** With high trust, you can associate an ACI-e164 pair. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164_highTrust() {
|
||||||
|
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
|
||||||
|
val recipient = Recipient.resolved(recipientId)
|
||||||
|
assertEquals(ACI_A, recipient.requireAci())
|
||||||
|
assertEquals(E164_A, recipient.requireE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** With low trust, you cannot associate an ACI-e164 pair, and therefore can only store the ACI. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164_lowTrust() {
|
||||||
|
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||||
|
|
||||||
|
val recipient = Recipient.resolved(recipientId)
|
||||||
|
assertEquals(ACI_A, recipient.requireAci())
|
||||||
|
assertFalse(recipient.hasE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// If the ACI maps to an existing user, but the E164 doesn't
|
||||||
|
// ==============================================================
|
||||||
|
|
||||||
|
/** 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 retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
assertEquals(existingId, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||||
|
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** With low trust, you cannot associate an ACI-e164 pair, and therefore cannot store the e164. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_lowTrust() {
|
||||||
|
val existingId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||||
|
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||||
|
assertEquals(existingId, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||||
|
assertFalse(retrievedRecipient.hasE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Basically the ‘change number’ case. High trust lets you update the existing user. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_highTrust() {
|
||||||
|
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||||
|
assertEquals(existingId, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||||
|
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Low trust means you can’t update the underlying data, but you also don’t need to create any new rows. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_lowTrust() {
|
||||||
|
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, false)
|
||||||
|
assertEquals(existingId, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||||
|
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// If the E164 maps to an existing user, but the ACI doesn't
|
||||||
|
// ==============================================================
|
||||||
|
|
||||||
|
/** With high trust, you can associate an e164 with an existing ACI. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_highTrust() {
|
||||||
|
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_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(E164_A, retrievedRecipient.requireE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** With low trust, you cannot associate an ACI-e164 pair, and therefore need to create a new person with just the ACI. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_lowTrust() {
|
||||||
|
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||||
|
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||||
|
assertNotEquals(existingId, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||||
|
assertFalse(retrievedRecipient.hasE164())
|
||||||
|
|
||||||
|
val existingRecipient = Recipient.resolved(existingId)
|
||||||
|
assertEquals(E164_A, existingRecipient.requireE164())
|
||||||
|
assertFalse(existingRecipient.hasAci())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. But high trust lets us take the e164 from the current holder. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_highTrust() {
|
||||||
|
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
|
||||||
|
assertNotEquals(existingId, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_B, retrievedRecipient.requireAci())
|
||||||
|
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||||
|
|
||||||
|
val existingRecipient = Recipient.resolved(existingId)
|
||||||
|
assertEquals(ACI_A, existingRecipient.requireAci())
|
||||||
|
assertFalse(existingRecipient.hasE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_lowTrust() {
|
||||||
|
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, false)
|
||||||
|
assertNotEquals(existingId, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_B, retrievedRecipient.requireAci())
|
||||||
|
assertFalse(retrievedRecipient.hasE164())
|
||||||
|
|
||||||
|
val existingRecipient = Recipient.resolved(existingId)
|
||||||
|
assertEquals(ACI_A, existingRecipient.requireAci())
|
||||||
|
assertEquals(E164_A, existingRecipient.requireE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// If both the ACI and E164 map to an existing user
|
||||||
|
// ==============================================================
|
||||||
|
|
||||||
|
/** Regardless of trust, if your ACI and e164 match, you’re good. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_highTrust() {
|
||||||
|
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
assertEquals(existingId, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||||
|
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust() {
|
||||||
|
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||||
|
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||||
|
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
assertEquals(existingAciId, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||||
|
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||||
|
|
||||||
|
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||||
|
assertEquals(retrievedId, existingE164Recipient.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 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())
|
||||||
|
assertFalse(retrievedRecipient.hasE164())
|
||||||
|
|
||||||
|
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||||
|
assertEquals(E164_A, existingE164Recipient.requireE164())
|
||||||
|
assertFalse(existingE164Recipient.hasAci())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Another high trust case. No new rules here, just a more complex scenario to show how different rules interact. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_highTrust() {
|
||||||
|
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||||
|
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
|
||||||
|
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
assertEquals(existingId1, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||||
|
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||||
|
|
||||||
|
val existingRecipient2 = Recipient.resolved(existingId2)
|
||||||
|
assertEquals(ACI_B, existingRecipient2.requireAci())
|
||||||
|
assertFalse(existingRecipient2.hasE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Another low trust case. No new rules here, just a more complex scenario to show how different rules interact. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_lowTrust() {
|
||||||
|
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||||
|
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
|
||||||
|
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||||
|
assertEquals(existingId1, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||||
|
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||||
|
|
||||||
|
val existingRecipient2 = Recipient.resolved(existingId2)
|
||||||
|
assertEquals(ACI_B, existingRecipient2.requireAci())
|
||||||
|
assertEquals(E164_A, existingRecipient2.requireE164())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Another high trust case that results in a merge. Nothing strictly new here, but this case is called out because it’s a merge but *also* an E164 change,
|
||||||
|
* which clients may need to know for UX purposes.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_mergeAndPhoneNumberChange_highTrust() {
|
||||||
|
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||||
|
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
|
||||||
|
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
assertEquals(existingId1, retrievedId)
|
||||||
|
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||||
|
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||||
|
|
||||||
|
assertFalse(recipientDatabase.getByE164(E164_B).isPresent)
|
||||||
|
|
||||||
|
val recipientWithId2 = Recipient.resolved(existingId2)
|
||||||
|
assertEquals(retrievedId, recipientWithId2.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// Misc
|
||||||
|
// ==============================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createByE164SanityCheck() {
|
||||||
|
// GIVEN one recipient
|
||||||
|
val recipientId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||||
|
|
||||||
|
// WHEN I retrieve one by E164
|
||||||
|
val possible: Optional<RecipientId> = recipientDatabase.getByE164(E164_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.e164.isPresent)
|
||||||
|
assertEquals(E164_A, recipient.e164.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createByUuidSanityCheck() {
|
||||||
|
// GIVEN one recipient
|
||||||
|
val recipientId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||||
|
|
||||||
|
// WHEN I retrieve one by UUID
|
||||||
|
val possible: Optional<RecipientId> = recipientDatabase.getByAci(ACI_A)
|
||||||
|
|
||||||
|
// THEN I get it back, and it has the properties I expect
|
||||||
|
assertTrue(possible.isPresent)
|
||||||
|
assertEquals(recipientId, possible.get())
|
||||||
|
|
||||||
|
val recipient = Recipient.resolved(recipientId)
|
||||||
|
assertTrue(recipient.aci.isPresent)
|
||||||
|
assertEquals(ACI_A, recipient.aci.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun getAndPossiblyMerge_noArgs_invalid() {
|
||||||
|
recipientDatabase.getAndPossiblyMerge(null, null, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureDbEmpty() {
|
||||||
|
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
|
||||||
|
assertTrue(cursor.moveToFirst())
|
||||||
|
assertEquals(0, cursor.getLong(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||||
|
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||||
|
|
||||||
|
val E164_A = "+12221234567"
|
||||||
|
val E164_B = "+13331234567"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
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.storageservice.protos.groups.local.DecryptedGroup
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||||
|
import org.signal.zkgroup.groups.GroupMasterKey
|
||||||
|
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.mms.IncomingMediaMessage
|
||||||
|
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.util.UuidUtil
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class RecipientDatabaseTest_merges {
|
||||||
|
|
||||||
|
private lateinit var recipientDatabase: RecipientDatabase
|
||||||
|
private lateinit var identityDatabase: IdentityDatabase
|
||||||
|
private lateinit var groupReceiptDatabase: GroupReceiptDatabase
|
||||||
|
private lateinit var groupDatabase: GroupDatabase
|
||||||
|
private lateinit var threadDatabase: ThreadDatabase
|
||||||
|
private lateinit var smsDatabase: MessageDatabase
|
||||||
|
private lateinit var mmsDatabase: MessageDatabase
|
||||||
|
private lateinit var sessionDatabase: SessionDatabase
|
||||||
|
private lateinit var mentionDatabase: MentionDatabase
|
||||||
|
private lateinit var reactionDatabase: ReactionDatabase
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
recipientDatabase = SignalDatabase.recipients
|
||||||
|
identityDatabase = SignalDatabase.identities
|
||||||
|
groupReceiptDatabase = SignalDatabase.groupReceipts
|
||||||
|
groupDatabase = SignalDatabase.groups
|
||||||
|
threadDatabase = SignalDatabase.threads
|
||||||
|
smsDatabase = SignalDatabase.sms
|
||||||
|
mmsDatabase = SignalDatabase.mms
|
||||||
|
sessionDatabase = SignalDatabase.sessions
|
||||||
|
mentionDatabase = SignalDatabase.mentions
|
||||||
|
reactionDatabase = SignalDatabase.reactions
|
||||||
|
|
||||||
|
ensureDbEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
|
||||||
|
@Test
|
||||||
|
fun getAndPossiblyMerge_general() {
|
||||||
|
// Setup
|
||||||
|
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||||
|
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||||
|
|
||||||
|
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
|
||||||
|
val smsId3: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId
|
||||||
|
|
||||||
|
val mmsId1: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
|
||||||
|
val mmsId2: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
|
||||||
|
val mmsId3: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
|
||||||
|
|
||||||
|
val threadIdAci: Long = threadDatabase.getThreadIdFor(recipientIdAci)!!
|
||||||
|
val threadIdE164: Long = threadDatabase.getThreadIdFor(recipientIdE164)!!
|
||||||
|
assertNotEquals(threadIdAci, threadIdE164)
|
||||||
|
|
||||||
|
mentionDatabase.insert(threadIdAci, mmsId1, listOf(Mention(recipientIdE164, 0, 1)))
|
||||||
|
mentionDatabase.insert(threadIdE164, mmsId2, listOf(Mention(recipientIdAci, 0, 1)))
|
||||||
|
|
||||||
|
groupReceiptDatabase.insert(listOf(recipientIdAci, recipientIdE164), mmsId1, 0, 3)
|
||||||
|
|
||||||
|
val identityKeyAci: IdentityKey = identityKey(1)
|
||||||
|
val identityKeyE164: IdentityKey = identityKey(2)
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1))
|
||||||
|
reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1))
|
||||||
|
|
||||||
|
// Merge
|
||||||
|
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||||
|
val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!!
|
||||||
|
assertEquals(recipientIdAci, retrievedId)
|
||||||
|
|
||||||
|
// Recipient validation
|
||||||
|
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||||
|
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||||
|
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||||
|
|
||||||
|
val existingE164Recipient = Recipient.resolved(recipientIdE164)
|
||||||
|
assertEquals(retrievedId, existingE164Recipient.id)
|
||||||
|
|
||||||
|
// Thread validation
|
||||||
|
assertEquals(threadIdAci, retrievedThreadId)
|
||||||
|
assertNull(threadDatabase.getThreadIdFor(recipientIdE164))
|
||||||
|
assertNull(threadDatabase.getThreadRecord(threadIdE164))
|
||||||
|
|
||||||
|
// SMS validation
|
||||||
|
val sms1: MessageRecord = smsDatabase.getMessageRecord(smsId1)!!
|
||||||
|
val sms2: MessageRecord = smsDatabase.getMessageRecord(smsId2)!!
|
||||||
|
val sms3: MessageRecord = smsDatabase.getMessageRecord(smsId3)!!
|
||||||
|
|
||||||
|
assertEquals(retrievedId, sms1.recipient.id)
|
||||||
|
assertEquals(retrievedId, sms2.recipient.id)
|
||||||
|
assertEquals(retrievedId, sms3.recipient.id)
|
||||||
|
|
||||||
|
assertEquals(retrievedThreadId, sms1.threadId)
|
||||||
|
assertEquals(retrievedThreadId, sms2.threadId)
|
||||||
|
assertEquals(retrievedThreadId, sms3.threadId)
|
||||||
|
|
||||||
|
// MMS validation
|
||||||
|
val mms1: MessageRecord = mmsDatabase.getMessageRecord(mmsId1)!!
|
||||||
|
val mms2: MessageRecord = mmsDatabase.getMessageRecord(mmsId2)!!
|
||||||
|
val mms3: MessageRecord = mmsDatabase.getMessageRecord(mmsId3)!!
|
||||||
|
|
||||||
|
assertEquals(retrievedId, mms1.recipient.id)
|
||||||
|
assertEquals(retrievedId, mms2.recipient.id)
|
||||||
|
assertEquals(retrievedId, mms3.recipient.id)
|
||||||
|
|
||||||
|
assertEquals(retrievedThreadId, mms1.threadId)
|
||||||
|
assertEquals(retrievedThreadId, mms2.threadId)
|
||||||
|
assertEquals(retrievedThreadId, mms3.threadId)
|
||||||
|
|
||||||
|
// Mention validation
|
||||||
|
val mention1: MentionModel = getMention(mmsId1)
|
||||||
|
assertEquals(retrievedId, mention1.recipientId)
|
||||||
|
assertEquals(retrievedThreadId, mention1.threadId)
|
||||||
|
|
||||||
|
val mention2: MentionModel = getMention(mmsId2)
|
||||||
|
assertEquals(retrievedId, mention2.recipientId)
|
||||||
|
assertEquals(retrievedThreadId, mention2.threadId)
|
||||||
|
|
||||||
|
// Group receipt validation
|
||||||
|
val groupReceipts: List<GroupReceiptDatabase.GroupReceiptInfo> = groupReceiptDatabase.getGroupReceiptInfo(mmsId1)
|
||||||
|
assertEquals(retrievedId, groupReceipts[0].recipientId)
|
||||||
|
assertEquals(retrievedId, groupReceipts[1].recipientId)
|
||||||
|
|
||||||
|
// Identity validation
|
||||||
|
assertEquals(identityKeyAci, identityDatabase.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
|
||||||
|
assertNull(identityDatabase.getIdentityStoreRecord(E164_A))
|
||||||
|
|
||||||
|
// Session validation
|
||||||
|
assertNotNull(sessionDatabase.load(SignalProtocolAddress(ACI_A.toString(), 1)))
|
||||||
|
|
||||||
|
// Reaction validation
|
||||||
|
val reactionsSms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(smsId1, false))
|
||||||
|
val reactionsMms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(mmsId1, true))
|
||||||
|
|
||||||
|
assertEquals(1, reactionsSms.size)
|
||||||
|
assertEquals(ReactionRecord("a", recipientIdAci, 1, 1), reactionsSms[0])
|
||||||
|
|
||||||
|
assertEquals(1, reactionsMms.size)
|
||||||
|
assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 identityKey(value: Byte): IdentityKey {
|
||||||
|
val bytes = ByteArray(33)
|
||||||
|
bytes[0] = 0x05
|
||||||
|
bytes[1] = value
|
||||||
|
return IdentityKey(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun groupMasterKey(value: Byte): GroupMasterKey {
|
||||||
|
val bytes = ByteArray(32)
|
||||||
|
bytes[0] = value
|
||||||
|
return GroupMasterKey(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptedGroup(members: Collection<UUID>): DecryptedGroup {
|
||||||
|
return DecryptedGroup.newBuilder()
|
||||||
|
.addAllMembers(members.map { DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(it)).build() })
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMention(messageId: Long): MentionModel {
|
||||||
|
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionDatabase.TABLE_NAME} WHERE ${MentionDatabase.MESSAGE_ID} = $messageId").use { cursor ->
|
||||||
|
cursor.moveToFirst()
|
||||||
|
return MentionModel(
|
||||||
|
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionDatabase.RECIPIENT_ID)),
|
||||||
|
threadId = CursorUtil.requireLong(cursor, MentionDatabase.THREAD_ID)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The normal mention model doesn't have a threadId, so we need to do it ourselves for the test */
|
||||||
|
data class MentionModel(
|
||||||
|
val recipientId: RecipientId,
|
||||||
|
val threadId: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||||
|
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||||
|
|
||||||
|
val E164_A = "+12221234567"
|
||||||
|
val E164_B = "+13331234567"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package org.thoughtcrime.securesms.lock;
|
package org.thoughtcrime.securesms.lock;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
import org.thoughtcrime.securesms.util.Hex;
|
import org.thoughtcrime.securesms.util.Hex;
|
||||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||||
import org.whispersystems.signalservice.api.kbs.KbsData;
|
import org.whispersystems.signalservice.api.kbs.KbsData;
|
||||||
@@ -12,6 +15,7 @@ import static org.junit.Assert.assertArrayEquals;
|
|||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class PinHashing_hashPin_Test {
|
public final class PinHashing_hashPin_Test {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
|
||||||
import com.facebook.flipper.core.FlipperClient;
|
|
||||||
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 org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter;
|
|
||||||
|
|
||||||
public class FlipperApplicationContext extends ApplicationContext {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
SoLoader.init(this, false);
|
|
||||||
|
|
||||||
FlipperClient client = AndroidFlipperClient.getInstance(this);
|
|
||||||
client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()));
|
|
||||||
client.addPlugin(new DatabasesFlipperPlugin(new FlipperSqlCipherAdapter(this)));
|
|
||||||
client.addPlugin(new SharedPreferencesFlipperPlugin(this));
|
|
||||||
client.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import com.facebook.flipper.android.AndroidFlipperClient
|
||||||
|
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||||
|
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||||
|
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
|
||||||
|
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
|
||||||
|
import com.facebook.soloader.SoLoader
|
||||||
|
import leakcanary.LeakCanary
|
||||||
|
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter
|
||||||
|
import shark.AndroidReferenceMatchers
|
||||||
|
|
||||||
|
class FlipperApplicationContext : ApplicationContext() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
SoLoader.init(this, false)
|
||||||
|
|
||||||
|
val client = AndroidFlipperClient.getInstance(this)
|
||||||
|
client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
||||||
|
client.addPlugin(DatabasesFlipperPlugin(FlipperSqlCipherAdapter(this)))
|
||||||
|
client.addPlugin(SharedPreferencesFlipperPlugin(this))
|
||||||
|
client.start()
|
||||||
|
|
||||||
|
LeakCanary.config = LeakCanary.config.copy(
|
||||||
|
referenceMatchers = AndroidReferenceMatchers.appDefaults +
|
||||||
|
AndroidReferenceMatchers.ignoredInstanceField(
|
||||||
|
className = "android.service.media.MediaBrowserService\$ServiceBinder",
|
||||||
|
fieldName = "this\$0"
|
||||||
|
) +
|
||||||
|
AndroidReferenceMatchers.ignoredInstanceField(
|
||||||
|
className = "androidx.media.MediaBrowserServiceCompat\$MediaBrowserServiceImplApi26\$MediaBrowserServiceApi26",
|
||||||
|
fieldName = "mBase"
|
||||||
|
) +
|
||||||
|
AndroidReferenceMatchers.ignoredInstanceField(
|
||||||
|
className = "android.support.v4.media.MediaBrowserCompat",
|
||||||
|
fieldName = "mImpl"
|
||||||
|
) +
|
||||||
|
AndroidReferenceMatchers.ignoredInstanceField(
|
||||||
|
className = "android.support.v4.media.session.MediaControllerCompat",
|
||||||
|
fieldName = "mToken"
|
||||||
|
) +
|
||||||
|
AndroidReferenceMatchers.ignoredInstanceField(
|
||||||
|
className = "android.support.v4.media.session.MediaControllerCompat",
|
||||||
|
fieldName = "mImpl"
|
||||||
|
) +
|
||||||
|
AndroidReferenceMatchers.ignoredInstanceField(
|
||||||
|
className = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService",
|
||||||
|
fieldName = "mApplication"
|
||||||
|
) +
|
||||||
|
AndroidReferenceMatchers.ignoredInstanceField(
|
||||||
|
className = "org.thoughtcrime.securesms.service.GenericForegroundService\$LocalBinder",
|
||||||
|
fieldName = "this\$0"
|
||||||
|
) +
|
||||||
|
AndroidReferenceMatchers.ignoredInstanceField(
|
||||||
|
className = "org.thoughtcrime.securesms.contacts.ContactsSyncAdapter",
|
||||||
|
fieldName = "mContext"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,12 +11,11 @@ import androidx.annotation.Nullable;
|
|||||||
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
|
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
|
||||||
import com.facebook.flipper.plugins.databases.DatabaseDriver;
|
import com.facebook.flipper.plugins.databases.DatabaseDriver;
|
||||||
|
|
||||||
import net.sqlcipher.DatabaseUtils;
|
import net.zetetic.database.DatabaseUtils;
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
import net.sqlcipher.database.SQLiteStatement;
|
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
|
||||||
import org.thoughtcrime.securesms.util.Hex;
|
import org.thoughtcrime.securesms.util.Hex;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
@@ -43,14 +42,11 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
|||||||
@Override
|
@Override
|
||||||
public List<Descriptor> getDatabases() {
|
public List<Descriptor> getDatabases() {
|
||||||
try {
|
try {
|
||||||
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
|
SignalDatabaseOpenHelper mainOpenHelper = Objects.requireNonNull(SignalDatabase.getInstance());
|
||||||
databaseHelperField.setAccessible(true);
|
SignalDatabaseOpenHelper keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
|
||||||
|
SignalDatabaseOpenHelper megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
|
||||||
SignalDatabase mainOpenHelper = Objects.requireNonNull((SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext())));
|
SignalDatabaseOpenHelper jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
|
||||||
SignalDatabase keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
|
SignalDatabaseOpenHelper metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
|
||||||
SignalDatabase megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
|
|
||||||
SignalDatabase jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
|
|
||||||
SignalDatabase metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
|
|
||||||
|
|
||||||
return Arrays.asList(new Descriptor(mainOpenHelper),
|
return Arrays.asList(new Descriptor(mainOpenHelper),
|
||||||
new Descriptor(keyValueOpenHelper),
|
new Descriptor(keyValueOpenHelper),
|
||||||
@@ -253,9 +249,9 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
|||||||
}
|
}
|
||||||
|
|
||||||
static class Descriptor implements DatabaseDescriptor {
|
static class Descriptor implements DatabaseDescriptor {
|
||||||
private final SignalDatabase sqlCipherOpenHelper;
|
private final SignalDatabaseOpenHelper sqlCipherOpenHelper;
|
||||||
|
|
||||||
Descriptor(@NonNull SignalDatabase sqlCipherOpenHelper) {
|
Descriptor(@NonNull SignalDatabaseOpenHelper sqlCipherOpenHelper) {
|
||||||
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
|
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/core_red_shade"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -101,6 +101,10 @@
|
|||||||
android:theme="@style/TextSecure.LightTheme"
|
android:theme="@style/TextSecure.LightTheme"
|
||||||
android:largeHeap="true">
|
android:largeHeap="true">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.wallet.api.enabled"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.geo.API_KEY"
|
android:name="com.google.android.geo.API_KEY"
|
||||||
android:value="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/>
|
android:value="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/>
|
||||||
@@ -395,7 +399,7 @@
|
|||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
|
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
|
||||||
android:theme="@style/Signal.DayNight.ConversationSettings"
|
android:theme="@style/Signal.DayNight.ConversationSettings"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden">
|
android:windowSoftInputMode="stateAlwaysHidden">
|
||||||
</activity>
|
</activity>
|
||||||
@@ -766,22 +770,6 @@
|
|||||||
|
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<provider android:name=".database.DatabaseContentProviders$Conversation"
|
|
||||||
android:authorities="${applicationId}.database.conversation"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<provider android:name=".database.DatabaseContentProviders$Attachment"
|
|
||||||
android:authorities="${applicationId}.database.attachment"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<provider android:name=".database.DatabaseContentProviders$Sticker"
|
|
||||||
android:authorities="${applicationId}.database.sticker"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<provider android:name=".database.DatabaseContentProviders$StickerPack"
|
|
||||||
android:authorities="${applicationId}.database.stickerpack"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<receiver android:name=".service.BootReceiver">
|
<receiver android:name=".service.BootReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||||
|
|||||||
@@ -37,10 +37,11 @@ import org.signal.core.util.tracing.Tracer;
|
|||||||
import org.signal.glide.SignalGlideCodecs;
|
import org.signal.glide.SignalGlideCodecs;
|
||||||
import org.signal.ringrtc.CallManager;
|
import org.signal.ringrtc.CallManager;
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||||
|
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
||||||
import org.thoughtcrime.securesms.database.LogDatabase;
|
import org.thoughtcrime.securesms.database.LogDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
|
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||||
@@ -51,9 +52,11 @@ import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
|||||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||||
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||||
@@ -61,8 +64,8 @@ import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
|
|||||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
|
||||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||||
@@ -76,14 +79,13 @@ import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
|||||||
import org.thoughtcrime.securesms.util.AppStartup;
|
import org.thoughtcrime.securesms.util.AppStartup;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
|
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
|
||||||
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
|
||||||
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
||||||
|
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
@@ -125,7 +127,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
|
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
|
||||||
.addBlocking("sqlcipher-init", () -> SqlCipherLibraryLoader.load())
|
.addBlocking("sqlcipher-init", () -> {
|
||||||
|
SqlCipherLibraryLoader.load();
|
||||||
|
SignalDatabase.init(this,
|
||||||
|
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
|
||||||
|
AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret());
|
||||||
|
})
|
||||||
.addBlocking("logging", () -> {
|
.addBlocking("logging", () -> {
|
||||||
initializeLogging();
|
initializeLogging();
|
||||||
Log.i(TAG, "onCreate()");
|
Log.i(TAG, "onCreate()");
|
||||||
@@ -161,7 +168,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
.addNonBlocking(this::cleanAvatarStorage)
|
.addNonBlocking(this::cleanAvatarStorage)
|
||||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||||
.addNonBlocking(this::initializeGcmCheck)
|
.addNonBlocking(this::initializeFcmCheck)
|
||||||
.addNonBlocking(this::initializeSignedPreKeyCheck)
|
.addNonBlocking(this::initializeSignedPreKeyCheck)
|
||||||
.addNonBlocking(this::initializePeriodicTasks)
|
.addNonBlocking(this::initializePeriodicTasks)
|
||||||
.addNonBlocking(this::initializeCircumvention)
|
.addNonBlocking(this::initializeCircumvention)
|
||||||
@@ -173,12 +180,13 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
||||||
.addNonBlocking(EmojiSource::refresh)
|
.addNonBlocking(EmojiSource::refresh)
|
||||||
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||||
|
.addNonBlocking(this::ensureProfileUploaded)
|
||||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||||
.addPostRender(this::initializeExpiringMessageManager)
|
.addPostRender(this::initializeExpiringMessageManager)
|
||||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||||
.addPostRender(() -> DatabaseFactory.getMessageLogDatabase(this).trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||||
@@ -191,8 +199,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
Log.i(TAG, "App is now visible.");
|
Log.i(TAG, "App is now visible.");
|
||||||
|
|
||||||
ApplicationDependencies.getFrameRateTracker().begin();
|
ApplicationDependencies.getFrameRateTracker().start();
|
||||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||||
|
ApplicationDependencies.getDeadlockDetector().start();
|
||||||
|
SubscriptionKeepAliveJob.launchSubscriberIdKeepAliveJobIfNecessary();
|
||||||
|
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
FeatureFlags.refreshIfNecessary();
|
FeatureFlags.refreshIfNecessary();
|
||||||
@@ -213,8 +223,9 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
Log.i(TAG, "App is no longer visible.");
|
Log.i(TAG, "App is no longer visible.");
|
||||||
KeyCachingService.onAppBackgrounded(this);
|
KeyCachingService.onAppBackgrounded(this);
|
||||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||||
ApplicationDependencies.getFrameRateTracker().end();
|
ApplicationDependencies.getFrameRateTracker().stop();
|
||||||
ApplicationDependencies.getShakeToReport().disable();
|
ApplicationDependencies.getShakeToReport().disable();
|
||||||
|
ApplicationDependencies.getDeadlockDetector().stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public PersistentLogger getPersistentLogger() {
|
public PersistentLogger getPersistentLogger() {
|
||||||
@@ -258,7 +269,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
|
|
||||||
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
|
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
|
||||||
|
|
||||||
SignalExecutors.UNBOUNDED.execute(() -> LogDatabase.getInstance(this).trimToSize());
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
|
Log.blockUntilAllWritesFinished();
|
||||||
|
LogDatabase.getInstance(this).trimToSize();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeCrashHandling() {
|
private void initializeCrashHandling() {
|
||||||
@@ -280,7 +294,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
|
|
||||||
private void initializeFirstEverAppLaunch() {
|
private void initializeFirstEverAppLaunch() {
|
||||||
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
|
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
|
||||||
if (!SQLCipherOpenHelper.databaseFileExists(this) || VersionTracker.getDaysSinceFirstInstalled(this) < 365) {
|
if (!SignalDatabase.databaseFileExists(this) || VersionTracker.getDaysSinceFirstInstalled(this) < 365) {
|
||||||
Log.i(TAG, "First ever app launch!");
|
Log.i(TAG, "First ever app launch!");
|
||||||
AppInitialization.onFirstEverAppLaunch(this);
|
AppInitialization.onFirstEverAppLaunch(this);
|
||||||
}
|
}
|
||||||
@@ -296,11 +310,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeGcmCheck() {
|
private void initializeFcmCheck() {
|
||||||
if (TextSecurePreferences.isPushRegistered(this)) {
|
if (SignalStore.account().isRegistered()) {
|
||||||
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6);
|
long nextSetTime = SignalStore.account().getFcmTokenLastSetTime() + TimeUnit.HOURS.toMillis(6);
|
||||||
|
|
||||||
if (TextSecurePreferences.getFcmToken(this) == null || nextSetTime <= System.currentTimeMillis()) {
|
if (SignalStore.account().getFcmToken() == null || nextSetTime <= System.currentTimeMillis()) {
|
||||||
ApplicationDependencies.getJobManager().add(new FcmRefreshJob());
|
ApplicationDependencies.getJobManager().add(new FcmRefreshJob());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,14 +352,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
|
|
||||||
private void initializeRingRtc() {
|
private void initializeRingRtc() {
|
||||||
try {
|
try {
|
||||||
if (RtcDeviceLists.hardwareAECBlocked()) {
|
|
||||||
WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!RtcDeviceLists.openSLESAllowed()) {
|
|
||||||
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
CallManager.initialize(this, new RingRtcLogger());
|
CallManager.initialize(this, new RingRtcLogger());
|
||||||
} catch (UnsatisfiedLinkError e) {
|
} catch (UnsatisfiedLinkError e) {
|
||||||
throw new AssertionError("Unable to load ringrtc library", e);
|
throw new AssertionError("Unable to load ringrtc library", e);
|
||||||
@@ -354,7 +360,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private void initializeCircumvention() {
|
private void initializeCircumvention() {
|
||||||
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
|
if (ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()) {
|
||||||
try {
|
try {
|
||||||
ProviderInstaller.installIfNeeded(ApplicationContext.this);
|
ProviderInstaller.installIfNeeded(ApplicationContext.this);
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
@@ -363,6 +369,13 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureProfileUploaded() {
|
||||||
|
if (SignalStore.account().isRegistered() && !SignalStore.registrationValues().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
|
||||||
|
Log.w(TAG, "User has a profile, but has not uploaded one. Uploading now.");
|
||||||
|
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void executePendingContactSync() {
|
private void executePendingContactSync() {
|
||||||
if (TextSecurePreferences.needsFullContactSync(this)) {
|
if (TextSecurePreferences.needsFullContactSync(this)) {
|
||||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
|
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
|
||||||
@@ -393,7 +406,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private void initializeCleanup() {
|
private void initializeCleanup() {
|
||||||
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
|
int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();
|
||||||
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
|||||||
// Intentionally Blank.
|
// Intentionally Blank.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void updateContactNameColor() {
|
||||||
|
// Intentionally Blank.
|
||||||
|
}
|
||||||
|
|
||||||
interface EventListener {
|
interface EventListener {
|
||||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import androidx.lifecycle.Lifecycle;
|
|||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ public final class BlockUnblockDialog {
|
|||||||
Resources resources = context.getResources();
|
Resources resources = context.getResources();
|
||||||
|
|
||||||
if (recipient.isGroup()) {
|
if (recipient.isGroup()) {
|
||||||
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
|
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
|
||||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_and_leave_s, recipient.getDisplayName(context)));
|
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_and_leave_s, recipient.getDisplayName(context)));
|
||||||
builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
|
builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
|
||||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_leave, ((dialog, which) -> onBlock.run()));
|
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_leave, ((dialog, which) -> onBlock.run()));
|
||||||
@@ -104,7 +104,7 @@ public final class BlockUnblockDialog {
|
|||||||
Resources resources = context.getResources();
|
Resources resources = context.getResources();
|
||||||
|
|
||||||
if (recipient.isGroup()) {
|
if (recipient.isGroup()) {
|
||||||
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
|
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
|
||||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||||
builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
|
builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
|
||||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import android.database.Cursor;
|
|||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -572,7 +571,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
|||||||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||||
|
|
||||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||||
return UsernameUtil.fetchUuidForUsername(requireContext(), contact.getNumber());
|
return UsernameUtil.fetchAciForUsername(requireContext(), contact.getNumber());
|
||||||
}, uuid -> {
|
}, uuid -> {
|
||||||
loadingDialog.dismiss();
|
loadingDialog.dismiss();
|
||||||
if (uuid.isPresent()) {
|
if (uuid.isPresent()) {
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||||||
Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext()));
|
Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext()));
|
||||||
|
|
||||||
TextSecurePreferences.setMultiDevice(DeviceActivity.this, true);
|
TextSecurePreferences.setMultiDevice(DeviceActivity.this, true);
|
||||||
TextSecurePreferences.setIsUnidentifiedDeliveryEnabled(context, false);
|
|
||||||
accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, profileKey, verificationCode);
|
accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, profileKey, verificationCode);
|
||||||
|
|
||||||
return SUCCESS;
|
return SUCCESS;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public final class GroupMembersDialog {
|
|||||||
.show();
|
.show();
|
||||||
|
|
||||||
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
|
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
|
||||||
|
memberListView.initializeAdapter(fragmentActivity);
|
||||||
|
|
||||||
LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId());
|
LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId());
|
||||||
LiveData<List<GroupMemberEntry.FullMember>> fullMembers = liveGroup.getFullMembers();
|
LiveData<List<GroupMemberEntry.FullMember>> fullMembers = liveGroup.getFullMembers();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import org.thoughtcrime.securesms.components.ContactFilterView;
|
|||||||
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
|
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
@@ -255,7 +255,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
|||||||
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
|
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
|
||||||
|
|
||||||
if (recipient.getContactUri() != null) {
|
if (recipient.getContactUri() != null) {
|
||||||
DatabaseFactory.getRecipientDatabase(context).setHasSentInvite(recipient.getId());
|
SignalDatabase.recipients().setHasSentInvite(recipient.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -568,26 +568,11 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
if (item == 0) {
|
if (item == 0) {
|
||||||
viewPagerListener.onPageSelected(0);
|
viewPagerListener.onPageSelected(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.registerContentObserver(new ContentObserver(new Handler(getMainLooper())) {
|
|
||||||
@Override
|
|
||||||
public void onChange(boolean selfChange) {
|
|
||||||
onMediaChange();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
mediaNotAvailable();
|
mediaNotAvailable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMediaChange() {
|
|
||||||
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
|
|
||||||
|
|
||||||
if (adapter != null) {
|
|
||||||
adapter.checkMedia(mediaPager.getCurrentItem());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) {
|
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) {
|
||||||
|
|
||||||
@@ -615,7 +600,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
if (adapter != null) {
|
if (adapter != null) {
|
||||||
MediaItem item = adapter.getMediaItemFor(position);
|
MediaItem item = adapter.getMediaItemFor(position);
|
||||||
if (item.recipient != null) item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar());
|
if (item != null && item.recipient != null) {
|
||||||
|
item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar());
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
|
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
|
||||||
initializeActionBar();
|
initializeActionBar();
|
||||||
}
|
}
|
||||||
@@ -628,7 +616,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
if (adapter != null) {
|
if (adapter != null) {
|
||||||
MediaItem item = adapter.getMediaItemFor(position);
|
MediaItem item = adapter.getMediaItemFor(position);
|
||||||
if (item.recipient != null) item.recipient.live().removeObservers(MediaPreviewActivity.this);
|
if (item != null && item.recipient != null) {
|
||||||
|
item.recipient.live().removeObservers(MediaPreviewActivity.this);
|
||||||
|
}
|
||||||
|
|
||||||
adapter.pause(position);
|
adapter.pause(position);
|
||||||
}
|
}
|
||||||
@@ -678,7 +668,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaItem getMediaItemFor(int position) {
|
public @Nullable MediaItem getMediaItemFor(int position) {
|
||||||
return new MediaItem(null, null, null, uri, mediaType, -1, true);
|
return new MediaItem(null, null, null, uri, mediaType, -1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,11 +691,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
public boolean hasFragmentFor(int position) {
|
public boolean hasFragmentFor(int position) {
|
||||||
return mediaPreviewFragment != null;
|
return mediaPreviewFragment != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkMedia(int currentItem) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) {
|
private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) {
|
||||||
@@ -789,8 +774,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
super.destroyItem(container, position, object);
|
super.destroyItem(container, position, object);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaItem getMediaItemFor(int position) {
|
public @Nullable MediaItem getMediaItemFor(int position) {
|
||||||
cursor.moveToPosition(getCursorPosition(position));
|
int cursorPosition = getCursorPosition(position);
|
||||||
|
|
||||||
|
if (cursor.isClosed() || cursorPosition < 0) {
|
||||||
|
Log.w(TAG, "Invalid cursor state! Closed: " + cursor.isClosed() + " Position: " + cursorPosition);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.moveToPosition(cursorPosition);
|
||||||
|
|
||||||
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
|
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
|
||||||
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
|
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
|
||||||
@@ -824,14 +816,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
return mediaFragments.containsKey(position);
|
return mediaFragments.containsKey(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkMedia(int position) {
|
|
||||||
MediaPreviewFragment fragment = mediaFragments.get(position);
|
|
||||||
if (fragment != null) {
|
|
||||||
fragment.checkMediaStillAvailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getCursorPosition(int position) {
|
private int getCursorPosition(int position) {
|
||||||
if (leftIsRecent) return position;
|
if (leftIsRecent) return position;
|
||||||
else return cursor.getCount() - 1 - position;
|
else return cursor.getCount() - 1 - position;
|
||||||
@@ -866,10 +850,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MediaItemAdapter {
|
interface MediaItemAdapter {
|
||||||
MediaItem getMediaItemFor(int position);
|
@Nullable MediaItem getMediaItemFor(int position);
|
||||||
void pause(int position);
|
void pause(int position);
|
||||||
@Nullable View getPlaybackControls(int position);
|
@Nullable View getPlaybackControls(int position);
|
||||||
boolean hasFragmentFor(int position);
|
boolean hasFragmentFor(int position);
|
||||||
void checkMedia(int currentItem);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,22 +33,19 @@ public class MuteDialog extends AlertDialog {
|
|||||||
public static void show(final Context context, final @NonNull MuteSelectionListener listener, @Nullable Runnable cancelListener) {
|
public static void show(final Context context, final @NonNull MuteSelectionListener listener, @Nullable Runnable cancelListener) {
|
||||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||||
builder.setTitle(R.string.MuteDialog_mute_notifications);
|
builder.setTitle(R.string.MuteDialog_mute_notifications);
|
||||||
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
|
builder.setItems(R.array.mute_durations, (dialog, which) -> {
|
||||||
@Override
|
final long muteUntil;
|
||||||
public void onClick(DialogInterface dialog, final int which) {
|
|
||||||
final long muteUntil;
|
|
||||||
|
|
||||||
switch (which) {
|
switch (which) {
|
||||||
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||||
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8); break;
|
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8); break;
|
||||||
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
|
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
|
||||||
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
|
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
|
||||||
case 4: muteUntil = Long.MAX_VALUE; break;
|
case 4: muteUntil = Long.MAX_VALUE; break;
|
||||||
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||||
}
|
|
||||||
|
|
||||||
listener.onMuted(muteUntil);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listener.onMuted(muteUntil);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cancelListener != null) {
|
if (cancelListener != null) {
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ import androidx.appcompat.app.AlertDialog;
|
|||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
@@ -67,7 +67,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
||||||
|
|
||||||
if (TextSecurePreferences.isPushRegistered(this) && NetworkConstraint.isMet(this)) {
|
if (SignalStore.account().isRegistered() && NetworkConstraint.isMet(this)) {
|
||||||
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
|
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
|
||||||
|
|
||||||
AlertDialog progress = SimpleProgressDialog.show(this);
|
AlertDialog progress = SimpleProgressDialog.show(this);
|
||||||
@@ -75,7 +75,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||||||
SimpleTask.run(getLifecycle(), () -> {
|
SimpleTask.run(getLifecycle(), () -> {
|
||||||
Recipient resolved = Recipient.external(this, number);
|
Recipient resolved = Recipient.external(this, number);
|
||||||
|
|
||||||
if (!resolved.isRegistered() || !resolved.hasUuid()) {
|
if (!resolved.isRegistered() || !resolved.hasAci()) {
|
||||||
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
||||||
try {
|
try {
|
||||||
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
|
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
|
||||||
@@ -103,7 +103,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void launch(Recipient recipient) {
|
private void launch(Recipient recipient) {
|
||||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
long existingThread = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
|
||||||
Intent intent = ConversationIntents.createBuilder(this, recipient.getId(), existingThread)
|
Intent intent = ConversationIntents.createBuilder(this, recipient.getId(), existingThread)
|
||||||
.withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT))
|
.withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT))
|
||||||
.withDataUri(getIntent().getData())
|
.withDataUri(getIntent().getData())
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BitmapTransformation which overlays the given bitmap with the given color.
|
||||||
|
*/
|
||||||
|
class OverlayTransformation(
|
||||||
|
@ColorInt private val color: Int
|
||||||
|
) : BitmapTransformation() {
|
||||||
|
|
||||||
|
private val id = "${OverlayTransformation::class.java.name}$color"
|
||||||
|
|
||||||
|
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||||
|
messageDigest.update(id.toByteArray(CHARSET))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
|
||||||
|
val outBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(outBitmap)
|
||||||
|
|
||||||
|
canvas.drawBitmap(toTransform, 0f, 0f, null)
|
||||||
|
canvas.drawColor(color)
|
||||||
|
|
||||||
|
return outBitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return (other as? OverlayTransformation)?.color == color
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return id.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
|||||||
protected final void onCreate(Bundle savedInstanceState) {
|
protected final void onCreate(Bundle savedInstanceState) {
|
||||||
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
|
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
|
||||||
AppStartup.getInstance().onCriticalRenderEventStart();
|
AppStartup.getInstance().onCriticalRenderEventStart();
|
||||||
this.networkAccess = new SignalServiceNetworkAccess(this);
|
this.networkAccess = ApplicationDependencies.getSignalServiceNetworkAccess();
|
||||||
onPreCreate();
|
onPreCreate();
|
||||||
|
|
||||||
final boolean locked = KeyCachingService.isLocked(this);
|
final boolean locked = KeyCachingService.isLocked(this);
|
||||||
@@ -84,7 +84,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
if (networkAccess.isCensored(this)) {
|
if (networkAccess.isCensored()) {
|
||||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob());
|
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device hardware capability lists.
|
|
||||||
* <p>
|
|
||||||
* Moved outside of ApplicationContext as the indirection was important for API19 support with desugaring: https://issuetracker.google.com/issues/183419297
|
|
||||||
*/
|
|
||||||
final class RtcDeviceLists {
|
|
||||||
|
|
||||||
private RtcDeviceLists() {}
|
|
||||||
|
|
||||||
static Set<String> hardwareAECBlockList() {
|
|
||||||
return new HashSet<String>() {{
|
|
||||||
add("Pixel");
|
|
||||||
add("Pixel XL");
|
|
||||||
add("Moto G5");
|
|
||||||
add("Moto G (5S) Plus");
|
|
||||||
add("Moto G4");
|
|
||||||
add("TA-1053");
|
|
||||||
add("Mi A1");
|
|
||||||
add("Mi A2");
|
|
||||||
add("E5823"); // Sony z5 compact
|
|
||||||
add("Redmi Note 5");
|
|
||||||
add("FP2"); // Fairphone FP2
|
|
||||||
add("MI 5");
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
static Set<String> openSlEsAllowList() {
|
|
||||||
return new HashSet<String>() {{
|
|
||||||
add("Pixel");
|
|
||||||
add("Pixel XL");
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean hardwareAECBlocked() {
|
|
||||||
return hardwareAECBlockList().contains(Build.MODEL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean openSLESAllowed() {
|
|
||||||
return openSlEsAllowList().contains(Build.MODEL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.Rfc5724Uri;
|
import org.thoughtcrime.securesms.util.Rfc5724Uri;
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ public class SmsSendtoActivity extends Activity {
|
|||||||
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
|
||||||
} else {
|
} else {
|
||||||
Recipient recipient = Recipient.external(this, destination.getDestination());
|
Recipient recipient = Recipient.external(this, destination.getDestination());
|
||||||
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
|
||||||
|
|
||||||
nextIntent = ConversationIntents.createBuilder(this, recipient.getId(), threadId)
|
nextIntent = ConversationIntents.createBuilder(this, recipient.getId(), threadId)
|
||||||
.withDraftText(destination.getBody())
|
.withDraftText(destination.getBody())
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.core.view.OneShotPreDrawListener;
|
import androidx.core.view.OneShotPreDrawListener;
|
||||||
@@ -64,6 +63,8 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
import org.signal.core.util.ThreadUtil;
|
import org.signal.core.util.ThreadUtil;
|
||||||
import org.signal.core.util.concurrent.SignalExecutors;
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
@@ -165,7 +166,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
|||||||
extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||||
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
|
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
|
||||||
extras.putParcelable(VerifyDisplayFragment.LOCAL_IDENTITY, new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this)));
|
extras.putParcelable(VerifyDisplayFragment.LOCAL_IDENTITY, new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this)));
|
||||||
extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, TextSecurePreferences.getLocalNumber(this));
|
extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, Recipient.self().requireE164());
|
||||||
extras.putBoolean(VerifyDisplayFragment.VERIFIED_STATE, getIntent().getBooleanExtra(VERIFIED_EXTRA, false));
|
extras.putBoolean(VerifyDisplayFragment.VERIFIED_STATE, getIntent().getBooleanExtra(VERIFIED_EXTRA, false));
|
||||||
|
|
||||||
scanFragment.setScanListener(this);
|
scanFragment.setScanListener(this);
|
||||||
@@ -322,23 +323,26 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
|||||||
//noinspection WrongThread
|
//noinspection WrongThread
|
||||||
Recipient resolved = recipient.resolve();
|
Recipient resolved = recipient.resolve();
|
||||||
|
|
||||||
if (FeatureFlags.verifyV2() && resolved.getUuid().isPresent()) {
|
if (FeatureFlags.verifyV2() && resolved.getAci().isPresent()) {
|
||||||
Log.i(TAG, "Using UUID (version 2).");
|
Log.i(TAG, "Using UUID (version 2).");
|
||||||
version = 2;
|
version = 2;
|
||||||
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
|
localId = Recipient.self().requireAci().toByteArray();
|
||||||
remoteId = UuidUtil.toByteArray(resolved.getUuid().get());
|
remoteId = resolved.requireAci().toByteArray();
|
||||||
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
|
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
|
||||||
Log.i(TAG, "Using E164 (version 1).");
|
Log.i(TAG, "Using E164 (version 1).");
|
||||||
version = 1;
|
version = 1;
|
||||||
localId = TextSecurePreferences.getLocalNumber(requireContext()).getBytes();
|
localId = Recipient.self().requireE164().getBytes();
|
||||||
remoteId = resolved.requireE164().getBytes();
|
remoteId = resolved.requireE164().getBytes();
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getUuid().isPresent(), resolved.getE164().isPresent()));
|
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getAci().isPresent(), resolved.getE164().isPresent()));
|
||||||
new AlertDialog.Builder(requireContext())
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
|
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
|
||||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish())
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish())
|
||||||
.setOnDismissListener(dialog -> requireActivity().finish())
|
.setOnDismissListener(dialog -> {
|
||||||
.show();
|
requireActivity().finish();
|
||||||
|
dialog.dismiss();
|
||||||
|
})
|
||||||
|
.show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -484,6 +484,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||||||
delayedFinish();
|
delayedFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleGlare(@NonNull Recipient recipient) {
|
||||||
|
Log.i(TAG, "handleGlare: " + recipient.getId());
|
||||||
|
|
||||||
|
callScreen.setStatus("");
|
||||||
|
}
|
||||||
|
|
||||||
private void handleCallRinging() {
|
private void handleCallRinging() {
|
||||||
callScreen.setStatus(getString(R.string.RedPhone_ringing));
|
callScreen.setStatus(getString(R.string.RedPhone_ringing));
|
||||||
}
|
}
|
||||||
@@ -629,6 +635,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||||||
handleCallRinging(); break;
|
handleCallRinging(); break;
|
||||||
case CALL_DISCONNECTED:
|
case CALL_DISCONNECTED:
|
||||||
handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
||||||
|
case CALL_DISCONNECTED_GLARE:
|
||||||
|
handleGlare(event.getRecipient()); break;
|
||||||
case CALL_ACCEPTED_ELSEWHERE:
|
case CALL_ACCEPTED_ELSEWHERE:
|
||||||
handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
|
handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
|
||||||
case CALL_DECLINED_ELSEWHERE:
|
case CALL_DECLINED_ELSEWHERE:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import org.signal.core.util.logging.Log;
|
|||||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||||
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
|
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
|
||||||
import org.thoughtcrime.securesms.media.MediaInput;
|
import org.thoughtcrime.securesms.media.MediaInput;
|
||||||
@@ -100,7 +100,7 @@ public final class AudioWaveForm {
|
|||||||
|
|
||||||
if (attachment instanceof DatabaseAttachment) {
|
if (attachment instanceof DatabaseAttachment) {
|
||||||
try {
|
try {
|
||||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
AttachmentDatabase attachmentDatabase = SignalDatabase.attachments();
|
||||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.avatar
|
package org.thoughtcrime.securesms.avatar
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import java.lang.IllegalStateException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class which encapsulates reading and writing Avatar objects to and from Bundles.
|
* Utility class which encapsulates reading and writing Avatar objects to and from Bundles.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.avatar
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.mediasend.Media
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
@@ -33,7 +33,7 @@ object AvatarPickerStorage {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun cleanOrphans(context: Context) {
|
fun cleanOrphans(context: Context) {
|
||||||
val avatarFiles = FileStorage.getAllFiles(context, DIRECTORY, FILENAME_BASE)
|
val avatarFiles = FileStorage.getAllFiles(context, DIRECTORY, FILENAME_BASE)
|
||||||
val database = DatabaseFactory.getAvatarPickerDatabase(context)
|
val database = SignalDatabase.avatarPicker
|
||||||
val photoAvatars = database
|
val photoAvatars = database
|
||||||
.getAllAvatars()
|
.getAllAvatars()
|
||||||
.filterIsInstance<Avatar.Photo>()
|
.filterIsInstance<Avatar.Photo>()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import org.signal.core.util.concurrent.SignalExecutors
|
|||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
|
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), Ima
|
|||||||
val inputStream = BlobProvider.getInstance().getStream(applicationContext, editedImageUri)
|
val inputStream = BlobProvider.getInstance().getStream(applicationContext, editedImageUri)
|
||||||
val onDiskUri = AvatarPickerStorage.save(applicationContext, inputStream)
|
val onDiskUri = AvatarPickerStorage.save(applicationContext, inputStream)
|
||||||
val photo = AvatarBundler.extractPhoto(args.photoAvatar)
|
val photo = AvatarBundler.extractPhoto(args.photoAvatar)
|
||||||
val database = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
val database = SignalDatabase.avatarPicker
|
||||||
val newPhoto = photo.copy(uri = onDiskUri, size = size)
|
val newPhoto = photo.copy(uri = onDiskUri, size = size)
|
||||||
|
|
||||||
database.update(newPhoto)
|
database.update(newPhoto)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
|||||||
import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
||||||
import org.thoughtcrime.securesms.avatar.Avatars
|
import org.thoughtcrime.securesms.avatar.Avatars
|
||||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.mediasend.Media
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||||
@@ -70,11 +70,11 @@ class AvatarPickerRepository(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getPersistedAvatarsForSelf(): Single<List<Avatar>> = Single.fromCallable {
|
fun getPersistedAvatarsForSelf(): Single<List<Avatar>> = Single.fromCallable {
|
||||||
DatabaseFactory.getAvatarPickerDatabase(applicationContext).getAvatarsForSelf()
|
SignalDatabase.avatarPicker.getAvatarsForSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPersistedAvatarsForGroup(groupId: GroupId): Single<List<Avatar>> = Single.fromCallable {
|
fun getPersistedAvatarsForGroup(groupId: GroupId): Single<List<Avatar>> = Single.fromCallable {
|
||||||
DatabaseFactory.getAvatarPickerDatabase(applicationContext).getAvatarsForGroup(groupId)
|
SignalDatabase.avatarPicker.getAvatarsForGroup(groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDefaultAvatarsForSelf(): Single<List<Avatar>> = Single.fromCallable {
|
fun getDefaultAvatarsForSelf(): Single<List<Avatar>> = Single.fromCallable {
|
||||||
@@ -97,7 +97,7 @@ class AvatarPickerRepository(context: Context) {
|
|||||||
|
|
||||||
fun persistAvatarForSelf(avatar: Avatar, onPersisted: (Avatar) -> Unit) {
|
fun persistAvatarForSelf(avatar: Avatar, onPersisted: (Avatar) -> Unit) {
|
||||||
SignalExecutors.BOUNDED.execute {
|
SignalExecutors.BOUNDED.execute {
|
||||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
val avatarDatabase = SignalDatabase.avatarPicker
|
||||||
val savedAvatar = avatarDatabase.saveAvatarForSelf(avatar)
|
val savedAvatar = avatarDatabase.saveAvatarForSelf(avatar)
|
||||||
avatarDatabase.markUsage(savedAvatar)
|
avatarDatabase.markUsage(savedAvatar)
|
||||||
onPersisted(savedAvatar)
|
onPersisted(savedAvatar)
|
||||||
@@ -106,7 +106,7 @@ class AvatarPickerRepository(context: Context) {
|
|||||||
|
|
||||||
fun persistAvatarForGroup(avatar: Avatar, groupId: GroupId, onPersisted: (Avatar) -> Unit) {
|
fun persistAvatarForGroup(avatar: Avatar, groupId: GroupId, onPersisted: (Avatar) -> Unit) {
|
||||||
SignalExecutors.BOUNDED.execute {
|
SignalExecutors.BOUNDED.execute {
|
||||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
val avatarDatabase = SignalDatabase.avatarPicker
|
||||||
val savedAvatar = avatarDatabase.saveAvatarForGroup(avatar, groupId)
|
val savedAvatar = avatarDatabase.saveAvatarForGroup(avatar, groupId)
|
||||||
avatarDatabase.markUsage(savedAvatar)
|
avatarDatabase.markUsage(savedAvatar)
|
||||||
onPersisted(savedAvatar)
|
onPersisted(savedAvatar)
|
||||||
@@ -180,7 +180,7 @@ class AvatarPickerRepository(context: Context) {
|
|||||||
fun delete(avatar: Avatar, onDelete: () -> Unit) {
|
fun delete(avatar: Avatar, onDelete: () -> Unit) {
|
||||||
SignalExecutors.BOUNDED.execute {
|
SignalExecutors.BOUNDED.execute {
|
||||||
if (avatar.databaseId is Avatar.DatabaseId.Saved) {
|
if (avatar.databaseId is Avatar.DatabaseId.Saved) {
|
||||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
val avatarDatabase = SignalDatabase.avatarPicker
|
||||||
avatarDatabase.deleteAvatar(avatar)
|
avatarDatabase.deleteAvatar(avatar)
|
||||||
}
|
}
|
||||||
onDelete()
|
onDelete()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import androidx.documentfile.provider.DocumentFile;
|
|||||||
import com.annimon.stream.function.Predicate;
|
import com.annimon.stream.function.Predicate;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.signal.core.util.Conversions;
|
import org.signal.core.util.Conversions;
|
||||||
@@ -43,7 +43,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|||||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||||
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
|
|
||||||
import org.thoughtcrime.securesms.util.SetUtil;
|
import org.thoughtcrime.securesms.util.SetUtil;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import android.util.Pair;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.signal.core.util.Conversions;
|
import org.signal.core.util.Conversions;
|
||||||
|
|||||||
@@ -1,75 +1,94 @@
|
|||||||
package org.thoughtcrime.securesms.badges
|
package org.thoughtcrime.securesms.badges
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import androidx.annotation.Px
|
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
import androidx.core.content.res.use
|
import androidx.core.content.res.use
|
||||||
import androidx.lifecycle.Lifecycle
|
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||||
import org.signal.core.util.logging.Log
|
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.badges.Badges.insetWithOutline
|
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil
|
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||||
import org.thoughtcrime.securesms.util.visible
|
import java.lang.IllegalArgumentException
|
||||||
|
|
||||||
private val TAG = Log.tag(BadgeImageView::class.java)
|
|
||||||
|
|
||||||
class BadgeImageView @JvmOverloads constructor(
|
class BadgeImageView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null
|
attrs: AttributeSet? = null
|
||||||
) : AppCompatImageView(context, attrs) {
|
) : AppCompatImageView(context, attrs) {
|
||||||
|
|
||||||
@Px
|
private var badgeSize: Int = 0
|
||||||
private var outlineWidth: Float = 0f
|
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
private var outlineColor: Int = Color.BLACK
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
context.obtainStyledAttributes(attrs, R.styleable.BadgeImageView).use {
|
context.obtainStyledAttributes(attrs, R.styleable.BadgeImageView).use {
|
||||||
outlineWidth = it.getDimension(R.styleable.BadgeImageView_badge_outline_width, 0f)
|
badgeSize = it.getInt(R.styleable.BadgeImageView_badge_size, 0)
|
||||||
outlineColor = it.getColor(R.styleable.BadgeImageView_badge_outline_color, Color.BLACK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isClickable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setOnClickListener(l: OnClickListener?) {
|
||||||
|
val wasClickable = isClickable
|
||||||
|
super.setOnClickListener(l)
|
||||||
|
this.isClickable = wasClickable
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBadgeFromRecipient(recipient: Recipient?) {
|
fun setBadgeFromRecipient(recipient: Recipient?) {
|
||||||
|
getGlideRequests()?.let {
|
||||||
|
setBadgeFromRecipient(recipient, it)
|
||||||
|
} ?: clearDrawable()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBadgeFromRecipient(recipient: Recipient?, glideRequests: GlideRequests) {
|
||||||
if (recipient == null || recipient.badges.isEmpty()) {
|
if (recipient == null || recipient.badges.isEmpty()) {
|
||||||
setBadge(null)
|
setBadge(null, glideRequests)
|
||||||
|
} else if (recipient.isSelf) {
|
||||||
|
val badge = recipient.featuredBadge
|
||||||
|
if (badge == null || !badge.visible || badge.isExpired()) {
|
||||||
|
setBadge(null, glideRequests)
|
||||||
|
} else {
|
||||||
|
setBadge(badge, glideRequests)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setBadge(recipient.badges[0])
|
setBadge(recipient.featuredBadge, glideRequests)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBadge(badge: Badge?) {
|
fun setBadge(badge: Badge?) {
|
||||||
visible = badge != null
|
getGlideRequests()?.let {
|
||||||
|
setBadge(badge, it)
|
||||||
val lifecycle = ViewUtil.getActivityLifecycle(this)
|
} ?: clearDrawable()
|
||||||
if (lifecycle?.currentState == Lifecycle.State.DESTROYED) {
|
|
||||||
Log.w(TAG, "Ignoring setBadge call for destroyed activity.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
GlideApp
|
|
||||||
.with(this)
|
|
||||||
.load(badge)
|
|
||||||
.into(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setImageDrawable(drawable: Drawable?) {
|
fun setBadge(badge: Badge?, glideRequests: GlideRequests) {
|
||||||
if (drawable == null || outlineWidth == 0f) {
|
if (badge != null) {
|
||||||
super.setImageDrawable(drawable)
|
glideRequests
|
||||||
|
.load(badge)
|
||||||
|
.downsample(DownsampleStrategy.NONE)
|
||||||
|
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), badge.imageDensity, ThemeUtil.isDarkTheme(context)))
|
||||||
|
.into(this)
|
||||||
|
|
||||||
|
isClickable = true
|
||||||
} else {
|
} else {
|
||||||
super.setImageDrawable(
|
glideRequests
|
||||||
drawable.insetWithOutline(
|
.clear(this)
|
||||||
outlineWidth, outlineColor
|
clearDrawable()
|
||||||
)
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
private fun clearDrawable() {
|
||||||
|
setImageDrawable(null)
|
||||||
|
isClickable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getGlideRequests(): GlideRequests? {
|
||||||
|
return try {
|
||||||
|
GlideApp.with(this)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// View not attached to an activity or activity destroyed
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,29 +4,38 @@ import android.content.Context
|
|||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
import org.thoughtcrime.securesms.util.ProfileUtil
|
import org.thoughtcrime.securesms.util.ProfileUtil
|
||||||
|
|
||||||
class BadgeRepository(context: Context) {
|
class BadgeRepository(context: Context) {
|
||||||
|
|
||||||
private val context = context.applicationContext
|
private val context = context.applicationContext
|
||||||
|
|
||||||
fun setVisibilityForAllBadges(displayBadgesOnProfile: Boolean): Completable = Completable.fromAction {
|
fun setVisibilityForAllBadges(
|
||||||
val badges = Recipient.self().badges.map { it.copy(visible = displayBadgesOnProfile) }
|
displayBadgesOnProfile: Boolean,
|
||||||
ProfileUtil.uploadProfileWithBadges(context, badges)
|
selfBadges: List<Badge> = Recipient.self().badges
|
||||||
|
): Completable = Completable.fromAction {
|
||||||
|
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
|
||||||
|
val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) }
|
||||||
|
|
||||||
|
ProfileUtil.uploadProfileWithBadges(context, badges)
|
||||||
|
SignalStore.donationsValues().setDisplayBadgesOnProfile(displayBadgesOnProfile)
|
||||||
|
recipientDatabase.markNeedsSync(Recipient.self().id)
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
|
|
||||||
val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
|
||||||
recipientDatabase.setBadges(Recipient.self().id, badges)
|
recipientDatabase.setBadges(Recipient.self().id, badges)
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
|
|
||||||
fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction {
|
fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction {
|
||||||
val badges = Recipient.self().badges
|
val badges = Recipient.self().badges
|
||||||
val reOrderedBadges = listOf(featuredBadge) + (badges - featuredBadge)
|
val reOrderedBadges = listOf(featuredBadge.copy(visible = true)) + (badges.filterNot { it.id == featuredBadge.id })
|
||||||
ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges)
|
ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges)
|
||||||
|
|
||||||
val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
|
||||||
recipientDatabase.setBadges(Recipient.self().id, reOrderedBadges)
|
recipientDatabase.setBadges(Recipient.self().id, reOrderedBadges)
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +1,46 @@
|
|||||||
package org.thoughtcrime.securesms.badges
|
package org.thoughtcrime.securesms.badges
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.net.Uri
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import androidx.annotation.Px
|
|
||||||
import androidx.core.graphics.withScale
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.lottie.SimpleColorFilter
|
|
||||||
import com.google.android.flexbox.AlignItems
|
import com.google.android.flexbox.AlignItems
|
||||||
import com.google.android.flexbox.FlexDirection
|
import com.google.android.flexbox.FlexDirection
|
||||||
import com.google.android.flexbox.FlexboxLayoutManager
|
import com.google.android.flexbox.FlexboxLayoutManager
|
||||||
import com.google.android.flexbox.JustifyContent
|
import com.google.android.flexbox.JustifyContent
|
||||||
|
import org.signal.core.util.DimensionUnit
|
||||||
|
import org.thoughtcrime.securesms.BuildConfig
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.badges.models.BadgeAnimator
|
import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
import org.thoughtcrime.securesms.util.customizeOnDraw
|
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
|
||||||
|
|
||||||
object Badges {
|
object Badges {
|
||||||
fun Drawable.insetWithOutline(
|
fun DSLConfiguration.displayBadges(
|
||||||
@Px outlineWidth: Float,
|
context: Context,
|
||||||
@ColorInt outlineColor: Int
|
badges: List<Badge>,
|
||||||
): Drawable {
|
selectedBadge: Badge? = null,
|
||||||
val clone = mutate().constantState?.newDrawable()?.mutate()
|
fadedBadgeId: String? = null
|
||||||
clone?.colorFilter = SimpleColorFilter(outlineColor)
|
) {
|
||||||
|
|
||||||
return customizeOnDraw { wrapped, canvas ->
|
|
||||||
clone?.bounds = wrapped.bounds
|
|
||||||
clone?.draw(canvas)
|
|
||||||
|
|
||||||
val scale = 1 - ((outlineWidth * 2) / canvas.width)
|
|
||||||
|
|
||||||
canvas.withScale(x = scale, y = scale, canvas.width / 2f, canvas.height / 2f) {
|
|
||||||
wrapped.draw(canvas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Drawable.selectable(
|
|
||||||
@Px outlineWidth: Float,
|
|
||||||
@ColorInt outlineColor: Int,
|
|
||||||
@ColorInt gapColor: Int,
|
|
||||||
animator: BadgeAnimator
|
|
||||||
): Drawable {
|
|
||||||
val outline = mutate().constantState?.newDrawable()?.mutate()
|
|
||||||
outline?.colorFilter = SimpleColorFilter(outlineColor)
|
|
||||||
|
|
||||||
val gap = mutate().constantState?.newDrawable()?.mutate()
|
|
||||||
gap?.colorFilter = SimpleColorFilter(gapColor)
|
|
||||||
|
|
||||||
return customizeOnDraw { wrapped, canvas ->
|
|
||||||
outline?.bounds = wrapped.bounds
|
|
||||||
gap?.bounds = wrapped.bounds
|
|
||||||
|
|
||||||
outline?.draw(canvas)
|
|
||||||
|
|
||||||
val scale = 1 - ((outlineWidth * 2) / wrapped.bounds.width())
|
|
||||||
val interpolatedScale = scale + (1f - scale) * animator.getFraction()
|
|
||||||
|
|
||||||
canvas.withScale(x = interpolatedScale, y = interpolatedScale, wrapped.bounds.width() / 2f, wrapped.bounds.height() / 2f) {
|
|
||||||
gap?.draw(canvas)
|
|
||||||
|
|
||||||
canvas.withScale(x = interpolatedScale, y = interpolatedScale, wrapped.bounds.width() / 2f, wrapped.bounds.height() / 2f) {
|
|
||||||
wrapped.draw(canvas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (animator.shouldInvalidate()) {
|
|
||||||
invalidateSelf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun DSLConfiguration.displayBadges(badges: List<Badge>, selectedBadge: Badge? = null) {
|
|
||||||
badges
|
badges
|
||||||
.map { Badge.Model(it, it == selectedBadge) }
|
.map {
|
||||||
|
Badge.Model(
|
||||||
|
badge = it,
|
||||||
|
isSelected = it == selectedBadge,
|
||||||
|
isFaded = it.id == fadedBadgeId
|
||||||
|
)
|
||||||
|
}
|
||||||
.forEach { customPref(it) }
|
.forEach { customPref(it) }
|
||||||
|
|
||||||
val empties = (4 - (badges.size % 4)) % 4
|
val badgeSize = DimensionUnit.DP.toPixels(88f)
|
||||||
|
val windowWidth = context.resources.displayMetrics.widthPixels
|
||||||
|
val perRow = (windowWidth / badgeSize).toInt()
|
||||||
|
|
||||||
|
val empties = ((perRow - (badges.size % perRow)) % perRow)
|
||||||
repeat(empties) {
|
repeat(empties) {
|
||||||
customPref(Badge.EmptyModel())
|
customPref(Badge.EmptyModel())
|
||||||
}
|
}
|
||||||
@@ -91,4 +55,68 @@ object Badges {
|
|||||||
|
|
||||||
return layoutManager
|
return layoutManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getBadgeImageUri(densityPath: String): Uri {
|
||||||
|
return Uri.parse(BuildConfig.BADGE_STATIC_ROOT).buildUpon()
|
||||||
|
.appendPath(densityPath)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBestBadgeImageUriForDevice(serviceBadge: SignalServiceProfile.Badge): Pair<Uri, String> {
|
||||||
|
return when (ScreenDensity.getBestDensityBucketForDevice()) {
|
||||||
|
"ldpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[0]), "ldpi")
|
||||||
|
"mdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[1]), "mdpi")
|
||||||
|
"hdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[2]), "hdpi")
|
||||||
|
"xxhdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[4]), "xxhdpi")
|
||||||
|
"xxxhdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[5]), "xxxhdpi")
|
||||||
|
else -> Pair(getBadgeImageUri(serviceBadge.sprites6[3]), "xhdpi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTimestamp(bigDecimal: BigDecimal): Long {
|
||||||
|
return Timestamp(bigDecimal.toLong() * 1000).time
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun fromDatabaseBadge(badge: BadgeList.Badge): Badge {
|
||||||
|
return Badge(
|
||||||
|
badge.id,
|
||||||
|
fromCode(badge.category),
|
||||||
|
badge.name,
|
||||||
|
badge.description,
|
||||||
|
Uri.parse(badge.imageUrl),
|
||||||
|
badge.imageDensity,
|
||||||
|
badge.expiration,
|
||||||
|
badge.visible
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun toDatabaseBadge(badge: Badge): BadgeList.Badge {
|
||||||
|
return BadgeList.Badge.newBuilder()
|
||||||
|
.setId(badge.id)
|
||||||
|
.setCategory(badge.category.code)
|
||||||
|
.setDescription(badge.description)
|
||||||
|
.setExpiration(badge.expirationTimestamp)
|
||||||
|
.setVisible(badge.visible)
|
||||||
|
.setName(badge.name)
|
||||||
|
.setImageUrl(badge.imageUrl.toString())
|
||||||
|
.setImageDensity(badge.imageDensity)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun fromServiceBadge(serviceBadge: SignalServiceProfile.Badge): Badge {
|
||||||
|
val uriAndDensity: Pair<Uri, String> = getBestBadgeImageUriForDevice(serviceBadge)
|
||||||
|
return Badge(
|
||||||
|
serviceBadge.id,
|
||||||
|
fromCode(serviceBadge.category),
|
||||||
|
serviceBadge.name,
|
||||||
|
serviceBadge.description,
|
||||||
|
uriAndDensity.first(),
|
||||||
|
uriAndDensity.second(),
|
||||||
|
serviceBadge.expiration?.let { getTimestamp(it) } ?: 0,
|
||||||
|
serviceBadge.isVisible
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package org.thoughtcrime.securesms.badges.glide
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cuts out the badge of the requested size from the sprite sheet.
|
||||||
|
*/
|
||||||
|
class BadgeSpriteTransformation(
|
||||||
|
private val size: Size,
|
||||||
|
private val density: String,
|
||||||
|
private val isDarkTheme: Boolean
|
||||||
|
) : BitmapTransformation() {
|
||||||
|
|
||||||
|
private val id = "BadgeSpriteTransformation(${size.code},$density,$isDarkTheme).$VERSION"
|
||||||
|
|
||||||
|
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||||
|
messageDigest.update(id.toByteArray(CHARSET))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return (other as? BadgeSpriteTransformation)?.id == id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return id.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
|
||||||
|
val outBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(outBitmap)
|
||||||
|
val inBounds = getInBounds(density, size, isDarkTheme)
|
||||||
|
val outBounds = Rect(0, 0, outWidth, outHeight)
|
||||||
|
|
||||||
|
canvas.drawBitmap(toTransform, inBounds, outBounds, null)
|
||||||
|
|
||||||
|
return outBitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Size(val code: String, val frameMap: Map<Density, FrameSet>) {
|
||||||
|
SMALL(
|
||||||
|
"small",
|
||||||
|
mapOf(
|
||||||
|
Density.LDPI to FrameSet(Frame(124, 1, 12, 12), Frame(145, 31, 12, 12)),
|
||||||
|
Density.MDPI to FrameSet(Frame(163, 1, 16, 16), Frame(189, 39, 16, 16)),
|
||||||
|
Density.HDPI to FrameSet(Frame(244, 1, 24, 24), Frame(283, 58, 24, 24)),
|
||||||
|
Density.XHDPI to FrameSet(Frame(323, 1, 32, 32), Frame(373, 75, 32, 32)),
|
||||||
|
Density.XXHDPI to FrameSet(Frame(483, 1, 48, 48), Frame(557, 111, 48, 48)),
|
||||||
|
Density.XXXHDPI to FrameSet(Frame(643, 1, 64, 64), Frame(741, 147, 64, 64))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
MEDIUM(
|
||||||
|
"medium",
|
||||||
|
mapOf(
|
||||||
|
Density.LDPI to FrameSet(Frame(124, 16, 18, 18), Frame(160, 31, 18, 18)),
|
||||||
|
Density.MDPI to FrameSet(Frame(163, 19, 24, 24), Frame(207, 39, 24, 24)),
|
||||||
|
Density.HDPI to FrameSet(Frame(244, 28, 36, 36), Frame(310, 58, 36, 36)),
|
||||||
|
Density.XHDPI to FrameSet(Frame(323, 35, 48, 48), Frame(407, 75, 48, 48)),
|
||||||
|
Density.XXHDPI to FrameSet(Frame(483, 51, 72, 72), Frame(607, 111, 72, 72)),
|
||||||
|
Density.XXXHDPI to FrameSet(Frame(643, 67, 96, 96), Frame(807, 147, 96, 96))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
LARGE(
|
||||||
|
"large",
|
||||||
|
mapOf(
|
||||||
|
Density.LDPI to FrameSet(Frame(145, 1, 27, 27), Frame(124, 46, 27, 27)),
|
||||||
|
Density.MDPI to FrameSet(Frame(189, 1, 36, 36), Frame(163, 57, 36, 36)),
|
||||||
|
Density.HDPI to FrameSet(Frame(283, 1, 54, 54), Frame(244, 85, 54, 54)),
|
||||||
|
Density.XHDPI to FrameSet(Frame(373, 1, 72, 72), Frame(323, 109, 72, 72)),
|
||||||
|
Density.XXHDPI to FrameSet(Frame(557, 1, 108, 108), Frame(483, 161, 108, 108)),
|
||||||
|
Density.XXXHDPI to FrameSet(Frame(741, 1, 144, 144), Frame(643, 213, 144, 144))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
BADGE_64(
|
||||||
|
"badge_64",
|
||||||
|
mapOf(
|
||||||
|
Density.LDPI to FrameSet(Frame(124, 73, 48, 48), Frame(124, 73, 48, 48)),
|
||||||
|
Density.MDPI to FrameSet(Frame(163, 97, 64, 64), Frame(163, 97, 64, 64)),
|
||||||
|
Density.HDPI to FrameSet(Frame(244, 145, 96, 96), Frame(244, 145, 96, 96)),
|
||||||
|
Density.XHDPI to FrameSet(Frame(323, 193, 128, 128), Frame(323, 193, 128, 128)),
|
||||||
|
Density.XXHDPI to FrameSet(Frame(483, 289, 192, 192), Frame(483, 289, 192, 192)),
|
||||||
|
Density.XXXHDPI to FrameSet(Frame(643, 385, 256, 256), Frame(643, 385, 256, 256))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
BADGE_112(
|
||||||
|
"badge_112",
|
||||||
|
mapOf(
|
||||||
|
Density.LDPI to FrameSet(Frame(181, 1, 84, 84), Frame(181, 1, 84, 84)),
|
||||||
|
Density.MDPI to FrameSet(Frame(233, 1, 112, 112), Frame(233, 1, 112, 112)),
|
||||||
|
Density.HDPI to FrameSet(Frame(349, 1, 168, 168), Frame(349, 1, 168, 168)),
|
||||||
|
Density.XHDPI to FrameSet(Frame(457, 1, 224, 224), Frame(457, 1, 224, 224)),
|
||||||
|
Density.XXHDPI to FrameSet(Frame(681, 1, 336, 336), Frame(681, 1, 336, 336)),
|
||||||
|
Density.XXXHDPI to FrameSet(Frame(905, 1, 448, 448), Frame(905, 1, 448, 448))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
XLARGE(
|
||||||
|
"xlarge",
|
||||||
|
mapOf(
|
||||||
|
Density.LDPI to FrameSet(Frame(1, 1, 120, 120), Frame(1, 1, 120, 120)),
|
||||||
|
Density.MDPI to FrameSet(Frame(1, 1, 160, 160), Frame(1, 1, 160, 160)),
|
||||||
|
Density.HDPI to FrameSet(Frame(1, 1, 240, 240), Frame(1, 1, 240, 240)),
|
||||||
|
Density.XHDPI to FrameSet(Frame(1, 1, 320, 320), Frame(1, 1, 320, 320)),
|
||||||
|
Density.XXHDPI to FrameSet(Frame(1, 1, 480, 480), Frame(1, 1, 480, 480)),
|
||||||
|
Density.XXXHDPI to FrameSet(Frame(1, 1, 640, 640), Frame(1, 1, 640, 640))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromInteger(integer: Int): Size {
|
||||||
|
return when (integer) {
|
||||||
|
0 -> SMALL
|
||||||
|
1 -> MEDIUM
|
||||||
|
2 -> LARGE
|
||||||
|
3 -> XLARGE
|
||||||
|
4 -> BADGE_64
|
||||||
|
5 -> BADGE_112
|
||||||
|
else -> LARGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Density(val density: String) {
|
||||||
|
LDPI("ldpi"),
|
||||||
|
MDPI("mdpi"),
|
||||||
|
HDPI("hdpi"),
|
||||||
|
XHDPI("xhdpi"),
|
||||||
|
XXHDPI("xxhdpi"),
|
||||||
|
XXXHDPI("xxxhdpi")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FrameSet(val light: Frame, val dark: Frame)
|
||||||
|
|
||||||
|
data class Frame(
|
||||||
|
val x: Int,
|
||||||
|
val y: Int,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int
|
||||||
|
) {
|
||||||
|
fun toBounds(): Rect {
|
||||||
|
return Rect(x, y, x + width, y + height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val VERSION = 3
|
||||||
|
|
||||||
|
private fun getDensity(density: String): Density {
|
||||||
|
return Density.values().first { it.density == density }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFrame(size: Size, density: Density, isDarkTheme: Boolean): Frame {
|
||||||
|
val frameSet: FrameSet = size.frameMap[density]!!
|
||||||
|
return if (isDarkTheme) frameSet.dark else frameSet.light
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInBounds(density: String, size: Size, isDarkTheme: Boolean): Rect {
|
||||||
|
return getFrame(size, getDensity(density), isDarkTheme).toBounds()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,66 +1,48 @@
|
|||||||
package org.thoughtcrime.securesms.badges.models
|
package org.thoughtcrime.securesms.badges.models
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.animation.ObjectAnimator
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.bumptech.glide.load.Key
|
import com.bumptech.glide.load.Key
|
||||||
import com.bumptech.glide.request.target.CustomViewTarget
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||||
import org.signal.core.util.DimensionUnit
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.badges.Badges.selectable
|
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
|
||||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
typealias OnBadgeClicked = (Badge, Boolean) -> Unit
|
typealias OnBadgeClicked = (Badge, Boolean, Boolean) -> Unit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Badge that can be collected and displayed by a user.
|
* A Badge that can be collected and displayed by a user.
|
||||||
*/
|
*/
|
||||||
|
@Parcelize
|
||||||
data class Badge(
|
data class Badge(
|
||||||
val id: String,
|
val id: String,
|
||||||
val category: Category,
|
val category: Category,
|
||||||
val imageUrl: Uri,
|
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
|
val imageUrl: Uri,
|
||||||
|
val imageDensity: String,
|
||||||
val expirationTimestamp: Long,
|
val expirationTimestamp: Long,
|
||||||
val visible: Boolean
|
val visible: Boolean,
|
||||||
) : Parcelable, Key {
|
) : Parcelable, Key {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
fun isExpired(): Boolean = expirationTimestamp < System.currentTimeMillis() && expirationTimestamp > 0
|
||||||
requireNotNull(parcel.readString()),
|
fun isBoost(): Boolean = id == BOOST_BADGE_ID
|
||||||
Category.fromCode(requireNotNull(parcel.readString())),
|
|
||||||
requireNotNull(parcel.readParcelable(Uri::class.java.classLoader)),
|
|
||||||
requireNotNull(parcel.readString()),
|
|
||||||
requireNotNull(parcel.readString()),
|
|
||||||
parcel.readLong(),
|
|
||||||
parcel.readByte() == 1.toByte()
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
|
||||||
parcel.writeString(id)
|
|
||||||
parcel.writeString(category.code)
|
|
||||||
parcel.writeParcelable(imageUrl, flags)
|
|
||||||
parcel.writeString(name)
|
|
||||||
parcel.writeString(description)
|
|
||||||
parcel.writeLong(expirationTimestamp)
|
|
||||||
parcel.writeByte(if (visible) 1 else 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||||
messageDigest.update(id.toByteArray(Key.CHARSET))
|
messageDigest.update(id.toByteArray(Key.CHARSET))
|
||||||
|
messageDigest.update(imageUrl.toString().toByteArray(Key.CHARSET))
|
||||||
|
messageDigest.update(imageDensity.toByteArray(Key.CHARSET))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolveDescription(shortName: String): String {
|
fun resolveDescription(shortName: String): String {
|
||||||
@@ -89,14 +71,18 @@ data class Badge(
|
|||||||
|
|
||||||
class Model(
|
class Model(
|
||||||
val badge: Badge,
|
val badge: Badge,
|
||||||
val isSelected: Boolean = false
|
val isSelected: Boolean = false,
|
||||||
|
val isFaded: Boolean = false
|
||||||
) : PreferenceModel<Model>() {
|
) : PreferenceModel<Model>() {
|
||||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||||
return newItem.badge.id == badge.id
|
return newItem.badge.id == badge.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||||
return super.areContentsTheSame(newItem) && badge == newItem.badge && isSelected == newItem.isSelected
|
return super.areContentsTheSame(newItem) &&
|
||||||
|
badge == newItem.badge &&
|
||||||
|
isSelected == newItem.isSelected &&
|
||||||
|
isFaded == newItem.isFaded
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChangePayload(newItem: Model): Any? {
|
override fun getChangePayload(newItem: Model): Any? {
|
||||||
@@ -110,32 +96,47 @@ data class Badge(
|
|||||||
|
|
||||||
class ViewHolder(itemView: View, private val onBadgeClicked: OnBadgeClicked) : MappingViewHolder<Model>(itemView) {
|
class ViewHolder(itemView: View, private val onBadgeClicked: OnBadgeClicked) : MappingViewHolder<Model>(itemView) {
|
||||||
|
|
||||||
|
private val check: ImageView = itemView.findViewById(R.id.checkmark)
|
||||||
private val badge: ImageView = itemView.findViewById(R.id.badge)
|
private val badge: ImageView = itemView.findViewById(R.id.badge)
|
||||||
private val name: TextView = itemView.findViewById(R.id.name)
|
private val name: TextView = itemView.findViewById(R.id.name)
|
||||||
private val target = Target(badge)
|
|
||||||
|
private var checkAnimator: ObjectAnimator? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
check.isSelected = true
|
||||||
|
}
|
||||||
|
|
||||||
override fun bind(model: Model) {
|
override fun bind(model: Model) {
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
onBadgeClicked(model.badge, model.isSelected)
|
onBadgeClicked(model.badge, model.isSelected, model.isFaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkAnimator?.cancel()
|
||||||
if (payload.isNotEmpty()) {
|
if (payload.isNotEmpty()) {
|
||||||
if (model.isSelected) {
|
checkAnimator = if (model.isSelected) {
|
||||||
target.animateToStart()
|
ObjectAnimator.ofFloat(check, "alpha", 1f)
|
||||||
} else {
|
} else {
|
||||||
target.animateToEnd()
|
ObjectAnimator.ofFloat(check, "alpha", 0f)
|
||||||
}
|
}
|
||||||
|
checkAnimator?.start()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
badge.alpha = if (model.badge.isExpired() || model.isFaded) 0.5f else 1f
|
||||||
|
|
||||||
GlideApp.with(badge)
|
GlideApp.with(badge)
|
||||||
.load(model.badge)
|
.load(model.badge)
|
||||||
.into(target)
|
.downsample(DownsampleStrategy.NONE)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.transform(
|
||||||
|
BadgeSpriteTransformation(BadgeSpriteTransformation.Size.BADGE_64, model.badge.imageDensity, ThemeUtil.isDarkTheme(context)),
|
||||||
|
)
|
||||||
|
.into(badge)
|
||||||
|
|
||||||
if (model.isSelected) {
|
if (model.isSelected) {
|
||||||
target.setAnimationToStart()
|
check.alpha = 1f
|
||||||
} else {
|
} else {
|
||||||
target.setAnimationToEnd()
|
check.alpha = 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
name.text = model.badge.name
|
name.text = model.badge.name
|
||||||
@@ -158,61 +159,11 @@ data class Badge(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Target(view: ImageView) : CustomViewTarget<ImageView, Drawable>(view) {
|
companion object {
|
||||||
|
const val BOOST_BADGE_ID = "BOOST"
|
||||||
|
|
||||||
private val animator: BadgeAnimator = BadgeAnimator()
|
|
||||||
|
|
||||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
|
||||||
view.setImageDrawable(errorDrawable)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
|
||||||
val drawable = resource.selectable(
|
|
||||||
DimensionUnit.DP.toPixels(2.5f),
|
|
||||||
ContextCompat.getColor(view.context, R.color.signal_inverse_primary),
|
|
||||||
ContextCompat.getColor(view.context, R.color.signal_background_primary),
|
|
||||||
animator
|
|
||||||
)
|
|
||||||
|
|
||||||
view.setImageDrawable(drawable)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceCleared(placeholder: Drawable?) {
|
|
||||||
view.setImageDrawable(placeholder)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAnimationToStart() {
|
|
||||||
animator.setState(BadgeAnimator.State.START)
|
|
||||||
view.drawable?.invalidateSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAnimationToEnd() {
|
|
||||||
animator.setState(BadgeAnimator.State.END)
|
|
||||||
view.drawable?.invalidateSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun animateToStart() {
|
|
||||||
animator.setState(BadgeAnimator.State.REVERSE)
|
|
||||||
view.drawable?.invalidateSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun animateToEnd() {
|
|
||||||
animator.setState(BadgeAnimator.State.FORWARD)
|
|
||||||
view.drawable?.invalidateSelf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<Badge> {
|
|
||||||
private val SELECTION_CHANGED = Any()
|
private val SELECTION_CHANGED = Any()
|
||||||
|
|
||||||
override fun createFromParcel(parcel: Parcel): Badge {
|
|
||||||
return Badge(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<Badge?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) {
|
fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) {
|
||||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onBadgeClicked) }, R.layout.badge_preference_view))
|
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onBadgeClicked) }, R.layout.badge_preference_view))
|
||||||
mappingAdapter.registerFactory(EmptyModel::class.java, MappingAdapter.LayoutFactory({ EmptyViewHolder(it) }, R.layout.badge_preference_view))
|
mappingAdapter.registerFactory(EmptyModel::class.java, MappingAdapter.LayoutFactory({ EmptyViewHolder(it) }, R.layout.badge_preference_view))
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.badges.models
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.util.Util
|
|
||||||
|
|
||||||
class BadgeAnimator {
|
|
||||||
|
|
||||||
val duration = 250L
|
|
||||||
|
|
||||||
var state: State = State.START
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var startTime: Long = 0L
|
|
||||||
|
|
||||||
fun getFraction(): Float {
|
|
||||||
return when (state) {
|
|
||||||
State.START -> 0f
|
|
||||||
State.END -> 1f
|
|
||||||
State.FORWARD -> Util.clamp((System.currentTimeMillis() - startTime) / duration.toFloat(), 0f, 1f)
|
|
||||||
State.REVERSE -> 1f - Util.clamp((System.currentTimeMillis() - startTime) / duration.toFloat(), 0f, 1f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setState(newState: State) {
|
|
||||||
shouldInvalidate()
|
|
||||||
|
|
||||||
if (state == newState) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newState == State.END || newState == State.START) {
|
|
||||||
state = newState
|
|
||||||
startTime = 0L
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State.START && newState == State.REVERSE) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State.END && newState == State.FORWARD) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State.START && newState == State.FORWARD) {
|
|
||||||
state = State.FORWARD
|
|
||||||
startTime = System.currentTimeMillis()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State.END && newState == State.REVERSE) {
|
|
||||||
state = State.REVERSE
|
|
||||||
startTime = System.currentTimeMillis()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State.FORWARD && newState == State.REVERSE) {
|
|
||||||
val elapsed = System.currentTimeMillis() - startTime
|
|
||||||
val delta = duration - elapsed
|
|
||||||
startTime -= delta
|
|
||||||
state = State.REVERSE
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State.REVERSE && newState == State.FORWARD) {
|
|
||||||
val elapsed = System.currentTimeMillis() - startTime
|
|
||||||
val delta = duration - elapsed
|
|
||||||
startTime -= delta
|
|
||||||
state = State.FORWARD
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun shouldInvalidate(): Boolean {
|
|
||||||
if (state == State.START || state == State.END) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State.FORWARD && getFraction() == 1f) {
|
|
||||||
state = State.END
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State.REVERSE && getFraction() == 0f) {
|
|
||||||
state = State.START
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class State {
|
|
||||||
START,
|
|
||||||
FORWARD,
|
|
||||||
REVERSE,
|
|
||||||
END
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package org.thoughtcrime.securesms.badges.models
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||||
|
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
|
||||||
|
object BadgePreview {
|
||||||
|
|
||||||
|
fun register(mappingAdapter: MappingAdapter) {
|
||||||
|
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.featured_badge_preview_preference))
|
||||||
|
mappingAdapter.registerFactory(SubscriptionModel::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.subscription_flow_badge_preview_preference))
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BadgeModel<T : BadgeModel<T>> : PreferenceModel<T>() {
|
||||||
|
abstract val badge: Badge?
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Model(override val badge: Badge?) : BadgeModel<Model>() {
|
||||||
|
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) && badge == newItem.badge
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(newItem: Model): Any? {
|
||||||
|
return Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SubscriptionModel(override val badge: Badge?) : BadgeModel<SubscriptionModel>() {
|
||||||
|
override fun areItemsTheSame(newItem: SubscriptionModel): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: SubscriptionModel): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) && badge == newItem.badge
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(newItem: SubscriptionModel): Any? {
|
||||||
|
return Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder<T : BadgeModel<T>>(itemView: View) : MappingViewHolder<T>(itemView) {
|
||||||
|
|
||||||
|
private val avatar: AvatarImageView = itemView.findViewById(R.id.avatar)
|
||||||
|
private val badge: BadgeImageView = itemView.findViewById(R.id.badge)
|
||||||
|
|
||||||
|
override fun bind(model: T) {
|
||||||
|
if (payload.isEmpty()) {
|
||||||
|
avatar.setRecipient(Recipient.self())
|
||||||
|
avatar.disableQuickContact()
|
||||||
|
}
|
||||||
|
|
||||||
|
badge.setBadge(model.badge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.thoughtcrime.securesms.badges.models
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
|
||||||
|
object ExpiredBadge {
|
||||||
|
|
||||||
|
class Model(val badge: Badge) : PreferenceModel<Model>() {
|
||||||
|
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||||
|
return newItem.badge.id == badge.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) && newItem.badge == badge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||||
|
|
||||||
|
private val badge: BadgeImageView = itemView.findViewById(R.id.expired_badge)
|
||||||
|
|
||||||
|
override fun bind(model: Model) {
|
||||||
|
badge.setBadge(model.badge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register(adapter: MappingAdapter) {
|
||||||
|
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.expired_badge_preference))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.badges.models
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.bumptech.glide.request.target.CustomViewTarget
|
|
||||||
import com.bumptech.glide.request.transition.Transition
|
|
||||||
import org.signal.core.util.DimensionUnit
|
|
||||||
import org.thoughtcrime.securesms.R
|
|
||||||
import org.thoughtcrime.securesms.badges.Badges.insetWithOutline
|
|
||||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
|
||||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
|
||||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
|
||||||
|
|
||||||
object FeaturedBadgePreview {
|
|
||||||
|
|
||||||
fun register(mappingAdapter: MappingAdapter) {
|
|
||||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.featured_badge_preview_preference))
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Model(val badge: Badge?) : PreferenceModel<Model>() {
|
|
||||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
|
||||||
return newItem.badge?.id == badge?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
|
||||||
return super.areContentsTheSame(newItem) && badge == newItem.badge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
|
||||||
|
|
||||||
private val avatar: AvatarImageView = itemView.findViewById(R.id.avatar)
|
|
||||||
private val badge: ImageView = itemView.findViewById(R.id.badge)
|
|
||||||
private val target: Target = Target(badge)
|
|
||||||
|
|
||||||
override fun bind(model: Model) {
|
|
||||||
avatar.setRecipient(Recipient.self())
|
|
||||||
avatar.disableQuickContact()
|
|
||||||
|
|
||||||
if (model.badge != null) {
|
|
||||||
GlideApp.with(badge)
|
|
||||||
.load(model.badge)
|
|
||||||
.into(target)
|
|
||||||
} else {
|
|
||||||
GlideApp.with(badge).clear(badge)
|
|
||||||
badge.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Target(view: ImageView) : CustomViewTarget<ImageView, Drawable>(view) {
|
|
||||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
|
||||||
view.setImageDrawable(errorDrawable)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
|
||||||
view.setImageDrawable(
|
|
||||||
resource.insetWithOutline(
|
|
||||||
DimensionUnit.DP.toPixels(2.5f),
|
|
||||||
ContextCompat.getColor(view.context, R.color.signal_background_primary)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceCleared(placeholder: Drawable?) {
|
|
||||||
view.setImageDrawable(placeholder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package org.thoughtcrime.securesms.badges.models
|
package org.thoughtcrime.securesms.badges.models
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.MappingModel
|
import org.thoughtcrime.securesms.util.MappingModel
|
||||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
@@ -13,13 +12,13 @@ data class LargeBadge(
|
|||||||
val badge: Badge
|
val badge: Badge
|
||||||
) {
|
) {
|
||||||
|
|
||||||
class Model(val largeBadge: LargeBadge, val shortName: String) : MappingModel<Model> {
|
class Model(val largeBadge: LargeBadge, val shortName: String, val maxLines: Int) : MappingModel<Model> {
|
||||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||||
return newItem.largeBadge.badge.id == largeBadge.badge.id
|
return newItem.largeBadge.badge.id == largeBadge.badge.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||||
return newItem.largeBadge == largeBadge && newItem.shortName == shortName
|
return newItem.largeBadge == largeBadge && newItem.shortName == shortName && newItem.maxLines == maxLines
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,17 +34,18 @@ data class LargeBadge(
|
|||||||
|
|
||||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||||
|
|
||||||
private val badge: ImageView = itemView.findViewById(R.id.badge)
|
private val badge: BadgeImageView = itemView.findViewById(R.id.badge)
|
||||||
private val name: TextView = itemView.findViewById(R.id.name)
|
private val name: TextView = itemView.findViewById(R.id.name)
|
||||||
private val description: TextView = itemView.findViewById(R.id.description)
|
private val description: TextView = itemView.findViewById(R.id.description)
|
||||||
|
|
||||||
override fun bind(model: Model) {
|
override fun bind(model: Model) {
|
||||||
GlideApp.with(badge)
|
badge.setBadge(model.largeBadge.badge)
|
||||||
.load(model.largeBadge.badge)
|
|
||||||
.into(badge)
|
|
||||||
|
|
||||||
name.text = model.largeBadge.badge.name
|
name.text = model.largeBadge.badge.name
|
||||||
description.text = model.largeBadge.badge.resolveDescription(model.shortName)
|
description.text = model.largeBadge.badge.resolveDescription(model.shortName)
|
||||||
|
description.setLines(model.maxLines)
|
||||||
|
description.maxLines = model.maxLines
|
||||||
|
description.minLines = model.maxLines
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package org.thoughtcrime.securesms.badges.self.expired
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import org.signal.core.util.DimensionUnit
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
|
import org.thoughtcrime.securesms.badges.models.ExpiredBadge
|
||||||
|
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.app.AppSettingsActivity
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bottom sheet displaying a fading badge with a notice and action for becoming a subscriber again.
|
||||||
|
*/
|
||||||
|
class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||||
|
peekHeightPercentage = 1f
|
||||||
|
) {
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
ExpiredBadge.register(adapter)
|
||||||
|
|
||||||
|
adapter.submitList(getConfiguration().toMappingModelList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(): DSLConfiguration {
|
||||||
|
val badge: Badge = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments()).badge
|
||||||
|
val isLikelyASustainer = SignalStore.donationsValues().isLikelyASustainer()
|
||||||
|
|
||||||
|
return configure {
|
||||||
|
customPref(ExpiredBadge.Model(badge))
|
||||||
|
|
||||||
|
sectionHeaderPref(
|
||||||
|
DSLSettingsText.from(
|
||||||
|
if (badge.isBoost()) {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__your_badge_has_expired
|
||||||
|
} else {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__subscription_cancelled
|
||||||
|
},
|
||||||
|
DSLSettingsText.CenterModifier
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
space(DimensionUnit.DP.toPixels(4f).toInt())
|
||||||
|
|
||||||
|
noPadTextPref(
|
||||||
|
DSLSettingsText.from(
|
||||||
|
if (badge.isBoost()) {
|
||||||
|
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_boost_badge_has_expired)
|
||||||
|
} else {
|
||||||
|
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer, badge.name)
|
||||||
|
},
|
||||||
|
DSLSettingsText.CenterModifier
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||||
|
|
||||||
|
noPadTextPref(
|
||||||
|
DSLSettingsText.from(
|
||||||
|
if (badge.isBoost()) {
|
||||||
|
if (isLikelyASustainer) {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_reactivate
|
||||||
|
} else {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__to_continue_supporting_technology
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can
|
||||||
|
},
|
||||||
|
DSLSettingsText.CenterModifier
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
space(DimensionUnit.DP.toPixels(92f).toInt())
|
||||||
|
|
||||||
|
primaryButton(
|
||||||
|
text = DSLSettingsText.from(
|
||||||
|
if (badge.isBoost()) {
|
||||||
|
if (isLikelyASustainer) {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__add_a_boost
|
||||||
|
} else {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__become_a_sustainer
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__renew_subscription
|
||||||
|
}
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
dismiss()
|
||||||
|
if (isLikelyASustainer) {
|
||||||
|
requireActivity().startActivity(AppSettingsActivity.boost(requireContext()))
|
||||||
|
} else {
|
||||||
|
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
secondaryButtonNoOutline(
|
||||||
|
text = DSLSettingsText.from(R.string.ExpiredBadgeBottomSheetDialogFragment__not_now),
|
||||||
|
onClick = {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun show(badge: Badge, fragmentManager: FragmentManager) {
|
||||||
|
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge).build()
|
||||||
|
val fragment = ExpiredBadgeBottomSheetDialogFragment()
|
||||||
|
fragment.arguments = args.toBundle()
|
||||||
|
|
||||||
|
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.badges.BadgeRepository
|
|||||||
import org.thoughtcrime.securesms.badges.Badges
|
import org.thoughtcrime.securesms.badges.Badges
|
||||||
import org.thoughtcrime.securesms.badges.Badges.displayBadges
|
import org.thoughtcrime.securesms.badges.Badges.displayBadges
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.badges.models.FeaturedBadgePreview
|
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
|
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
@@ -51,14 +51,14 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
Badge.register(adapter) { badge, isSelected ->
|
Badge.register(adapter) { badge, isSelected, _ ->
|
||||||
if (!isSelected) {
|
if (!isSelected) {
|
||||||
viewModel.setSelectedBadge(badge)
|
viewModel.setSelectedBadge(badge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val previewView: View = requireView().findViewById(R.id.preview)
|
val previewView: View = requireView().findViewById(R.id.preview)
|
||||||
val previewViewHolder = FeaturedBadgePreview.ViewHolder(previewView)
|
val previewViewHolder = BadgePreview.ViewHolder<BadgePreview.Model>(previewView)
|
||||||
|
|
||||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||||
lifecycleDisposable += viewModel.events.subscribe { event: SelectFeaturedBadgeEvent ->
|
lifecycleDisposable += viewModel.events.subscribe { event: SelectFeaturedBadgeEvent ->
|
||||||
@@ -69,9 +69,17 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasBoundPreview = false
|
||||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||||
save.isEnabled = state.stage == SelectFeaturedBadgeState.Stage.READY
|
save.isEnabled = state.stage == SelectFeaturedBadgeState.Stage.READY
|
||||||
previewViewHolder.bind(FeaturedBadgePreview.Model(state.selectedBadge))
|
|
||||||
|
if (hasBoundPreview) {
|
||||||
|
previewViewHolder.setPayload(listOf(Unit))
|
||||||
|
} else {
|
||||||
|
hasBoundPreview = true
|
||||||
|
}
|
||||||
|
|
||||||
|
previewViewHolder.bind(BadgePreview.Model(state.selectedBadge))
|
||||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +87,7 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment(
|
|||||||
private fun getConfiguration(state: SelectFeaturedBadgeState): DSLConfiguration {
|
private fun getConfiguration(state: SelectFeaturedBadgeState): DSLConfiguration {
|
||||||
return configure {
|
return configure {
|
||||||
sectionHeaderPref(R.string.SelectFeaturedBadgeFragment__select_a_badge)
|
sectionHeaderPref(R.string.SelectFeaturedBadgeFragment__select_a_badge)
|
||||||
displayBadges(state.allUnlockedBadges, state.selectedBadge)
|
displayBadges(requireContext(), state.allUnlockedBadges, state.selectedBadge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ class SelectFeaturedBadgeViewModel(private val repository: BadgeRepository) : Vi
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
store.update(Recipient.live(Recipient.self().id).liveDataResolved) { recipient, state ->
|
store.update(Recipient.live(Recipient.self().id).liveDataResolved) { recipient, state ->
|
||||||
|
val unexpiredBadges = recipient.badges.filterNot { it.isExpired() }
|
||||||
state.copy(
|
state.copy(
|
||||||
stage = if (state.stage == SelectFeaturedBadgeState.Stage.INIT) SelectFeaturedBadgeState.Stage.READY else state.stage,
|
stage = if (state.stage == SelectFeaturedBadgeState.Stage.INIT) SelectFeaturedBadgeState.Stage.READY else state.stage,
|
||||||
selectedBadge = recipient.badges.firstOrNull(),
|
selectedBadge = unexpiredBadges.firstOrNull(),
|
||||||
allUnlockedBadges = recipient.badges
|
allUnlockedBadges = unexpiredBadges
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package org.thoughtcrime.securesms.badges.self.none
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import org.signal.core.util.DimensionUnit
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||||
|
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.app.AppSettingsActivity
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||||
|
|
||||||
|
class BecomeASustainerFragment : DSLSettingsBottomSheetFragment() {
|
||||||
|
|
||||||
|
private val viewModel: BecomeASustainerViewModel by viewModels(
|
||||||
|
factoryProducer = {
|
||||||
|
BecomeASustainerViewModel.Factory(SubscriptionsRepository(ApplicationDependencies.getDonationsService()))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
BadgePreview.register(adapter)
|
||||||
|
|
||||||
|
viewModel.state.observe(viewLifecycleOwner) {
|
||||||
|
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(state: BecomeASustainerState): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
customPref(BadgePreview.Model(badge = state.badge))
|
||||||
|
|
||||||
|
sectionHeaderPref(
|
||||||
|
title = DSLSettingsText.from(
|
||||||
|
R.string.BecomeASustainerFragment__get_badges,
|
||||||
|
DSLSettingsText.CenterModifier,
|
||||||
|
DSLSettingsText.Title2BoldModifier
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
space(DimensionUnit.DP.toPixels(8f).toInt())
|
||||||
|
|
||||||
|
noPadTextPref(
|
||||||
|
title = DSLSettingsText.from(
|
||||||
|
R.string.BecomeASustainerFragment__signal_is_a_non_profit,
|
||||||
|
DSLSettingsText.CenterModifier
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
space(DimensionUnit.DP.toPixels(77f).toInt())
|
||||||
|
|
||||||
|
primaryButton(
|
||||||
|
text = DSLSettingsText.from(
|
||||||
|
R.string.BecomeASustainerMegaphone__become_a_sustainer
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
requireActivity().finish()
|
||||||
|
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
space(DimensionUnit.DP.toPixels(8f).toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun show(fragmentManager: FragmentManager) {
|
||||||
|
BecomeASustainerFragment().show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.thoughtcrime.securesms.badges.self.none
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
|
|
||||||
|
data class BecomeASustainerState(
|
||||||
|
val badge: Badge? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package org.thoughtcrime.securesms.badges.self.none
|
||||||
|
|
||||||
|
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 io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
|
||||||
|
class BecomeASustainerViewModel(subscriptionsRepository: SubscriptionsRepository) : ViewModel() {
|
||||||
|
|
||||||
|
private val store = Store(BecomeASustainerState())
|
||||||
|
|
||||||
|
val state: LiveData<BecomeASustainerState> = store.stateLiveData
|
||||||
|
|
||||||
|
private val disposables = CompositeDisposable()
|
||||||
|
|
||||||
|
init {
|
||||||
|
disposables += subscriptionsRepository.getSubscriptions().subscribeBy(
|
||||||
|
onError = { Log.w(TAG, "Could not load subscriptions.") },
|
||||||
|
onSuccess = { subscriptions ->
|
||||||
|
store.update {
|
||||||
|
it.copy(badge = subscriptions.firstOrNull()?.badge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
disposables.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(BecomeASustainerViewModel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(private val subscriptionsRepository: SubscriptionsRepository) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return modelClass.cast(BecomeASustainerViewModel(subscriptionsRepository))!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,9 @@ import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
|||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||||
import org.thoughtcrime.securesms.components.settings.configure
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
|
|
||||||
@@ -26,11 +28,19 @@ class BadgesOverviewFragment : DSLSettingsFragment(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private val lifecycleDisposable = LifecycleDisposable()
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
private val viewModel: BadgesOverviewViewModel by viewModels(factoryProducer = { BadgesOverviewViewModel.Factory(BadgeRepository(requireContext())) })
|
private val viewModel: BadgesOverviewViewModel by viewModels(
|
||||||
|
factoryProducer = {
|
||||||
|
BadgesOverviewViewModel.Factory(BadgeRepository(requireContext()), SubscriptionsRepository(ApplicationDependencies.getDonationsService()))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
Badge.register(adapter) { badge, _ ->
|
Badge.register(adapter) { badge, _, isFaded ->
|
||||||
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, Recipient.self().id, badge)
|
if (badge.isExpired() || isFaded) {
|
||||||
|
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge))
|
||||||
|
} else {
|
||||||
|
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, Recipient.self().id, badge)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||||
@@ -52,11 +62,17 @@ class BadgesOverviewFragment : DSLSettingsFragment(
|
|||||||
return configure {
|
return configure {
|
||||||
sectionHeaderPref(R.string.BadgesOverviewFragment__my_badges)
|
sectionHeaderPref(R.string.BadgesOverviewFragment__my_badges)
|
||||||
|
|
||||||
displayBadges(state.allUnlockedBadges)
|
displayBadges(
|
||||||
|
context = requireContext(),
|
||||||
|
badges = state.allUnlockedBadges,
|
||||||
|
fadedBadgeId = state.fadedBadgeId
|
||||||
|
)
|
||||||
|
|
||||||
switchPref(
|
asyncSwitchPref(
|
||||||
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__display_badges_on_profile),
|
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__display_badges_on_profile),
|
||||||
isChecked = state.displayBadgesOnProfile,
|
isChecked = state.displayBadgesOnProfile,
|
||||||
|
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges && state.hasInternet,
|
||||||
|
isProcessing = state.stage == BadgesOverviewState.Stage.UPDATING_BADGE_DISPLAY_STATE,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setDisplayBadgesOnProfile(!state.displayBadgesOnProfile)
|
viewModel.setDisplayBadgesOnProfile(!state.displayBadgesOnProfile)
|
||||||
}
|
}
|
||||||
@@ -65,7 +81,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
|
|||||||
clickPref(
|
clickPref(
|
||||||
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__featured_badge),
|
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__featured_badge),
|
||||||
summary = state.featuredBadge?.name?.let { DSLSettingsText.from(it) },
|
summary = state.featuredBadge?.name?.let { DSLSettingsText.from(it) },
|
||||||
isEnabled = state.stage == BadgesOverviewState.Stage.READY,
|
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges && state.hasInternet,
|
||||||
onClick = {
|
onClick = {
|
||||||
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToFeaturedBadgeFragment())
|
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToFeaturedBadgeFragment())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ data class BadgesOverviewState(
|
|||||||
val stage: Stage = Stage.INIT,
|
val stage: Stage = Stage.INIT,
|
||||||
val allUnlockedBadges: List<Badge> = listOf(),
|
val allUnlockedBadges: List<Badge> = listOf(),
|
||||||
val featuredBadge: Badge? = null,
|
val featuredBadge: Badge? = null,
|
||||||
val displayBadgesOnProfile: Boolean = false
|
val displayBadgesOnProfile: Boolean = false,
|
||||||
|
val fadedBadgeId: String? = null,
|
||||||
|
val hasInternet: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val hasUnexpiredBadges = allUnlockedBadges.any { it.expirationTimestamp > System.currentTimeMillis() }
|
||||||
|
|
||||||
enum class Stage {
|
enum class Stage {
|
||||||
INIT,
|
INIT,
|
||||||
READY,
|
READY,
|
||||||
UPDATING
|
UPDATING_BADGE_DISPLAY_STATE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,26 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||||
import org.thoughtcrime.securesms.util.livedata.Store
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional
|
||||||
|
|
||||||
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
|
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
|
||||||
|
|
||||||
class BadgesOverviewViewModel(private val badgeRepository: BadgeRepository) : ViewModel() {
|
class BadgesOverviewViewModel(
|
||||||
|
private val badgeRepository: BadgeRepository,
|
||||||
|
private val subscriptionsRepository: SubscriptionsRepository
|
||||||
|
) : ViewModel() {
|
||||||
private val store = Store(BadgesOverviewState())
|
private val store = Store(BadgesOverviewState())
|
||||||
private val eventSubject = PublishSubject.create<BadgesOverviewEvent>()
|
private val eventSubject = PublishSubject.create<BadgesOverviewEvent>()
|
||||||
|
|
||||||
@@ -29,12 +38,38 @@ class BadgesOverviewViewModel(private val badgeRepository: BadgeRepository) : Vi
|
|||||||
state.copy(
|
state.copy(
|
||||||
stage = if (state.stage == BadgesOverviewState.Stage.INIT) BadgesOverviewState.Stage.READY else state.stage,
|
stage = if (state.stage == BadgesOverviewState.Stage.INIT) BadgesOverviewState.Stage.READY else state.stage,
|
||||||
allUnlockedBadges = recipient.badges,
|
allUnlockedBadges = recipient.badges,
|
||||||
displayBadgesOnProfile = recipient.badges.firstOrNull()?.visible == true
|
displayBadgesOnProfile = SignalStore.donationsValues().getDisplayBadgesOnProfile(),
|
||||||
|
featuredBadge = recipient.featuredBadge
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disposables += InternetConnectionObserver.observe()
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.subscribeBy { isConnected ->
|
||||||
|
store.update { it.copy(hasInternet = isConnected) }
|
||||||
|
}
|
||||||
|
|
||||||
|
disposables += Single.zip(
|
||||||
|
subscriptionsRepository.getActiveSubscription(),
|
||||||
|
subscriptionsRepository.getSubscriptions()
|
||||||
|
) { active, all ->
|
||||||
|
if (!active.isActive && active.activeSubscription?.willCancelAtPeriodEnd() == true) {
|
||||||
|
Optional.fromNullable<String>(all.firstOrNull { it.level == active.activeSubscription?.level }?.badge?.id)
|
||||||
|
} else {
|
||||||
|
Optional.absent()
|
||||||
|
}
|
||||||
|
}.subscribeBy(
|
||||||
|
onSuccess = { badgeId ->
|
||||||
|
store.update { it.copy(fadedBadgeId = badgeId.orNull()) }
|
||||||
|
},
|
||||||
|
onError = { throwable ->
|
||||||
|
Log.w(TAG, "Could not retrieve data from server", throwable)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDisplayBadgesOnProfile(displayBadgesOnProfile: Boolean) {
|
fun setDisplayBadgesOnProfile(displayBadgesOnProfile: Boolean) {
|
||||||
|
store.update { it.copy(stage = BadgesOverviewState.Stage.UPDATING_BADGE_DISPLAY_STATE) }
|
||||||
disposables += badgeRepository.setVisibilityForAllBadges(displayBadgesOnProfile)
|
disposables += badgeRepository.setVisibilityForAllBadges(displayBadgesOnProfile)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{
|
{
|
||||||
@@ -52,9 +87,16 @@ class BadgesOverviewViewModel(private val badgeRepository: BadgeRepository) : Vi
|
|||||||
disposables.clear()
|
disposables.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory(private val badgeRepository: BadgeRepository) : ViewModelProvider.Factory {
|
class Factory(
|
||||||
|
private val badgeRepository: BadgeRepository,
|
||||||
|
private val subscriptionsRepository: SubscriptionsRepository
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
return requireNotNull(modelClass.cast(BadgesOverviewViewModel(badgeRepository)))
|
return requireNotNull(modelClass.cast(BadgesOverviewViewModel(badgeRepository, subscriptionsRepository)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package org.thoughtcrime.securesms.badges.view
|
package org.thoughtcrime.securesms.badges.view
|
||||||
|
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Rect
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
@@ -15,11 +18,18 @@ import org.thoughtcrime.securesms.badges.BadgeRepository
|
|||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.badges.models.LargeBadge
|
import org.thoughtcrime.securesms.badges.models.LargeBadge
|
||||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
import org.thoughtcrime.securesms.util.visible
|
import org.thoughtcrime.securesms.util.visible
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||||
|
|
||||||
@@ -27,6 +37,13 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||||||
|
|
||||||
override val peekHeightPercentage: Float = 1f
|
override val peekHeightPercentage: Float = 1f
|
||||||
|
|
||||||
|
private val textWidth: Float
|
||||||
|
get() = (resources.displayMetrics.widthPixels - ViewUtil.dpToPx(64)).toFloat()
|
||||||
|
private val textBounds: Rect = Rect()
|
||||||
|
private val textPaint: Paint = Paint().apply {
|
||||||
|
textSize = ViewUtil.spToPx(16f).toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
return inflater.inflate(R.layout.view_badge_bottom_sheet_dialog_fragment, container, false)
|
return inflater.inflate(R.layout.view_badge_bottom_sheet_dialog_fragment, container, false)
|
||||||
}
|
}
|
||||||
@@ -37,11 +54,31 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||||||
val pager: ViewPager2 = view.findViewById(R.id.pager)
|
val pager: ViewPager2 = view.findViewById(R.id.pager)
|
||||||
val tabs: TabLayout = view.findViewById(R.id.tab_layout)
|
val tabs: TabLayout = view.findViewById(R.id.tab_layout)
|
||||||
val action: MaterialButton = view.findViewById(R.id.action)
|
val action: MaterialButton = view.findViewById(R.id.action)
|
||||||
|
val noSupport: View = view.findViewById(R.id.no_support)
|
||||||
|
|
||||||
if (getRecipientId() == Recipient.self().id) {
|
if (getRecipientId() == Recipient.self().id) {
|
||||||
action.visible = false
|
action.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("CascadeIf")
|
||||||
|
if (PlayServicesUtil.getPlayServicesStatus(requireContext()) != PlayServicesUtil.PlayServicesStatus.SUCCESS) {
|
||||||
|
noSupport.visible = true
|
||||||
|
action.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_open_20)
|
||||||
|
action.setText(R.string.preferences__donate_to_signal)
|
||||||
|
action.setOnClickListener {
|
||||||
|
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url))
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
FeatureFlags.donorBadges() &&
|
||||||
|
Recipient.self().badges.none { it.category == Badge.Category.Donor && !it.isBoost() && !it.isExpired() }
|
||||||
|
) {
|
||||||
|
action.setOnClickListener {
|
||||||
|
startActivity(AppSettingsActivity.subscriptions(requireContext()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
action.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
val adapter = MappingAdapter()
|
val adapter = MappingAdapter()
|
||||||
|
|
||||||
LargeBadge.register(adapter)
|
LargeBadge.register(adapter)
|
||||||
@@ -68,9 +105,19 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||||||
dismissAllowingStateLoss()
|
dismissAllowingStateLoss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tabs.visible = state.allBadgesVisibleOnProfile.size > 1
|
||||||
|
|
||||||
|
var maxLines = 3
|
||||||
|
state.allBadgesVisibleOnProfile.forEach { badge ->
|
||||||
|
val text = badge.resolveDescription(state.recipient.getShortDisplayName(requireContext()))
|
||||||
|
textPaint.getTextBounds(text, 0, text.length, textBounds)
|
||||||
|
val estimatedLines = ceil(textBounds.width().toFloat() / textWidth).toInt()
|
||||||
|
maxLines = max(maxLines, estimatedLines)
|
||||||
|
}
|
||||||
|
|
||||||
adapter.submitList(
|
adapter.submitList(
|
||||||
state.allBadgesVisibleOnProfile.map {
|
state.allBadgesVisibleOnProfile.map {
|
||||||
LargeBadge.Model(LargeBadge(it), state.recipient.getShortDisplayName(requireContext()))
|
LargeBadge.Model(LargeBadge(it), state.recipient.getShortDisplayName(requireContext()), maxLines + 1)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val stateSelectedIndex = state.allBadgesVisibleOnProfile.indexOf(state.selectedBadge)
|
val stateSelectedIndex = state.allBadgesVisibleOnProfile.indexOf(state.selectedBadge)
|
||||||
@@ -96,6 +143,10 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||||||
recipientId: RecipientId,
|
recipientId: RecipientId,
|
||||||
startBadge: Badge? = null
|
startBadge: Badge? = null
|
||||||
) {
|
) {
|
||||||
|
if (!FeatureFlags.displayDonorBadges() && recipientId != Recipient.self().id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ViewBadgeBottomSheetDialogFragment().apply {
|
ViewBadgeBottomSheetDialogFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putParcelable(ARG_START_BADGE, startBadge)
|
putParcelable(ARG_START_BADGE, startBadge)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import androidx.core.util.Consumer;
|
|||||||
|
|
||||||
import org.signal.core.util.concurrent.SignalExecutors;
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
@@ -32,7 +32,7 @@ class BlockedUsersRepository {
|
|||||||
|
|
||||||
void getBlocked(@NonNull Consumer<List<Recipient>> blockedUsers) {
|
void getBlocked(@NonNull Consumer<List<Recipient>> blockedUsers) {
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
|
RecipientDatabase db = SignalDatabase.recipients();
|
||||||
try (RecipientDatabase.RecipientReader reader = db.readerForBlocked(db.getBlocked())) {
|
try (RecipientDatabase.RecipientReader reader = db.readerForBlocked(db.getBlocked())) {
|
||||||
int count = reader.getCount();
|
int count = reader.getCount();
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ public final class AudioView extends FrameLayout {
|
|||||||
|
|
||||||
lottieDirection = REVERSE;
|
lottieDirection = REVERSE;
|
||||||
this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
|
this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
|
||||||
|
this.playPauseButton.setOnLongClickListener(v -> performLongClick());
|
||||||
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
||||||
|
|
||||||
setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE));
|
setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE));
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||||||
blurred = shouldBlur;
|
blurred = shouldBlur;
|
||||||
|
|
||||||
GlideRequest<Drawable> request = requestManager.load(photo.contactPhoto)
|
GlideRequest<Drawable> request = requestManager.load(photo.contactPhoto)
|
||||||
|
.dontAnimate()
|
||||||
.fallback(fallbackContactPhotoDrawable)
|
.fallback(fallbackContactPhotoDrawable)
|
||||||
.error(fallbackContactPhotoDrawable)
|
.error(fallbackContactPhotoDrawable)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||||
@@ -290,6 +291,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||||||
|
|
||||||
GlideApp.with(this)
|
GlideApp.with(this)
|
||||||
.load(avatarBytes)
|
.load(avatarBytes)
|
||||||
|
.dontAnimate()
|
||||||
.fallback(fallback)
|
.fallback(fallback)
|
||||||
.error(fallback)
|
.error(fallback)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package org.thoughtcrime.securesms.components
|
package org.thoughtcrime.securesms.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
|
||||||
@@ -24,7 +26,9 @@ class ButtonStripItemView @JvmOverloads constructor(
|
|||||||
|
|
||||||
val array = context.obtainStyledAttributes(attrs, R.styleable.ButtonStripItemView)
|
val array = context.obtainStyledAttributes(attrs, R.styleable.ButtonStripItemView)
|
||||||
|
|
||||||
val icon = array.getDrawable(R.styleable.ButtonStripItemView_bsiv_icon)
|
val iconId = array.getResourceId(R.styleable.ButtonStripItemView_bsiv_icon, -1)
|
||||||
|
val icon: Drawable? = if (iconId > 0) AppCompatResources.getDrawable(context, iconId) else null
|
||||||
|
|
||||||
val contentDescription = array.getString(R.styleable.ButtonStripItemView_bsiv_icon_contentDescription)
|
val contentDescription = array.getString(R.styleable.ButtonStripItemView_bsiv_icon_contentDescription)
|
||||||
val label = array.getString(R.styleable.ButtonStripItemView_bsiv_label)
|
val label = array.getString(R.styleable.ButtonStripItemView_bsiv_label)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import com.airbnb.lottie.model.KeyPath;
|
|||||||
import org.signal.core.util.concurrent.SignalExecutors;
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
@@ -360,8 +360,11 @@ public class ConversationItemFooter extends ConstraintLayout {
|
|||||||
long id = messageRecord.getId();
|
long id = messageRecord.getId();
|
||||||
boolean mms = messageRecord.isMms();
|
boolean mms = messageRecord.isMms();
|
||||||
|
|
||||||
if (mms) DatabaseFactory.getMmsDatabase(getContext()).markExpireStarted(id);
|
if (mms) {
|
||||||
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
|
SignalDatabase.mms().markExpireStarted(id);
|
||||||
|
} else {
|
||||||
|
SignalDatabase.sms().markExpireStarted(id);
|
||||||
|
}
|
||||||
|
|
||||||
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
|
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat;
|
|||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.whispersystems.libsignal.util.Pair;
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
@@ -26,6 +27,9 @@ public class ConversationTypingView extends ConstraintLayout {
|
|||||||
private AvatarImageView avatar1;
|
private AvatarImageView avatar1;
|
||||||
private AvatarImageView avatar2;
|
private AvatarImageView avatar2;
|
||||||
private AvatarImageView avatar3;
|
private AvatarImageView avatar3;
|
||||||
|
private BadgeImageView badge1;
|
||||||
|
private BadgeImageView badge2;
|
||||||
|
private BadgeImageView badge3;
|
||||||
private View bubble;
|
private View bubble;
|
||||||
private TypingIndicatorView indicator;
|
private TypingIndicatorView indicator;
|
||||||
private TextView typistCount;
|
private TextView typistCount;
|
||||||
@@ -41,6 +45,9 @@ public class ConversationTypingView extends ConstraintLayout {
|
|||||||
avatar1 = findViewById(R.id.typing_avatar_1);
|
avatar1 = findViewById(R.id.typing_avatar_1);
|
||||||
avatar2 = findViewById(R.id.typing_avatar_2);
|
avatar2 = findViewById(R.id.typing_avatar_2);
|
||||||
avatar3 = findViewById(R.id.typing_avatar_3);
|
avatar3 = findViewById(R.id.typing_avatar_3);
|
||||||
|
badge1 = findViewById(R.id.typing_badge_1);
|
||||||
|
badge2 = findViewById(R.id.typing_badge_2);
|
||||||
|
badge3 = findViewById(R.id.typing_badge_3);
|
||||||
typistCount = findViewById(R.id.typing_count);
|
typistCount = findViewById(R.id.typing_count);
|
||||||
bubble = findViewById(R.id.typing_bubble);
|
bubble = findViewById(R.id.typing_bubble);
|
||||||
indicator = findViewById(R.id.typing_indicator);
|
indicator = findViewById(R.id.typing_indicator);
|
||||||
@@ -55,6 +62,9 @@ public class ConversationTypingView extends ConstraintLayout {
|
|||||||
avatar1.setVisibility(GONE);
|
avatar1.setVisibility(GONE);
|
||||||
avatar2.setVisibility(GONE);
|
avatar2.setVisibility(GONE);
|
||||||
avatar3.setVisibility(GONE);
|
avatar3.setVisibility(GONE);
|
||||||
|
badge1.setVisibility(GONE);
|
||||||
|
badge2.setVisibility(GONE);
|
||||||
|
badge3.setVisibility(GONE);
|
||||||
typistCount.setVisibility(GONE);
|
typistCount.setVisibility(GONE);
|
||||||
|
|
||||||
if (isGroupThread) {
|
if (isGroupThread) {
|
||||||
@@ -75,15 +85,21 @@ public class ConversationTypingView extends ConstraintLayout {
|
|||||||
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) {
|
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) {
|
||||||
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1);
|
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1);
|
||||||
avatar1.setVisibility(VISIBLE);
|
avatar1.setVisibility(VISIBLE);
|
||||||
|
badge1.setBadgeFromRecipient(typists.get(0), glideRequests);
|
||||||
|
badge1.setVisibility(VISIBLE);
|
||||||
|
|
||||||
if (typists.size() > 1) {
|
if (typists.size() > 1) {
|
||||||
avatar2.setAvatar(glideRequests, typists.get(1), false);
|
avatar2.setAvatar(glideRequests, typists.get(1), false);
|
||||||
avatar2.setVisibility(VISIBLE);
|
avatar2.setVisibility(VISIBLE);
|
||||||
|
badge2.setBadgeFromRecipient(typists.get(1), glideRequests);
|
||||||
|
badge2.setVisibility(VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typists.size() == 3) {
|
if (typists.size() == 3) {
|
||||||
avatar3.setAvatar(glideRequests, typists.get(2), false);
|
avatar3.setAvatar(glideRequests, typists.get(2), false);
|
||||||
avatar3.setVisibility(VISIBLE);
|
avatar3.setVisibility(VISIBLE);
|
||||||
|
badge3.setBadgeFromRecipient(typists.get(2), glideRequests);
|
||||||
|
badge3.setVisibility(VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typists.size() > 3) {
|
if (typists.size() > 3) {
|
||||||
|
|||||||
@@ -17,19 +17,11 @@ public class DeliveryStatusView extends FrameLayout {
|
|||||||
|
|
||||||
private static final String TAG = Log.tag(DeliveryStatusView.class);
|
private static final String TAG = Log.tag(DeliveryStatusView.class);
|
||||||
|
|
||||||
private static final RotateAnimation ROTATION_ANIMATION = new RotateAnimation(0, 360f,
|
private final RotateAnimation rotationAnimation;
|
||||||
Animation.RELATIVE_TO_SELF, 0.5f,
|
private final ImageView pendingIndicator;
|
||||||
Animation.RELATIVE_TO_SELF, 0.5f);
|
private final ImageView sentIndicator;
|
||||||
static {
|
private final ImageView deliveredIndicator;
|
||||||
ROTATION_ANIMATION.setInterpolator(new LinearInterpolator());
|
private final ImageView readIndicator;
|
||||||
ROTATION_ANIMATION.setDuration(1500);
|
|
||||||
ROTATION_ANIMATION.setRepeatCount(Animation.INFINITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final ImageView pendingIndicator;
|
|
||||||
private final ImageView sentIndicator;
|
|
||||||
private final ImageView deliveredIndicator;
|
|
||||||
private final ImageView readIndicator;
|
|
||||||
|
|
||||||
public DeliveryStatusView(Context context) {
|
public DeliveryStatusView(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
@@ -44,10 +36,17 @@ public class DeliveryStatusView extends FrameLayout {
|
|||||||
|
|
||||||
inflate(context, R.layout.delivery_status_view, this);
|
inflate(context, R.layout.delivery_status_view, this);
|
||||||
|
|
||||||
this.deliveredIndicator = findViewById(R.id.delivered_indicator);
|
this.deliveredIndicator = findViewById(R.id.delivered_indicator);
|
||||||
this.sentIndicator = findViewById(R.id.sent_indicator);
|
this.sentIndicator = findViewById(R.id.sent_indicator);
|
||||||
this.pendingIndicator = findViewById(R.id.pending_indicator);
|
this.pendingIndicator = findViewById(R.id.pending_indicator);
|
||||||
this.readIndicator = findViewById(R.id.read_indicator);
|
this.readIndicator = findViewById(R.id.read_indicator);
|
||||||
|
|
||||||
|
rotationAnimation = new RotateAnimation(0, 360f,
|
||||||
|
Animation.RELATIVE_TO_SELF, 0.5f,
|
||||||
|
Animation.RELATIVE_TO_SELF, 0.5f);
|
||||||
|
rotationAnimation.setInterpolator(new LinearInterpolator());
|
||||||
|
rotationAnimation.setDuration(1500);
|
||||||
|
rotationAnimation.setRepeatCount(Animation.INFINITE);
|
||||||
|
|
||||||
if (attrs != null) {
|
if (attrs != null) {
|
||||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DeliveryStatusView, 0, 0);
|
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DeliveryStatusView, 0, 0);
|
||||||
@@ -67,7 +66,7 @@ public class DeliveryStatusView extends FrameLayout {
|
|||||||
public void setPending() {
|
public void setPending() {
|
||||||
this.setVisibility(View.VISIBLE);
|
this.setVisibility(View.VISIBLE);
|
||||||
pendingIndicator.setVisibility(View.VISIBLE);
|
pendingIndicator.setVisibility(View.VISIBLE);
|
||||||
pendingIndicator.startAnimation(ROTATION_ANIMATION);
|
pendingIndicator.startAnimation(rotationAnimation);
|
||||||
sentIndicator.setVisibility(View.GONE);
|
sentIndicator.setVisibility(View.GONE);
|
||||||
deliveredIndicator.setVisibility(View.GONE);
|
deliveredIndicator.setVisibility(View.GONE);
|
||||||
readIndicator.setVisibility(View.GONE);
|
readIndicator.setVisibility(View.GONE);
|
||||||
|
|||||||
@@ -5,14 +5,10 @@ import android.graphics.PorterDuff;
|
|||||||
import android.graphics.PorterDuffColorFilter;
|
import android.graphics.PorterDuffColorFilter;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.style.CharacterStyle;
|
import android.text.style.CharacterStyle;
|
||||||
import android.text.style.MetricAffectingSpan;
|
|
||||||
import android.text.style.StyleSpan;
|
|
||||||
import android.text.style.TypefaceSpan;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -20,7 +16,6 @@ import androidx.core.content.ContextCompat;
|
|||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
|
||||||
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
|
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||||
@@ -32,9 +27,6 @@ public class FromTextView extends SimpleEmojiTextView {
|
|||||||
|
|
||||||
private static final String TAG = Log.tag(FromTextView.class);
|
private static final String TAG = Log.tag(FromTextView.class);
|
||||||
|
|
||||||
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
|
|
||||||
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
|
|
||||||
|
|
||||||
public FromTextView(Context context) {
|
public FromTextView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
@@ -52,8 +44,10 @@ public class FromTextView extends SimpleEmojiTextView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
|
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
|
||||||
String fromString = recipient.getDisplayName(getContext());
|
setText(recipient, recipient.getDisplayName(getContext()), read, suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix) {
|
||||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||||
SpannableString fromSpan = new SpannableString(fromString);
|
SpannableString fromSpan = new SpannableString(fromString);
|
||||||
fromSpan.setSpan(getFontSpan(!read), 0, fromSpan.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
fromSpan.setSpan(getFontSpan(!read), 0, fromSpan.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.text.TextUtils
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.ThrottledDebouncer
|
||||||
import org.whispersystems.libsignal.util.guava.Optional
|
import org.whispersystems.libsignal.util.guava.Optional
|
||||||
|
|
||||||
open class SimpleEmojiTextView @JvmOverloads constructor(
|
open class SimpleEmojiTextView @JvmOverloads constructor(
|
||||||
@@ -14,6 +15,7 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
|
|||||||
) : AppCompatTextView(context, attrs, defStyleAttr) {
|
) : AppCompatTextView(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
private var bufferType: BufferType? = null
|
private var bufferType: BufferType? = null
|
||||||
|
private val sizeChangeDebouncer: ThrottledDebouncer = ThrottledDebouncer(200)
|
||||||
|
|
||||||
override fun setText(text: CharSequence?, type: BufferType?) {
|
override fun setText(text: CharSequence?, type: BufferType?) {
|
||||||
bufferType = type
|
bufferType = type
|
||||||
@@ -44,8 +46,10 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
|
|||||||
|
|
||||||
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
|
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
|
||||||
super.onSizeChanged(width, height, oldWidth, oldHeight)
|
super.onSizeChanged(width, height, oldWidth, oldHeight)
|
||||||
if (width > 0 && oldWidth != width) {
|
sizeChangeDebouncer.publish {
|
||||||
setText(text, bufferType ?: BufferType.SPANNABLE)
|
if (width > 0 && oldWidth != width) {
|
||||||
|
setText(text, bufferType ?: BufferType.SPANNABLE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import androidx.appcompat.app.AlertDialog;
|
|||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import androidx.appcompat.app.AlertDialog;
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.menu
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an action to be rendered via [SignalContextMenu] or [SignalBottomActionBar]
|
||||||
|
*/
|
||||||
|
data class ActionItem(
|
||||||
|
@DrawableRes val iconRes: Int,
|
||||||
|
val title: CharSequence,
|
||||||
|
val action: Runnable
|
||||||
|
)
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.menu
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bar that displays a set of action buttons. Intended as a replacement for ActionModes, this gives you a simple interface to add a bunch of actions, and
|
||||||
|
* the bar itself will handle putting things in the overflow and whatnot.
|
||||||
|
*
|
||||||
|
* Overflow items are rendered in a [SignalContextMenu].
|
||||||
|
*/
|
||||||
|
class SignalBottomActionBar(context: Context, attributeSet: AttributeSet) : LinearLayout(context, attributeSet) {
|
||||||
|
|
||||||
|
val items: MutableList<ActionItem> = mutableListOf()
|
||||||
|
|
||||||
|
val enterAnimation: Animation by lazy {
|
||||||
|
AnimationUtils.loadAnimation(context, R.anim.slide_fade_from_bottom).apply {
|
||||||
|
duration = 250
|
||||||
|
interpolator = FastOutSlowInInterpolator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val exitAnimation: Animation by lazy {
|
||||||
|
AnimationUtils.loadAnimation(context, R.anim.slide_fade_to_bottom).apply {
|
||||||
|
duration = 250
|
||||||
|
interpolator = FastOutSlowInInterpolator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
orientation = HORIZONTAL
|
||||||
|
setBackgroundResource(R.drawable.signal_bottom_action_bar_background)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
elevation = 20f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItems(items: List<ActionItem>) {
|
||||||
|
this.items.clear()
|
||||||
|
this.items.addAll(items)
|
||||||
|
present(this.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun present(items: List<ActionItem>) {
|
||||||
|
if (width == 0) {
|
||||||
|
post { present(items) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val widthDp: Float = ViewUtil.pxToDp(width.toFloat())
|
||||||
|
val minButtonWidthDp = 80
|
||||||
|
val maxButtons: Int = (widthDp / minButtonWidthDp).toInt()
|
||||||
|
val usableButtonCount = when {
|
||||||
|
items.size <= maxButtons -> items.size
|
||||||
|
else -> maxButtons - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
val renderableItems: List<ActionItem> = items.subList(0, usableButtonCount)
|
||||||
|
val overflowItems: List<ActionItem> = if (renderableItems.size < items.size) items.subList(usableButtonCount, items.size) else emptyList()
|
||||||
|
|
||||||
|
removeAllViews()
|
||||||
|
|
||||||
|
renderableItems.forEach { item ->
|
||||||
|
val view: View = LayoutInflater.from(context).inflate(R.layout.signal_bottom_action_bar_item, this, false)
|
||||||
|
addView(view)
|
||||||
|
bindItem(view, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overflowItems.isNotEmpty()) {
|
||||||
|
val view: View = LayoutInflater.from(context).inflate(R.layout.signal_bottom_action_bar_item, this, false)
|
||||||
|
addView(view)
|
||||||
|
bindItem(
|
||||||
|
view,
|
||||||
|
ActionItem(
|
||||||
|
iconRes = R.drawable.ic_more_horiz_24,
|
||||||
|
title = context.getString(R.string.SignalBottomActionBar_more),
|
||||||
|
action = {
|
||||||
|
SignalContextMenu.Builder(view, parent as ViewGroup)
|
||||||
|
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.END)
|
||||||
|
.offsetY(ViewUtil.dpToPx(8))
|
||||||
|
.show(overflowItems)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindItem(view: View, item: ActionItem) {
|
||||||
|
val icon: ImageView = view.findViewById(R.id.signal_bottom_action_bar_item_icon)
|
||||||
|
val title: TextView = view.findViewById(R.id.signal_bottom_action_bar_item_title)
|
||||||
|
|
||||||
|
icon.setImageResource(item.iconRes)
|
||||||
|
title.text = item.title
|
||||||
|
view.setOnClickListener { item.action.run() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.menu
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.PopupWindow
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom context menu that will show next to an anchor view and display several options. Basically a PopupMenu with custom UI and positioning rules.
|
||||||
|
*
|
||||||
|
* This will prefer showing the menu underneath the anchor, but if there's not enough space in the container, it will show it above the anchor and reverse the
|
||||||
|
* order of the menu items. If there's not enough room for either, it'll show it centered above the anchor. If there's not enough room then, it'll center it,
|
||||||
|
* chop off the part that doesn't fit, and make the menu scrollable.
|
||||||
|
*/
|
||||||
|
class SignalContextMenu private constructor(
|
||||||
|
val anchor: View,
|
||||||
|
val container: ViewGroup,
|
||||||
|
val items: List<ActionItem>,
|
||||||
|
val baseOffsetX: Int = 0,
|
||||||
|
val baseOffsetY: Int = 0,
|
||||||
|
val horizontalPosition: HorizontalPosition = HorizontalPosition.START,
|
||||||
|
val onDismiss: Runnable? = null
|
||||||
|
) : PopupWindow(
|
||||||
|
LayoutInflater.from(anchor.context).inflate(R.layout.signal_context_menu, null),
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
) {
|
||||||
|
|
||||||
|
val context: Context = anchor.context
|
||||||
|
|
||||||
|
val mappingAdapter = MappingAdapter().apply {
|
||||||
|
registerFactory(DisplayItem::class.java, ItemViewHolderFactory())
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
|
||||||
|
|
||||||
|
isFocusable = true
|
||||||
|
|
||||||
|
if (onDismiss != null) {
|
||||||
|
setOnDismissListener { onDismiss.run() }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
elevation = 20f
|
||||||
|
}
|
||||||
|
|
||||||
|
contentView.findViewById<RecyclerView>(R.id.signal_context_menu_list).apply {
|
||||||
|
adapter = mappingAdapter
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
itemAnimator = null
|
||||||
|
}
|
||||||
|
|
||||||
|
mappingAdapter.submitList(items.toAdapterItems())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun show() {
|
||||||
|
if (anchor.width == 0 || anchor.height == 0) {
|
||||||
|
anchor.post(this::show)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentView.measure(
|
||||||
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||||
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
)
|
||||||
|
|
||||||
|
val anchorRect = Rect(anchor.left, anchor.top, anchor.right, anchor.bottom).also {
|
||||||
|
if (anchor.parent != container) {
|
||||||
|
container.offsetDescendantRectToMyCoords(anchor, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val menuBottomBound = anchorRect.bottom + contentView.measuredHeight + baseOffsetY
|
||||||
|
val menuTopBound = anchorRect.top - contentView.measuredHeight - baseOffsetY
|
||||||
|
|
||||||
|
val screenBottomBound = container.height
|
||||||
|
val screenTopBound = container.y
|
||||||
|
|
||||||
|
val offsetY: Int
|
||||||
|
|
||||||
|
if (menuBottomBound < screenBottomBound) {
|
||||||
|
offsetY = baseOffsetY
|
||||||
|
} else if (menuTopBound > screenTopBound) {
|
||||||
|
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
|
||||||
|
mappingAdapter.submitList(items.reversed().toAdapterItems())
|
||||||
|
} else {
|
||||||
|
offsetY = -((anchorRect.height() / 2) + (contentView.measuredHeight / 2) + baseOffsetY)
|
||||||
|
}
|
||||||
|
|
||||||
|
val offsetX: Int = when (horizontalPosition) {
|
||||||
|
HorizontalPosition.START -> {
|
||||||
|
if (ViewUtil.isLtr(context)) {
|
||||||
|
baseOffsetX
|
||||||
|
} else {
|
||||||
|
-(baseOffsetX + contentView.measuredWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalPosition.END -> {
|
||||||
|
if (ViewUtil.isLtr(context)) {
|
||||||
|
-(baseOffsetX + contentView.measuredWidth - anchorRect.width())
|
||||||
|
} else {
|
||||||
|
baseOffsetX - anchorRect.width()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showAsDropDown(anchor, offsetX, offsetY)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<ActionItem>.toAdapterItems(): List<DisplayItem> {
|
||||||
|
return this.mapIndexed { index, item ->
|
||||||
|
val displayType: DisplayType = when {
|
||||||
|
this.size == 1 -> DisplayType.ONLY
|
||||||
|
index == 0 -> DisplayType.TOP
|
||||||
|
index == this.size - 1 -> DisplayType.BOTTOM
|
||||||
|
else -> DisplayType.MIDDLE
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayItem(item, displayType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class DisplayItem(
|
||||||
|
val item: ActionItem,
|
||||||
|
val displayType: DisplayType
|
||||||
|
) : MappingModel<DisplayItem> {
|
||||||
|
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
|
||||||
|
return this == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: DisplayItem): Boolean {
|
||||||
|
return this == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class DisplayType {
|
||||||
|
TOP, BOTTOM, MIDDLE, ONLY
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ItemViewHolder(itemView: View) : MappingViewHolder<DisplayItem>(itemView) {
|
||||||
|
val icon: ImageView = itemView.findViewById(R.id.signal_context_menu_item_icon)
|
||||||
|
val title: TextView = itemView.findViewById(R.id.signal_context_menu_item_title)
|
||||||
|
|
||||||
|
override fun bind(model: DisplayItem) {
|
||||||
|
icon.setImageResource(model.item.iconRes)
|
||||||
|
title.text = model.item.title
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
model.item.action.run()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
when (model.displayType) {
|
||||||
|
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_top)
|
||||||
|
DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_bottom)
|
||||||
|
DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_middle)
|
||||||
|
DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_only)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ItemViewHolderFactory : MappingAdapter.Factory<DisplayItem> {
|
||||||
|
override fun createViewHolder(parent: ViewGroup): MappingViewHolder<DisplayItem> {
|
||||||
|
return ItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.signal_context_menu_item, parent, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class HorizontalPosition {
|
||||||
|
START, END
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param anchor The view to put the pop-up on
|
||||||
|
* @param container A parent of [anchor] that represents the acceptable boundaries of the popup
|
||||||
|
*/
|
||||||
|
class Builder(
|
||||||
|
val anchor: View,
|
||||||
|
val container: ViewGroup
|
||||||
|
) {
|
||||||
|
|
||||||
|
var onDismiss: Runnable? = null
|
||||||
|
var offsetX = 0
|
||||||
|
var offsetY = 0
|
||||||
|
var horizontalPosition = HorizontalPosition.START
|
||||||
|
|
||||||
|
fun onDismiss(onDismiss: Runnable): Builder {
|
||||||
|
this.onDismiss = onDismiss
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun offsetX(offsetPx: Int): Builder {
|
||||||
|
this.offsetX = offsetPx
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun offsetY(offsetPx: Int): Builder {
|
||||||
|
this.offsetY = offsetPx
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun preferredHorizontalPosition(horizontalPosition: HorizontalPosition): Builder {
|
||||||
|
this.horizontalPosition = horizontalPosition
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(items: List<ActionItem>) {
|
||||||
|
SignalContextMenu(
|
||||||
|
anchor = anchor,
|
||||||
|
container = container,
|
||||||
|
items = items,
|
||||||
|
baseOffsetX = offsetX,
|
||||||
|
baseOffsetY = offsetY,
|
||||||
|
horizontalPosition = horizontalPosition,
|
||||||
|
onDismiss = onDismiss
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components.recyclerview;
|
|
||||||
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
public class DeleteItemAnimator extends DefaultItemAnimator {
|
|
||||||
|
|
||||||
public DeleteItemAnimator() {
|
|
||||||
setSupportsChangeAnimations(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean animateAdd(RecyclerView.ViewHolder viewHolder) {
|
|
||||||
dispatchAddFinished(viewHolder);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean animateMove(RecyclerView.ViewHolder viewHolder, int fromX, int fromY, int toX, int toY) {
|
|
||||||
dispatchMoveFinished(viewHolder);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|
||||||
@SuppressLint("BatteryLife")
|
@SuppressLint("BatteryLife")
|
||||||
@@ -31,18 +32,13 @@ public class DozeReminder extends Reminder {
|
|||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
});
|
});
|
||||||
|
|
||||||
setDismissListener(new View.OnClickListener() {
|
setDismissListener(v -> TextSecurePreferences.setPromptedOptimizeDoze(context, true));
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
TextSecurePreferences.setPromptedOptimizeDoze(context, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isEligible(Context context) {
|
public static boolean isEligible(Context context) {
|
||||||
return TextSecurePreferences.isFcmDisabled(context) &&
|
return !SignalStore.account().isFcmEnabled() &&
|
||||||
!TextSecurePreferences.hasPromptedOptimizeDoze(context) &&
|
!TextSecurePreferences.hasPromptedOptimizeDoze(context) &&
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
Build.VERSION.SDK_INT >= 23 &&
|
||||||
!((PowerManager)context.getSystemService(Context.POWER_SERVICE)).isIgnoringBatteryOptimizations(context.getPackageName());
|
!((PowerManager)context.getSystemService(Context.POWER_SERVICE)).isIgnoringBatteryOptimizations(context.getPackageName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.reminder;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|
||||||
@@ -21,6 +22,6 @@ public class PushRegistrationReminder extends Reminder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isEligible(Context context) {
|
public static boolean isEligible(Context context) {
|
||||||
return !TextSecurePreferences.isPushRegistered(context);
|
return !SignalStore.account().isRegistered();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ import androidx.annotation.CallSuper
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.models.AsyncSwitch
|
||||||
|
import org.thoughtcrime.securesms.components.settings.models.Button
|
||||||
|
import org.thoughtcrime.securesms.components.settings.models.Space
|
||||||
|
import org.thoughtcrime.securesms.components.settings.models.Text
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
@@ -30,6 +35,10 @@ class DSLSettingsAdapter : MappingAdapter() {
|
|||||||
registerFactory(SectionHeaderPreference::class.java, LayoutFactory(::SectionHeaderPreferenceViewHolder, R.layout.dsl_section_header))
|
registerFactory(SectionHeaderPreference::class.java, LayoutFactory(::SectionHeaderPreferenceViewHolder, R.layout.dsl_section_header))
|
||||||
registerFactory(SwitchPreference::class.java, LayoutFactory(::SwitchPreferenceViewHolder, R.layout.dsl_switch_preference_item))
|
registerFactory(SwitchPreference::class.java, LayoutFactory(::SwitchPreferenceViewHolder, R.layout.dsl_switch_preference_item))
|
||||||
registerFactory(RadioPreference::class.java, LayoutFactory(::RadioPreferenceViewHolder, R.layout.dsl_radio_preference_item))
|
registerFactory(RadioPreference::class.java, LayoutFactory(::RadioPreferenceViewHolder, R.layout.dsl_radio_preference_item))
|
||||||
|
Text.register(this)
|
||||||
|
Space.register(this)
|
||||||
|
Button.register(this)
|
||||||
|
AsyncSwitch.register(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,8 +106,13 @@ class RadioListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<Radio
|
|||||||
override fun bind(model: RadioListPreference) {
|
override fun bind(model: RadioListPreference) {
|
||||||
super.bind(model)
|
super.bind(model)
|
||||||
|
|
||||||
summaryView.visibility = View.VISIBLE
|
if (model.selected >= 0) {
|
||||||
summaryView.text = model.listItems[model.selected]
|
summaryView.visibility = View.VISIBLE
|
||||||
|
summaryView.text = model.listItems[model.selected]
|
||||||
|
} else {
|
||||||
|
summaryView.visibility = View.GONE
|
||||||
|
Log.w(TAG, "Detected a radio list without a default selection: ${model.dialogTitle}")
|
||||||
|
}
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
var selection = -1
|
var selection = -1
|
||||||
@@ -128,6 +142,10 @@ class RadioListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<Radio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(RadioListPreference::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MultiSelectListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<MultiSelectListPreference>(itemView) {
|
class MultiSelectListPreferenceViewHolder(itemView: View) : PreferenceViewHolder<MultiSelectListPreference>(itemView) {
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.EdgeEffect
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||||
|
|
||||||
|
abstract class DSLSettingsBottomSheetFragment(
|
||||||
|
@LayoutRes private val layoutId: Int = R.layout.dsl_settings_bottom_sheet,
|
||||||
|
val layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) },
|
||||||
|
override val peekHeightPercentage: Float = 1f
|
||||||
|
) : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
protected lateinit var recyclerView: RecyclerView
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(layoutId, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
recyclerView = view.findViewById(R.id.recycler)
|
||||||
|
recyclerView.edgeEffectFactory = EdgeEffectFactory()
|
||||||
|
val adapter = DSLSettingsAdapter()
|
||||||
|
|
||||||
|
recyclerView.layoutManager = layoutManagerProducer(requireContext())
|
||||||
|
recyclerView.adapter = adapter
|
||||||
|
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
|
||||||
|
|
||||||
|
bindAdapter(adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun bindAdapter(adapter: DSLSettingsAdapter)
|
||||||
|
|
||||||
|
private class EdgeEffectFactory : RecyclerView.EdgeEffectFactory() {
|
||||||
|
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
|
||||||
|
return super.createEdgeEffect(view, direction).apply {
|
||||||
|
if (Build.VERSION.SDK_INT > 21) {
|
||||||
|
color =
|
||||||
|
requireNotNull(ContextCompat.getColor(view.context, R.color.settings_ripple_color))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,35 +3,76 @@ package org.thoughtcrime.securesms.components.settings
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.util.SpanUtil
|
import org.thoughtcrime.securesms.util.SpanUtil
|
||||||
|
|
||||||
sealed class DSLSettingsText {
|
sealed class DSLSettingsText {
|
||||||
|
|
||||||
|
protected abstract val modifiers: List<Modifier>
|
||||||
|
|
||||||
private data class FromResource(
|
private data class FromResource(
|
||||||
@StringRes private val stringId: Int,
|
@StringRes private val stringId: Int,
|
||||||
@ColorInt private val textColor: Int?
|
override val modifiers: List<Modifier>
|
||||||
) : DSLSettingsText() {
|
) : DSLSettingsText() {
|
||||||
override fun resolve(context: Context): CharSequence {
|
override fun getCharSequence(context: Context): CharSequence {
|
||||||
val text = context.getString(stringId)
|
return context.getString(stringId)
|
||||||
|
|
||||||
return if (textColor == null) {
|
|
||||||
text
|
|
||||||
} else {
|
|
||||||
SpanUtil.color(textColor, text)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class FromCharSequence(private val charSequence: CharSequence) : DSLSettingsText() {
|
private data class FromCharSequence(
|
||||||
override fun resolve(context: Context): CharSequence = charSequence
|
private val charSequence: CharSequence,
|
||||||
|
override val modifiers: List<Modifier>
|
||||||
|
) : DSLSettingsText() {
|
||||||
|
override fun getCharSequence(context: Context): CharSequence = charSequence
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun resolve(context: Context): CharSequence
|
protected abstract fun getCharSequence(context: Context): CharSequence
|
||||||
|
|
||||||
|
fun resolve(context: Context): CharSequence {
|
||||||
|
val text: CharSequence = getCharSequence(context)
|
||||||
|
return modifiers.fold(text) { t, m -> m.modify(context, t) }
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(@StringRes stringId: Int, @ColorInt textColor: Int? = null): DSLSettingsText =
|
fun from(@StringRes stringId: Int, @ColorInt textColor: Int): DSLSettingsText =
|
||||||
FromResource(stringId, textColor)
|
FromResource(stringId, listOf(ColorModifier(textColor)))
|
||||||
|
|
||||||
fun from(charSequence: CharSequence): DSLSettingsText = FromCharSequence(charSequence)
|
fun from(@StringRes stringId: Int, vararg modifiers: Modifier): DSLSettingsText =
|
||||||
|
FromResource(stringId, modifiers.toList())
|
||||||
|
|
||||||
|
fun from(charSequence: CharSequence, vararg modifiers: Modifier): DSLSettingsText =
|
||||||
|
FromCharSequence(charSequence, modifiers.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Modifier {
|
||||||
|
fun modify(context: Context, charSequence: CharSequence): CharSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
class ColorModifier(@ColorInt private val textColor: Int) : Modifier {
|
||||||
|
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
||||||
|
return SpanUtil.color(textColor, charSequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object CenterModifier : Modifier {
|
||||||
|
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
||||||
|
return SpanUtil.center(charSequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Title2BoldModifier : TextAppearanceModifier(R.style.TextAppearance_Signal_Title2_Bold)
|
||||||
|
object Body1BoldModifier : TextAppearanceModifier(R.style.TextAppearance_Signal_Body1_Bold)
|
||||||
|
|
||||||
|
open class TextAppearanceModifier(@StyleRes private val textAppearance: Int) : Modifier {
|
||||||
|
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
||||||
|
return SpanUtil.textAppearance(context, textAppearance, charSequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object BoldModifier : Modifier {
|
||||||
|
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
||||||
|
return SpanUtil.bold(charSequence)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
|
import io.reactivex.rxjava3.subjects.Subject
|
||||||
import org.thoughtcrime.securesms.MainActivity
|
import org.thoughtcrime.securesms.MainActivity
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||||
import org.thoughtcrime.securesms.help.HelpFragment
|
import org.thoughtcrime.securesms.help.HelpFragment
|
||||||
import org.thoughtcrime.securesms.keyvalue.SettingsValues
|
import org.thoughtcrime.securesms.keyvalue.SettingsValues
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
@@ -18,10 +22,13 @@ private const val START_LOCATION = "app.settings.start.location"
|
|||||||
private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_PREFERENCES"
|
private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_PREFERENCES"
|
||||||
private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
|
private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
|
||||||
|
|
||||||
class AppSettingsActivity : DSLSettingsActivity() {
|
class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||||
|
|
||||||
private var wasConfigurationUpdated = false
|
private var wasConfigurationUpdated = false
|
||||||
|
|
||||||
|
override val donationPaymentRepository: DonationPaymentRepository by lazy { DonationPaymentRepository(this) }
|
||||||
|
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
if (intent?.hasExtra(ARG_NAV_GRAPH) != true) {
|
if (intent?.hasExtra(ARG_NAV_GRAPH) != true) {
|
||||||
intent?.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
intent?.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
||||||
@@ -40,6 +47,9 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
|||||||
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
|
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
|
||||||
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
|
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
|
||||||
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
|
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
|
||||||
|
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToSubscriptions()
|
||||||
|
StartLocation.BOOST -> AppSettingsFragmentDirections.actionAppSettingsFragmentToBoostsFragment()
|
||||||
|
StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +76,12 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
finish()
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
outState.putBoolean(STATE_WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated)
|
outState.putBoolean(STATE_WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated)
|
||||||
@@ -79,8 +95,12 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun home(context: Context): Intent = getIntentForStartLocation(context, StartLocation.HOME)
|
fun home(context: Context): Intent = getIntentForStartLocation(context, StartLocation.HOME)
|
||||||
|
|
||||||
@@ -102,6 +122,15 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun changeNumber(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CHANGE_NUMBER)
|
fun changeNumber(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CHANGE_NUMBER)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun subscriptions(context: Context): Intent = getIntentForStartLocation(context, StartLocation.SUBSCRIPTIONS)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun boost(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BOOST)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun manageSubscriptions(context: Context): Intent = getIntentForStartLocation(context, StartLocation.MANAGE_SUBSCRIPTIONS)
|
||||||
|
|
||||||
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
|
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
|
||||||
return Intent(context, AppSettingsActivity::class.java)
|
return Intent(context, AppSettingsActivity::class.java)
|
||||||
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
||||||
@@ -115,7 +144,10 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
|||||||
HELP(2),
|
HELP(2),
|
||||||
PROXY(3),
|
PROXY(3),
|
||||||
NOTIFICATIONS(4),
|
NOTIFICATIONS(4),
|
||||||
CHANGE_NUMBER(5);
|
CHANGE_NUMBER(5),
|
||||||
|
SUBSCRIPTIONS(6),
|
||||||
|
BOOST(7),
|
||||||
|
MANAGE_SUBSCRIPTIONS(8);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromCode(code: Int?): StartLocation {
|
fun fromCode(code: Int?): StartLocation {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package org.thoughtcrime.securesms.components.settings.app
|
|||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
@@ -14,27 +15,40 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
|||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||||
import org.thoughtcrime.securesms.components.settings.configure
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||||
|
|
||||||
class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__menu_settings) {
|
class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__menu_settings) {
|
||||||
|
|
||||||
|
private val viewModel: AppSettingsViewModel by viewModels(
|
||||||
|
factoryProducer = {
|
||||||
|
AppSettingsViewModel.Factory(SubscriptionsRepository(ApplicationDependencies.getDonationsService()))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
adapter.registerFactory(BioPreference::class.java, MappingAdapter.LayoutFactory(::BioPreferenceViewHolder, R.layout.bio_preference_item))
|
adapter.registerFactory(BioPreference::class.java, MappingAdapter.LayoutFactory(::BioPreferenceViewHolder, R.layout.bio_preference_item))
|
||||||
adapter.registerFactory(PaymentsPreference::class.java, MappingAdapter.LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference))
|
adapter.registerFactory(PaymentsPreference::class.java, MappingAdapter.LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference))
|
||||||
|
adapter.registerFactory(SubscriptionPreference::class.java, MappingAdapter.LayoutFactory(::SubscriptionPreferenceViewHolder, R.layout.dsl_preference_item))
|
||||||
val viewModel = ViewModelProvider(this)[AppSettingsViewModel::class.java]
|
|
||||||
|
|
||||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
viewModel.refreshActiveSubscription()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getConfiguration(state: AppSettingsState): DSLConfiguration {
|
private fun getConfiguration(state: AppSettingsState): DSLConfiguration {
|
||||||
return configure {
|
return configure {
|
||||||
|
|
||||||
@@ -130,11 +144,41 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
externalLinkPref(
|
if (FeatureFlags.donorBadges() && PlayServicesUtil.getPlayServicesStatus(requireContext()) == PlayServicesUtil.PlayServicesStatus.SUCCESS) {
|
||||||
title = DSLSettingsText.from(R.string.preferences__donate_to_signal),
|
customPref(
|
||||||
icon = DSLSettingsIcon.from(R.drawable.ic_heart_24),
|
SubscriptionPreference(
|
||||||
linkId = R.string.donate_url
|
title = DSLSettingsText.from(
|
||||||
)
|
if (state.hasActiveSubscription) {
|
||||||
|
R.string.preferences__subscription
|
||||||
|
} else {
|
||||||
|
R.string.preferences__become_a_signal_sustainer
|
||||||
|
}
|
||||||
|
),
|
||||||
|
icon = DSLSettingsIcon.from(R.drawable.ic_heart_24),
|
||||||
|
isActive = state.hasActiveSubscription,
|
||||||
|
onClick = { isActive ->
|
||||||
|
if (isActive) {
|
||||||
|
findNavController().navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToManageDonationsFragment())
|
||||||
|
} else {
|
||||||
|
findNavController().navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToSubscribeFragment())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__signal_boost),
|
||||||
|
icon = DSLSettingsIcon.from(R.drawable.ic_boost_24),
|
||||||
|
onClick = {
|
||||||
|
findNavController().navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToBoostsFragment())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
externalLinkPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__donate_to_signal),
|
||||||
|
icon = DSLSettingsIcon.from(R.drawable.ic_heart_24),
|
||||||
|
linkId = R.string.donate_url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (FeatureFlags.internalUser()) {
|
if (FeatureFlags.internalUser()) {
|
||||||
dividerPref()
|
dividerPref()
|
||||||
@@ -149,6 +193,30 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SubscriptionPreference(
|
||||||
|
override val title: DSLSettingsText,
|
||||||
|
override val summary: DSLSettingsText? = null,
|
||||||
|
override val icon: DSLSettingsIcon? = null,
|
||||||
|
override val isEnabled: Boolean = true,
|
||||||
|
val isActive: Boolean = false,
|
||||||
|
val onClick: (Boolean) -> Unit
|
||||||
|
) : PreferenceModel<SubscriptionPreference>() {
|
||||||
|
override fun areItemsTheSame(newItem: SubscriptionPreference): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: SubscriptionPreference): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) && isActive == newItem.isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SubscriptionPreferenceViewHolder(itemView: View) : PreferenceViewHolder<SubscriptionPreference>(itemView) {
|
||||||
|
override fun bind(model: SubscriptionPreference) {
|
||||||
|
super.bind(model)
|
||||||
|
itemView.setOnClickListener { model.onClick(model.isActive) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class BioPreference(val recipient: Recipient, val onClick: () -> Unit) : PreferenceModel<BioPreference>() {
|
private class BioPreference(val recipient: Recipient, val onClick: () -> Unit) : PreferenceModel<BioPreference>() {
|
||||||
override fun areContentsTheSame(newItem: BioPreference): Boolean {
|
override fun areContentsTheSame(newItem: BioPreference): Boolean {
|
||||||
return super.areContentsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
|
return super.areContentsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
|
||||||
|
|||||||
@@ -2,4 +2,8 @@ package org.thoughtcrime.securesms.components.settings.app
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
|
||||||
data class AppSettingsState(val self: Recipient, val unreadPaymentsCount: Int)
|
data class AppSettingsState(
|
||||||
|
val self: Recipient,
|
||||||
|
val unreadPaymentsCount: Int,
|
||||||
|
val hasActiveSubscription: Boolean
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,18 +2,65 @@ package org.thoughtcrime.securesms.components.settings.app
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData
|
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class AppSettingsViewModel : ViewModel() {
|
class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRepository) : ViewModel() {
|
||||||
|
|
||||||
val unreadPaymentsLiveData = UnreadPaymentsLiveData()
|
private val store = Store(AppSettingsState(Recipient.self(), 0, false))
|
||||||
val selfLiveData: LiveData<Recipient> = Recipient.self().live().liveData
|
|
||||||
|
|
||||||
val state: LiveData<AppSettingsState> = LiveDataUtil.combineLatest(unreadPaymentsLiveData, selfLiveData) { payments, self ->
|
private val unreadPaymentsLiveData = UnreadPaymentsLiveData()
|
||||||
val unreadPaymentsCount = payments.transform { it.unreadCount }.or(0)
|
private val selfLiveData: LiveData<Recipient> = Recipient.self().live().liveData
|
||||||
|
|
||||||
AppSettingsState(self, unreadPaymentsCount)
|
val state: LiveData<AppSettingsState> = store.stateLiveData
|
||||||
|
|
||||||
|
init {
|
||||||
|
store.update(unreadPaymentsLiveData) { payments, state -> state.copy(unreadPaymentsCount = payments.transform { it.unreadCount }.or(0)) }
|
||||||
|
store.update(selfLiveData) { self, state -> state.copy(self = self) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshActiveSubscription() {
|
||||||
|
if (!FeatureFlags.donorBadges()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update {
|
||||||
|
it.copy(hasActiveSubscription = TimeUnit.SECONDS.toMillis(SignalStore.donationsValues().getLastEndOfPeriod()) > System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptionsRepository.getActiveSubscription().subscribeBy(
|
||||||
|
onSuccess = { subscription -> store.update { it.copy(hasActiveSubscription = subscription.activeSubscription != null) } },
|
||||||
|
onError = { throwable ->
|
||||||
|
if (throwable.isNotFoundException()) {
|
||||||
|
Log.w(TAG, "Could not load active subscription due to unset SubscriberId (404).")
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.w(TAG, "Could not load active subscription", throwable)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(private val subscriptionsRepository: SubscriptionsRepository) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return modelClass.cast(AppSettingsViewModel(subscriptionsRepository)) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(AppSettingsViewModel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Throwable.isNotFoundException(): Boolean {
|
||||||
|
return this is PushNetworkException && this.cause is NotFoundException || this is NotFoundException
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNum
|
|||||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments
|
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments
|
||||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getViewModel
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.registration.fragments.BaseEnterCodeFragment
|
import org.thoughtcrime.securesms.registration.fragments.BaseEnterSmsCodeFragment
|
||||||
|
|
||||||
class ChangeNumberEnterCodeFragment : BaseEnterCodeFragment<ChangeNumberViewModel>(R.layout.fragment_change_number_enter_code) {
|
class ChangeNumberEnterSmsCodeFragment : BaseEnterSmsCodeFragment<ChangeNumberViewModel>(R.layout.fragment_change_number_enter_code) {
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
@@ -54,10 +54,10 @@ class ChangeNumberEnterCodeFragment : BaseEnterCodeFragment<ChangeNumberViewMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToRegistrationLock(timeRemaining: Long) {
|
override fun navigateToRegistrationLock(timeRemaining: Long) {
|
||||||
findNavController().navigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
|
findNavController().navigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun navigateToKbsAccountLocked() {
|
override fun navigateToKbsAccountLocked() {
|
||||||
findNavController().navigate(ChangeNumberEnterCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
|
findNavController().navigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
|||||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
|
|
||||||
private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java)
|
private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java)
|
||||||
@@ -53,11 +52,11 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
|||||||
disposables.add(
|
disposables.add(
|
||||||
changeNumberRepository.whoAmI()
|
changeNumberRepository.whoAmI()
|
||||||
.flatMap { whoAmI ->
|
.flatMap { whoAmI ->
|
||||||
if (Objects.equals(whoAmI.number, TextSecurePreferences.getLocalNumber(this))) {
|
if (Objects.equals(whoAmI.number, SignalStore.account().e164)) {
|
||||||
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
|
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
|
||||||
Single.just(false)
|
Single.just(false)
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "Local (${TextSecurePreferences.getLocalNumber(this)}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||||
changeNumberRepository.changeLocalNumber(whoAmI.number)
|
changeNumberRepository.changeLocalNumber(whoAmI.number)
|
||||||
.map { true }
|
.map { true }
|
||||||
}
|
}
|
||||||
@@ -72,7 +71,7 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
|||||||
|
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed)
|
.setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed)
|
||||||
.setMessage(getString(R.string.ChangeNumberLockActivity__your_number_has_been_confirmed_as_s, PhoneNumberFormatter.prettyPrint(TextSecurePreferences.getLocalNumber(this))))
|
.setMessage(getString(R.string.ChangeNumberLockActivity__your_number_has_been_confirmed_as_s, PhoneNumberFormatter.prettyPrint(SignalStore.account().e164!!)))
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
startActivity(MainActivity.clearTop(this))
|
startActivity(MainActivity.clearTop(this))
|
||||||
finish()
|
finish()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.annotation.WorkerThread
|
|||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.CertificateType
|
import org.thoughtcrime.securesms.keyvalue.CertificateType
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
@@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.pin.KbsRepository
|
|||||||
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
|
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
|
||||||
import org.thoughtcrime.securesms.pin.TokenData
|
import org.thoughtcrime.securesms.pin.TokenData
|
||||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
|
||||||
import org.whispersystems.signalservice.api.KbsPinData
|
import org.whispersystems.signalservice.api.KbsPinData
|
||||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||||
@@ -60,9 +59,9 @@ class ChangeNumberRepository(private val context: Context) {
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun changeLocalNumber(e164: String): Single<Unit> {
|
fun changeLocalNumber(e164: String): Single<Unit> {
|
||||||
DatabaseFactory.getRecipientDatabase(context).updateSelfPhone(e164)
|
SignalDatabase.recipients.updateSelfPhone(e164)
|
||||||
|
|
||||||
TextSecurePreferences.setLocalNumber(context, e164)
|
SignalStore.account().setE164(e164)
|
||||||
|
|
||||||
ApplicationDependencies.closeConnections()
|
ApplicationDependencies.closeConnections()
|
||||||
ApplicationDependencies.getIncomingMessageObserver()
|
ApplicationDependencies.getIncomingMessageObserver()
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.registration.VerifyProcessor
|
|||||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
|
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
|
||||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData
|
import org.thoughtcrime.securesms.util.DefaultValueLiveData
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
|
||||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
@@ -167,8 +166,8 @@ class ChangeNumberViewModel(
|
|||||||
|
|
||||||
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
|
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
|
||||||
val context: Application = ApplicationDependencies.getApplication()
|
val context: Application = ApplicationDependencies.getApplication()
|
||||||
val localNumber: String = TextSecurePreferences.getLocalNumber(context)
|
val localNumber: String = SignalStore.account().e164!!
|
||||||
val password: String = TextSecurePreferences.getPushServerPassword(context)
|
val password: String = SignalStore.account().servicePassword!!
|
||||||
|
|
||||||
val viewModel = ChangeNumberViewModel(
|
val viewModel = ChangeNumberViewModel(
|
||||||
localNumber = localNumber,
|
localNumber = localNumber,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.chats
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
@@ -19,7 +19,7 @@ class ChatsSettingsRepository {
|
|||||||
SignalExecutors.BOUNDED.execute {
|
SignalExecutors.BOUNDED.execute {
|
||||||
val isLinkPreviewsEnabled = SignalStore.settings().isLinkPreviewsEnabled
|
val isLinkPreviewsEnabled = SignalStore.settings().isLinkPreviewsEnabled
|
||||||
|
|
||||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
|
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||||
StorageSyncHelper.scheduleSyncForDataChange()
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
ApplicationDependencies.getJobManager().add(
|
ApplicationDependencies.getJobManager().add(
|
||||||
MultiDeviceConfigurationUpdateJob(
|
MultiDeviceConfigurationUpdateJob(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
|||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
import org.thoughtcrime.securesms.components.settings.configure
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||||
import org.thoughtcrime.securesms.util.Util
|
import org.thoughtcrime.securesms.util.Util
|
||||||
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
|
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@@ -18,6 +19,8 @@ class DataAndStorageSettingsFragment : DSLSettingsFragment(R.string.preferences_
|
|||||||
private val autoDownloadValues by lazy { resources.getStringArray(R.array.pref_media_download_entries) }
|
private val autoDownloadValues by lazy { resources.getStringArray(R.array.pref_media_download_entries) }
|
||||||
private val autoDownloadLabels by lazy { resources.getStringArray(R.array.pref_media_download_values) }
|
private val autoDownloadLabels by lazy { resources.getStringArray(R.array.pref_media_download_values) }
|
||||||
|
|
||||||
|
private val sentMediaQualityLabels by lazy { SentMediaQuality.getLabels(requireContext()) }
|
||||||
|
|
||||||
private val callBandwidthLabels by lazy { resources.getStringArray(R.array.pref_data_and_storage_call_bandwidth_values) }
|
private val callBandwidthLabels by lazy { resources.getStringArray(R.array.pref_data_and_storage_call_bandwidth_values) }
|
||||||
|
|
||||||
private lateinit var viewModel: DataAndStorageSettingsViewModel
|
private lateinit var viewModel: DataAndStorageSettingsViewModel
|
||||||
@@ -84,6 +87,21 @@ class DataAndStorageSettingsFragment : DSLSettingsFragment(R.string.preferences_
|
|||||||
|
|
||||||
dividerPref()
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.DataAndStorageSettingsFragment__media_quality)
|
||||||
|
|
||||||
|
radioListPref(
|
||||||
|
title = DSLSettingsText.from(R.string.DataAndStorageSettingsFragment__sent_media_quality),
|
||||||
|
listItems = sentMediaQualityLabels,
|
||||||
|
selected = SentMediaQuality.values().indexOf(state.sentMediaQuality),
|
||||||
|
onSelected = { viewModel.setSentMediaQuality(SentMediaQuality.values()[it]) }
|
||||||
|
)
|
||||||
|
|
||||||
|
textPref(
|
||||||
|
summary = DSLSettingsText.from(R.string.DataAndStorageSettingsFragment__sending_high_quality_media_will_use_more_data)
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
sectionHeaderPref(R.string.DataAndStorageSettingsFragment__calls)
|
sectionHeaderPref(R.string.DataAndStorageSettingsFragment__calls)
|
||||||
|
|
||||||
radioListPref(
|
radioListPref(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.data
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
|
||||||
class DataAndStorageSettingsRepository {
|
class DataAndStorageSettingsRepository {
|
||||||
@@ -11,7 +11,7 @@ class DataAndStorageSettingsRepository {
|
|||||||
|
|
||||||
fun getTotalStorageUse(consumer: (Long) -> Unit) {
|
fun getTotalStorageUse(consumer: (Long) -> Unit) {
|
||||||
SignalExecutors.BOUNDED.execute {
|
SignalExecutors.BOUNDED.execute {
|
||||||
val breakdown = DatabaseFactory.getMediaDatabase(context).storageBreakdown
|
val breakdown = SignalDatabase.media.storageBreakdown
|
||||||
|
|
||||||
consumer(listOf(breakdown.audioSize, breakdown.documentSize, breakdown.photoSize, breakdown.videoSize).sum())
|
consumer(listOf(breakdown.audioSize, breakdown.documentSize, breakdown.photoSize, breakdown.videoSize).sum())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.components.settings.app.data
|
package org.thoughtcrime.securesms.components.settings.app.data
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||||
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
|
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
|
||||||
|
|
||||||
data class DataAndStorageSettingsState(
|
data class DataAndStorageSettingsState(
|
||||||
@@ -8,5 +9,6 @@ data class DataAndStorageSettingsState(
|
|||||||
val wifiAutoDownloadValues: Set<String>,
|
val wifiAutoDownloadValues: Set<String>,
|
||||||
val roamingAutoDownloadValues: Set<String>,
|
val roamingAutoDownloadValues: Set<String>,
|
||||||
val callBandwidthMode: CallBandwidthMode,
|
val callBandwidthMode: CallBandwidthMode,
|
||||||
val isProxyEnabled: Boolean
|
val isProxyEnabled: Boolean,
|
||||||
|
val sentMediaQuality: SentMediaQuality
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.util.livedata.Store
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
|
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
|
||||||
@@ -46,6 +47,11 @@ class DataAndStorageSettingsViewModel(
|
|||||||
getStateAndCopyStorageUsage()
|
getStateAndCopyStorageUsage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSentMediaQuality(sentMediaQuality: SentMediaQuality) {
|
||||||
|
SignalStore.settings().sentMediaQuality = sentMediaQuality
|
||||||
|
getStateAndCopyStorageUsage()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getStateAndCopyStorageUsage() {
|
private fun getStateAndCopyStorageUsage() {
|
||||||
store.update { getState().copy(totalStorageUse = it.totalStorageUse) }
|
store.update { getState().copy(totalStorageUse = it.totalStorageUse) }
|
||||||
}
|
}
|
||||||
@@ -62,7 +68,8 @@ class DataAndStorageSettingsViewModel(
|
|||||||
ApplicationDependencies.getApplication()
|
ApplicationDependencies.getApplication()
|
||||||
),
|
),
|
||||||
callBandwidthMode = SignalStore.settings().callBandwidthMode,
|
callBandwidthMode = SignalStore.settings().callBandwidthMode,
|
||||||
isProxyEnabled = SignalStore.proxy().isProxyEnabled
|
isProxyEnabled = SignalStore.proxy().isProxyEnabled,
|
||||||
|
sentMediaQuality = SignalStore.settings().sentMediaQuality
|
||||||
)
|
)
|
||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
|
|||||||
@@ -15,16 +15,20 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
|||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
import org.thoughtcrime.securesms.components.settings.configure
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
|
||||||
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
|
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob
|
||||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||||
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob
|
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob
|
||||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob
|
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob
|
||||||
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
|
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
|
||||||
|
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.payments.DataExportUtil
|
import org.thoughtcrime.securesms.payments.DataExportUtil
|
||||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask
|
||||||
|
|
||||||
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {
|
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {
|
||||||
@@ -234,6 +238,14 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_force_emoji_download),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_force_emoji_download_description),
|
||||||
|
onClick = {
|
||||||
|
ApplicationDependencies.getJobManager().add(DownloadLatestEmojiDataJob(true))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
dividerPref()
|
dividerPref()
|
||||||
|
|
||||||
sectionHeaderPref(R.string.preferences__internal_sender_key)
|
sectionHeaderPref(R.string.preferences__internal_sender_key)
|
||||||
@@ -308,6 +320,17 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (FeatureFlags.donorBadges() && SignalStore.donationsValues().getSubscriber() != null) {
|
||||||
|
sectionHeaderPref(R.string.preferences__internal_badges)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_badges_enqueue_redemption),
|
||||||
|
onClick = {
|
||||||
|
enqueueSubscriptionRedemption()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,13 +399,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun clearAllSenderKeyState() {
|
private fun clearAllSenderKeyState() {
|
||||||
DatabaseFactory.getSenderKeyDatabase(requireContext()).deleteAll()
|
SignalDatabase.senderKeys.deleteAll()
|
||||||
DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll()
|
SignalDatabase.senderKeyShared.deleteAll()
|
||||||
Toast.makeText(context, "Deleted all sender key state.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Deleted all sender key state.", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearAllSenderKeySharedState() {
|
private fun clearAllSenderKeySharedState() {
|
||||||
DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll()
|
SignalDatabase.senderKeyShared.deleteAll()
|
||||||
Toast.makeText(context, "Deleted all sender key shared state.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Deleted all sender key shared state.", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,4 +413,8 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
|||||||
LocalMetricsDatabase.getInstance(ApplicationDependencies.getApplication()).clear()
|
LocalMetricsDatabase.getInstance(ApplicationDependencies.getApplication()).clear()
|
||||||
Toast.makeText(context, "Cleared all local metrics state.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Cleared all local metrics state.", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun enqueueSubscriptionRedemption() {
|
||||||
|
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.components.settings.app.notifications
|
package org.thoughtcrime.securesms.components.settings.app.notifications
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.ColorFilter
|
import android.graphics.ColorFilter
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
@@ -10,6 +11,7 @@ import android.net.Uri
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
@@ -241,7 +243,6 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun launchMessageSoundSelectionIntent() {
|
private fun launchMessageSoundSelectionIntent() {
|
||||||
val current = SignalStore.settings().messageNotificationSound
|
val current = SignalStore.settings().messageNotificationSound
|
||||||
|
|
||||||
@@ -255,7 +256,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
|||||||
)
|
)
|
||||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current)
|
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current)
|
||||||
|
|
||||||
startActivityForResult(intent, MESSAGE_SOUND_SELECT)
|
openRingtonePicker(intent, MESSAGE_SOUND_SELECT)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(26)
|
@RequiresApi(26)
|
||||||
@@ -269,7 +270,6 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun launchCallRingtoneSelectionIntent() {
|
private fun launchCallRingtoneSelectionIntent() {
|
||||||
val current = SignalStore.settings().callRingtone
|
val current = SignalStore.settings().callRingtone
|
||||||
|
|
||||||
@@ -283,7 +283,16 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
|||||||
)
|
)
|
||||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current)
|
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current)
|
||||||
|
|
||||||
startActivityForResult(intent, CALL_RINGTONE_SELECT)
|
openRingtonePicker(intent, CALL_RINGTONE_SELECT)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun openRingtonePicker(intent: Intent, requestCode: Int) {
|
||||||
|
try {
|
||||||
|
startActivityForResult(intent, requestCode)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(requireContext(), R.string.NotificationSettingsFragment__failed_to_open_picker, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LedColorPreference(
|
private class LedColorPreference(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.privacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
@@ -16,7 +16,7 @@ class PrivacySettingsRepository {
|
|||||||
|
|
||||||
fun getBlockedCount(consumer: (Int) -> Unit) {
|
fun getBlockedCount(consumer: (Int) -> Unit) {
|
||||||
SignalExecutors.BOUNDED.execute {
|
SignalExecutors.BOUNDED.execute {
|
||||||
val recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
val recipientDatabase = SignalDatabase.recipients
|
||||||
|
|
||||||
consumer(recipientDatabase.blocked.count)
|
consumer(recipientDatabase.blocked.count)
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ class PrivacySettingsRepository {
|
|||||||
|
|
||||||
fun syncReadReceiptState() {
|
fun syncReadReceiptState() {
|
||||||
SignalExecutors.BOUNDED.execute {
|
SignalExecutors.BOUNDED.execute {
|
||||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
|
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||||
StorageSyncHelper.scheduleSyncForDataChange()
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
ApplicationDependencies.getJobManager().add(
|
ApplicationDependencies.getJobManager().add(
|
||||||
MultiDeviceConfigurationUpdateJob(
|
MultiDeviceConfigurationUpdateJob(
|
||||||
@@ -40,7 +40,7 @@ class PrivacySettingsRepository {
|
|||||||
fun syncTypingIndicatorsState() {
|
fun syncTypingIndicatorsState() {
|
||||||
val enabled = TextSecurePreferences.isTypingIndicatorsEnabled(context)
|
val enabled = TextSecurePreferences.isTypingIndicatorsEnabled(context)
|
||||||
|
|
||||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
|
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||||
StorageSyncHelper.scheduleSyncForDataChange()
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
ApplicationDependencies.getJobManager().add(
|
ApplicationDependencies.getJobManager().add(
|
||||||
MultiDeviceConfigurationUpdateJob(
|
MultiDeviceConfigurationUpdateJob(
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
|||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
import org.thoughtcrime.securesms.components.settings.configure
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity
|
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
import org.thoughtcrime.securesms.util.SpanUtil
|
import org.thoughtcrime.securesms.util.SpanUtil
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
|
|
||||||
class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__advanced) {
|
class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__advanced) {
|
||||||
@@ -168,7 +168,7 @@ class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences
|
|||||||
|
|
||||||
private fun getPushToggleSummary(isPushEnabled: Boolean): String {
|
private fun getPushToggleSummary(isPushEnabled: Boolean): String {
|
||||||
return if (isPushEnabled) {
|
return if (isPushEnabled) {
|
||||||
PhoneNumberFormatter.prettyPrint(TextSecurePreferences.getLocalNumber(requireContext()))
|
PhoneNumberFormatter.prettyPrint(SignalStore.account().e164!!)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.preferences__free_private_messages_and_calls)
|
getString(R.string.preferences__free_private_messages_and_calls)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import com.google.android.gms.tasks.Tasks
|
|||||||
import com.google.firebase.installations.FirebaseInstallations
|
import com.google.firebase.installations.FirebaseInstallations
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
@@ -30,7 +30,7 @@ class AdvancedPrivacySettingsRepository(private val context: Context) {
|
|||||||
} catch (e: AuthorizationFailedException) {
|
} catch (e: AuthorizationFailedException) {
|
||||||
Log.w(TAG, e)
|
Log.w(TAG, e)
|
||||||
}
|
}
|
||||||
if (!TextSecurePreferences.isFcmDisabled(context)) {
|
if (SignalStore.account().fcmEnabled) {
|
||||||
Tasks.await(FirebaseInstallations.getInstance().delete())
|
Tasks.await(FirebaseInstallations.getInstance().delete())
|
||||||
}
|
}
|
||||||
DisablePushMessagesResult.SUCCESS
|
DisablePushMessagesResult.SUCCESS
|
||||||
@@ -51,7 +51,7 @@ class AdvancedPrivacySettingsRepository(private val context: Context) {
|
|||||||
|
|
||||||
fun syncShowSealedSenderIconState() {
|
fun syncShowSealedSenderIconState() {
|
||||||
SignalExecutors.BOUNDED.execute {
|
SignalExecutors.BOUNDED.execute {
|
||||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
|
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||||
StorageSyncHelper.scheduleSyncForDataChange()
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
ApplicationDependencies.getJobManager().add(
|
ApplicationDependencies.getJobManager().add(
|
||||||
MultiDeviceConfigurationUpdateJob(
|
MultiDeviceConfigurationUpdateJob(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class AdvancedPrivacySettingsViewModel(
|
|||||||
repository.disablePushMessages {
|
repository.disablePushMessages {
|
||||||
when (it) {
|
when (it) {
|
||||||
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
|
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
|
||||||
TextSecurePreferences.setPushRegistered(ApplicationDependencies.getApplication(), false)
|
SignalStore.account().setRegistered(false)
|
||||||
SignalStore.registrationValues().clearRegistrationComplete()
|
SignalStore.registrationValues().clearRegistrationComplete()
|
||||||
SignalStore.registrationValues().clearHasUploadedProfile()
|
SignalStore.registrationValues().clearHasUploadedProfile()
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ class AdvancedPrivacySettingsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getState() = AdvancedPrivacySettingsState(
|
private fun getState() = AdvancedPrivacySettingsState(
|
||||||
isPushEnabled = TextSecurePreferences.isPushRegistered(ApplicationDependencies.getApplication()),
|
isPushEnabled = SignalStore.account().isRegistered,
|
||||||
alwaysRelayCalls = TextSecurePreferences.isTurnOnly(ApplicationDependencies.getApplication()),
|
alwaysRelayCalls = TextSecurePreferences.isTurnOnly(ApplicationDependencies.getApplication()),
|
||||||
showSealedSenderStatusIcon = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(
|
showSealedSenderStatusIcon = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(
|
||||||
ApplicationDependencies.getApplication()
|
ApplicationDependencies.getApplication()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.groups.GroupChangeException
|
import org.thoughtcrime.securesms.groups.GroupChangeException
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager
|
import org.thoughtcrime.securesms.groups.GroupManager
|
||||||
@@ -36,7 +36,7 @@ class ExpireTimerSettingsRepository(val context: Context) {
|
|||||||
consumer.invoke(Result.failure(e))
|
consumer.invoke(Result.failure(e))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipientId, newExpirationTime)
|
SignalDatabase.recipients.setExpireMessages(recipientId, newExpirationTime)
|
||||||
val outgoingMessage = OutgoingExpirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L)
|
val outgoingMessage = OutgoingExpirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L)
|
||||||
MessageSender.send(context, outgoingMessage, getThreadId(recipientId), false, null, null)
|
MessageSender.send(context, outgoingMessage, getThreadId(recipientId), false, null, null)
|
||||||
consumer.invoke(Result.success(newExpirationTime))
|
consumer.invoke(Result.success(newExpirationTime))
|
||||||
@@ -46,7 +46,7 @@ class ExpireTimerSettingsRepository(val context: Context) {
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getThreadId(recipientId: RecipientId): Long {
|
private fun getThreadId(recipientId: RecipientId): Long {
|
||||||
val threadDatabase: ThreadDatabase = DatabaseFactory.getThreadDatabase(context)
|
val threadDatabase: ThreadDatabase = SignalDatabase.threads
|
||||||
val recipient: Recipient = Recipient.resolved(recipientId)
|
val recipient: Recipient = Recipient.resolved(recipientId)
|
||||||
return threadDatabase.getOrCreateThreadIdFor(recipient)
|
return threadDatabase.getOrCreateThreadIdFor(recipient)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events that can arise from use of the donations apis.
|
||||||
|
*/
|
||||||
|
sealed class DonationEvent {
|
||||||
|
class GooglePayUnavailableError(val throwable: Throwable) : DonationEvent()
|
||||||
|
object RequestTokenSuccess : DonationEvent()
|
||||||
|
class RequestTokenError(val throwable: Throwable) : DonationEvent()
|
||||||
|
class PaymentConfirmationError(val throwable: Throwable) : DonationEvent()
|
||||||
|
class PaymentConfirmationSuccess(val badge: Badge) : DonationEvent()
|
||||||
|
class SubscriptionCancellationFailed(val throwable: Throwable) : DonationEvent()
|
||||||
|
object SubscriptionCancelled : DonationEvent()
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||||
|
|
||||||
|
class DonationExceptions {
|
||||||
|
class SetupFailed(reason: Throwable) : Exception(reason)
|
||||||
|
object TimedOutWaitingForTokenRedemption : Exception()
|
||||||
|
object RedemptionFailed : Exception()
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import io.reactivex.rxjava3.subjects.Subject
|
||||||
|
|
||||||
|
interface DonationPaymentComponent {
|
||||||
|
val donationPaymentRepository: DonationPaymentRepository
|
||||||
|
val googlePayResultPublisher: Subject<GooglePayResult>
|
||||||
|
|
||||||
|
class GooglePayResult(val requestCode: Int, val resultCode: Int, val data: Intent?)
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import com.google.android.gms.wallet.PaymentData
|
||||||
|
import io.reactivex.rxjava3.core.Completable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.core.util.money.FiatMoney
|
||||||
|
import org.signal.donations.GooglePayApi
|
||||||
|
import org.signal.donations.GooglePayPaymentSource
|
||||||
|
import org.signal.donations.StripeApi
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||||
|
import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob
|
||||||
|
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
|
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||||
|
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
|
||||||
|
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||||
|
import org.thoughtcrime.securesms.util.Environment
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.SubscriptionClientSecret
|
||||||
|
import org.whispersystems.signalservice.internal.EmptyResponse
|
||||||
|
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages bindings with payment APIs
|
||||||
|
*
|
||||||
|
* Steps for setting up payments for a subscription:
|
||||||
|
* 1. Ask GooglePay for a payment token. This will pop up the Google Pay Sheet, which allows the user to select a payment method.
|
||||||
|
* 1. Generate and send a SubscriberId, which is a 32 byte ID representing this user, to Signal Service, which creates a Stripe Customer
|
||||||
|
* 1. Create a SetupIntent via the Stripe API
|
||||||
|
* 1. Create a PaymentMethod vai the Stripe API, utilizing the token from Google Pay
|
||||||
|
* 1. Confirm the SetupIntent via the Stripe API
|
||||||
|
* 1. Set the default PaymentMethod for the customer, using the PaymentMethod id, via the Signal service
|
||||||
|
*
|
||||||
|
* For Boosts:
|
||||||
|
* 1. Ask GooglePay for a payment token. This will pop up the Google Pay Sheet, which allows the user to select a payment method.
|
||||||
|
* 1. Create a PaymentIntent via the Stripe API
|
||||||
|
* 1. Create a PaymentMethod vai the Stripe API, utilizing the token from Google Pay
|
||||||
|
* 1. Confirm the PaymentIntent via the Stripe API
|
||||||
|
*/
|
||||||
|
class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper {
|
||||||
|
|
||||||
|
private val application = activity.application
|
||||||
|
private val googlePayApi = GooglePayApi(activity, StripeApi.Gateway(Environment.Donations.STRIPE_CONFIGURATION), Environment.Donations.GOOGLE_PAY_CONFIGURATION)
|
||||||
|
private val stripeApi = StripeApi(Environment.Donations.STRIPE_CONFIGURATION, this, this, ApplicationDependencies.getOkHttpClient())
|
||||||
|
|
||||||
|
fun isGooglePayAvailable(): Completable = googlePayApi.queryIsReadyToPay()
|
||||||
|
|
||||||
|
fun scheduleSyncForAccountRecordChange() {
|
||||||
|
SignalExecutors.BOUNDED.execute {
|
||||||
|
scheduleSyncForAccountRecordChangeSync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleSyncForAccountRecordChangeSync() {
|
||||||
|
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestTokenFromGooglePay(price: FiatMoney, label: String, requestCode: Int) {
|
||||||
|
Log.d(TAG, "Requesting a token from google pay...")
|
||||||
|
googlePayApi.requestPayment(price, label, requestCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onActivityResult(
|
||||||
|
requestCode: Int,
|
||||||
|
resultCode: Int,
|
||||||
|
data: Intent?,
|
||||||
|
expectedRequestCode: Int,
|
||||||
|
paymentsRequestCallback: GooglePayApi.PaymentRequestCallback
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Processing possible google pay result...")
|
||||||
|
googlePayApi.onActivityResult(requestCode, resultCode, data, expectedRequestCode, paymentsRequestCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun continuePayment(price: FiatMoney, paymentData: PaymentData): Completable {
|
||||||
|
Log.d(TAG, "Creating payment intent...", true)
|
||||||
|
return stripeApi.createPaymentIntent(price, application.getString(R.string.Boost__thank_you_for_your_donation))
|
||||||
|
.onErrorResumeNext { Single.error(DonationExceptions.SetupFailed(it)) }
|
||||||
|
.flatMapCompletable { result ->
|
||||||
|
Log.d(TAG, "Created payment intent.", true)
|
||||||
|
when (result) {
|
||||||
|
is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Completable.error(DonationExceptions.SetupFailed(Exception("Boost amount is too small")))
|
||||||
|
is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Completable.error(DonationExceptions.SetupFailed(Exception("Boost amount is too large")))
|
||||||
|
is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Completable.error(DonationExceptions.SetupFailed(Exception("Boost currency is not supported")))
|
||||||
|
is StripeApi.CreatePaymentIntentResult.Success -> confirmPayment(paymentData, result.paymentIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun continueSubscriptionSetup(paymentData: PaymentData): Completable {
|
||||||
|
Log.d(TAG, "Continuing subscription setup...", true)
|
||||||
|
return stripeApi.createSetupIntent()
|
||||||
|
.flatMapCompletable { result ->
|
||||||
|
Log.d(TAG, "Retrieved SetupIntent, confirming...", true)
|
||||||
|
stripeApi.confirmSetupIntent(GooglePayPaymentSource(paymentData), result.setupIntent).doOnComplete {
|
||||||
|
Log.d(TAG, "Confirmed SetupIntent...", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelActiveSubscription(): Completable {
|
||||||
|
Log.d(TAG, "Canceling active subscription...", true)
|
||||||
|
val localSubscriber = SignalStore.donationsValues().requireSubscriber()
|
||||||
|
return ApplicationDependencies.getDonationsService()
|
||||||
|
.cancelSubscription(localSubscriber.subscriberId)
|
||||||
|
.flatMap(ServiceResponse<EmptyResponse>::flattenResult)
|
||||||
|
.ignoreElement()
|
||||||
|
.doOnComplete { Log.d(TAG, "Cancelled active subscription.", true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ensureSubscriberId(): Completable {
|
||||||
|
Log.d(TAG, "Ensuring SubscriberId exists on Signal service...", true)
|
||||||
|
val subscriberId = SignalStore.donationsValues().getSubscriber()?.subscriberId ?: SubscriberId.generate()
|
||||||
|
return ApplicationDependencies
|
||||||
|
.getDonationsService()
|
||||||
|
.putSubscription(subscriberId)
|
||||||
|
.flatMap(ServiceResponse<EmptyResponse>::flattenResult).ignoreElement()
|
||||||
|
.doOnComplete {
|
||||||
|
Log.d(TAG, "Successfully set SubscriberId exists on Signal service.", true)
|
||||||
|
|
||||||
|
SignalStore
|
||||||
|
.donationsValues()
|
||||||
|
.setSubscriber(Subscriber(subscriberId, SignalStore.donationsValues().getSubscriptionCurrency().currencyCode))
|
||||||
|
|
||||||
|
scheduleSyncForAccountRecordChangeSync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun confirmPayment(paymentData: PaymentData, paymentIntent: StripeApi.PaymentIntent): Completable {
|
||||||
|
Log.d(TAG, "Confirming payment intent...", true)
|
||||||
|
val confirmPayment = stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), paymentIntent)
|
||||||
|
val waitOnRedemption = Completable.create {
|
||||||
|
Log.d(TAG, "Confirmed payment intent.", true)
|
||||||
|
|
||||||
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
var finalJobState: JobTracker.JobState? = null
|
||||||
|
|
||||||
|
BoostReceiptRequestResponseJob.createJobChain(paymentIntent).enqueue { _, jobState ->
|
||||||
|
if (jobState.isComplete) {
|
||||||
|
finalJobState = jobState
|
||||||
|
countDownLatch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||||
|
when (finalJobState) {
|
||||||
|
JobTracker.JobState.SUCCESS -> {
|
||||||
|
Log.d(TAG, "Boost request response job chain succeeded.", true)
|
||||||
|
it.onComplete()
|
||||||
|
}
|
||||||
|
JobTracker.JobState.FAILURE -> {
|
||||||
|
Log.d(TAG, "Boost request response job chain failed permanently.", true)
|
||||||
|
it.onError(DonationExceptions.RedemptionFailed)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Boost request response job chain ignored due to in-progress jobs.", true)
|
||||||
|
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Boost redemption timed out waiting for job completion.", true)
|
||||||
|
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||||
|
}
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
Log.d(TAG, "Boost redemption job interrupted", e, true)
|
||||||
|
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmPayment.andThen(waitOnRedemption)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSubscriptionLevel(subscriptionLevel: String): Completable {
|
||||||
|
return getOrCreateLevelUpdateOperation(subscriptionLevel)
|
||||||
|
.flatMapCompletable { levelUpdateOperation ->
|
||||||
|
val subscriber = SignalStore.donationsValues().requireSubscriber()
|
||||||
|
|
||||||
|
Log.d(TAG, "Attempting to set user subscription level to $subscriptionLevel", true)
|
||||||
|
ApplicationDependencies.getDonationsService().updateSubscriptionLevel(
|
||||||
|
subscriber.subscriberId,
|
||||||
|
subscriptionLevel,
|
||||||
|
subscriber.currencyCode,
|
||||||
|
levelUpdateOperation.idempotencyKey.serialize(),
|
||||||
|
SubscriptionReceiptRequestResponseJob.MUTEX
|
||||||
|
).flatMapCompletable {
|
||||||
|
if (it.status == 200 || it.status == 204) {
|
||||||
|
Log.d(TAG, "Successfully set user subscription to level $subscriptionLevel with response code ${it.status}", true)
|
||||||
|
SignalStore.donationsValues().clearUserManuallyCancelled()
|
||||||
|
SignalStore.donationsValues().clearLevelOperations()
|
||||||
|
LevelUpdate.updateProcessingState(false)
|
||||||
|
Completable.complete()
|
||||||
|
} else {
|
||||||
|
if (it.applicationError.isPresent) {
|
||||||
|
Log.w(TAG, "Failed to set user subscription to level $subscriptionLevel with response code ${it.status}", it.applicationError.get(), true)
|
||||||
|
SignalStore.donationsValues().clearLevelOperations()
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Failed to set user subscription to level $subscriptionLevel", it.executionError.orNull(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
LevelUpdate.updateProcessingState(false)
|
||||||
|
it.flattenResult().ignoreElement()
|
||||||
|
}
|
||||||
|
}.andThen {
|
||||||
|
Log.d(TAG, "Enqueuing request response job chain.", true)
|
||||||
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
var finalJobState: JobTracker.JobState? = null
|
||||||
|
|
||||||
|
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue { _, jobState ->
|
||||||
|
if (jobState.isComplete) {
|
||||||
|
finalJobState = jobState
|
||||||
|
countDownLatch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||||
|
when (finalJobState) {
|
||||||
|
JobTracker.JobState.SUCCESS -> {
|
||||||
|
Log.d(TAG, "Subscription request response job chain succeeded.", true)
|
||||||
|
it.onComplete()
|
||||||
|
}
|
||||||
|
JobTracker.JobState.FAILURE -> {
|
||||||
|
Log.d(TAG, "Subscription request response job chain failed permanently.", true)
|
||||||
|
it.onError(DonationExceptions.RedemptionFailed)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Subscription request response job chain ignored due to in-progress jobs.", true)
|
||||||
|
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Subscription request response job timed out.", true)
|
||||||
|
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||||
|
}
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
Log.w(TAG, "Subscription request response interrupted.", e, true)
|
||||||
|
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.doOnError {
|
||||||
|
LevelUpdate.updateProcessingState(false)
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOrCreateLevelUpdateOperation(subscriptionLevel: String): Single<LevelUpdateOperation> = Single.fromCallable {
|
||||||
|
Log.d(TAG, "Retrieving level update operation for $subscriptionLevel")
|
||||||
|
val levelUpdateOperation = SignalStore.donationsValues().getLevelOperation(subscriptionLevel)
|
||||||
|
if (levelUpdateOperation == null) {
|
||||||
|
val newOperation = LevelUpdateOperation(
|
||||||
|
idempotencyKey = IdempotencyKey.generate(),
|
||||||
|
level = subscriptionLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalStore.donationsValues().setLevelOperation(newOperation)
|
||||||
|
LevelUpdate.updateProcessingState(true)
|
||||||
|
Log.d(TAG, "Created a new operation for $subscriptionLevel")
|
||||||
|
newOperation
|
||||||
|
} else {
|
||||||
|
LevelUpdate.updateProcessingState(true)
|
||||||
|
Log.d(TAG, "Reusing operation for $subscriptionLevel")
|
||||||
|
levelUpdateOperation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchPaymentIntent(price: FiatMoney, description: String?): Single<StripeApi.PaymentIntent> {
|
||||||
|
Log.d(TAG, "Fetching payment intent from Signal service...")
|
||||||
|
return ApplicationDependencies
|
||||||
|
.getDonationsService()
|
||||||
|
.createDonationIntentWithAmount(price.minimumUnitPrecisionString, price.currency.currencyCode, description)
|
||||||
|
.flatMap(ServiceResponse<SubscriptionClientSecret>::flattenResult)
|
||||||
|
.map {
|
||||||
|
StripeApi.PaymentIntent(it.id, it.clientSecret)
|
||||||
|
}.doOnSuccess {
|
||||||
|
Log.d(TAG, "Got payment intent from Signal service!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchSetupIntent(): Single<StripeApi.SetupIntent> {
|
||||||
|
Log.d(TAG, "Fetching setup intent from Signal service...")
|
||||||
|
return Single.fromCallable { SignalStore.donationsValues().requireSubscriber() }
|
||||||
|
.flatMap { ApplicationDependencies.getDonationsService().createSubscriptionPaymentMethod(it.subscriberId) }
|
||||||
|
.flatMap(ServiceResponse<SubscriptionClientSecret>::flattenResult)
|
||||||
|
.map { StripeApi.SetupIntent(it.id, it.clientSecret) }
|
||||||
|
.doOnSuccess {
|
||||||
|
Log.d(TAG, "Got setup intent from Signal service!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDefaultPaymentMethod(paymentMethodId: String): Completable {
|
||||||
|
Log.d(TAG, "Setting default payment method via Signal service...")
|
||||||
|
return Single.fromCallable {
|
||||||
|
SignalStore.donationsValues().requireSubscriber()
|
||||||
|
}.flatMap {
|
||||||
|
ApplicationDependencies.getDonationsService().setDefaultPaymentMethodId(it.subscriberId, paymentMethodId)
|
||||||
|
}.flatMap(ServiceResponse<EmptyResponse>::flattenResult).ignoreElement().doOnComplete {
|
||||||
|
Log.d(TAG, "Set default payment method via Signal service!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(DonationPaymentRepository::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import org.signal.core.util.money.FiatMoney
|
||||||
|
import org.thoughtcrime.securesms.badges.Badges
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.subscription.Subscription
|
||||||
|
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||||
|
import org.whispersystems.signalservice.api.services.DonationsService
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels
|
||||||
|
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||||
|
import java.util.Currency
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository which can query for the user's active subscription as well as a list of available subscriptions,
|
||||||
|
* in the currency indicated.
|
||||||
|
*/
|
||||||
|
class SubscriptionsRepository(private val donationsService: DonationsService) {
|
||||||
|
|
||||||
|
fun getActiveSubscription(): Single<ActiveSubscription> {
|
||||||
|
val localSubscription = SignalStore.donationsValues().getSubscriber()
|
||||||
|
return if (localSubscription != null) {
|
||||||
|
donationsService.getSubscription(localSubscription.subscriberId)
|
||||||
|
.flatMap(ServiceResponse<ActiveSubscription>::flattenResult)
|
||||||
|
} else {
|
||||||
|
Single.just(ActiveSubscription(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSubscriptions(): Single<List<Subscription>> = donationsService.getSubscriptionLevels(Locale.getDefault())
|
||||||
|
.flatMap(ServiceResponse<SubscriptionLevels>::flattenResult)
|
||||||
|
.map { subscriptionLevels ->
|
||||||
|
subscriptionLevels.levels.map { (code, level) ->
|
||||||
|
Subscription(
|
||||||
|
id = code,
|
||||||
|
name = level.name,
|
||||||
|
badge = Badges.fromServiceBadge(level.badge),
|
||||||
|
prices = level.currencies.filter {
|
||||||
|
PlatformCurrencyUtil
|
||||||
|
.getAvailableCurrencyCodes()
|
||||||
|
.contains(it.key)
|
||||||
|
}.map { (currencyCode, price) ->
|
||||||
|
FiatMoney(price, Currency.getInstance(currencyCode))
|
||||||
|
}.toSet(),
|
||||||
|
level = code.toInt()
|
||||||
|
)
|
||||||
|
}.sortedBy {
|
||||||
|
it.level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorSet
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.text.method.DigitsKeyListener
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
|
import androidx.core.animation.doOnEnd
|
||||||
|
import androidx.core.text.isDigitsOnly
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import org.signal.core.util.money.FiatMoney
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||||
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
|
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
import org.thoughtcrime.securesms.util.StringUtil
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
|
import java.lang.Integer.min
|
||||||
|
import java.text.DecimalFormatSymbols
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.util.Currency
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Signal Boost is a one-time ephemeral show of support. Each boost level
|
||||||
|
* can unlock a corresponding badge for a time determined by the server.
|
||||||
|
*/
|
||||||
|
data class Boost(
|
||||||
|
val price: FiatMoney
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A heading containing a 96dp rendering of the boost's badge.
|
||||||
|
*/
|
||||||
|
class HeadingModel(
|
||||||
|
val boostBadge: Badge
|
||||||
|
) : PreferenceModel<HeadingModel>() {
|
||||||
|
override fun areItemsTheSame(newItem: HeadingModel): Boolean = true
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: HeadingModel): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) && newItem.boostBadge == boostBadge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadingModel : PreferenceModel<LoadingModel>() {
|
||||||
|
override fun areItemsTheSame(newItem: LoadingModel): Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadingViewHolder(itemView: View) : MappingViewHolder<LoadingModel>(itemView) {
|
||||||
|
|
||||||
|
private val animator: Animator = AnimatorSet().apply {
|
||||||
|
val fadeTo25Animator = ObjectAnimator.ofFloat(itemView, "alpha", 0.8f, 0.25f).apply {
|
||||||
|
duration = 1000L
|
||||||
|
}
|
||||||
|
|
||||||
|
val fadeTo80Animator = ObjectAnimator.ofFloat(itemView, "alpha", 0.25f, 0.8f).apply {
|
||||||
|
duration = 300L
|
||||||
|
}
|
||||||
|
|
||||||
|
playSequentially(fadeTo25Animator, fadeTo80Animator)
|
||||||
|
doOnEnd {
|
||||||
|
if (itemView.isAttachedToWindow) {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(model: LoadingModel) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
if (animator.isStarted) {
|
||||||
|
animator.resume()
|
||||||
|
} else {
|
||||||
|
animator.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
animator.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A widget that allows a user to select from six different amounts, or enter a custom amount.
|
||||||
|
*/
|
||||||
|
class SelectionModel(
|
||||||
|
val boosts: List<Boost>,
|
||||||
|
val selectedBoost: Boost?,
|
||||||
|
val currency: Currency,
|
||||||
|
override val isEnabled: Boolean,
|
||||||
|
val onBoostClick: (View, Boost) -> Unit,
|
||||||
|
val isCustomAmountFocused: Boolean,
|
||||||
|
val onCustomAmountChanged: (String) -> Unit,
|
||||||
|
val onCustomAmountFocusChanged: (Boolean) -> Unit,
|
||||||
|
) : PreferenceModel<SelectionModel>(isEnabled = isEnabled) {
|
||||||
|
override fun areItemsTheSame(newItem: SelectionModel): Boolean = true
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: SelectionModel): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) &&
|
||||||
|
newItem.boosts == boosts &&
|
||||||
|
newItem.selectedBoost == selectedBoost &&
|
||||||
|
newItem.currency == currency &&
|
||||||
|
newItem.isCustomAmountFocused == isCustomAmountFocused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SelectionViewHolder(itemView: View) : MappingViewHolder<SelectionModel>(itemView) {
|
||||||
|
|
||||||
|
private val boost1: MaterialButton = itemView.findViewById(R.id.boost_1)
|
||||||
|
private val boost2: MaterialButton = itemView.findViewById(R.id.boost_2)
|
||||||
|
private val boost3: MaterialButton = itemView.findViewById(R.id.boost_3)
|
||||||
|
private val boost4: MaterialButton = itemView.findViewById(R.id.boost_4)
|
||||||
|
private val boost5: MaterialButton = itemView.findViewById(R.id.boost_5)
|
||||||
|
private val boost6: MaterialButton = itemView.findViewById(R.id.boost_6)
|
||||||
|
private val custom: AppCompatEditText = itemView.findViewById(R.id.boost_custom)
|
||||||
|
|
||||||
|
private val boostButtons: List<MaterialButton>
|
||||||
|
get() {
|
||||||
|
return if (ViewUtil.isLtr(context)) {
|
||||||
|
listOf(boost1, boost2, boost3, boost4, boost5, boost6)
|
||||||
|
} else {
|
||||||
|
listOf(boost3, boost2, boost1, boost6, boost5, boost4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var filter: MoneyFilter? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
custom.filters = emptyArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(model: SelectionModel) {
|
||||||
|
itemView.isEnabled = model.isEnabled
|
||||||
|
|
||||||
|
model.boosts.zip(boostButtons).forEach { (boost, button) ->
|
||||||
|
button.isSelected = boost == model.selectedBoost && !model.isCustomAmountFocused
|
||||||
|
button.text = FiatMoneyUtil.format(
|
||||||
|
context.resources,
|
||||||
|
boost.price,
|
||||||
|
FiatMoneyUtil
|
||||||
|
.formatOptions()
|
||||||
|
.trimZerosAfterDecimal()
|
||||||
|
)
|
||||||
|
button.setOnClickListener {
|
||||||
|
model.onBoostClick(it, boost)
|
||||||
|
custom.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter == null || filter?.currency != model.currency) {
|
||||||
|
custom.removeTextChangedListener(filter)
|
||||||
|
|
||||||
|
filter = MoneyFilter(model.currency, custom) {
|
||||||
|
model.onCustomAmountChanged(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
custom.keyListener = filter
|
||||||
|
custom.addTextChangedListener(filter)
|
||||||
|
|
||||||
|
custom.setText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
custom.setOnFocusChangeListener { _, hasFocus ->
|
||||||
|
model.onCustomAmountFocusChanged(hasFocus)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.isCustomAmountFocused && !custom.hasFocus()) {
|
||||||
|
ViewUtil.focusAndShowKeyboard(custom)
|
||||||
|
} else if (!model.isCustomAmountFocused && custom.hasFocus()) {
|
||||||
|
ViewUtil.hideKeyboard(context, custom)
|
||||||
|
custom.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class HeadingViewHolder(itemView: View) : MappingViewHolder<HeadingModel>(itemView) {
|
||||||
|
|
||||||
|
private val badgeImageView: BadgeImageView = itemView as BadgeImageView
|
||||||
|
|
||||||
|
override fun bind(model: HeadingModel) {
|
||||||
|
badgeImageView.setBadge(model.boostBadge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
class MoneyFilter(val currency: Currency, private val text: AppCompatEditText? = null, private val onCustomAmountChanged: (String) -> Unit = {}) : DigitsKeyListener(false, true), TextWatcher {
|
||||||
|
|
||||||
|
val separator = DecimalFormatSymbols.getInstance().decimalSeparator
|
||||||
|
val separatorCount = min(1, currency.defaultFractionDigits)
|
||||||
|
val symbol: String = currency.getSymbol(Locale.getDefault())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From Character.isDigit:
|
||||||
|
*
|
||||||
|
* * '\u0030' through '\u0039', ISO-LATIN-1 digits ('0' through '9')
|
||||||
|
* * '\u0660' through '\u0669', Arabic-Indic digits
|
||||||
|
* * '\u06F0' through '\u06F9', Extended Arabic-Indic digits
|
||||||
|
* * '\u0966' through '\u096F', Devanagari digits
|
||||||
|
* * '\uFF10' through '\uFF19', Fullwidth digits
|
||||||
|
*/
|
||||||
|
val digitsGroup: String = "[\\u0030-\\u0039]|[\\u0660-\\u0669]|[\\u06F0-\\u06F9]|[\\u0966-\\u096F]|[\\uFF10-\\uFF19]"
|
||||||
|
|
||||||
|
val pattern: Pattern = "($digitsGroup)*([$separator]){0,$separatorCount}($digitsGroup){0,${currency.defaultFractionDigits}}".toPattern()
|
||||||
|
val symbolPattern: Regex = """\s*${Regex.escape(symbol)}\s*""".toRegex()
|
||||||
|
val leadingZeroesPattern: Regex = """^0*""".toRegex()
|
||||||
|
|
||||||
|
override fun filter(
|
||||||
|
source: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
dest: Spanned,
|
||||||
|
dstart: Int,
|
||||||
|
dend: Int
|
||||||
|
): CharSequence? {
|
||||||
|
|
||||||
|
val result = dest.subSequence(0, dstart).toString() + source.toString() + dest.subSequence(dend, dest.length)
|
||||||
|
val resultWithoutCurrencyPrefix = StringUtil.stripBidiIndicator(result.removePrefix(symbol).removeSuffix(symbol).trim())
|
||||||
|
|
||||||
|
if (resultWithoutCurrencyPrefix.length == 1 && !resultWithoutCurrencyPrefix.isDigitsOnly() && resultWithoutCurrencyPrefix != separator.toString()) {
|
||||||
|
return dest.subSequence(dstart, dend)
|
||||||
|
}
|
||||||
|
|
||||||
|
val matcher = pattern.matcher(resultWithoutCurrencyPrefix)
|
||||||
|
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
return dest.subSequence(dstart, dend)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
if (s.isNullOrEmpty()) return
|
||||||
|
|
||||||
|
val hasSymbol = s.startsWith(symbol) || s.endsWith(symbol)
|
||||||
|
if (hasSymbol && symbolPattern.matchEntire(s.toString()) != null) {
|
||||||
|
s.clear()
|
||||||
|
} else if (!hasSymbol) {
|
||||||
|
val formatter = NumberFormat.getCurrencyInstance()
|
||||||
|
formatter.currency = currency
|
||||||
|
formatter.minimumFractionDigits = 0
|
||||||
|
formatter.maximumFractionDigits = currency.defaultFractionDigits
|
||||||
|
|
||||||
|
val value = s.toString().toDoubleOrNull()
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
val formatted = formatter.format(value)
|
||||||
|
|
||||||
|
text?.removeTextChangedListener(this)
|
||||||
|
|
||||||
|
s.replace(0, s.length, formatted)
|
||||||
|
if (formatted.endsWith(symbol)) {
|
||||||
|
val result: MatchResult? = symbolPattern.find(formatted)
|
||||||
|
if (result != null && result.range.first < s.length) {
|
||||||
|
text?.setSelection(result.range.first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text?.addTextChangedListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val withoutSymbol = s.removePrefix(symbol).removeSuffix(symbol).trim().toString()
|
||||||
|
val withoutLeadingZeroes: String = try {
|
||||||
|
NumberFormat.getInstance().apply {
|
||||||
|
isGroupingUsed = false
|
||||||
|
}.format(withoutSymbol.toBigDecimal()) + (if (withoutSymbol.endsWith(separator)) separator else "")
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
withoutSymbol
|
||||||
|
}.replace(leadingZeroesPattern, "")
|
||||||
|
|
||||||
|
if (withoutSymbol != withoutLeadingZeroes) {
|
||||||
|
text?.removeTextChangedListener(this)
|
||||||
|
|
||||||
|
val start = s.indexOf(withoutSymbol)
|
||||||
|
s.replace(start, start + withoutSymbol.length, withoutLeadingZeroes)
|
||||||
|
|
||||||
|
text?.addTextChangedListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
onCustomAmountChanged(s.removePrefix(symbol).removeSuffix(symbol).trim().toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun register(adapter: MappingAdapter) {
|
||||||
|
adapter.registerFactory(SelectionModel::class.java, MappingAdapter.LayoutFactory({ SelectionViewHolder(it) }, R.layout.boost_preference))
|
||||||
|
adapter.registerFactory(HeadingModel::class.java, MappingAdapter.LayoutFactory({ HeadingViewHolder(it) }, R.layout.boost_preview_preference))
|
||||||
|
adapter.registerFactory(LoadingModel::class.java, MappingAdapter.LayoutFactory({ LoadingViewHolder(it) }, R.layout.boost_loading_preference))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.view.View
|
||||||
|
import com.airbnb.lottie.LottieAnimationView
|
||||||
|
import com.airbnb.lottie.LottieDrawable
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.animation.AnimationCompleteListener
|
||||||
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple mapping model to show a boost animation.
|
||||||
|
*/
|
||||||
|
object BoostAnimation {
|
||||||
|
|
||||||
|
class Model : PreferenceModel<Model>(isEnabled = true) {
|
||||||
|
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||||
|
|
||||||
|
private val lottie: LottieAnimationView = findViewById(R.id.boost_animation_view)
|
||||||
|
|
||||||
|
override fun bind(model: Model) {
|
||||||
|
lottie.playAnimation()
|
||||||
|
lottie.addAnimatorListener(object : AnimationCompleteListener() {
|
||||||
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
lottie.removeAnimatorListener(this)
|
||||||
|
lottie.setMinAndMaxFrame(30, 91)
|
||||||
|
lottie.repeatMode = LottieDrawable.RESTART
|
||||||
|
lottie.repeatCount = LottieDrawable.INFINITE
|
||||||
|
lottie.frame = 30
|
||||||
|
lottie.playAnimation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register(adapter: MappingAdapter) {
|
||||||
|
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.boost_animation_pref))
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user