Compare commits
650 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8501fdffc6 | ||
|
|
4c4cbecd85 | ||
|
|
84833c9ad3 | ||
|
|
131a400921 | ||
|
|
54d937d036 | ||
|
|
a8659bf8e5 | ||
|
|
dfe78cdae6 | ||
|
|
9434894dff | ||
|
|
d028165b51 | ||
|
|
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 | ||
|
|
d82b1ec69b | ||
|
|
8052c13526 | ||
|
|
ed8538547f | ||
|
|
eb8de536e0 | ||
|
|
76728c43e0 | ||
|
|
52cfb57d36 | ||
|
|
a385cb0b68 | ||
|
|
e01381379c | ||
|
|
d01a52c5a8 | ||
|
|
204fff1b9b | ||
|
|
1eda1477a8 | ||
|
|
3135685c0e | ||
|
|
58fdb26f04 | ||
|
|
9bcb1bad8e | ||
|
|
eb6ef3d005 | ||
|
|
f40ba0bf68 | ||
|
|
89df0a2c04 | ||
|
|
69fbd4f3fc | ||
|
|
45267f3590 | ||
|
|
3fb8c6eda8 | ||
|
|
27ce0fd65e | ||
|
|
7e91132e7e | ||
|
|
705839068a | ||
|
|
6625ac02d5 | ||
|
|
4b3580d98a | ||
|
|
6dbbec2631 | ||
|
|
a7b6ebe7fc | ||
|
|
76f52b9086 | ||
|
|
3310246351 | ||
|
|
f3d0b4a671 | ||
|
|
83b9fbac11 | ||
|
|
5ca843825f | ||
|
|
e92c83401b | ||
|
|
e18d9e665f | ||
|
|
cc99febe32 | ||
|
|
e72be42eff | ||
|
|
bad382e2f3 | ||
|
|
e637f15a43 | ||
|
|
6c55916cda | ||
|
|
fbabab0b70 | ||
|
|
e268887255 | ||
|
|
6b07922757 | ||
|
|
a464e57079 | ||
|
|
b5af691cc4 | ||
|
|
5c1b57e4ba | ||
|
|
a5c51ff801 | ||
|
|
d755e1e29e | ||
|
|
b9361112b6 | ||
|
|
32101f7dda | ||
|
|
29e697265c | ||
|
|
4cd9ccc0f1 | ||
|
|
8936d81bc7 | ||
|
|
cc36f83d77 | ||
|
|
64996a8db7 | ||
|
|
7267d77dcb | ||
|
|
2281e83607 | ||
|
|
e6b03b1a4a | ||
|
|
fb86fdfcd9 | ||
|
|
77cf029fdc | ||
|
|
556ca5a573 | ||
|
|
91645e6adc | ||
|
|
4d6bb95aa4 | ||
|
|
55ee68fa2d | ||
|
|
747bc7c3bf | ||
|
|
9c17201eaf | ||
|
|
a24b3d9a60 | ||
|
|
c93d882fe1 | ||
|
|
67403a6a9f | ||
|
|
5f9e72bb3c | ||
|
|
091b38ceb8 | ||
|
|
83dfb984fb | ||
|
|
9f14831fc4 | ||
|
|
48bfcc9b16 | ||
|
|
7028ca9411 | ||
|
|
5175375483 | ||
|
|
e2dbaa605b | ||
|
|
93fd6e7a55 | ||
|
|
b070e6962f | ||
|
|
b1d1b7e31e | ||
|
|
1a5ae603d5 | ||
|
|
3b42bda63d | ||
|
|
2f70a71a6c | ||
|
|
aae368c049 | ||
|
|
dee3d2ff2d | ||
|
|
da2e2a99af | ||
|
|
0c00426c0c | ||
|
|
ccc96d5bfa | ||
|
|
82c12c2f6b | ||
|
|
9416beb4aa | ||
|
|
39b80a48c7 | ||
|
|
d5491a2e84 | ||
|
|
07b19402e6 | ||
|
|
318b4703f2 | ||
|
|
d389697f27 | ||
|
|
7bcc338a49 | ||
|
|
ce2c2002c6 | ||
|
|
d5fbd10406 | ||
|
|
6f6da699a3 | ||
|
|
62d8c115ba | ||
|
|
fd01ee2a87 | ||
|
|
9aa517ad99 | ||
|
|
6c3e1b6a29 | ||
|
|
5d5063ef5f | ||
|
|
b48668455c | ||
|
|
18ba5fa291 | ||
|
|
5e968eb831 | ||
|
|
b4465953d8 | ||
|
|
08a7da3339 | ||
|
|
c1a08616ab | ||
|
|
3761859681 | ||
|
|
8984b763fb | ||
|
|
59c62671b9 | ||
|
|
bcfe8909e5 | ||
|
|
c43fe44e3e | ||
|
|
4ac1134a9b | ||
|
|
08d03cb456 | ||
|
|
e5c172a819 | ||
|
|
4569011e0b | ||
|
|
1031a4e96c | ||
|
|
cdf8e4e1ed | ||
|
|
b589449c34 | ||
|
|
7d7dd101df | ||
|
|
e687fea567 | ||
|
|
e2cb522e87 | ||
|
|
662ba85c5a | ||
|
|
d29ebc7768 | ||
|
|
95fabd7ed1 | ||
|
|
5d5251054c | ||
|
|
c766ba9808 | ||
|
|
8b5fe79849 | ||
|
|
903c5c6db6 | ||
|
|
e2e0caa94a | ||
|
|
520fe481e9 | ||
|
|
7262aefa34 | ||
|
|
8815cdc3de | ||
|
|
8df86962e9 | ||
|
|
573c0fad7f | ||
|
|
a8419d5f02 | ||
|
|
6088f16e3a | ||
|
|
1a5ae592a7 | ||
|
|
6880dfeb62 | ||
|
|
dfecb0efd8 | ||
|
|
2eaadd4337 | ||
|
|
655f3c1219 | ||
|
|
1494a3559d | ||
|
|
f61d7a9f77 | ||
|
|
ee0ab8f035 | ||
|
|
6e85c74e3f | ||
|
|
3b1aa5b176 | ||
|
|
715ad0d459 | ||
|
|
05f7dce503 | ||
|
|
ecfbeb69c5 | ||
|
|
c7fb343b93 | ||
|
|
b0d0814b88 | ||
|
|
9f3765d368 | ||
|
|
fe82c4e487 | ||
|
|
e9bbb1b9ae | ||
|
|
fd3e88707c | ||
|
|
8e8def8b03 | ||
|
|
5b1069018f | ||
|
|
abc71f4fb4 | ||
|
|
2f5e95c0e3 | ||
|
|
d1fd70a807 | ||
|
|
1b924c606a | ||
|
|
9b175fa0dd | ||
|
|
7e7bbad788 | ||
|
|
d8c82add78 | ||
|
|
6e1657b1bd | ||
|
|
1f7b1d91c4 | ||
|
|
e7833df539 | ||
|
|
fae80a242d | ||
|
|
77ff25ec49 | ||
|
|
bb446ac1d5 | ||
|
|
b6a4d01d42 | ||
|
|
bd4dd25460 | ||
|
|
f86c1fe508 | ||
|
|
38f6efbcae | ||
|
|
30a542234b | ||
|
|
8c9bf678fa | ||
|
|
4b465b74e8 | ||
|
|
58a22f0eea | ||
|
|
ddad9acef1 | ||
|
|
1dbb6013cb | ||
|
|
9cc1ae4a29 | ||
|
|
4eb24c3303 | ||
|
|
ec1935572e | ||
|
|
e419d70d51 | ||
|
|
2af5526879 | ||
|
|
6a5aa089ae | ||
|
|
6b5f4ca8c2 | ||
|
|
53e110560a | ||
|
|
82e9c620e8 | ||
|
|
076facbdc2 | ||
|
|
a805f9b6b4 | ||
|
|
969e763997 | ||
|
|
9347227ff5 | ||
|
|
c9ba0432a0 | ||
|
|
e3b7fe7509 | ||
|
|
5332669321 | ||
|
|
a086305c38 | ||
|
|
a712622891 | ||
|
|
1514f91687 | ||
|
|
0dfa6aab09 | ||
|
|
4b6dbac758 | ||
|
|
b816f901a5 | ||
|
|
76d1490810 | ||
|
|
f2ab0b6423 | ||
|
|
e09d162c1e | ||
|
|
c84de8fa60 | ||
|
|
8e020c05f6 | ||
|
|
8c9eb880cf | ||
|
|
d7ddd85a90 | ||
|
|
7d994b2ae1 | ||
|
|
664d6475d9 | ||
|
|
a940487611 | ||
|
|
9f995d61f4 | ||
|
|
a6690e1bde | ||
|
|
d507df2e7e | ||
|
|
fa26eb2017 | ||
|
|
0b53ba8950 | ||
|
|
7447e2497b | ||
|
|
7ac83625d3 | ||
|
|
50dfe7bc25 | ||
|
|
8e32592218 | ||
|
|
a3d72fc06c | ||
|
|
f5a6d61362 | ||
|
|
bca2205945 | ||
|
|
1241f4c0e9 | ||
|
|
f6253ad0bb | ||
|
|
083301185c | ||
|
|
0273d0f285 | ||
|
|
3dc1ce3353 | ||
|
|
f8e077b824 | ||
|
|
aec2ca1d87 |
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
|
||||
|
||||
### 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 -->
|
||||
|
||||
|
||||
4
.github/workflows/android.yml
vendored
@@ -16,10 +16,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: set up JDK 1.8
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
java-version: 11
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
32
.idea/codeStyles/Project.xml
generated
@@ -8,6 +8,38 @@
|
||||
<option name="DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION" value="true" />
|
||||
<option name="ALIGN_MULTILINE_ANNOTATION_PARAMETERS" value="true" />
|
||||
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="androidx" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="junit" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="net" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="org" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="android" withSubpackages="true" static="true" />
|
||||
<package name="androidx" withSubpackages="true" static="true" />
|
||||
<package name="com" withSubpackages="true" static="true" />
|
||||
<package name="junit" withSubpackages="true" static="true" />
|
||||
<package name="net" withSubpackages="true" static="true" />
|
||||
<package name="org" withSubpackages="true" static="true" />
|
||||
<package name="java" withSubpackages="true" static="true" />
|
||||
<package name="javax" withSubpackages="true" static="true" />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
|
||||
@@ -63,4 +63,4 @@ Copyright 2013-2021 Signal
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Google Play and the Google Play logo are trademarks of Google Inc.
|
||||
Google Play and the Google Play logo are trademarks of Google LLC.
|
||||
|
||||
276
app/build.gradle
@@ -9,14 +9,9 @@ apply from: 'translations.gradle'
|
||||
apply from: 'witness-verifications.gradle'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'app.cash.exhaustive'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
|
||||
content {
|
||||
includeGroupByRegex "com\\.github\\.chrisbanes.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
|
||||
content {
|
||||
@@ -29,12 +24,10 @@ repositories {
|
||||
includeGroupByRegex "org\\.signal.*"
|
||||
}
|
||||
}
|
||||
maven { // textdrawable
|
||||
url 'https://dl.bintray.com/amulyakhare/maven'
|
||||
content {
|
||||
includeGroupByRegex "com\\.amulyakhare.*"
|
||||
}
|
||||
maven {
|
||||
url "https://www.jitpack.io"
|
||||
}
|
||||
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
@@ -43,14 +36,10 @@ repositories {
|
||||
}
|
||||
jcenter {
|
||||
content {
|
||||
includeVersion "com.google.android.exoplayer", "exoplayer-core", "2.9.1"
|
||||
includeVersion "com.google.android.exoplayer", "exoplayer-ui", "2.9.1"
|
||||
includeVersion "com.google.android.exoplayer", "extension-mediasession", "2.9.1"
|
||||
includeVersion "mobi.upod", "time-duration-picker", "1.1.3"
|
||||
includeVersion "cn.carbswang.android", "NumberPickerView", "1.0.9"
|
||||
includeVersion "com.takisoft.fix", "colorpicker", "0.9.1"
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -58,7 +47,7 @@ repositories {
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.10.0'
|
||||
artifact = 'com.google.protobuf:protoc:3.11.4'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
@@ -71,8 +60,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 910
|
||||
def canonicalVersionName = "5.22.6"
|
||||
def canonicalVersionCode = 971
|
||||
def canonicalVersionName = "5.27.12"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -84,16 +73,9 @@ def abiPostFix = ['universal' : 0,
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
def selectableVariants = [
|
||||
'internalProdFlipper',
|
||||
'internalProdPerf',
|
||||
'internalProdRelease',
|
||||
'internalStagingFlipper',
|
||||
'internalStagingPerf',
|
||||
'internalStagingRelease',
|
||||
'nightlyProdFlipper',
|
||||
'nightlyProdPerf',
|
||||
'nightlyProdRelease',
|
||||
'nightlyStagingPerf',
|
||||
'playProdDebug',
|
||||
'playProdFlipper',
|
||||
'playProdPerf',
|
||||
@@ -149,11 +131,12 @@ android {
|
||||
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDSH_URL", "\"https://cdsh.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
|
||||
@@ -162,23 +145,27 @@ android {
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
|
||||
buildConfigField "String", "CDSH_CODE_HASH", "\"ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
||||
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
|
||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
||||
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
|
||||
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 "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
||||
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44}"
|
||||
buildConfigField "int[]", "MOBILE_COIN_BLACKLIST", "new int[]{98,963,53,850,7}"
|
||||
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
||||
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_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 {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
@@ -196,6 +183,11 @@ android {
|
||||
}
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -293,14 +285,6 @@ android {
|
||||
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 {
|
||||
dimension 'distribution'
|
||||
versionNameSuffix "-nightly-untagged-${getDateSuffix()}"
|
||||
@@ -334,23 +318,24 @@ android {
|
||||
|
||||
applicationIdSuffix ".staging"
|
||||
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.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", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
||||
"\"51a56084c0b21c6b8f62b1bc792ec9bedac4c7c3964bb08ddcab868158c09982\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
|
||||
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
|
||||
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", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
|
||||
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
|
||||
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,153 +384,163 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.5.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.5'
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation libs.androidx.fragment.ktx
|
||||
lintChecks project(':lintchecks')
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
coreLibraryDesugaring libs.android.tools.desugar
|
||||
|
||||
implementation ('androidx.appcompat:appcompat:1.2.0') {
|
||||
force = true
|
||||
implementation (libs.androidx.appcompat) {
|
||||
version {
|
||||
strictly '1.2.0'
|
||||
}
|
||||
}
|
||||
implementation "androidx.window:window:1.0.0-alpha09"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.navigation:navigation-fragment:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-ui:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-reactivestreams-ktx:2.1.0'
|
||||
implementation "androidx.camera:camera-core:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-camera2:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-lifecycle:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha18"
|
||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||
implementation "androidx.autofill:autofill:1.0.0"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
||||
implementation libs.androidx.window
|
||||
implementation libs.androidx.recyclerview
|
||||
implementation libs.material.material
|
||||
implementation libs.androidx.legacy.support
|
||||
implementation libs.androidx.cardview
|
||||
implementation libs.androidx.preference
|
||||
implementation libs.androidx.legacy.preference
|
||||
implementation libs.androidx.gridlayout
|
||||
implementation libs.androidx.exifinterface
|
||||
implementation libs.androidx.constraintlayout
|
||||
implementation libs.androidx.multidex
|
||||
implementation libs.androidx.navigation.fragment.ktx
|
||||
implementation libs.androidx.navigation.ui.ktx
|
||||
implementation libs.androidx.lifecycle.extensions
|
||||
implementation libs.androidx.lifecycle.viewmodel.savedstate
|
||||
implementation libs.androidx.lifecycle.common.java8
|
||||
implementation libs.androidx.lifecycle.reactivestreams.ktx
|
||||
implementation libs.androidx.camera.core
|
||||
implementation libs.androidx.camera.camera2
|
||||
implementation libs.androidx.camera.lifecycle
|
||||
implementation libs.androidx.camera.view
|
||||
implementation libs.androidx.concurrent.futures
|
||||
implementation libs.androidx.autofill
|
||||
implementation libs.androidx.biometric
|
||||
implementation libs.androidx.sharetarget
|
||||
|
||||
implementation ('com.google.firebase:firebase-messaging:22.0.0') {
|
||||
implementation (libs.firebase.messaging) {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
}
|
||||
|
||||
implementation 'com.google.android.gms:play-services-maps:16.1.0'
|
||||
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
||||
implementation libs.google.play.services.maps
|
||||
implementation libs.google.play.services.auth
|
||||
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:extension-mediasession:2.9.1'
|
||||
implementation libs.bundles.exoplayer
|
||||
|
||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
implementation libs.conscrypt.android
|
||||
implementation libs.signal.aesgcmprovider
|
||||
|
||||
implementation project(':libsignal-service')
|
||||
implementation project(':paging')
|
||||
implementation project(':core-util')
|
||||
implementation project(':video')
|
||||
implementation project(':device-transfer')
|
||||
implementation project(':image-editor')
|
||||
implementation project(':donations')
|
||||
|
||||
implementation 'org.signal:zkgroup-android:0.7.0'
|
||||
implementation "org.whispersystems:signal-client-android:${LIBSIGNAL_CLIENT_VERSION}"
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||
implementation libs.signal.client.android
|
||||
implementation libs.google.protobuf.javalite
|
||||
|
||||
implementation('com.mobilecoin:android-sdk:1.1.0') {
|
||||
implementation(libs.mobilecoin) {
|
||||
exclude group: 'com.google.protobuf'
|
||||
}
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
implementation(libs.signal.argon2) {
|
||||
artifact {
|
||||
type = "aar"
|
||||
}
|
||||
}
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.11.1'
|
||||
implementation libs.signal.ringrtc
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.22"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
|
||||
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
kapt 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
implementation 'pl.tajchert:waitingdots:0.1.0'
|
||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.google.zxing:core:3.2.1'
|
||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||
implementation libs.leolin.shortcutbadger
|
||||
implementation libs.emilsjolander.stickylistheaders
|
||||
implementation libs.jpardogo.materialtabstrip
|
||||
implementation libs.apache.httpclient.android
|
||||
implementation libs.photoview
|
||||
implementation libs.glide.glide
|
||||
kapt libs.glide.compiler
|
||||
kapt libs.androidx.annotation
|
||||
implementation libs.roundedimageview
|
||||
implementation libs.materialish.progress
|
||||
implementation libs.greenrobot.eventbus
|
||||
implementation libs.waitingdots
|
||||
implementation libs.floatingactionbutton
|
||||
implementation libs.google.zxing.android.integration
|
||||
implementation libs.time.duration.picker
|
||||
implementation libs.google.zxing.core
|
||||
implementation (libs.subsampling.scale.image.view) {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
|
||||
implementation (libs.numberpickerview) {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
|
||||
implementation (libs.android.tooltips) {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
|
||||
implementation (libs.android.smsmms) {
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp'
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
||||
}
|
||||
implementation 'com.annimon:stream:1.1.8'
|
||||
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
|
||||
implementation libs.stream
|
||||
implementation (libs.colorpicker) {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
exclude group: 'com.android.support', module: 'recyclerview-v7'
|
||||
}
|
||||
|
||||
implementation 'com.airbnb.android:lottie:3.6.0'
|
||||
implementation libs.lottie
|
||||
|
||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||
implementation libs.stickyheadergrid
|
||||
implementation libs.circular.progress.button
|
||||
|
||||
implementation "net.zetetic:android-database-sqlcipher:4.4.3"
|
||||
implementation "androidx.sqlite:sqlite:2.1.0"
|
||||
implementation libs.signal.android.database.sqlcipher
|
||||
implementation libs.androidx.sqlite
|
||||
|
||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||
implementation (libs.google.ez.vcard) {
|
||||
exclude group: 'com.fasterxml.jackson.core'
|
||||
exclude group: 'org.freemarker'
|
||||
}
|
||||
implementation 'dnsjava:dnsjava:2.1.9'
|
||||
implementation libs.dnsjava
|
||||
|
||||
flipperImplementation 'com.facebook.flipper:flipper:0.91.0'
|
||||
flipperImplementation 'com.facebook.soloader:soloader:0.10.1'
|
||||
flipperImplementation libs.facebook.flipper
|
||||
flipperImplementation libs.facebook.soloader
|
||||
flipperImplementation libs.square.leakcanary
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation 'org.mockito:mockito-core:2.8.9'
|
||||
testImplementation 'org.powermock:powermock-api-mockito2:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.4'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4'
|
||||
testImplementation testLibs.junit.junit
|
||||
testImplementation testLibs.assertj.core
|
||||
testImplementation testLibs.mockito.core
|
||||
testImplementation testLibs.powermock.api.mockito
|
||||
testImplementation testLibs.powermock.module.junit4.core
|
||||
testImplementation testLibs.powermock.module.junit4.rule
|
||||
testImplementation testLibs.powermock.classloading.xstream
|
||||
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation ('org.robolectric:robolectric:4.4') {
|
||||
testImplementation testLibs.androidx.test.core
|
||||
testImplementation (testLibs.robolectric.robolectric) {
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
testImplementation 'org.hamcrest:hamcrest:2.2'
|
||||
testImplementation testLibs.robolectric.shadows.multidex
|
||||
testImplementation testLibs.hamcrest.hamcrest
|
||||
|
||||
testImplementation(testFixtures(project(":libsignal-service")))
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation testLibs.androidx.test.ext.junit
|
||||
androidTestImplementation testLibs.espresso.core
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0"
|
||||
testImplementation testLibs.espresso.core
|
||||
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
|
||||
implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1'
|
||||
implementation libs.kotlin.stdlib.jdk8
|
||||
implementation libs.kotlin.reflect
|
||||
implementation libs.jackson.module.kotlin
|
||||
|
||||
implementation libs.rxjava3.rxandroid
|
||||
implementation libs.rxjava3.rxkotlin
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.0'
|
||||
}
|
||||
|
||||
dependencyVerification {
|
||||
@@ -595,7 +590,8 @@ def getCurrentGitTag() {
|
||||
def output = stdout.toString().trim()
|
||||
|
||||
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 {
|
||||
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;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
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.assertTrue;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class PinHashing_hashPin_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.DatabaseDriver;
|
||||
|
||||
import net.sqlcipher.DatabaseUtils;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
import net.zetetic.database.DatabaseUtils;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@@ -43,14 +42,11 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
||||
@Override
|
||||
public List<Descriptor> getDatabases() {
|
||||
try {
|
||||
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
|
||||
databaseHelperField.setAccessible(true);
|
||||
|
||||
SignalDatabase mainOpenHelper = Objects.requireNonNull((SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext())));
|
||||
SignalDatabase keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
|
||||
SignalDatabaseOpenHelper mainOpenHelper = Objects.requireNonNull(SignalDatabase.getInstance());
|
||||
SignalDatabaseOpenHelper keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
|
||||
SignalDatabaseOpenHelper megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
|
||||
SignalDatabaseOpenHelper jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
|
||||
SignalDatabaseOpenHelper metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
|
||||
|
||||
return Arrays.asList(new Descriptor(mainOpenHelper),
|
||||
new Descriptor(keyValueOpenHelper),
|
||||
@@ -253,9 +249,9 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
||||
}
|
||||
|
||||
static class Descriptor implements DatabaseDescriptor {
|
||||
private final SignalDatabase sqlCipherOpenHelper;
|
||||
private final SignalDatabaseOpenHelper sqlCipherOpenHelper;
|
||||
|
||||
Descriptor(@NonNull SignalDatabase sqlCipherOpenHelper) {
|
||||
Descriptor(@NonNull SignalDatabaseOpenHelper sqlCipherOpenHelper) {
|
||||
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,10 @@
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:largeHeap="true">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.wallet.api.enabled"
|
||||
android:value="true" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/>
|
||||
@@ -176,7 +180,6 @@
|
||||
<activity android:name=".sharing.ShareActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
@@ -318,7 +321,8 @@
|
||||
<activity android:name=".messagedetails.MessageDetailsActivity"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
@@ -366,17 +370,18 @@
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediasend.MediaSendActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
<activity android:name=".mediasend.v2.MediaSelectionActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphraseChangeActivity"
|
||||
android:label="@string/AndroidManifest__change_passphrase"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".VerifyIdentityActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".components.settings.app.AppSettingsActivity"
|
||||
@@ -389,8 +394,12 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".components.settings.app.changenumber.ChangeNumberLockActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<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:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
@@ -761,22 +770,6 @@
|
||||
|
||||
</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">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 164 KiB |
BIN
app/src/main/assets/emoji/People_8.webp
Normal file
|
After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 91 KiB |
@@ -60,6 +60,7 @@ import androidx.camera.core.impl.LensFacingConverter;
|
||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||
import androidx.camera.core.impl.utils.futures.Futures;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
@@ -130,6 +131,11 @@ public final class SignalCameraView extends FrameLayout {
|
||||
// For accessibility event
|
||||
private MotionEvent mUpEvent;
|
||||
|
||||
// BEGIN Custom Signal Code Block
|
||||
private Consumer<Throwable> errorConsumer;
|
||||
private Throwable pendingError;
|
||||
// END Custom Signal Code Block
|
||||
|
||||
public SignalCameraView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
@@ -167,14 +173,32 @@ public final class SignalCameraView extends FrameLayout {
|
||||
* androidx.lifecycle.Lifecycle.State#DESTROYED} state.
|
||||
* @throws IllegalStateException if camera permissions are not granted.
|
||||
*/
|
||||
// BEGIN Custom Signal Code Block
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner) {
|
||||
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner, Consumer<Throwable> errorConsumer) {
|
||||
mCameraModule.bindToLifecycle(lifecycleOwner);
|
||||
this.errorConsumer = errorConsumer;
|
||||
if (pendingError != null) {
|
||||
errorConsumer.accept(pendingError);
|
||||
}
|
||||
}
|
||||
// END Custom Signal Code Block
|
||||
|
||||
|
||||
private void init(Context context, @Nullable AttributeSet attrs) {
|
||||
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
|
||||
mCameraModule = new SignalCameraXModule(this);
|
||||
|
||||
// Begin custom signal code block
|
||||
mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
|
||||
mCameraModule = new SignalCameraXModule(this, error -> {
|
||||
if (errorConsumer != null) {
|
||||
errorConsumer.accept(error);
|
||||
} else {
|
||||
pendingError = error;
|
||||
}
|
||||
});
|
||||
// End custom signal code block
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
|
||||
|
||||
@@ -46,6 +46,7 @@ import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||
import androidx.camera.core.impl.utils.futures.Futures;
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.core.util.Preconditions;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
@@ -123,7 +124,9 @@ final class SignalCameraXModule {
|
||||
@Nullable
|
||||
ProcessCameraProvider mCameraProvider;
|
||||
|
||||
SignalCameraXModule(SignalCameraView view) {
|
||||
// BEGIN Custom Signal Code Block
|
||||
SignalCameraXModule(SignalCameraView view, Consumer<Throwable> errorConsumer) {
|
||||
// END Custom Signal Code Block
|
||||
mCameraView = view;
|
||||
|
||||
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
|
||||
@@ -141,7 +144,9 @@ final class SignalCameraXModule {
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
throw new RuntimeException("CameraX failed to initialize.", t);
|
||||
// BEGIN Custom Signal Code Block
|
||||
errorConsumer.accept(t);
|
||||
// END Custom Signal Code Block
|
||||
}
|
||||
}, CameraXExecutors.mainThreadExecutor());
|
||||
|
||||
@@ -222,17 +227,10 @@ final class SignalCameraXModule {
|
||||
// End Signal Custom Code Block
|
||||
|
||||
Rational targetAspectRatio;
|
||||
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3;
|
||||
} else {
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||
}
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||
// End Signal Custom Code Block
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());
|
||||
|
||||
@@ -19,6 +19,6 @@ public final class AppCapabilities {
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS);
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, FeatureFlags.changeNumber());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -29,6 +28,7 @@ import androidx.multidex.MultiDexApplication;
|
||||
import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.AndroidLogger;
|
||||
@@ -37,10 +37,11 @@ import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
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.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.ApplicationDependencyProvider;
|
||||
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.GroupV1MigrationJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
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.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||
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.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
||||
|
||||
import java.security.Security;
|
||||
@@ -125,7 +127,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
super.onCreate();
|
||||
|
||||
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
|
||||
.addBlocking("sqlcipher-init", () -> SqlCipherLibraryLoader.load(this))
|
||||
.addBlocking("sqlcipher-init", () -> {
|
||||
SqlCipherLibraryLoader.load();
|
||||
SignalDatabase.init(this,
|
||||
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
|
||||
AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret());
|
||||
})
|
||||
.addBlocking("logging", () -> {
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
@@ -135,6 +142,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
|
||||
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
|
||||
})
|
||||
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
|
||||
.addBlocking("app-dependencies", this::initializeAppDependencies)
|
||||
.addBlocking("notification-channels", () -> NotificationChannels.create(this))
|
||||
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
|
||||
@@ -160,7 +168,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(this::cleanAvatarStorage)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
.addNonBlocking(this::initializeGcmCheck)
|
||||
.addNonBlocking(this::initializeFcmCheck)
|
||||
.addNonBlocking(this::initializeSignedPreKeyCheck)
|
||||
.addNonBlocking(this::initializePeriodicTasks)
|
||||
.addNonBlocking(this::initializeCircumvention)
|
||||
@@ -171,12 +179,14 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
||||
.addNonBlocking(EmojiSource::refresh)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||
.addNonBlocking(this::ensureProfileUploaded)
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> DatabaseFactory.getMessageLogDatabase(this).trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
@@ -189,8 +199,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
long startTime = System.currentTimeMillis();
|
||||
Log.i(TAG, "App is now visible.");
|
||||
|
||||
ApplicationDependencies.getFrameRateTracker().begin();
|
||||
ApplicationDependencies.getFrameRateTracker().start();
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
ApplicationDependencies.getDeadlockDetector().start();
|
||||
SubscriptionKeepAliveJob.launchSubscriberIdKeepAliveJobIfNecessary();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
@@ -211,8 +223,9 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
Log.i(TAG, "App is no longer visible.");
|
||||
KeyCachingService.onAppBackgrounded(this);
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||
ApplicationDependencies.getFrameRateTracker().end();
|
||||
ApplicationDependencies.getFrameRateTracker().stop();
|
||||
ApplicationDependencies.getShakeToReport().disable();
|
||||
ApplicationDependencies.getDeadlockDetector().stop();
|
||||
}
|
||||
|
||||
public PersistentLogger getPersistentLogger() {
|
||||
@@ -256,7 +269,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute(() -> LogDatabase.getInstance(this).trimToSize());
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
Log.blockUntilAllWritesFinished();
|
||||
LogDatabase.getInstance(this).trimToSize();
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeCrashHandling() {
|
||||
@@ -278,7 +294,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
private void initializeFirstEverAppLaunch() {
|
||||
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!");
|
||||
AppInitialization.onFirstEverAppLaunch(this);
|
||||
}
|
||||
@@ -294,11 +310,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeGcmCheck() {
|
||||
if (TextSecurePreferences.isPushRegistered(this)) {
|
||||
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6);
|
||||
private void initializeFcmCheck() {
|
||||
if (SignalStore.account().isRegistered()) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -336,14 +352,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
private void initializeRingRtc() {
|
||||
try {
|
||||
if (RtcDeviceLists.hardwareAECBlocked()) {
|
||||
WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
|
||||
}
|
||||
|
||||
if (!RtcDeviceLists.openSLESAllowed()) {
|
||||
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
|
||||
}
|
||||
|
||||
CallManager.initialize(this, new RingRtcLogger());
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
throw new AssertionError("Unable to load ringrtc library", e);
|
||||
@@ -352,7 +360,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
@WorkerThread
|
||||
private void initializeCircumvention() {
|
||||
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
|
||||
if (ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()) {
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(ApplicationContext.this);
|
||||
} catch (Throwable t) {
|
||||
@@ -361,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() {
|
||||
if (TextSecurePreferences.needsFullContactSync(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
|
||||
@@ -391,7 +406,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
@WorkerThread
|
||||
private void initializeCleanup() {
|
||||
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
|
||||
int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();
|
||||
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,8 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
int appCompatNightMode = getDelegate().getLocalNightMode() != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED ? getDelegate().getLocalNightMode()
|
||||
: AppCompatDelegate.getDefaultNightMode();
|
||||
|
||||
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
|
||||
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
|
||||
configuration.orientation = Configuration.ORIENTATION_UNDEFINED;
|
||||
|
||||
applyOverrideConfiguration(configuration);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -12,7 +10,6 @@ import androidx.lifecycle.Observer;
|
||||
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
@@ -29,7 +26,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
@@ -49,11 +45,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
boolean pulseMention,
|
||||
boolean hasWallpaper,
|
||||
boolean isMessageRequestAccepted,
|
||||
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||
boolean canPlayInline,
|
||||
@NonNull Colorizer colorizer);
|
||||
|
||||
ConversationMessage getConversationMessage();
|
||||
@NonNull ConversationMessage getConversationMessage();
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
@@ -61,6 +56,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
// Intentionally Blank.
|
||||
}
|
||||
|
||||
default void updateContactNameColor() {
|
||||
// Intentionally Blank.
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
||||
@@ -71,7 +70,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||
void onReactionClicked(@NonNull MultiselectPart multiselectPart, long messageId, boolean isMms);
|
||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord);
|
||||
@@ -92,6 +91,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onPlayInlineContent(ConversationMessage conversationMessage);
|
||||
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
|
||||
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
|
||||
void onChangeNumberUpdateContact(@NonNull Recipient recipient);
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.lifecycle.Lifecycle;
|
||||
|
||||
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.util.concurrent.SimpleTask;
|
||||
|
||||
@@ -65,7 +65,7 @@ public final class BlockUnblockDialog {
|
||||
Resources resources = context.getResources();
|
||||
|
||||
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.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
|
||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_leave, ((dialog, which) -> onBlock.run()));
|
||||
@@ -104,7 +104,7 @@ public final class BlockUnblockDialog {
|
||||
Resources resources = context.getResources();
|
||||
|
||||
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.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
|
||||
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.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -572,7 +571,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
return UsernameUtil.fetchUuidForUsername(requireContext(), contact.getNumber());
|
||||
return UsernameUtil.fetchAciForUsername(requireContext(), contact.getNumber());
|
||||
}, uuid -> {
|
||||
loadingDialog.dismiss();
|
||||
if (uuid.isPresent()) {
|
||||
|
||||
@@ -191,7 +191,6 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext()));
|
||||
|
||||
TextSecurePreferences.setMultiDevice(DeviceActivity.this, true);
|
||||
TextSecurePreferences.setIsUnidentifiedDeliveryEnabled(context, false);
|
||||
accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, profileKey, verificationCode);
|
||||
|
||||
return SUCCESS;
|
||||
|
||||
@@ -35,6 +35,7 @@ public final class GroupMembersDialog {
|
||||
.show();
|
||||
|
||||
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
|
||||
memberListView.initializeAdapter(fragmentActivity);
|
||||
|
||||
LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId());
|
||||
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.contacts.ContactsCursorLoader.DisplayMode;
|
||||
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.recipients.Recipient;
|
||||
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);
|
||||
|
||||
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) {
|
||||
viewPagerListener.onPageSelected(0);
|
||||
}
|
||||
|
||||
cursor.registerContentObserver(new ContentObserver(new Handler(getMainLooper())) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
onMediaChange();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mediaNotAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
private void onMediaChange() {
|
||||
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
|
||||
|
||||
if (adapter != null) {
|
||||
adapter.checkMedia(mediaPager.getCurrentItem());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) {
|
||||
|
||||
@@ -615,7 +600,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (adapter != null) {
|
||||
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);
|
||||
initializeActionBar();
|
||||
}
|
||||
@@ -628,7 +616,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (adapter != null) {
|
||||
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);
|
||||
}
|
||||
@@ -678,7 +668,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
public @Nullable MediaItem getMediaItemFor(int position) {
|
||||
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) {
|
||||
return mediaPreviewFragment != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkMedia(int currentItem) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) {
|
||||
@@ -789,8 +774,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
super.destroyItem(container, position, object);
|
||||
}
|
||||
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
cursor.moveToPosition(getCursorPosition(position));
|
||||
public @Nullable MediaItem getMediaItemFor(int 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);
|
||||
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
|
||||
@@ -824,14 +816,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
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) {
|
||||
if (leftIsRecent) return position;
|
||||
else return cursor.getCount() - 1 - position;
|
||||
@@ -866,10 +850,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
interface MediaItemAdapter {
|
||||
MediaItem getMediaItemFor(int position);
|
||||
@Nullable MediaItem getMediaItemFor(int position);
|
||||
void pause(int position);
|
||||
@Nullable View getPlaybackControls(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) {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||
builder.setTitle(R.string.MuteDialog_mute_notifications);
|
||||
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, final int which) {
|
||||
final long muteUntil;
|
||||
builder.setItems(R.array.mute_durations, (dialog, which) -> {
|
||||
final long muteUntil;
|
||||
|
||||
switch (which) {
|
||||
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8); break;
|
||||
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
|
||||
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
|
||||
case 4: muteUntil = Long.MAX_VALUE; break;
|
||||
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||
}
|
||||
|
||||
listener.onMuted(muteUntil);
|
||||
switch (which) {
|
||||
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8); break;
|
||||
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
|
||||
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
|
||||
case 4: muteUntil = Long.MAX_VALUE; break;
|
||||
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
|
||||
}
|
||||
|
||||
listener.onMuted(muteUntil);
|
||||
});
|
||||
|
||||
if (cancelListener != null) {
|
||||
|
||||
@@ -26,12 +26,12 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
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.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
@@ -67,7 +67,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
} else {
|
||||
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.");
|
||||
|
||||
AlertDialog progress = SimpleProgressDialog.show(this);
|
||||
@@ -75,7 +75,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
Recipient resolved = Recipient.external(this, number);
|
||||
|
||||
if (!resolved.isRegistered() || !resolved.hasUuid()) {
|
||||
if (!resolved.isRegistered() || !resolved.hasAci()) {
|
||||
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
||||
try {
|
||||
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
|
||||
@@ -103,7 +103,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
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)
|
||||
.withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT))
|
||||
.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()
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.devicetransfer.TransferStatus;
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
|
||||
@@ -50,6 +51,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
private static final int STATE_CREATE_SIGNAL_PIN = 7;
|
||||
private static final int STATE_TRANSFER_ONGOING = 8;
|
||||
private static final int STATE_TRANSFER_LOCKED = 9;
|
||||
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
@@ -58,7 +60,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
protected final void onCreate(Bundle savedInstanceState) {
|
||||
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
|
||||
AppStartup.getInstance().onCriticalRenderEventStart();
|
||||
this.networkAccess = new SignalServiceNetworkAccess(this);
|
||||
this.networkAccess = ApplicationDependencies.getSignalServiceNetworkAccess();
|
||||
onPreCreate();
|
||||
|
||||
final boolean locked = KeyCachingService.isLocked(this);
|
||||
@@ -82,7 +84,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (networkAccess.isCensored(this)) {
|
||||
if (networkAccess.isCensored()) {
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob());
|
||||
}
|
||||
}
|
||||
@@ -153,6 +155,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
|
||||
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
|
||||
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
|
||||
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -176,6 +179,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return STATE_TRANSFER_ONGOING;
|
||||
} else if (SignalStore.misc().isOldDeviceTransferLocked()) {
|
||||
return STATE_TRANSFER_LOCKED;
|
||||
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class) {
|
||||
return STATE_CHANGE_NUMBER_LOCK;
|
||||
} else {
|
||||
return STATE_NORMAL;
|
||||
}
|
||||
@@ -243,6 +248,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return MainActivity.clearTop(this);
|
||||
}
|
||||
|
||||
private Intent getChangeNumberLockIntent() {
|
||||
return ChangeNumberLockActivity.createIntent(this);
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(this, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
|
||||
@@ -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.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.util.Rfc5724Uri;
|
||||
|
||||
@@ -48,7 +48,7 @@ public class SmsSendtoActivity extends Activity {
|
||||
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
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)
|
||||
.withDraftText(destination.getBody())
|
||||
|
||||
@@ -43,32 +43,39 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnticipateInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextSwitcher;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.OneShotPreDrawListener;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.ShapeScrim;
|
||||
import org.thoughtcrime.securesms.components.camera.CameraView;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@@ -79,6 +86,7 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
@@ -109,13 +117,13 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
private static final String IDENTITY_EXTRA = "recipient_identity";
|
||||
private static final String VERIFIED_EXTRA = "verified_state";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
private final VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
|
||||
private final VerifyScanFragment scanFragment = new VerifyScanFragment();
|
||||
|
||||
public static Intent newIntent(@NonNull Context context,
|
||||
@NonNull IdentityDatabase.IdentityRecord identityRecord)
|
||||
@NonNull IdentityRecord identityRecord)
|
||||
{
|
||||
return newIntent(context,
|
||||
identityRecord.getRecipientId(),
|
||||
@@ -124,7 +132,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
}
|
||||
|
||||
public static Intent newIntent(@NonNull Context context,
|
||||
@NonNull IdentityDatabase.IdentityRecord identityRecord,
|
||||
@NonNull IdentityRecord identityRecord,
|
||||
boolean verified)
|
||||
{
|
||||
return newIntent(context,
|
||||
@@ -154,14 +162,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.AndroidManifest__verify_safety_number);
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
|
||||
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));
|
||||
|
||||
scanFragment.setScanListener(this);
|
||||
@@ -214,7 +219,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
|
||||
public static class VerifyDisplayFragment extends Fragment implements ViewTreeObserver.OnScrollChangedListener {
|
||||
|
||||
public static final String RECIPIENT_ID = "recipient_id";
|
||||
public static final String REMOTE_NUMBER = "remote_number";
|
||||
@@ -228,28 +233,41 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
private IdentityKey remoteIdentity;
|
||||
private Fingerprint fingerprint;
|
||||
|
||||
private Toolbar toolbar;
|
||||
private ScrollView scrollView;
|
||||
private View container;
|
||||
private View numbersContainer;
|
||||
private View loading;
|
||||
private View qrCodeContainer;
|
||||
private ImageView qrCode;
|
||||
private ImageView qrVerified;
|
||||
private TextView tapLabel;
|
||||
private TextSwitcher tapLabel;
|
||||
private TextView description;
|
||||
private View.OnClickListener clickListener;
|
||||
private SwitchCompat verified;
|
||||
private Button verifyButton;
|
||||
private View toolbarShadow;
|
||||
private View bottomShadow;
|
||||
|
||||
private TextView[] codes = new TextView[12];
|
||||
private boolean animateSuccessOnDraw = false;
|
||||
private boolean animateFailureOnDraw = false;
|
||||
private boolean currentVerifiedState = false;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
|
||||
this.toolbar = container.findViewById(R.id.toolbar);
|
||||
this.scrollView = container.findViewById(R.id.scroll_view);
|
||||
this.numbersContainer = container.findViewById(R.id.number_table);
|
||||
this.loading = container.findViewById(R.id.loading);
|
||||
this.qrCodeContainer = container.findViewById(R.id.qr_code_container);
|
||||
this.qrCode = container.findViewById(R.id.qr_code);
|
||||
this.verified = container.findViewById(R.id.verified_switch);
|
||||
this.verifyButton = container.findViewById(R.id.verify_button);
|
||||
this.qrVerified = container.findViewById(R.id.qr_verified);
|
||||
this.description = container.findViewById(R.id.description);
|
||||
this.tapLabel = container.findViewById(R.id.tap_label);
|
||||
this.toolbarShadow = container.findViewById(R.id.toolbar_shadow);
|
||||
this.bottomShadow = container.findViewById(R.id.verify_identity_bottom_shadow);
|
||||
this.codes[0] = container.findViewById(R.id.code_first);
|
||||
this.codes[1] = container.findViewById(R.id.code_second);
|
||||
this.codes[2] = container.findViewById(R.id.code_third);
|
||||
@@ -263,15 +281,25 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
this.codes[10] = container.findViewById(R.id.code_eleventh);
|
||||
this.codes[11] = container.findViewById(R.id.code_twelth);
|
||||
|
||||
this.qrCode.setOnClickListener(clickListener);
|
||||
this.qrCodeContainer.setOnClickListener(clickListener);
|
||||
this.registerForContextMenu(numbersContainer);
|
||||
|
||||
this.verified.setChecked(getArguments().getBoolean(VERIFIED_STATE, false));
|
||||
this.verified.setOnCheckedChangeListener(this);
|
||||
updateVerifyButton(getArguments().getBoolean(VERIFIED_STATE, false), false);
|
||||
this.verifyButton.setOnClickListener((button -> updateVerifyButton(!currentVerifiedState, true)));
|
||||
|
||||
this.scrollView.getViewTreeObserver().addOnScrollChangedListener(this);
|
||||
|
||||
((AppCompatActivity)requireActivity()).setSupportActionBar(toolbar);
|
||||
((AppCompatActivity)requireActivity()).setTitle(R.string.AndroidManifest__verify_safety_number);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@Override public void onDestroyView() {
|
||||
this.scrollView.getViewTreeObserver().removeOnScrollChangedListener(this);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
@@ -295,23 +323,26 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
//noinspection WrongThread
|
||||
Recipient resolved = recipient.resolve();
|
||||
|
||||
if (FeatureFlags.verifyV2() && resolved.getUuid().isPresent()) {
|
||||
if (FeatureFlags.verifyV2() && resolved.getAci().isPresent()) {
|
||||
Log.i(TAG, "Using UUID (version 2).");
|
||||
version = 2;
|
||||
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
|
||||
remoteId = UuidUtil.toByteArray(resolved.getUuid().get());
|
||||
localId = Recipient.self().requireAci().toByteArray();
|
||||
remoteId = resolved.requireAci().toByteArray();
|
||||
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
|
||||
Log.i(TAG, "Using E164 (version 1).");
|
||||
version = 1;
|
||||
localId = TextSecurePreferences.getLocalNumber(requireContext()).getBytes();
|
||||
localId = Recipient.self().requireE164().getBytes();
|
||||
remoteId = resolved.requireE164().getBytes();
|
||||
} 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()));
|
||||
new AlertDialog.Builder(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())
|
||||
.setOnDismissListener(dialog -> requireActivity().finish())
|
||||
.show();
|
||||
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 MaterialAlertDialogBuilder(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())
|
||||
.setOnDismissListener(dialog -> {
|
||||
requireActivity().finish();
|
||||
dialog.dismiss();
|
||||
})
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -327,6 +358,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Fingerprint fingerprint) {
|
||||
if (getActivity() == null) return;
|
||||
VerifyDisplayFragment.this.fingerprint = fingerprint;
|
||||
setFingerprintViews(fingerprint, true);
|
||||
getActivity().supportInvalidateOptionsMenu();
|
||||
@@ -353,6 +385,8 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
animateFailureOnDraw = false;
|
||||
animateVerifiedFailure();
|
||||
}
|
||||
|
||||
ThreadUtil.postToMain(this::onScrollChanged);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -410,9 +444,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
this.animateFailureOnDraw = true;
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show();
|
||||
this.animateFailureOnDraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,7 +516,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
}
|
||||
|
||||
private void setRecipientText(Recipient recipient) {
|
||||
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
|
||||
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
|
||||
description.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
@@ -501,9 +537,11 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
if (animate) {
|
||||
ViewUtil.fadeIn(qrCode, 1000);
|
||||
ViewUtil.fadeIn(tapLabel, 1000);
|
||||
ViewUtil.fadeOut(loading, 300, View.GONE);
|
||||
} else {
|
||||
qrCode.setVisibility(View.VISIBLE);
|
||||
tapLabel.setVisibility(View.VISIBLE);
|
||||
loading.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,6 +597,8 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
qrVerified.setImageBitmap(qrSuccess);
|
||||
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__successful_match));
|
||||
|
||||
animateVerified();
|
||||
}
|
||||
|
||||
@@ -569,6 +609,8 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
qrVerified.setImageBitmap(qrSuccess);
|
||||
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__failed_to_verify_safety_number));
|
||||
|
||||
animateVerified();
|
||||
}
|
||||
|
||||
@@ -576,7 +618,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
|
||||
scaleAnimation.setInterpolator(new OvershootInterpolator());
|
||||
scaleAnimation.setInterpolator(new FastOutSlowInInterpolator());
|
||||
scaleAnimation.setDuration(800);
|
||||
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
@@ -594,6 +636,9 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
scaleAnimation.setInterpolator(new AnticipateInterpolator());
|
||||
scaleAnimation.setDuration(500);
|
||||
ViewUtil.animateOut(qrVerified, scaleAnimation, View.GONE);
|
||||
ViewUtil.fadeIn(qrCode, 800);
|
||||
qrCodeContainer.setEnabled(true);
|
||||
tapLabel.setText(getString(R.string.verify_display_fragment__tap_to_scan));
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
@@ -602,40 +647,70 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
public void onAnimationRepeat(Animation animation) {}
|
||||
});
|
||||
|
||||
ViewUtil.fadeOut(qrCode, 200, View.INVISIBLE);
|
||||
ViewUtil.animateIn(qrVerified, scaleAnimation);
|
||||
qrCodeContainer.setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, final boolean isChecked) {
|
||||
final Recipient recipient = this.recipient.get();
|
||||
final RecipientId recipientId = recipient.getId();
|
||||
private void updateVerifyButton(boolean verified, boolean update) {
|
||||
currentVerifiedState = verified;
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
if (isChecked) {
|
||||
Log.i(TAG, "Saving identity: " + recipientId);
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
.saveIdentity(recipientId,
|
||||
remoteIdentity,
|
||||
VerifiedStatus.VERIFIED, false,
|
||||
System.currentTimeMillis(), true);
|
||||
} else {
|
||||
DatabaseFactory.getIdentityDatabase(getActivity())
|
||||
.setVerified(recipientId,
|
||||
remoteIdentity,
|
||||
VerifiedStatus.DEFAULT);
|
||||
if (verified) {
|
||||
verifyButton.setText(R.string.verify_display_fragment__clear_verification);
|
||||
} else {
|
||||
verifyButton.setText(R.string.verify_display_fragment__mark_as_verified);
|
||||
}
|
||||
|
||||
if (update) {
|
||||
final RecipientId recipientId = recipient.getId();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
|
||||
if (verified) {
|
||||
Log.i(TAG, "Saving identity: " + recipientId);
|
||||
ApplicationDependencies.getIdentityStore()
|
||||
.saveIdentityWithoutSideEffects(recipientId,
|
||||
remoteIdentity,
|
||||
VerifiedStatus.VERIFIED,
|
||||
false,
|
||||
System.currentTimeMillis(),
|
||||
true);
|
||||
} else {
|
||||
ApplicationDependencies.getIdentityStore().setVerified(recipientId, remoteIdentity, VerifiedStatus.DEFAULT);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
|
||||
remoteIdentity,
|
||||
verified ? VerifiedStatus.VERIFIED
|
||||
: VerifiedStatus.DEFAULT));
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), verified, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
|
||||
remoteIdentity,
|
||||
isChecked ? VerifiedStatus.VERIFIED
|
||||
: VerifiedStatus.DEFAULT));
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient, isChecked, false);
|
||||
@Override public void onScrollChanged() {
|
||||
if (scrollView.canScrollVertically(-1)) {
|
||||
if (toolbarShadow.getVisibility() != View.VISIBLE) {
|
||||
ViewUtil.fadeIn(toolbarShadow, 250);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (toolbarShadow.getVisibility() != View.GONE) {
|
||||
ViewUtil.fadeOut(toolbarShadow, 250);
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollView.canScrollVertically(1)) {
|
||||
if (bottomShadow.getVisibility() != View.VISIBLE) {
|
||||
ViewUtil.fadeIn(bottomShadow, 250);
|
||||
}
|
||||
} else {
|
||||
ViewUtil.fadeOut(bottomShadow, 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,12 +718,23 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
|
||||
private View container;
|
||||
private CameraView cameraView;
|
||||
private ShapeScrim cameraScrim;
|
||||
private ImageView cameraMarks;
|
||||
private ScanningThread scanningThread;
|
||||
private ScanListener scanListener;
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
|
||||
this.cameraView = container.findViewById(R.id.scanner);
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
|
||||
this.cameraView = container.findViewById(R.id.scanner);
|
||||
this.cameraScrim = container.findViewById(R.id.camera_scrim);
|
||||
this.cameraMarks = container.findViewById(R.id.camera_marks);
|
||||
|
||||
OneShotPreDrawListener.add(cameraScrim, () -> {
|
||||
int width = cameraScrim.getScrimWidth();
|
||||
int height = cameraScrim.getScrimHeight();
|
||||
|
||||
ViewUtil.updateLayoutParams(cameraMarks, width, height);
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -685,5 +771,4 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PictureInPictureParams;
|
||||
@@ -79,6 +77,7 @@ import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
@@ -86,6 +85,8 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
|
||||
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
private static final String TAG = Log.tag(WebRtcCallActivity.class);
|
||||
@@ -366,15 +367,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
private void handleSetAudioHandset() {
|
||||
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(false);
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.EARPIECE);
|
||||
}
|
||||
|
||||
private void handleSetAudioSpeaker() {
|
||||
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(true);
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.SPEAKER_PHONE);
|
||||
}
|
||||
|
||||
private void handleSetAudioBluetooth() {
|
||||
ApplicationDependencies.getSignalCallManager().setAudioBluetooth(true);
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.BLUETOOTH);
|
||||
}
|
||||
|
||||
private void handleSetMuteAudio(boolean enabled) {
|
||||
@@ -483,6 +484,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
delayedFinish();
|
||||
}
|
||||
|
||||
private void handleGlare(@NonNull Recipient recipient) {
|
||||
Log.i(TAG, "handleGlare: " + recipient.getId());
|
||||
|
||||
callScreen.setStatus("");
|
||||
}
|
||||
|
||||
private void handleCallRinging() {
|
||||
callScreen.setStatus(getString(R.string.RedPhone_ringing));
|
||||
}
|
||||
@@ -628,6 +635,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
handleCallRinging(); break;
|
||||
case CALL_DISCONNECTED:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
||||
case CALL_DISCONNECTED_GLARE:
|
||||
handleGlare(event.getRecipient()); break;
|
||||
case CALL_ACCEPTED_ELSEWHERE:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
|
||||
case CALL_DECLINED_ELSEWHERE:
|
||||
|
||||
@@ -14,7 +14,6 @@ import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.Interpolator
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
|
||||
private const val POSITION_ON_SCREEN = "signal.circleavatartransition.positiononscreen"
|
||||
private const val WIDTH = "signal.circleavatartransition.width"
|
||||
@@ -36,7 +35,7 @@ class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transitio
|
||||
private fun captureValues(transitionValues: TransitionValues) {
|
||||
val view: View = transitionValues.view
|
||||
|
||||
if (view is AvatarImageView) {
|
||||
if (view.transitionName == "avatar") {
|
||||
val topLeft = intArrayOf(0, 0)
|
||||
view.getLocationOnScreen(topLeft)
|
||||
transitionValues.values[POSITION_ON_SCREEN] = topLeft
|
||||
@@ -51,7 +50,7 @@ class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transitio
|
||||
}
|
||||
|
||||
val view: View = endValues.view
|
||||
if (view !is AvatarImageView || view.transitionName != "avatar") {
|
||||
if (view.transitionName != "avatar") {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
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.media.DecryptableUriMediaInput;
|
||||
import org.thoughtcrime.securesms.media.MediaInput;
|
||||
@@ -100,7 +100,7 @@ public final class AudioWaveForm {
|
||||
|
||||
if (attachment instanceof DatabaseAttachment) {
|
||||
try {
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
AttachmentDatabase attachmentDatabase = SignalDatabase.attachments();
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.avatar
|
||||
|
||||
import android.os.Bundle
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
/**
|
||||
* 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.net.Uri
|
||||
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.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
@@ -33,7 +33,7 @@ object AvatarPickerStorage {
|
||||
@JvmStatic
|
||||
fun cleanOrphans(context: Context) {
|
||||
val avatarFiles = FileStorage.getAllFiles(context, DIRECTORY, FILENAME_BASE)
|
||||
val database = DatabaseFactory.getAvatarPickerDatabase(context)
|
||||
val database = SignalDatabase.avatarPicker
|
||||
val photoAvatars = database
|
||||
.getAllAvatars()
|
||||
.filterIsInstance<Avatar.Photo>()
|
||||
|
||||
@@ -48,8 +48,9 @@ object AvatarRenderer {
|
||||
avatar: Avatar.Text,
|
||||
inverted: Boolean = false,
|
||||
size: Int = DIMENSIONS,
|
||||
synchronous: Boolean = false
|
||||
): Drawable {
|
||||
return TextAvatarDrawable(context, avatar, inverted, size)
|
||||
return TextAvatarDrawable(context, avatar, inverted, size, synchronous)
|
||||
}
|
||||
|
||||
private fun renderVector(context: Context, avatar: Avatar.Vector, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
|
||||
@@ -66,7 +67,7 @@ object AvatarRenderer {
|
||||
|
||||
private fun renderText(context: Context, avatar: Avatar.Text, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
|
||||
renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas ->
|
||||
val textDrawable = createTextDrawable(context, avatar)
|
||||
val textDrawable = createTextDrawable(context, avatar, synchronous = true)
|
||||
|
||||
canvas.drawColor(avatar.color.backgroundColor)
|
||||
textDrawable.draw(canvas)
|
||||
|
||||
@@ -3,52 +3,59 @@ package org.thoughtcrime.securesms.avatar
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
import android.text.Layout
|
||||
import android.text.SpannableString
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import androidx.core.graphics.withTranslation
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider
|
||||
|
||||
/**
|
||||
* Uses EmojiTextView to properly render a Text Avatar with emoji in it.
|
||||
*/
|
||||
class TextAvatarDrawable(
|
||||
context: Context,
|
||||
avatar: Avatar.Text,
|
||||
private val context: Context,
|
||||
private val avatar: Avatar.Text,
|
||||
inverted: Boolean = false,
|
||||
private val size: Int = AvatarRenderer.DIMENSIONS,
|
||||
private val synchronous: Boolean = false
|
||||
) : Drawable() {
|
||||
|
||||
private val layout: FrameLayout = FrameLayout(context)
|
||||
private val textView: EmojiTextView = EmojiTextView(context)
|
||||
|
||||
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
|
||||
init {
|
||||
textView.typeface = AvatarRenderer.getTypeface(context)
|
||||
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f))
|
||||
textView.text = avatar.text
|
||||
textView.gravity = Gravity.CENTER
|
||||
textView.setTextColor(if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor)
|
||||
textView.setForceCustomEmoji(true)
|
||||
textPaint.typeface = AvatarRenderer.getTypeface(context)
|
||||
textPaint.color = if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor
|
||||
textPaint.density = context.resources.displayMetrics.density
|
||||
|
||||
layout.addView(textView)
|
||||
|
||||
textView.updateLayoutParams {
|
||||
width = size
|
||||
height = size
|
||||
}
|
||||
|
||||
layout.measure(size, size)
|
||||
layout.layout(0, 0, size, size)
|
||||
setBounds(0, 0, size, size)
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight(): Int = size
|
||||
|
||||
override fun getIntrinsicWidth(): Int = size
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
layout.draw(canvas)
|
||||
val textSize = Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f)
|
||||
val width = bounds.width()
|
||||
val candidates = EmojiProvider.getCandidates(avatar.text)
|
||||
var hasEmoji = false
|
||||
|
||||
textPaint.textSize = textSize
|
||||
|
||||
val newText = if (candidates == null || candidates.size() == 0) {
|
||||
SpannableString(avatar.text)
|
||||
} else {
|
||||
EmojiProvider.emojify(context, candidates, avatar.text, textPaint, synchronous)
|
||||
}
|
||||
|
||||
if (newText == null) return
|
||||
|
||||
val layout = StaticLayout(SpannableString(newText), textPaint, width, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true)
|
||||
layout.draw(canvas, getStartX(layout), ((bounds.height() / 2) - ((layout.height / 2))).toFloat())
|
||||
}
|
||||
|
||||
private fun getStartX(layout: StaticLayout): Float {
|
||||
val direction = layout.getParagraphDirection(0)
|
||||
val lineWidth = layout.getLineWidth(0)
|
||||
val width = bounds.width()
|
||||
val xPos = (width - lineWidth) / 2
|
||||
return if (direction == Layout.DIR_LEFT_TO_RIGHT) xPos else -xPos
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) = Unit
|
||||
@@ -56,4 +63,10 @@ class TextAvatarDrawable(
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||
|
||||
private fun Layout.draw(canvas: Canvas, x: Float, y: Float) {
|
||||
canvas.withTranslation(x, y) {
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
||||
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.scribbles.ImageEditorFragment
|
||||
|
||||
@@ -44,7 +44,7 @@ class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), Ima
|
||||
val inputStream = BlobProvider.getInstance().getStream(applicationContext, editedImageUri)
|
||||
val onDiskUri = AvatarPickerStorage.save(applicationContext, inputStream)
|
||||
val photo = AvatarBundler.extractPhoto(args.photoAvatar)
|
||||
val database = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
val database = SignalDatabase.avatarPicker
|
||||
val newPhoto = photo.copy(uri = onDiskUri, size = size)
|
||||
|
||||
database.update(newPhoto)
|
||||
@@ -57,6 +57,16 @@ class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), Ima
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelEditing() {
|
||||
Navigation.findNavController(requireView()).popBackStack()
|
||||
}
|
||||
|
||||
override fun onMainImageLoaded() {
|
||||
}
|
||||
|
||||
override fun onMainImageFailedToLoad() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY_EDIT = "org.thoughtcrime.securesms.avatar.photo.EDIT"
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
||||
@@ -30,7 +31,6 @@ import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
* Primary Avatar picker fragment, displays current user avatar and a list of recently used avatars and defaults.
|
||||
@@ -111,7 +111,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
putParcelable(SELECT_AVATAR_MEDIA, it)
|
||||
}
|
||||
)
|
||||
Navigation.findNavController(v).popBackStack()
|
||||
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
|
||||
},
|
||||
{
|
||||
setFragmentResult(
|
||||
@@ -120,7 +120,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
putBoolean(SELECT_AVATAR_CLEAR, true)
|
||||
}
|
||||
)
|
||||
Navigation.findNavController(v).popBackStack()
|
||||
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -147,9 +147,10 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
ViewUtil.hideKeyboard(requireContext(), requireView())
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_CODE_SELECT_IMAGE && resultCode == Activity.RESULT_OK && data != null) {
|
||||
val media: Media = Objects.requireNonNull(data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA))
|
||||
val media: Media = requireNotNull(data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA))
|
||||
viewModel.onAvatarPhotoSelectionCompleted(media)
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
@@ -195,23 +196,24 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
}
|
||||
}
|
||||
|
||||
fun openPhotoEditor(photo: Avatar.Photo) {
|
||||
private fun openPhotoEditor(photo: Avatar.Photo) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToAvatarPhotoEditorFragment(AvatarBundler.bundlePhoto(photo)))
|
||||
}
|
||||
|
||||
fun openVectorEditor(vector: Avatar.Vector) {
|
||||
private fun openVectorEditor(vector: Avatar.Vector) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToVectorAvatarCreationFragment(AvatarBundler.bundleVector(vector)))
|
||||
}
|
||||
|
||||
fun openTextEditor(text: Avatar.Text?) {
|
||||
private fun openTextEditor(text: Avatar.Text?) {
|
||||
val bundle = if (text != null) AvatarBundler.bundleText(text) else null
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToTextAvatarCreationFragment(bundle))
|
||||
}
|
||||
|
||||
fun openCameraCapture() {
|
||||
@Suppress("DEPRECATION")
|
||||
private fun openCameraCapture() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
@@ -226,7 +228,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
.execute()
|
||||
}
|
||||
|
||||
fun openGallery() {
|
||||
@Suppress("DEPRECATION")
|
||||
private fun openGallery() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
||||
import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
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.mediasend.Media
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
@@ -70,11 +70,11 @@ class AvatarPickerRepository(context: Context) {
|
||||
}
|
||||
|
||||
fun getPersistedAvatarsForSelf(): Single<List<Avatar>> = Single.fromCallable {
|
||||
DatabaseFactory.getAvatarPickerDatabase(applicationContext).getAvatarsForSelf()
|
||||
SignalDatabase.avatarPicker.getAvatarsForSelf()
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -97,7 +97,7 @@ class AvatarPickerRepository(context: Context) {
|
||||
|
||||
fun persistAvatarForSelf(avatar: Avatar, onPersisted: (Avatar) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
val avatarDatabase = SignalDatabase.avatarPicker
|
||||
val savedAvatar = avatarDatabase.saveAvatarForSelf(avatar)
|
||||
avatarDatabase.markUsage(savedAvatar)
|
||||
onPersisted(savedAvatar)
|
||||
@@ -106,7 +106,7 @@ class AvatarPickerRepository(context: Context) {
|
||||
|
||||
fun persistAvatarForGroup(avatar: Avatar, groupId: GroupId, onPersisted: (Avatar) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
val avatarDatabase = SignalDatabase.avatarPicker
|
||||
val savedAvatar = avatarDatabase.saveAvatarForGroup(avatar, groupId)
|
||||
avatarDatabase.markUsage(savedAvatar)
|
||||
onPersisted(savedAvatar)
|
||||
@@ -180,7 +180,7 @@ class AvatarPickerRepository(context: Context) {
|
||||
fun delete(avatar: Avatar, onDelete: () -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
if (avatar.databaseId is Avatar.DatabaseId.Saved) {
|
||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
val avatarDatabase = SignalDatabase.avatarPicker
|
||||
avatarDatabase.deleteAvatar(avatar)
|
||||
}
|
||||
onDelete()
|
||||
|
||||
@@ -106,7 +106,7 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
|
||||
Navigation.findNavController(v).popBackStack()
|
||||
}
|
||||
|
||||
textInput.setOnEditorActionListener { v, actionId, event ->
|
||||
textInput.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
tabLayout.getTabAt(1)?.select()
|
||||
true
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.documentfile.provider.DocumentFile;
|
||||
import com.annimon.stream.function.Predicate;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
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.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
@@ -11,7 +11,7 @@ import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.Conversions;
|
||||
@@ -162,9 +162,8 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream)
|
||||
throws IOException
|
||||
{
|
||||
File partsDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE);
|
||||
File dataFile = File.createTempFile("part", ".mms", partsDirectory);
|
||||
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
|
||||
File dataFile = AttachmentDatabase.newFile(context);
|
||||
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.thoughtcrime.securesms.badges
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.core.content.res.use
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
class BadgeImageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : AppCompatImageView(context, attrs) {
|
||||
|
||||
private var badgeSize: Int = 0
|
||||
|
||||
init {
|
||||
context.obtainStyledAttributes(attrs, R.styleable.BadgeImageView).use {
|
||||
badgeSize = it.getInt(R.styleable.BadgeImageView_badge_size, 0)
|
||||
}
|
||||
|
||||
isClickable = false
|
||||
}
|
||||
|
||||
override fun setOnClickListener(l: OnClickListener?) {
|
||||
val wasClickable = isClickable
|
||||
super.setOnClickListener(l)
|
||||
this.isClickable = wasClickable
|
||||
}
|
||||
|
||||
fun setBadgeFromRecipient(recipient: Recipient?) {
|
||||
getGlideRequests()?.let {
|
||||
setBadgeFromRecipient(recipient, it)
|
||||
} ?: clearDrawable()
|
||||
}
|
||||
|
||||
fun setBadgeFromRecipient(recipient: Recipient?, glideRequests: GlideRequests) {
|
||||
if (recipient == null || recipient.badges.isEmpty()) {
|
||||
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 {
|
||||
setBadge(recipient.featuredBadge, glideRequests)
|
||||
}
|
||||
}
|
||||
|
||||
fun setBadge(badge: Badge?) {
|
||||
getGlideRequests()?.let {
|
||||
setBadge(badge, it)
|
||||
} ?: clearDrawable()
|
||||
}
|
||||
|
||||
fun setBadge(badge: Badge?, glideRequests: GlideRequests) {
|
||||
if (badge != null) {
|
||||
glideRequests
|
||||
.load(badge)
|
||||
.downsample(DownsampleStrategy.NONE)
|
||||
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), badge.imageDensity, ThemeUtil.isDarkTheme(context)))
|
||||
.into(this)
|
||||
|
||||
isClickable = true
|
||||
} else {
|
||||
glideRequests
|
||||
.clear(this)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.thoughtcrime.securesms.badges
|
||||
|
||||
import android.content.Context
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
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.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil
|
||||
|
||||
class BadgeRepository(context: Context) {
|
||||
|
||||
private val context = context.applicationContext
|
||||
|
||||
fun setVisibilityForAllBadges(
|
||||
displayBadgesOnProfile: Boolean,
|
||||
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()
|
||||
|
||||
recipientDatabase.setBadges(Recipient.self().id, badges)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
|
||||
fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction {
|
||||
val badges = Recipient.self().badges
|
||||
val reOrderedBadges = listOf(featuredBadge.copy(visible = true)) + (badges.filterNot { it.id == featuredBadge.id })
|
||||
ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges)
|
||||
|
||||
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
|
||||
recipientDatabase.setBadges(Recipient.self().id, reOrderedBadges)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
122
app/src/main/java/org/thoughtcrime/securesms/badges/Badges.kt
Normal file
@@ -0,0 +1,122 @@
|
||||
package org.thoughtcrime.securesms.badges
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.flexbox.AlignItems
|
||||
import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
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.Category.Companion.fromCode
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||
import org.thoughtcrime.securesms.util.ScreenDensity
|
||||
import org.whispersystems.libsignal.util.Pair
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import java.math.BigDecimal
|
||||
import java.sql.Timestamp
|
||||
|
||||
object Badges {
|
||||
fun DSLConfiguration.displayBadges(
|
||||
context: Context,
|
||||
badges: List<Badge>,
|
||||
selectedBadge: Badge? = null,
|
||||
fadedBadgeId: String? = null
|
||||
) {
|
||||
badges
|
||||
.map {
|
||||
Badge.Model(
|
||||
badge = it,
|
||||
isSelected = it == selectedBadge,
|
||||
isFaded = it.id == fadedBadgeId
|
||||
)
|
||||
}
|
||||
.forEach { customPref(it) }
|
||||
|
||||
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) {
|
||||
customPref(Badge.EmptyModel())
|
||||
}
|
||||
}
|
||||
|
||||
fun createLayoutManagerForGridWithBadges(context: Context): RecyclerView.LayoutManager {
|
||||
val layoutManager = FlexboxLayoutManager(context)
|
||||
|
||||
layoutManager.flexDirection = FlexDirection.ROW
|
||||
layoutManager.alignItems = AlignItems.CENTER
|
||||
layoutManager.justifyContent = JustifyContent.CENTER
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.bumptech.glide.load.Key
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import java.security.MessageDigest
|
||||
|
||||
typealias OnBadgeClicked = (Badge, Boolean, Boolean) -> Unit
|
||||
|
||||
/**
|
||||
* A Badge that can be collected and displayed by a user.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Badge(
|
||||
val id: String,
|
||||
val category: Category,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val imageUrl: Uri,
|
||||
val imageDensity: String,
|
||||
val expirationTimestamp: Long,
|
||||
val visible: Boolean,
|
||||
) : Parcelable, Key {
|
||||
|
||||
fun isExpired(): Boolean = expirationTimestamp < System.currentTimeMillis() && expirationTimestamp > 0
|
||||
fun isBoost(): Boolean = id == BOOST_BADGE_ID
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(id.toByteArray(Key.CHARSET))
|
||||
messageDigest.update(imageUrl.toString().toByteArray(Key.CHARSET))
|
||||
messageDigest.update(imageDensity.toByteArray(Key.CHARSET))
|
||||
}
|
||||
|
||||
fun resolveDescription(shortName: String): String {
|
||||
return description.replace("{short_name}", shortName)
|
||||
}
|
||||
|
||||
class EmptyModel : PreferenceModel<EmptyModel>() {
|
||||
override fun areItemsTheSame(newItem: EmptyModel): Boolean = true
|
||||
}
|
||||
|
||||
class EmptyViewHolder(itemView: View) : MappingViewHolder<EmptyModel>(itemView) {
|
||||
|
||||
private val name: TextView = itemView.findViewById(R.id.name)
|
||||
|
||||
init {
|
||||
itemView.isEnabled = false
|
||||
itemView.isFocusable = false
|
||||
itemView.isClickable = false
|
||||
itemView.visibility = View.INVISIBLE
|
||||
|
||||
name.text = " "
|
||||
}
|
||||
|
||||
override fun bind(model: EmptyModel) = Unit
|
||||
}
|
||||
|
||||
class Model(
|
||||
val badge: Badge,
|
||||
val isSelected: Boolean = false,
|
||||
val isFaded: Boolean = false
|
||||
) : 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 &&
|
||||
isSelected == newItem.isSelected &&
|
||||
isFaded == newItem.isFaded
|
||||
}
|
||||
|
||||
override fun getChangePayload(newItem: Model): Any? {
|
||||
return if (badge == newItem.badge && isSelected != newItem.isSelected) {
|
||||
SELECTION_CHANGED
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 name: TextView = itemView.findViewById(R.id.name)
|
||||
|
||||
private var checkAnimator: ObjectAnimator? = null
|
||||
|
||||
init {
|
||||
check.isSelected = true
|
||||
}
|
||||
|
||||
override fun bind(model: Model) {
|
||||
itemView.setOnClickListener {
|
||||
onBadgeClicked(model.badge, model.isSelected, model.isFaded)
|
||||
}
|
||||
|
||||
checkAnimator?.cancel()
|
||||
if (payload.isNotEmpty()) {
|
||||
checkAnimator = if (model.isSelected) {
|
||||
ObjectAnimator.ofFloat(check, "alpha", 1f)
|
||||
} else {
|
||||
ObjectAnimator.ofFloat(check, "alpha", 0f)
|
||||
}
|
||||
checkAnimator?.start()
|
||||
return
|
||||
}
|
||||
|
||||
badge.alpha = if (model.badge.isExpired() || model.isFaded) 0.5f else 1f
|
||||
|
||||
GlideApp.with(badge)
|
||||
.load(model.badge)
|
||||
.downsample(DownsampleStrategy.NONE)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transform(
|
||||
BadgeSpriteTransformation(BadgeSpriteTransformation.Size.BADGE_64, model.badge.imageDensity, ThemeUtil.isDarkTheme(context)),
|
||||
)
|
||||
.into(badge)
|
||||
|
||||
if (model.isSelected) {
|
||||
check.alpha = 1f
|
||||
} else {
|
||||
check.alpha = 0f
|
||||
}
|
||||
|
||||
name.text = model.badge.name
|
||||
}
|
||||
}
|
||||
|
||||
enum class Category(val code: String) {
|
||||
Donor("donor"),
|
||||
Other("other"),
|
||||
Testing("testing"); // Will be removed before final release
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String): Category {
|
||||
return when (code) {
|
||||
"donor" -> Donor
|
||||
"testing" -> Testing
|
||||
else -> Other
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BOOST_BADGE_ID = "BOOST"
|
||||
|
||||
private val SELECTION_CHANGED = Any()
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.thoughtcrime.securesms.badges.models
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
data class LargeBadge(
|
||||
val badge: Badge
|
||||
) {
|
||||
|
||||
class Model(val largeBadge: LargeBadge, val shortName: String, val maxLines: Int) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return newItem.largeBadge.badge.id == largeBadge.badge.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return newItem.largeBadge == largeBadge && newItem.shortName == shortName && newItem.maxLines == maxLines
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyModel : MappingModel<EmptyModel> {
|
||||
override fun areItemsTheSame(newItem: EmptyModel): Boolean = true
|
||||
override fun areContentsTheSame(newItem: EmptyModel): Boolean = true
|
||||
}
|
||||
|
||||
class EmptyViewHolder(itemView: View) : MappingViewHolder<EmptyModel>(itemView) {
|
||||
override fun bind(model: EmptyModel) {
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val badge: BadgeImageView = itemView.findViewById(R.id.badge)
|
||||
private val name: TextView = itemView.findViewById(R.id.name)
|
||||
private val description: TextView = itemView.findViewById(R.id.description)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
badge.setBadge(model.largeBadge.badge)
|
||||
|
||||
name.text = model.largeBadge.badge.name
|
||||
description.text = model.largeBadge.badge.resolveDescription(model.shortName)
|
||||
description.setLines(model.maxLines)
|
||||
description.maxLines = model.maxLines
|
||||
description.minLines = model.maxLines
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page))
|
||||
mappingAdapter.registerFactory(EmptyModel::class.java, MappingAdapter.LayoutFactory({ EmptyViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.badges.self.featured
|
||||
|
||||
enum class SelectFeaturedBadgeEvent {
|
||||
NO_BADGE_SELECTED,
|
||||
FAILED_TO_UPDATE_PROFILE,
|
||||
SAVE_SUCCESSFUL
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.thoughtcrime.securesms.badges.self.featured
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.Badges.displayBadges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
/**
|
||||
* Fragment which allows user to select one of their badges to be their "Featured" badge.
|
||||
*/
|
||||
class SelectFeaturedBadgeFragment : DSLSettingsFragment(
|
||||
titleId = R.string.BadgesOverviewFragment__featured_badge,
|
||||
layoutId = R.layout.select_featured_badge_fragment,
|
||||
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
|
||||
) {
|
||||
|
||||
private val viewModel: SelectFeaturedBadgeViewModel by viewModels(factoryProducer = { SelectFeaturedBadgeViewModel.Factory(BadgeRepository(requireContext())) })
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private lateinit var scrollShadow: View
|
||||
private lateinit var save: View
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
scrollShadow = view.findViewById(R.id.scroll_shadow)
|
||||
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
save = view.findViewById(R.id.save)
|
||||
save.setOnClickListener {
|
||||
viewModel.save()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||
return ToolbarShadowAnimationHelper(scrollShadow)
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
Badge.register(adapter) { badge, isSelected, _ ->
|
||||
if (!isSelected) {
|
||||
viewModel.setSelectedBadge(badge)
|
||||
}
|
||||
}
|
||||
|
||||
val previewView: View = requireView().findViewById(R.id.preview)
|
||||
val previewViewHolder = BadgePreview.ViewHolder<BadgePreview.Model>(previewView)
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
lifecycleDisposable += viewModel.events.subscribe { event: SelectFeaturedBadgeEvent ->
|
||||
when (event) {
|
||||
SelectFeaturedBadgeEvent.NO_BADGE_SELECTED -> Toast.makeText(requireContext(), R.string.SelectFeaturedBadgeFragment__you_must_select_a_badge, Toast.LENGTH_LONG).show()
|
||||
SelectFeaturedBadgeEvent.FAILED_TO_UPDATE_PROFILE -> Toast.makeText(requireContext(), R.string.SelectFeaturedBadgeFragment__failed_to_update_profile, Toast.LENGTH_LONG).show()
|
||||
SelectFeaturedBadgeEvent.SAVE_SUCCESSFUL -> findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
var hasBoundPreview = false
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
save.isEnabled = state.stage == SelectFeaturedBadgeState.Stage.READY
|
||||
|
||||
if (hasBoundPreview) {
|
||||
previewViewHolder.setPayload(listOf(Unit))
|
||||
} else {
|
||||
hasBoundPreview = true
|
||||
}
|
||||
|
||||
previewViewHolder.bind(BadgePreview.Model(state.selectedBadge))
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: SelectFeaturedBadgeState): DSLConfiguration {
|
||||
return configure {
|
||||
sectionHeaderPref(R.string.SelectFeaturedBadgeFragment__select_a_badge)
|
||||
displayBadges(requireContext(), state.allUnlockedBadges, state.selectedBadge)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.badges.self.featured
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
|
||||
data class SelectFeaturedBadgeState(
|
||||
val stage: Stage = Stage.INIT,
|
||||
val selectedBadge: Badge? = null,
|
||||
val allUnlockedBadges: List<Badge> = listOf()
|
||||
) {
|
||||
enum class Stage {
|
||||
INIT,
|
||||
READY,
|
||||
SAVING
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.thoughtcrime.securesms.badges.self.featured
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
private val TAG = Log.tag(SelectFeaturedBadgeViewModel::class.java)
|
||||
|
||||
class SelectFeaturedBadgeViewModel(private val repository: BadgeRepository) : ViewModel() {
|
||||
|
||||
private val store = Store(SelectFeaturedBadgeState())
|
||||
private val eventSubject = PublishSubject.create<SelectFeaturedBadgeEvent>()
|
||||
|
||||
val state: LiveData<SelectFeaturedBadgeState> = store.stateLiveData
|
||||
val events: Observable<SelectFeaturedBadgeEvent> = eventSubject.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
store.update(Recipient.live(Recipient.self().id).liveDataResolved) { recipient, state ->
|
||||
val unexpiredBadges = recipient.badges.filterNot { it.isExpired() }
|
||||
state.copy(
|
||||
stage = if (state.stage == SelectFeaturedBadgeState.Stage.INIT) SelectFeaturedBadgeState.Stage.READY else state.stage,
|
||||
selectedBadge = unexpiredBadges.firstOrNull(),
|
||||
allUnlockedBadges = unexpiredBadges
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelectedBadge(badge: Badge) {
|
||||
store.update { it.copy(selectedBadge = badge) }
|
||||
}
|
||||
|
||||
fun save() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.selectedBadge == null) {
|
||||
eventSubject.onNext(SelectFeaturedBadgeEvent.NO_BADGE_SELECTED)
|
||||
return
|
||||
}
|
||||
|
||||
store.update { it.copy(stage = SelectFeaturedBadgeState.Stage.SAVING) }
|
||||
disposables += repository.setFeaturedBadge(snapshot.selectedBadge).subscribeBy(
|
||||
onComplete = {
|
||||
eventSubject.onNext(SelectFeaturedBadgeEvent.SAVE_SUCCESSFUL)
|
||||
},
|
||||
onError = { error ->
|
||||
Log.e(TAG, "Failed to update profile.", error)
|
||||
eventSubject.onNext(SelectFeaturedBadgeEvent.FAILED_TO_UPDATE_PROFILE)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
class Factory(private val badgeRepository: BadgeRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(SelectFeaturedBadgeViewModel(badgeRepository)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
enum class BadgesOverviewEvent {
|
||||
FAILED_TO_UPDATE_PROFILE
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.Badges.displayBadges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
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.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
/**
|
||||
* Fragment to allow user to manage options related to the badges they've unlocked.
|
||||
*/
|
||||
class BadgesOverviewFragment : DSLSettingsFragment(
|
||||
titleId = R.string.ManageProfileFragment_badges,
|
||||
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
|
||||
) {
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private val viewModel: BadgesOverviewViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
BadgesOverviewViewModel.Factory(BadgeRepository(requireContext()), SubscriptionsRepository(ApplicationDependencies.getDonationsService()))
|
||||
}
|
||||
)
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
Badge.register(adapter) { badge, _, isFaded ->
|
||||
if (badge.isExpired() || isFaded) {
|
||||
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge))
|
||||
} else {
|
||||
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, Recipient.self().id, badge)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
|
||||
lifecycleDisposable.add(
|
||||
viewModel.events.subscribe { event: BadgesOverviewEvent ->
|
||||
when (event) {
|
||||
BadgesOverviewEvent.FAILED_TO_UPDATE_PROFILE -> Toast.makeText(requireContext(), R.string.BadgesOverviewFragment__failed_to_update_profile, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: BadgesOverviewState): DSLConfiguration {
|
||||
return configure {
|
||||
sectionHeaderPref(R.string.BadgesOverviewFragment__my_badges)
|
||||
|
||||
displayBadges(
|
||||
context = requireContext(),
|
||||
badges = state.allUnlockedBadges,
|
||||
fadedBadgeId = state.fadedBadgeId
|
||||
)
|
||||
|
||||
asyncSwitchPref(
|
||||
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__display_badges_on_profile),
|
||||
isChecked = state.displayBadgesOnProfile,
|
||||
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges && state.hasInternet,
|
||||
isProcessing = state.stage == BadgesOverviewState.Stage.UPDATING_BADGE_DISPLAY_STATE,
|
||||
onClick = {
|
||||
viewModel.setDisplayBadgesOnProfile(!state.displayBadgesOnProfile)
|
||||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__featured_badge),
|
||||
summary = state.featuredBadge?.name?.let { DSLSettingsText.from(it) },
|
||||
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges && state.hasInternet,
|
||||
onClick = {
|
||||
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToFeaturedBadgeFragment())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
|
||||
data class BadgesOverviewState(
|
||||
val stage: Stage = Stage.INIT,
|
||||
val allUnlockedBadges: List<Badge> = listOf(),
|
||||
val featuredBadge: Badge? = null,
|
||||
val displayBadgesOnProfile: Boolean = false,
|
||||
val fadedBadgeId: String? = null,
|
||||
val hasInternet: Boolean = false
|
||||
) {
|
||||
|
||||
val hasUnexpiredBadges = allUnlockedBadges.any { it.expirationTimestamp > System.currentTimeMillis() }
|
||||
|
||||
enum class Stage {
|
||||
INIT,
|
||||
READY,
|
||||
UPDATING_BADGE_DISPLAY_STATE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package org.thoughtcrime.securesms.badges.self.overview
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
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.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
|
||||
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
|
||||
|
||||
class BadgesOverviewViewModel(
|
||||
private val badgeRepository: BadgeRepository,
|
||||
private val subscriptionsRepository: SubscriptionsRepository
|
||||
) : ViewModel() {
|
||||
private val store = Store(BadgesOverviewState())
|
||||
private val eventSubject = PublishSubject.create<BadgesOverviewEvent>()
|
||||
|
||||
val state: LiveData<BadgesOverviewState> = store.stateLiveData
|
||||
val events: Observable<BadgesOverviewEvent> = eventSubject.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
store.update(Recipient.live(Recipient.self().id).liveDataResolved) { recipient, state ->
|
||||
state.copy(
|
||||
stage = if (state.stage == BadgesOverviewState.Stage.INIT) BadgesOverviewState.Stage.READY else state.stage,
|
||||
allUnlockedBadges = recipient.badges,
|
||||
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) {
|
||||
store.update { it.copy(stage = BadgesOverviewState.Stage.UPDATING_BADGE_DISPLAY_STATE) }
|
||||
disposables += badgeRepository.setVisibilityForAllBadges(displayBadgesOnProfile)
|
||||
.subscribe(
|
||||
{
|
||||
store.update { it.copy(stage = BadgesOverviewState.Stage.READY) }
|
||||
},
|
||||
{ error ->
|
||||
Log.e(TAG, "Failed to update visibility.", error)
|
||||
store.update { it.copy(stage = BadgesOverviewState.Stage.READY) }
|
||||
eventSubject.onNext(BadgesOverviewEvent.FAILED_TO_UPDATE_PROFILE)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val badgeRepository: BadgeRepository,
|
||||
private val subscriptionsRepository: SubscriptionsRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(BadgesOverviewViewModel(badgeRepository, subscriptionsRepository)))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package org.thoughtcrime.securesms.badges.view
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.LargeBadge
|
||||
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.RecipientId
|
||||
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.PlayServicesUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
|
||||
class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
private val viewModel: ViewBadgeViewModel by viewModels(factoryProducer = { ViewBadgeViewModel.Factory(getStartBadge(), getRecipientId(), BadgeRepository(requireContext())) })
|
||||
|
||||
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? {
|
||||
return inflater.inflate(R.layout.view_badge_bottom_sheet_dialog_fragment, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
postponeEnterTransition()
|
||||
|
||||
val pager: ViewPager2 = view.findViewById(R.id.pager)
|
||||
val tabs: TabLayout = view.findViewById(R.id.tab_layout)
|
||||
val action: MaterialButton = view.findViewById(R.id.action)
|
||||
val noSupport: View = view.findViewById(R.id.no_support)
|
||||
|
||||
if (getRecipientId() == Recipient.self().id) {
|
||||
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()
|
||||
|
||||
LargeBadge.register(adapter)
|
||||
pager.adapter = adapter
|
||||
adapter.submitList(listOf(LargeBadge.EmptyModel()))
|
||||
|
||||
TabLayoutMediator(tabs, pager) { _, _ ->
|
||||
}.attach()
|
||||
|
||||
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
if (adapter.getModel(position).map { it is LargeBadge.Model }.orElse(false)) {
|
||||
viewModel.onPageSelected(position)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
if (state.recipient == null || state.badgeLoadState == ViewBadgeState.LoadState.INIT) {
|
||||
return@observe
|
||||
}
|
||||
|
||||
if (state.allBadgesVisibleOnProfile.isEmpty()) {
|
||||
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(
|
||||
state.allBadgesVisibleOnProfile.map {
|
||||
LargeBadge.Model(LargeBadge(it), state.recipient.getShortDisplayName(requireContext()), maxLines + 1)
|
||||
}
|
||||
) {
|
||||
val stateSelectedIndex = state.allBadgesVisibleOnProfile.indexOf(state.selectedBadge)
|
||||
if (state.selectedBadge != null && pager.currentItem != stateSelectedIndex) {
|
||||
pager.currentItem = stateSelectedIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStartBadge(): Badge? = requireArguments().getParcelable(ARG_START_BADGE)
|
||||
|
||||
private fun getRecipientId(): RecipientId = requireNotNull(requireArguments().getParcelable(ARG_RECIPIENT_ID))
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_START_BADGE = "start_badge"
|
||||
private const val ARG_RECIPIENT_ID = "recipient_id"
|
||||
|
||||
@JvmStatic
|
||||
fun show(
|
||||
fragmentManager: FragmentManager,
|
||||
recipientId: RecipientId,
|
||||
startBadge: Badge? = null
|
||||
) {
|
||||
if (!FeatureFlags.displayDonorBadges() && recipientId != Recipient.self().id) {
|
||||
return
|
||||
}
|
||||
|
||||
ViewBadgeBottomSheetDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_START_BADGE, startBadge)
|
||||
putParcelable(ARG_RECIPIENT_ID, recipientId)
|
||||
}
|
||||
|
||||
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms.badges.view
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class ViewBadgeState(
|
||||
val allBadgesVisibleOnProfile: List<Badge> = listOf(),
|
||||
val badgeLoadState: LoadState = LoadState.INIT,
|
||||
val selectedBadge: Badge? = null,
|
||||
val recipient: Recipient? = null
|
||||
) {
|
||||
enum class LoadState {
|
||||
INIT,
|
||||
LOADED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms.badges.view
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class ViewBadgeViewModel(
|
||||
private val startBadge: Badge?,
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: BadgeRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private val store = Store(ViewBadgeState())
|
||||
|
||||
val state: LiveData<ViewBadgeState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
store.update(Recipient.live(recipientId).liveData) { recipient, state ->
|
||||
state.copy(
|
||||
recipient = recipient,
|
||||
allBadgesVisibleOnProfile = recipient.badges,
|
||||
selectedBadge = startBadge ?: recipient.badges.firstOrNull(),
|
||||
badgeLoadState = ViewBadgeState.LoadState.LOADED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun onPageSelected(position: Int) {
|
||||
if (position > store.state.allBadgesVisibleOnProfile.size - 1 || position < 0) {
|
||||
return
|
||||
}
|
||||
|
||||
store.update {
|
||||
it.copy(selectedBadge = it.allBadgesVisibleOnProfile[position])
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val startBadge: Badge?,
|
||||
private val recipientId: RecipientId,
|
||||
private val repository: BadgeRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(ViewBadgeViewModel(startBadge, recipientId, repository)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import androidx.core.util.Consumer;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -32,7 +32,7 @@ class BlockedUsersRepository {
|
||||
|
||||
void getBlocked(@NonNull Consumer<List<Recipient>> blockedUsers) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
|
||||
RecipientDatabase db = SignalDatabase.recipients();
|
||||
try (RecipientDatabase.RecipientReader reader = db.readerForBlocked(db.getBlocked())) {
|
||||
int count = reader.getCount();
|
||||
if (count == 0) {
|
||||
|
||||
@@ -119,6 +119,7 @@ public final class AudioView extends FrameLayout {
|
||||
|
||||
lottieDirection = REVERSE;
|
||||
this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
|
||||
this.playPauseButton.setOnLongClickListener(v -> performLongClick());
|
||||
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
||||
|
||||
setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE));
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.BlurTransformation;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -207,8 +208,8 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
this.chatColors = chatColors;
|
||||
recipientContactPhoto = photo;
|
||||
|
||||
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
|
||||
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
|
||||
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider, ViewUtil.getWidth(this))
|
||||
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider, ViewUtil.getWidth(this));
|
||||
|
||||
if (fixedSizeTarget != null) {
|
||||
requestManager.clear(fixedSizeTarget);
|
||||
@@ -224,6 +225,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
blurred = shouldBlur;
|
||||
|
||||
GlideRequest<Drawable> request = requestManager.load(photo.contactPhoto)
|
||||
.dontAnimate()
|
||||
.fallback(fallbackContactPhotoDrawable)
|
||||
.error(fallbackContactPhotoDrawable)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
@@ -289,6 +291,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
GlideApp.with(this)
|
||||
.load(avatarBytes)
|
||||
.dontAnimate()
|
||||
.fallback(fallback)
|
||||
.error(fallback)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
@@ -24,7 +26,9 @@ class ButtonStripItemView @JvmOverloads constructor(
|
||||
|
||||
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 label = array.getString(R.styleable.ButtonStripItemView_bsiv_label)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -27,7 +28,7 @@ import com.airbnb.lottie.model.KeyPath;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
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.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -207,9 +208,9 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
setBackground(null);
|
||||
}
|
||||
|
||||
public @Nullable Projection getProjection() {
|
||||
public @Nullable Projection getProjection(@NonNull ViewGroup coordinateRoot) {
|
||||
if (getVisibility() == VISIBLE) {
|
||||
return Projection.relativeToViewRoot(this, new Projection.Corners(ViewUtil.dpToPx(11)));
|
||||
return Projection.relativeToParent(coordinateRoot, this, new Projection.Corners(ViewUtil.dpToPx(11)));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -359,8 +360,11 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
long id = messageRecord.getId();
|
||||
boolean mms = messageRecord.isMms();
|
||||
|
||||
if (mms) DatabaseFactory.getMmsDatabase(getContext()).markExpireStarted(id);
|
||||
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
|
||||
if (mms) {
|
||||
SignalDatabase.mms().markExpireStarted(id);
|
||||
} else {
|
||||
SignalDatabase.sms().markExpireStarted(id);
|
||||
}
|
||||
|
||||
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
@@ -26,6 +27,9 @@ public class ConversationTypingView extends ConstraintLayout {
|
||||
private AvatarImageView avatar1;
|
||||
private AvatarImageView avatar2;
|
||||
private AvatarImageView avatar3;
|
||||
private BadgeImageView badge1;
|
||||
private BadgeImageView badge2;
|
||||
private BadgeImageView badge3;
|
||||
private View bubble;
|
||||
private TypingIndicatorView indicator;
|
||||
private TextView typistCount;
|
||||
@@ -41,6 +45,9 @@ public class ConversationTypingView extends ConstraintLayout {
|
||||
avatar1 = findViewById(R.id.typing_avatar_1);
|
||||
avatar2 = findViewById(R.id.typing_avatar_2);
|
||||
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);
|
||||
bubble = findViewById(R.id.typing_bubble);
|
||||
indicator = findViewById(R.id.typing_indicator);
|
||||
@@ -55,6 +62,9 @@ public class ConversationTypingView extends ConstraintLayout {
|
||||
avatar1.setVisibility(GONE);
|
||||
avatar2.setVisibility(GONE);
|
||||
avatar3.setVisibility(GONE);
|
||||
badge1.setVisibility(GONE);
|
||||
badge2.setVisibility(GONE);
|
||||
badge3.setVisibility(GONE);
|
||||
typistCount.setVisibility(GONE);
|
||||
|
||||
if (isGroupThread) {
|
||||
@@ -75,15 +85,21 @@ public class ConversationTypingView extends ConstraintLayout {
|
||||
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) {
|
||||
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1);
|
||||
avatar1.setVisibility(VISIBLE);
|
||||
badge1.setBadgeFromRecipient(typists.get(0), glideRequests);
|
||||
badge1.setVisibility(VISIBLE);
|
||||
|
||||
if (typists.size() > 1) {
|
||||
avatar2.setAvatar(glideRequests, typists.get(1), false);
|
||||
avatar2.setVisibility(VISIBLE);
|
||||
badge2.setBadgeFromRecipient(typists.get(1), glideRequests);
|
||||
badge2.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (typists.size() == 3) {
|
||||
avatar3.setAvatar(glideRequests, typists.get(2), false);
|
||||
avatar3.setVisibility(VISIBLE);
|
||||
badge3.setBadgeFromRecipient(typists.get(2), glideRequests);
|
||||
badge3.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
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 RotateAnimation ROTATION_ANIMATION = new RotateAnimation(0, 360f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
static {
|
||||
ROTATION_ANIMATION.setInterpolator(new LinearInterpolator());
|
||||
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;
|
||||
private final RotateAnimation rotationAnimation;
|
||||
private final ImageView pendingIndicator;
|
||||
private final ImageView sentIndicator;
|
||||
private final ImageView deliveredIndicator;
|
||||
private final ImageView readIndicator;
|
||||
|
||||
public DeliveryStatusView(Context context) {
|
||||
this(context, null);
|
||||
@@ -44,10 +36,17 @@ public class DeliveryStatusView extends FrameLayout {
|
||||
|
||||
inflate(context, R.layout.delivery_status_view, this);
|
||||
|
||||
this.deliveredIndicator = findViewById(R.id.delivered_indicator);
|
||||
this.sentIndicator = findViewById(R.id.sent_indicator);
|
||||
this.pendingIndicator = findViewById(R.id.pending_indicator);
|
||||
this.readIndicator = findViewById(R.id.read_indicator);
|
||||
this.deliveredIndicator = findViewById(R.id.delivered_indicator);
|
||||
this.sentIndicator = findViewById(R.id.sent_indicator);
|
||||
this.pendingIndicator = findViewById(R.id.pending_indicator);
|
||||
this.readIndicator = findViewById(R.id.read_indicator);
|
||||
|
||||
rotationAnimation = new RotateAnimation(0, 360f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
rotationAnimation.setInterpolator(new LinearInterpolator());
|
||||
rotationAnimation.setDuration(1500);
|
||||
rotationAnimation.setRepeatCount(Animation.INFINITE);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DeliveryStatusView, 0, 0);
|
||||
@@ -67,7 +66,7 @@ public class DeliveryStatusView extends FrameLayout {
|
||||
public void setPending() {
|
||||
this.setVisibility(View.VISIBLE);
|
||||
pendingIndicator.setVisibility(View.VISIBLE);
|
||||
pendingIndicator.startAnimation(ROTATION_ANIMATION);
|
||||
pendingIndicator.startAnimation(rotationAnimation);
|
||||
sentIndicator.setVisibility(View.GONE);
|
||||
deliveredIndicator.setVisibility(View.GONE);
|
||||
readIndicator.setVisibility(View.GONE);
|
||||
|
||||
@@ -8,7 +8,7 @@ import android.graphics.drawable.Drawable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -16,13 +16,14 @@ import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class FromTextView extends EmojiTextView {
|
||||
public class FromTextView extends SimpleEmojiTextView {
|
||||
|
||||
private static final String TAG = Log.tag(FromTextView.class);
|
||||
|
||||
@@ -43,22 +44,13 @@ public class FromTextView extends EmojiTextView {
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
|
||||
String fromString = recipient.getDisplayName(getContext());
|
||||
|
||||
int typeface;
|
||||
|
||||
if (!read) {
|
||||
typeface = Typeface.BOLD;
|
||||
} else {
|
||||
typeface = Typeface.NORMAL;
|
||||
}
|
||||
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
|
||||
SpannableString fromSpan = new SpannableString(fromString);
|
||||
fromSpan.setSpan(new StyleSpan(typeface), 0, builder.length(),
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
setText(recipient, recipient.getDisplayName(getContext()), read, suffix);
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
SpannableString fromSpan = new SpannableString(fromString);
|
||||
fromSpan.setSpan(getFontSpan(!read), 0, fromSpan.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
if (recipient.isSelf()) {
|
||||
builder.append(getContext().getString(R.string.note_to_self));
|
||||
@@ -85,4 +77,8 @@ public class FromTextView extends EmojiTextView {
|
||||
|
||||
return mutedDrawable;
|
||||
}
|
||||
|
||||
private CharacterStyle getFontSpan(boolean isBold) {
|
||||
return isBold ? SpanUtil.getBoldSpan() : SpanUtil.getNormalSpan();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Fullscreen Dialog Fragment which will dismiss itself when the keyboard is closed
|
||||
*/
|
||||
abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
|
||||
DialogFragment(contentLayoutId),
|
||||
KeyboardAwareLinearLayout.OnKeyboardShownListener,
|
||||
KeyboardAwareLinearLayout.OnKeyboardHiddenListener {
|
||||
|
||||
private var hasShown = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setStyle(STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
|
||||
dialog.window?.setDimAmount(0f)
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
hasShown = false
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
return if (view is KeyboardAwareLinearLayout) {
|
||||
view.addOnKeyboardShownListener(this)
|
||||
view.addOnKeyboardHiddenListener(this)
|
||||
view
|
||||
} else {
|
||||
throw IllegalStateException("Expected parent of view hierarchy to be keyboard aware.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyboardShown() {
|
||||
hasShown = true
|
||||
}
|
||||
|
||||
override fun onKeyboardHidden() {
|
||||
if (hasShown) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class MaskView extends View {
|
||||
|
||||
private MaskTarget maskTarget;
|
||||
private ViewGroup activityContentView;
|
||||
private Paint maskPaint;
|
||||
private Rect drawingRect = new Rect();
|
||||
private float targetParentTranslationY;
|
||||
|
||||
private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate;
|
||||
|
||||
public MaskView(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MaskView(@NonNull Context context, @Nullable AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
setLayerType(LAYER_TYPE_HARDWARE, maskPaint);
|
||||
|
||||
maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
activityContentView = getRootView().findViewById(android.R.id.content);
|
||||
}
|
||||
|
||||
public void setTarget(@Nullable MaskTarget maskTarget) {
|
||||
if (this.maskTarget != null) {
|
||||
removeOnDrawListener(this.maskTarget, onDrawListener);
|
||||
}
|
||||
|
||||
this.maskTarget = maskTarget;
|
||||
|
||||
if (this.maskTarget != null) {
|
||||
addOnDrawListener(maskTarget, onDrawListener);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setTargetParentTranslationY(float targetParentTranslationY) {
|
||||
this.targetParentTranslationY = targetParentTranslationY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(@NonNull Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (nothingToMask(maskTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
maskTarget.getPrimaryTarget().getDrawingRect(drawingRect);
|
||||
activityContentView.offsetDescendantRectToMyCoords(maskTarget.getPrimaryTarget(), drawingRect);
|
||||
|
||||
drawingRect.top += targetParentTranslationY;
|
||||
drawingRect.bottom += targetParentTranslationY;
|
||||
|
||||
Bitmap mask = Bitmap.createBitmap(maskTarget.getPrimaryTarget().getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888);
|
||||
Canvas maskCanvas = new Canvas(mask);
|
||||
|
||||
maskTarget.draw(maskCanvas);
|
||||
|
||||
canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom()));
|
||||
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) maskTarget.getPrimaryTarget().getLayoutParams();
|
||||
canvas.drawBitmap(mask, params.leftMargin, drawingRect.top, maskPaint);
|
||||
|
||||
mask.recycle();
|
||||
}
|
||||
|
||||
private static void removeOnDrawListener(@NonNull MaskTarget maskTarget, @NonNull ViewTreeObserver.OnDrawListener onDrawListener) {
|
||||
for (View view : maskTarget.getAllTargets()) {
|
||||
if (view != null) {
|
||||
view.getViewTreeObserver().removeOnDrawListener(onDrawListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addOnDrawListener(@NonNull MaskTarget maskTarget, @NonNull ViewTreeObserver.OnDrawListener onDrawListener) {
|
||||
for (View view : maskTarget.getAllTargets()) {
|
||||
if (view != null) {
|
||||
view.getViewTreeObserver().addOnDrawListener(onDrawListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean nothingToMask(@Nullable MaskTarget maskTarget) {
|
||||
if (maskTarget == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (View view : maskTarget.getAllTargets()) {
|
||||
if (view == null || !view.isAttachedToWindow()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class MaskTarget {
|
||||
|
||||
private final View primaryTarget;
|
||||
|
||||
public MaskTarget(@NonNull View primaryTarget) {
|
||||
this.primaryTarget = primaryTarget;
|
||||
}
|
||||
|
||||
final @NonNull View getPrimaryTarget() {
|
||||
return primaryTarget;
|
||||
}
|
||||
|
||||
protected @NonNull List<View> getAllTargets() {
|
||||
return Collections.singletonList(primaryTarget);
|
||||
}
|
||||
|
||||
protected void draw(@NonNull Canvas canvas) {
|
||||
primaryTarget.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
@@ -228,55 +229,63 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
bodyView.setVisibility(GONE);
|
||||
mediaDescriptionText.setVisibility(VISIBLE);
|
||||
|
||||
List<Slide> audioSlides = Stream.of(attachments.getSlides()).filter(Slide::hasAudio).limit(1).toList();
|
||||
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
|
||||
List<Slide> imageSlides = Stream.of(attachments.getSlides()).filter(Slide::hasImage).limit(1).toList();
|
||||
List<Slide> videoSlides = Stream.of(attachments.getSlides()).filter(Slide::hasVideo).limit(1).toList();
|
||||
List<Slide> stickerSlides = Stream.of(attachments.getSlides()).filter(Slide::hasSticker).limit(1).toList();
|
||||
List<Slide> viewOnceSlides = Stream.of(attachments.getSlides()).filter(Slide::hasViewOnce).limit(1).toList();
|
||||
Slide audioSlide = attachments.getSlides().stream().filter(Slide::hasAudio).findFirst().orElse(null);
|
||||
Slide documentSlide = attachments.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
|
||||
Slide imageSlide = attachments.getSlides().stream().filter(Slide::hasImage).findFirst().orElse(null);
|
||||
Slide videoSlide = attachments.getSlides().stream().filter(Slide::hasVideo).findFirst().orElse(null);
|
||||
Slide stickerSlide = attachments.getSlides().stream().filter(Slide::hasSticker).findFirst().orElse(null);
|
||||
Slide viewOnceSlide = attachments.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
|
||||
|
||||
// Given that most types have images, we specifically check images last
|
||||
if (!viewOnceSlides.isEmpty()) {
|
||||
if (viewOnceSlide != null) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_view_once_media);
|
||||
} else if (!audioSlides.isEmpty()) {
|
||||
} else if (audioSlide != null) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_audio);
|
||||
} else if (!documentSlides.isEmpty()) {
|
||||
} else if (documentSlide != null) {
|
||||
mediaDescriptionText.setVisibility(GONE);
|
||||
} else if (!videoSlides.isEmpty()) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_video);
|
||||
} else if (!stickerSlides.isEmpty()) {
|
||||
} else if (videoSlide != null) {
|
||||
if (videoSlide.isVideoGif()) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_gif);
|
||||
} else {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_video);
|
||||
}
|
||||
} else if (stickerSlide != null) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_sticker);
|
||||
} else if (!imageSlides.isEmpty()) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_photo);
|
||||
} else if (imageSlide != null) {
|
||||
if (MediaUtil.isGif(imageSlide.getContentType())) {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_gif);
|
||||
} else {
|
||||
mediaDescriptionText.setText(R.string.QuoteView_photo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
|
||||
List<Slide> imageVideoSlides = Stream.of(slideDeck.getSlides()).filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).limit(1).toList();
|
||||
List<Slide> documentSlides = Stream.of(attachments.getSlides()).filter(Slide::hasDocument).limit(1).toList();
|
||||
List<Slide> viewOnceSlides = Stream.of(attachments.getSlides()).filter(Slide::hasViewOnce).limit(1).toList();
|
||||
Slide imageVideoSlide = slideDeck.getSlides().stream().filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).findFirst().orElse(null);
|
||||
Slide documentSlide = slideDeck.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
|
||||
Slide viewOnceSlide = slideDeck.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
|
||||
|
||||
attachmentVideoOverlayView.setVisibility(GONE);
|
||||
|
||||
if (!viewOnceSlides.isEmpty()) {
|
||||
if (viewOnceSlide != null) {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
} else if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getUri() != null) {
|
||||
} else if (imageVideoSlide != null && imageVideoSlide.getUri() != null) {
|
||||
thumbnailView.setVisibility(VISIBLE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
dismissView.setBackgroundResource(R.drawable.dismiss_background);
|
||||
if (imageVideoSlides.get(0).hasVideo()) {
|
||||
if (imageVideoSlide.hasVideo() && !imageVideoSlide.isVideoGif()) {
|
||||
attachmentVideoOverlayView.setVisibility(VISIBLE);
|
||||
}
|
||||
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getUri()))
|
||||
glideRequests.load(new DecryptableUri(imageVideoSlide.getUri()))
|
||||
.centerCrop()
|
||||
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.into(thumbnailView);
|
||||
} else if (!documentSlides.isEmpty()){
|
||||
} else if (documentSlide != null){
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(VISIBLE);
|
||||
attachmentNameView.setText(documentSlides.get(0).getFileName().or(""));
|
||||
attachmentNameView.setText(documentSlide.getFileName().or(""));
|
||||
} else {
|
||||
thumbnailView.setVisibility(GONE);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.graphics.PixelFormat;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.Xfermode;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -100,6 +101,10 @@ public final class RotatableGradientDrawable extends Drawable {
|
||||
fillPaint.setShader(new LinearGradient(fillRect.left, fillRect.top, fillRect.right, fillRect.bottom, colors, positions, Shader.TileMode.CLAMP));
|
||||
}
|
||||
|
||||
public void setXfermode(@NonNull Xfermode xfermode) {
|
||||
fillPaint.setXfermode(xfermode);
|
||||
}
|
||||
|
||||
private static Point cornerPrime(@NonNull Point origin, @NonNull Point corner, float degrees) {
|
||||
return new Point(xPrime(origin, corner, Math.toRadians(degrees)), yPrime(origin, corner, Math.toRadians(degrees)));
|
||||
}
|
||||
|
||||
@@ -23,9 +23,12 @@ public class ShapeScrim extends View {
|
||||
private final Paint eraser;
|
||||
private final ShapeType shape;
|
||||
private final float radius;
|
||||
private final int canvasColor;
|
||||
|
||||
private Bitmap scrim;
|
||||
private Canvas scrimCanvas;
|
||||
private int scrimWidth;
|
||||
private int scrimHeight;
|
||||
|
||||
public ShapeScrim(Context context) {
|
||||
this(context, null);
|
||||
@@ -57,13 +60,30 @@ public class ShapeScrim extends View {
|
||||
this.eraser = new Paint();
|
||||
this.eraser.setColor(0xFFFFFFFF);
|
||||
this.eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
|
||||
this.canvasColor = Color.parseColor("#55BDBDBD");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
int shortDimension = Math.min(getWidth(), getHeight());
|
||||
float drawRadius = shortDimension * radius;
|
||||
|
||||
float left = (getMeasuredWidth() / 2 ) - drawRadius;
|
||||
float top = (getMeasuredHeight() / 2) - drawRadius;
|
||||
float right = left + (drawRadius * 2);
|
||||
float bottom = top + (drawRadius * 2);
|
||||
|
||||
scrimWidth = (int) (right - left);
|
||||
scrimHeight = (int) (bottom - top);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
int shortDimension = getWidth() < getHeight() ? getWidth() : getHeight();
|
||||
int shortDimension = Math.min(getWidth(), getHeight());
|
||||
float drawRadius = shortDimension * radius;
|
||||
|
||||
if (scrimCanvas == null) {
|
||||
@@ -72,7 +92,7 @@ public class ShapeScrim extends View {
|
||||
}
|
||||
|
||||
scrim.eraseColor(Color.TRANSPARENT);
|
||||
scrimCanvas.drawColor(Color.parseColor("#55BDBDBD"));
|
||||
scrimCanvas.drawColor(canvasColor);
|
||||
|
||||
if (shape == ShapeType.CIRCLE) drawCircle(scrimCanvas, drawRadius, eraser);
|
||||
else drawSquare(scrimCanvas, drawRadius, eraser);
|
||||
@@ -104,4 +124,12 @@ public class ShapeScrim extends View {
|
||||
|
||||
canvas.drawRoundRect(square, 25, 25, eraser);
|
||||
}
|
||||
|
||||
public int getScrimWidth() {
|
||||
return scrimWidth;
|
||||
}
|
||||
|
||||
public int getScrimHeight() {
|
||||
return scrimHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.graphics.drawable.shapes.Shape;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -438,8 +439,10 @@ public class ThumbnailView extends FrameLayout {
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.transition(withCrossFade()), fit);
|
||||
|
||||
if (slide.isInProgress()) return request;
|
||||
else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
|
||||
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
|
||||
|
||||
if (slide.isInProgress() || doNotShowMissingThumbnailImage) return request;
|
||||
else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
|
||||
}
|
||||
|
||||
private RequestBuilder buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
|
||||
@@ -66,6 +66,7 @@ public class ZoomingImageView extends FrameLayout {
|
||||
this.photoView = findViewById(R.id.image_view);
|
||||
this.subsamplingImageView = findViewById(R.id.subsampling_image_view);
|
||||
|
||||
this.subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF);
|
||||
this.subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF);
|
||||
|
||||
this.photoView.setZoomTransitionDuration(ZOOM_TRANSITION_DURATION);
|
||||
|
||||