mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-16 16:06:08 +00:00
Compare commits
788 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0068d62122 | ||
|
|
3f1fa59e09 | ||
|
|
df5114c62c | ||
|
|
956e3924ff | ||
|
|
20ad166e0f | ||
|
|
12ea88f409 | ||
|
|
0c5648bfb1 | ||
|
|
91ca19f294 | ||
|
|
71250afd2c | ||
|
|
cfdef7bca7 | ||
|
|
872f935fd5 | ||
|
|
0ed1f73990 | ||
|
|
349a2f72cb | ||
|
|
2b4a4d6109 | ||
|
|
9f882d2fbb | ||
|
|
cb4a9730aa | ||
|
|
e0657d09d8 | ||
|
|
01b9cb13b4 | ||
|
|
2c7260557c | ||
|
|
9e5156ab73 | ||
|
|
3dc1614fbc | ||
|
|
2f69a9c38e | ||
|
|
5e536c3fa5 | ||
|
|
6bb9d27d4e | ||
|
|
2d1bf33902 | ||
|
|
985a220fca | ||
|
|
31e137cf6d | ||
|
|
f796447815 | ||
|
|
936e772ba0 | ||
|
|
ecee797d00 | ||
|
|
357a8fc124 | ||
|
|
1233af0ddd | ||
|
|
a264d10685 | ||
|
|
ed17701a0a | ||
|
|
49e1ccea28 | ||
|
|
4c43b0d1e3 | ||
|
|
5ce09defca | ||
|
|
da9064b714 | ||
|
|
7bb53e4b06 | ||
|
|
6a4ce1b658 | ||
|
|
f84595e1e8 | ||
|
|
41d5c54033 | ||
|
|
b9d6b63c09 | ||
|
|
506ad0b3f1 | ||
|
|
c8302174a9 | ||
|
|
39cebfbb4e | ||
|
|
d36ec9af47 | ||
|
|
5f6d971bf7 | ||
|
|
7a722d92a3 | ||
|
|
0bf0eba450 | ||
|
|
d40783f794 | ||
|
|
88733473e2 | ||
|
|
7b65533095 | ||
|
|
52b533c121 | ||
|
|
a4fa2e14fb | ||
|
|
6933f1d818 | ||
|
|
b5d6cb2a8d | ||
|
|
d1478c5ce0 | ||
|
|
fbe62f0f3e | ||
|
|
f84705b756 | ||
|
|
cf2189c11a | ||
|
|
dfc4178252 | ||
|
|
07952f2146 | ||
|
|
a90dad22a9 | ||
|
|
64f7330609 | ||
|
|
5e382c120b | ||
|
|
3eea568f5f | ||
|
|
0077b29d6e | ||
|
|
dfa6306b61 | ||
|
|
a4bf075a1a | ||
|
|
373d622535 | ||
|
|
ba1df58eb3 | ||
|
|
9fb85f7c76 | ||
|
|
5e58f0a212 | ||
|
|
8fa01f13e9 | ||
|
|
4ce136be17 | ||
|
|
4099154dc0 | ||
|
|
3f983a5c82 | ||
|
|
9743e3689a | ||
|
|
1363f55f77 | ||
|
|
f1d98f6c7b | ||
|
|
9279a54d28 | ||
|
|
81889d8130 | ||
|
|
6aecb8fbc1 | ||
|
|
8aa413032d | ||
|
|
5bc4686eb8 | ||
|
|
f676d1c61c | ||
|
|
ac54b5cbdf | ||
|
|
b4b1e5b605 | ||
|
|
5eace49739 | ||
|
|
e93d7518f3 | ||
|
|
9c97cd8816 | ||
|
|
90f20c36c5 | ||
|
|
9f8dd7992a | ||
|
|
f4d3fe9176 | ||
|
|
ffc7c13717 | ||
|
|
daf93c473b | ||
|
|
d21782696a | ||
|
|
3357475fc4 | ||
|
|
ead64d92a5 | ||
|
|
5eaac6cb17 | ||
|
|
b3f0a44f10 | ||
|
|
e4d0e2f730 | ||
|
|
492a42883e | ||
|
|
b182f73415 | ||
|
|
e766b9737e | ||
|
|
2335f93579 | ||
|
|
1730260343 | ||
|
|
27506e9ed8 | ||
|
|
dc64a186d5 | ||
|
|
3163e09b98 | ||
|
|
dcb9978bb1 | ||
|
|
4a94a0a5c5 | ||
|
|
8a2d20403e | ||
|
|
ec706e95cc | ||
|
|
bd3b14a27f | ||
|
|
082d9e852c | ||
|
|
36da519b26 | ||
|
|
06ffdde892 | ||
|
|
1ec57c080c | ||
|
|
a635f27c68 | ||
|
|
ee3d7a9a35 | ||
|
|
9a1c869efe | ||
|
|
837ed76f85 | ||
|
|
b46589cd14 | ||
|
|
d04e4606d2 | ||
|
|
385bd0eb8a | ||
|
|
089656e5c4 | ||
|
|
84ec6dd458 | ||
|
|
322c139c26 | ||
|
|
babe1833bb | ||
|
|
9effa47dd8 | ||
|
|
7ef57cc0cf | ||
|
|
97420aae1b | ||
|
|
415e6309f9 | ||
|
|
83e63ff854 | ||
|
|
de7f103130 | ||
|
|
2cb912681d | ||
|
|
04bdf94b78 | ||
|
|
c7389ddaa7 | ||
|
|
e778ab2e3a | ||
|
|
533d86607f | ||
|
|
cb2096670f | ||
|
|
284f221a9d | ||
|
|
bc639dd438 | ||
|
|
1baddbb40e | ||
|
|
f784dab868 | ||
|
|
85192aaa21 | ||
|
|
054c705fe2 | ||
|
|
07b0d8cf6e | ||
|
|
597d16f566 | ||
|
|
0ca2c781c3 | ||
|
|
f642de9c41 | ||
|
|
8965388d05 | ||
|
|
58c4582f15 | ||
|
|
44bc1b5cc0 | ||
|
|
714ebb3e08 | ||
|
|
8f871c2e3a | ||
|
|
5cdc5bc441 | ||
|
|
8d060837ad | ||
|
|
1d230d4cd6 | ||
|
|
3636ae7667 | ||
|
|
9ffb5112c6 | ||
|
|
ca5d574cd7 | ||
|
|
c80283dbcc | ||
|
|
3fcaddf2d3 | ||
|
|
6ecff5bce9 | ||
|
|
a103c7dcb6 | ||
|
|
63746bbb47 | ||
|
|
ed0be6fc9a | ||
|
|
26404ff5d7 | ||
|
|
adf1674877 | ||
|
|
ab2235fc88 | ||
|
|
441a6d3fe7 | ||
|
|
e00397620a | ||
|
|
38fa58c0a3 | ||
|
|
b40fd7b243 | ||
|
|
ae34877496 | ||
|
|
599cf1e5cb | ||
|
|
474963dcf1 | ||
|
|
e22384b6b4 | ||
|
|
fb00652396 | ||
|
|
a5dbb5d91f | ||
|
|
e75a03b6f8 | ||
|
|
eb7fe7f3e0 | ||
|
|
3179808f17 | ||
|
|
fde9f05bd0 | ||
|
|
8de4290c5b | ||
|
|
19c74c8872 | ||
|
|
50edb5d1f4 | ||
|
|
c6ccfd7e75 | ||
|
|
3796ce69e4 | ||
|
|
9835e31b46 | ||
|
|
a35040c909 | ||
|
|
a4c94638ca | ||
|
|
e70a8ae6a0 | ||
|
|
100359e38d | ||
|
|
cd995aca56 | ||
|
|
3a4bae88ca | ||
|
|
e60eae27fb | ||
|
|
cd6c01e230 | ||
|
|
0af264429f | ||
|
|
a6d3862350 | ||
|
|
3fca4850dd | ||
|
|
ba7e41d9a6 | ||
|
|
fe33ce3413 | ||
|
|
4e25e8aaa2 | ||
|
|
91be826c7d | ||
|
|
fdfe0cddb8 | ||
|
|
e8ef62116f | ||
|
|
caf8bb39d8 | ||
|
|
222ba6ee53 | ||
|
|
8dcda73072 | ||
|
|
810365d334 | ||
|
|
4b31510589 | ||
|
|
dfce9a34b8 | ||
|
|
dc9370c32b | ||
|
|
8dbc721c08 | ||
|
|
6448b84430 | ||
|
|
93d6ce40c3 | ||
|
|
ce5be2c1be | ||
|
|
20fe837022 | ||
|
|
e3ce18fa3e | ||
|
|
864a1d5e93 | ||
|
|
9cf7eec247 | ||
|
|
d9c15621f6 | ||
|
|
fea14218a9 | ||
|
|
dbbded5250 | ||
|
|
d65cfc7981 | ||
|
|
dc9124f291 | ||
|
|
4cd433b6bc | ||
|
|
f9a9ee6b0c | ||
|
|
1741f7ed58 | ||
|
|
d459c751be | ||
|
|
34ef8b52f6 | ||
|
|
5ae96905bb | ||
|
|
b1fdbc0151 | ||
|
|
a5ad27b5f2 | ||
|
|
efcd5052a2 | ||
|
|
f2b10c0ba8 | ||
|
|
f182be2d79 | ||
|
|
41b10630bb | ||
|
|
45915bed90 | ||
|
|
a2c2ab428a | ||
|
|
a05f74d302 | ||
|
|
74e94f3a97 | ||
|
|
15ee8c6cac | ||
|
|
18957b1f41 | ||
|
|
29930cac41 | ||
|
|
e3338dc3ff | ||
|
|
97b7b4a501 | ||
|
|
b471a72856 | ||
|
|
fed7d911a3 | ||
|
|
ca442970a3 | ||
|
|
9dbb77c10a | ||
|
|
1116502bc0 | ||
|
|
edaf17bdd4 | ||
|
|
c61d731358 | ||
|
|
a8415a3484 | ||
|
|
cd2467085e | ||
|
|
64efb3d2a4 | ||
|
|
e05f137bd8 | ||
|
|
0c73ddc08b | ||
|
|
19cc43c442 | ||
|
|
7108fc81a9 | ||
|
|
5943b9d7d6 | ||
|
|
0271e4c918 | ||
|
|
9dc33eff3a | ||
|
|
5aef1c8a68 | ||
|
|
c608a05270 | ||
|
|
e2cfd247c3 | ||
|
|
97eb9154b2 | ||
|
|
d7ff635445 | ||
|
|
aff57fb54e | ||
|
|
e89285a219 | ||
|
|
706f43caa8 | ||
|
|
dc4faf57cb | ||
|
|
7baf8052a2 | ||
|
|
d3c59585fd | ||
|
|
859bb8dc79 | ||
|
|
58cd2e07ba | ||
|
|
a5a6fb590a | ||
|
|
3619993e68 | ||
|
|
88e12c78fa | ||
|
|
5c285b4ac6 | ||
|
|
c6b729c470 | ||
|
|
890014759e | ||
|
|
68c1c43381 | ||
|
|
d0dfcaaad5 | ||
|
|
3cffaddc0a | ||
|
|
bf4cac0c82 | ||
|
|
f680749a00 | ||
|
|
13a67980d9 | ||
|
|
f110d595d2 | ||
|
|
9c8857352b | ||
|
|
c09a1fdba8 | ||
|
|
cdc7033a51 | ||
|
|
fa30c759d7 | ||
|
|
d040be2df0 | ||
|
|
935c831a7f | ||
|
|
867e95eef1 | ||
|
|
2ee04bd1b6 | ||
|
|
75d567e555 | ||
|
|
d8a489971c | ||
|
|
19ce5b5c76 | ||
|
|
7c70ea4d3e | ||
|
|
2784285d47 | ||
|
|
c946a7a1d5 | ||
|
|
3e60b49b8b | ||
|
|
4e7331bbb8 | ||
|
|
b8c7e86223 | ||
|
|
3b925f8674 | ||
|
|
f1f6d41c73 | ||
|
|
29ef1cb1be | ||
|
|
4296085d65 | ||
|
|
c797b09228 | ||
|
|
a870ef0030 | ||
|
|
43ed9e7310 | ||
|
|
bcd27355f9 | ||
|
|
6a14dc69c0 | ||
|
|
ed9acd25f9 | ||
|
|
7b24e66ed3 | ||
|
|
abd3d4b546 | ||
|
|
4040c4240a | ||
|
|
1ee747f3ef | ||
|
|
f88874bec8 | ||
|
|
ed440a2150 | ||
|
|
2fd46b196b | ||
|
|
12dfcaf7e7 | ||
|
|
f4a199f621 | ||
|
|
bb708e0aa3 | ||
|
|
d625740ca4 | ||
|
|
250402e9b9 | ||
|
|
1d2ffe56fb | ||
|
|
d16c0d2887 | ||
|
|
b3555f2f94 | ||
|
|
83a638fc6d | ||
|
|
f1534a710f | ||
|
|
a16845340b | ||
|
|
ffa4725f8e | ||
|
|
7792c66c64 | ||
|
|
1a3985d709 | ||
|
|
4714895c59 | ||
|
|
1e37951701 | ||
|
|
e8be1ad752 | ||
|
|
e316a70b6c | ||
|
|
40a8d21c15 | ||
|
|
28d5ca7ed9 | ||
|
|
110b18545f | ||
|
|
a478605da4 | ||
|
|
f5f1589813 | ||
|
|
0c332b6adb | ||
|
|
ba712ce357 | ||
|
|
2d2395accf | ||
|
|
8634289b7a | ||
|
|
45043fb9a8 | ||
|
|
0449795725 | ||
|
|
a96093f1b7 | ||
|
|
bd4f7691e9 | ||
|
|
e12acbae70 | ||
|
|
48dc4eac10 | ||
|
|
a869c92eee | ||
|
|
4fefd14538 | ||
|
|
c09dbfa47c | ||
|
|
d3c9f66de6 | ||
|
|
01d7694108 | ||
|
|
1425b651d4 | ||
|
|
b1befbeefc | ||
|
|
3a9a84a0b1 | ||
|
|
368284cccc | ||
|
|
ef777f4db9 | ||
|
|
a8e4e8e882 | ||
|
|
cf93760d00 | ||
|
|
dd8b9ff8fb | ||
|
|
bfed03b7b5 | ||
|
|
860f06ec9e | ||
|
|
b58376920f | ||
|
|
4ace075ddf | ||
|
|
dda98a474d | ||
|
|
f1c0df7d87 | ||
|
|
c78e098cb4 | ||
|
|
a3438c4f8d | ||
|
|
92ecf2d5de | ||
|
|
f18b653725 | ||
|
|
5128438cfb | ||
|
|
f29f25822b | ||
|
|
ecfe218840 | ||
|
|
dd33d2b5d0 | ||
|
|
12a8d4e10b | ||
|
|
c5c2fb31b1 | ||
|
|
343b7faf98 | ||
|
|
18aa8bbf60 | ||
|
|
a358d1630f | ||
|
|
01375b321c | ||
|
|
d2739d52e0 | ||
|
|
4668510106 | ||
|
|
ffcd311c90 | ||
|
|
b94a636542 | ||
|
|
a7aec6bfbc | ||
|
|
190ca9eddd | ||
|
|
2cf9eb69eb | ||
|
|
ffcb90da52 | ||
|
|
878b0c9275 | ||
|
|
5505cb0dea | ||
|
|
7ac14dccda | ||
|
|
6cffd0a723 | ||
|
|
220ebf93c7 | ||
|
|
d0681a5592 | ||
|
|
09d167c16d | ||
|
|
477bb45df7 | ||
|
|
e006306036 | ||
|
|
065cbcf0f9 | ||
|
|
7a6b958bbe | ||
|
|
ef6a5b6599 | ||
|
|
cdae919b5e | ||
|
|
12889f4549 | ||
|
|
089d59b691 | ||
|
|
b3e247e9cc | ||
|
|
56392b87f7 | ||
|
|
1b1a4aeb38 | ||
|
|
16147e0c08 | ||
|
|
139317cf1b | ||
|
|
72b94127fb | ||
|
|
1f1fc94d22 | ||
|
|
a574fe026c | ||
|
|
aa82083d30 | ||
|
|
08d5df70c2 | ||
|
|
29b8fa5897 | ||
|
|
e96faf31d4 | ||
|
|
157a73aa99 | ||
|
|
bdd298c8a0 | ||
|
|
3f7dd21186 | ||
|
|
086b708cf7 | ||
|
|
57e0e57f48 | ||
|
|
4b7efbfdc0 | ||
|
|
7dc2653042 | ||
|
|
e428453835 | ||
|
|
f84c8229de | ||
|
|
a73427d68d | ||
|
|
e4456bb236 | ||
|
|
06eadd0c15 | ||
|
|
3c90dfa660 | ||
|
|
ace1b8ee71 | ||
|
|
676356e800 | ||
|
|
f732e54c22 | ||
|
|
cdc2e74f68 | ||
|
|
724f3e872b | ||
|
|
d63e5165eb | ||
|
|
9892c4392e | ||
|
|
5ced1a775c | ||
|
|
761de1318e | ||
|
|
02508512d5 | ||
|
|
6e6105af05 | ||
|
|
d569419e13 | ||
|
|
93f1641803 | ||
|
|
ff52bf93fa | ||
|
|
a039275a0c | ||
|
|
a98d10104d | ||
|
|
8924bc59b1 | ||
|
|
eefe60a9c9 | ||
|
|
fe1cb3d904 | ||
|
|
0448278a78 | ||
|
|
99c0c2ff4c | ||
|
|
b369b734ca | ||
|
|
57150a20fd | ||
|
|
1634d7d531 | ||
|
|
d563de4207 | ||
|
|
5cd4b82ed0 | ||
|
|
5f728d348c | ||
|
|
596c4b6e40 | ||
|
|
36d1e7c44a | ||
|
|
25c17082f2 | ||
|
|
810ccf8e94 | ||
|
|
c8ed0b19f0 | ||
|
|
9e09444c65 | ||
|
|
5923fa0cd5 | ||
|
|
b2d4c5d14b | ||
|
|
0bb9c1d650 | ||
|
|
fbfa3abffd | ||
|
|
b5656aa5dd | ||
|
|
d53fd6a109 | ||
|
|
b0650b926b | ||
|
|
845f6a0a93 | ||
|
|
d8daa83c79 | ||
|
|
7bb0199e83 | ||
|
|
f014dadf06 | ||
|
|
393e54ce91 | ||
|
|
fdf4ad9543 | ||
|
|
5f0d384c9e | ||
|
|
4271700046 | ||
|
|
e153b0ab78 | ||
|
|
26868ae668 | ||
|
|
17c0364eda | ||
|
|
b28ac7af8c | ||
|
|
2dcaa21a44 | ||
|
|
33cc8363f9 | ||
|
|
9b61e1c85c | ||
|
|
6f53fdc02d | ||
|
|
6f850f5a55 | ||
|
|
a482a4b1f4 | ||
|
|
3664e6f96d | ||
|
|
dda8808173 | ||
|
|
63a24c23cc | ||
|
|
1ec3a72f79 | ||
|
|
566285ec0e | ||
|
|
d5ba82338d | ||
|
|
cbecd2a2fc | ||
|
|
3772dd40ac | ||
|
|
f69a0f0261 | ||
|
|
cb323ffb84 | ||
|
|
0db73e71a0 | ||
|
|
eeb0c838db | ||
|
|
dc48ee5aed | ||
|
|
c0acfa57a9 | ||
|
|
3e166ef927 | ||
|
|
4942d83de5 | ||
|
|
4c30b39e71 | ||
|
|
e55f4fe6b6 | ||
|
|
aff74cffa0 | ||
|
|
8b29bb8664 | ||
|
|
3cee57b6c2 | ||
|
|
857f4a4fc8 | ||
|
|
a942293a74 | ||
|
|
550b121990 | ||
|
|
cc84901a49 | ||
|
|
9d3764c5d9 | ||
|
|
0950235ccd | ||
|
|
8ed7fc894e | ||
|
|
e504ffa225 | ||
|
|
9c63b37bb4 | ||
|
|
5c110ca359 | ||
|
|
1ab61beeb9 | ||
|
|
8e45a546c9 | ||
|
|
745a7f76ea | ||
|
|
8cb9ab3204 | ||
|
|
12533d1414 | ||
|
|
bd1c164d57 | ||
|
|
7446c2096d | ||
|
|
8ce5c4b885 | ||
|
|
ab76112f5f | ||
|
|
9c54e39eae | ||
|
|
61eab44474 | ||
|
|
f6285ec710 | ||
|
|
ed878ec4b4 | ||
|
|
e38d41d67a | ||
|
|
3d237d72bd | ||
|
|
8044d2390c | ||
|
|
6b82e6b5ac | ||
|
|
842e6a93e2 | ||
|
|
f140f054e5 | ||
|
|
5cd4726e23 | ||
|
|
bccc58d693 | ||
|
|
e25f1c1481 | ||
|
|
fc4e690996 | ||
|
|
dadb2f9d37 | ||
|
|
5bf15b0587 | ||
|
|
5f9c0c3204 | ||
|
|
dfa4f0c309 | ||
|
|
f0063b4b0d | ||
|
|
5dc51c34ea | ||
|
|
5bf7a55bfa | ||
|
|
eb9ae8d5dc | ||
|
|
2a133587cc | ||
|
|
0e4a19c368 | ||
|
|
813c820227 | ||
|
|
870cee5707 | ||
|
|
4e55d2d941 | ||
|
|
8e962bf992 | ||
|
|
0815715f7b | ||
|
|
85e4697b7f | ||
|
|
16fdb9bf4c | ||
|
|
46f3d50a54 | ||
|
|
3a38240fb2 | ||
|
|
662f0b8fb6 | ||
|
|
96ce42ae91 | ||
|
|
93f587b851 | ||
|
|
89a940ec81 | ||
|
|
a33771b15d | ||
|
|
9a566e5559 | ||
|
|
6e75d42a92 | ||
|
|
575413cac9 | ||
|
|
6a9476c6d0 | ||
|
|
5468f1705c | ||
|
|
5ea132e712 | ||
|
|
8128fcf8bc | ||
|
|
e89655f793 | ||
|
|
2db2b068c4 | ||
|
|
a59e214317 | ||
|
|
ae2b6e4d7a | ||
|
|
b10fc6a0b0 | ||
|
|
70977e5228 | ||
|
|
4482391574 | ||
|
|
bd078fc883 | ||
|
|
644af87782 | ||
|
|
1ce36c1069 | ||
|
|
0a71005ecc | ||
|
|
698618a4b3 | ||
|
|
f9642dd79f | ||
|
|
85d1a3c016 | ||
|
|
38c74c81a6 | ||
|
|
4c04991b70 | ||
|
|
293a339fed | ||
|
|
5255a527f9 | ||
|
|
9440dfb66c | ||
|
|
7a019eee19 | ||
|
|
93f56a5dc8 | ||
|
|
68264228b8 | ||
|
|
66c1b8e26c | ||
|
|
5776c048ea | ||
|
|
76dd09bc50 | ||
|
|
73d18d3abd | ||
|
|
c1c9d0c8a3 | ||
|
|
64420ead7c | ||
|
|
6d035c6888 | ||
|
|
833ca8cce9 | ||
|
|
d02d506b13 | ||
|
|
f306056e5d | ||
|
|
58ec669d15 | ||
|
|
d1b61bfed3 | ||
|
|
325e0c6781 | ||
|
|
8d66cd52b5 | ||
|
|
4b9277629c | ||
|
|
6515a6188b | ||
|
|
8b3ca52502 | ||
|
|
fae003e085 | ||
|
|
4b961d2d8f | ||
|
|
e27fc512b4 | ||
|
|
8f0f600b6b | ||
|
|
5950610690 | ||
|
|
fce3df0c82 | ||
|
|
e2021231c6 | ||
|
|
f61dd7509e | ||
|
|
db2b64e58c | ||
|
|
d70999c386 | ||
|
|
eb6ecc59ab | ||
|
|
1e0e2fadfd | ||
|
|
4325f714b9 | ||
|
|
137cd45497 | ||
|
|
f3dbe4416f | ||
|
|
7fb55c0f51 | ||
|
|
fdc6cbc507 | ||
|
|
072085ae82 | ||
|
|
04a8996348 | ||
|
|
c26dcc2618 | ||
|
|
a4dc340bbc | ||
|
|
3c069fb588 | ||
|
|
1fe38f5ed1 | ||
|
|
841c9424e9 | ||
|
|
9c44a0c7d3 | ||
|
|
2883d2eb31 | ||
|
|
f5aade943e | ||
|
|
d17c3f39d0 | ||
|
|
9ac9ace6b8 | ||
|
|
c9d2cef58d | ||
|
|
a9e30eefdc | ||
|
|
1a895db9bd | ||
|
|
a955bc3b9b | ||
|
|
96e888a4f5 | ||
|
|
99ff0c1e3c | ||
|
|
599e89b1f9 | ||
|
|
33c527f15e | ||
|
|
eb02dacfdc | ||
|
|
e6a0e5b858 | ||
|
|
545ba80697 | ||
|
|
1e250ee95c | ||
|
|
5a12eedc2c | ||
|
|
5605fde777 | ||
|
|
9ac142688a | ||
|
|
2791790bf5 | ||
|
|
1752972be9 | ||
|
|
c877aba09f | ||
|
|
70e33518a9 | ||
|
|
cb81a9f783 | ||
|
|
b6b499d865 | ||
|
|
6704ad8193 | ||
|
|
942628a261 | ||
|
|
4ea8bac10d | ||
|
|
eafccc5721 | ||
|
|
a01bec3a11 | ||
|
|
3868175b85 | ||
|
|
904cb01067 | ||
|
|
5c0cb425a6 | ||
|
|
9dbb2ef630 | ||
|
|
bafd2817ee | ||
|
|
3380293923 | ||
|
|
a549c1ec8b | ||
|
|
ad84997ce0 | ||
|
|
42e2576813 | ||
|
|
31b995fa98 | ||
|
|
0364bec995 | ||
|
|
aa39f3d0a3 | ||
|
|
db545f43ea | ||
|
|
bbe003a454 | ||
|
|
819f0f68f6 | ||
|
|
8c0160937b | ||
|
|
6de789dfe3 | ||
|
|
afa2bb3bf5 | ||
|
|
89e66c0741 | ||
|
|
0dc4afba99 | ||
|
|
152578e576 | ||
|
|
63d6ab6fa7 | ||
|
|
75c8c59d78 | ||
|
|
87a59b6a9b | ||
|
|
2001fa86cf | ||
|
|
52747782a7 | ||
|
|
66f2668326 | ||
|
|
b262efc24c | ||
|
|
ce7ad76447 | ||
|
|
9e98b6616e | ||
|
|
f4c9eaa904 | ||
|
|
f8a0988e5f | ||
|
|
bf919207ed | ||
|
|
dac6b5c992 | ||
|
|
7f8043777e | ||
|
|
854b3feb36 | ||
|
|
22447e6ddb | ||
|
|
be2ec36e1f | ||
|
|
98cf16479d | ||
|
|
584735cbd0 | ||
|
|
3741493cb7 | ||
|
|
4ea861fe5c | ||
|
|
cd3df4d3c1 | ||
|
|
881a1edccb | ||
|
|
1b7b574289 | ||
|
|
d1d7498447 | ||
|
|
50c18727e7 | ||
|
|
e9bfde470a | ||
|
|
68f718a210 | ||
|
|
c3e528ad4b | ||
|
|
28af97c400 | ||
|
|
c2e4c343ab | ||
|
|
8a78589c2f | ||
|
|
841ee18435 | ||
|
|
71f54701d2 | ||
|
|
1c99939dfa | ||
|
|
50462cecd0 | ||
|
|
aa6a32f023 | ||
|
|
c4dc9064e3 | ||
|
|
bc5be10a0e | ||
|
|
98d9b57379 | ||
|
|
021a16050a | ||
|
|
555104aff0 | ||
|
|
95d63b78f4 | ||
|
|
80f9e1f4f1 | ||
|
|
a77997a4de | ||
|
|
ec4eb8e2a9 | ||
|
|
1bdeade71e | ||
|
|
629ba105cb | ||
|
|
891a1af995 | ||
|
|
0fbc6ac151 | ||
|
|
a6384d1b73 | ||
|
|
2fb9514890 | ||
|
|
fe89794505 | ||
|
|
08800c9faf | ||
|
|
469a4700d2 | ||
|
|
6707f974a5 | ||
|
|
c122cada2b | ||
|
|
96f02d8c95 | ||
|
|
dd717b60b8 | ||
|
|
3c20c7f4b4 | ||
|
|
1a09e70a04 | ||
|
|
027453bbd2 | ||
|
|
b621efa4a5 | ||
|
|
2915e4698c | ||
|
|
b687b1a4c5 | ||
|
|
b53827f32b | ||
|
|
d9641128a8 | ||
|
|
dfb5562142 | ||
|
|
d467c04749 | ||
|
|
3d7cffef2b | ||
|
|
f2fe81d9b5 | ||
|
|
cf98a22269 | ||
|
|
49f75d7036 | ||
|
|
ce940235b0 | ||
|
|
f5626f678d | ||
|
|
b3a59c3946 | ||
|
|
93c390c4fc | ||
|
|
941ab5a98f | ||
|
|
2ecdf803c0 | ||
|
|
5b2a399392 | ||
|
|
a9ea1d7606 | ||
|
|
1ce8ac2de6 | ||
|
|
e2019579fb | ||
|
|
fb3c6e56ee | ||
|
|
3fad007ae0 | ||
|
|
8891b6c930 | ||
|
|
400c592acf | ||
|
|
e13f3254ad |
1
.github/workflows/android.yml
vendored
1
.github/workflows/android.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
branches:
|
||||
- 'master'
|
||||
- '4.**'
|
||||
- '5.**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
18
.github/workflows/docker.yml
vendored
Normal file
18
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Reproducible Build Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build image
|
||||
run: cd reproducible-builds && docker build -t signal-android . && cd ..
|
||||
|
||||
- name: Test build
|
||||
run: docker run --rm -v $(pwd):/project -w /project signal-android ./gradlew clean assembleRelease
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
.classpath
|
||||
captures/
|
||||
project.properties
|
||||
keystore.debug.properties
|
||||
keystore.staging.properties
|
||||
.project
|
||||
.settings
|
||||
bin/
|
||||
@@ -23,5 +25,5 @@ ffpr
|
||||
test/androidTestEspresso/res/values/arrays.xml
|
||||
obj/
|
||||
jni/libspeex/.deps/
|
||||
*.sh
|
||||
pkcs11.password
|
||||
dev.keystore
|
||||
|
||||
@@ -83,7 +83,7 @@ There are several other ways to get involved:
|
||||
* Try to reproduce issues and help with troubleshooting.
|
||||
* Discover solutions to open issues and post any relevant findings.
|
||||
* Test other people's pull requests.
|
||||
* Contribute to Signal via the [Freedom of the Press Foundation's donation page](https://freedom.press/crowdfunding/signal/).
|
||||
* [Donate to Signal.](https://signal.org/donate/)
|
||||
* Share Signal with your friends and family.
|
||||
|
||||
Signal is made for you. Thank you for your feedback and support.
|
||||
|
||||
25
Dockerfile
25
Dockerfile
@@ -1,25 +0,0 @@
|
||||
FROM ubuntu:17.10
|
||||
|
||||
RUN dpkg --add-architecture i386 && \
|
||||
apt-get update -y && \
|
||||
apt-get install -y software-properties-common && \
|
||||
apt-get update -y && \
|
||||
apt-get install -y libc6:i386=2.26-0ubuntu2.1 libncurses5:i386=6.0+20160625-1ubuntu1 libstdc++6:i386=7.2.0-8ubuntu3.2 lib32z1=1:1.2.11.dfsg-0ubuntu2 wget openjdk-8-jdk=8u171-b11-0ubuntu0.17.10.1 git unzip opensc pcscd && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean
|
||||
|
||||
ENV ANDROID_SDK_FILENAME android-sdk_r24.4.1-linux.tgz
|
||||
ENV ANDROID_SDK_URL https://dl.google.com/android/${ANDROID_SDK_FILENAME}
|
||||
ENV ANDROID_API_LEVELS android-28
|
||||
ENV ANDROID_BUILD_TOOLS_VERSION 28.0.3
|
||||
ENV ANDROID_HOME /usr/local/android-sdk-linux
|
||||
ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools
|
||||
RUN cd /usr/local/ && \
|
||||
wget -q ${ANDROID_SDK_URL} && \
|
||||
tar -xzf ${ANDROID_SDK_FILENAME} && \
|
||||
rm ${ANDROID_SDK_FILENAME}
|
||||
RUN echo y | android update sdk --no-ui -a --filter ${ANDROID_API_LEVELS}
|
||||
RUN echo y | android update sdk --no-ui -a --filter extra-android-m2repository,extra-android-support,extra-google-google_play_services,extra-google-m2repository
|
||||
RUN echo y | android update sdk --no-ui -a --filter tools,platform-tools,build-tools-${ANDROID_BUILD_TOOLS_VERSION}
|
||||
RUN rm -rf ${ANDROID_HOME}/tools && unzip ${ANDROID_HOME}/temp/*.zip -d ${ANDROID_HOME}
|
||||
157
app/build.gradle
157
app/build.gradle
@@ -15,7 +15,7 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
|
||||
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
|
||||
}
|
||||
@@ -80,8 +80,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 655
|
||||
def canonicalVersionName = "4.63.2"
|
||||
def canonicalVersionCode = 736
|
||||
def canonicalVersionName = "4.76.3"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -90,22 +90,35 @@ def abiPostFix = ['universal' : 0,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4]
|
||||
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
android {
|
||||
flavorDimensions "none"
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
flavorDimensions 'distribution', 'environment'
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
dexOptions {
|
||||
javaMaxHeapSize "4g"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (keystores.debug != null) {
|
||||
debug {
|
||||
storeFile file("${project.rootDir}/${keystores.debug.storeFile}")
|
||||
storePassword keystores.debug.storePassword
|
||||
keyAlias keystores.debug.keyAlias
|
||||
keyPassword keystores.debug.keyPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
targetSdkVersion 29
|
||||
multiDexEnabled true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -122,9 +135,11 @@ android {
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
|
||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
||||
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
||||
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
|
||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new 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[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
@@ -163,8 +178,16 @@ android {
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!blazeface.tfl'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
if (keystores['debug'] != null) {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
isDefault true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||
'proguard/proguard-firebase-messaging.pro',
|
||||
@@ -188,22 +211,9 @@ android {
|
||||
testProguardFiles 'proguard/proguard-automation.pro',
|
||||
'proguard/proguard.cfg'
|
||||
}
|
||||
staging {
|
||||
initWith debug
|
||||
|
||||
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", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
|
||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||
}
|
||||
flipper {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
}
|
||||
release {
|
||||
@@ -214,18 +224,52 @@ android {
|
||||
|
||||
productFlavors {
|
||||
play {
|
||||
dimension "none"
|
||||
dimension 'distribution'
|
||||
isDefault true
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
}
|
||||
|
||||
website {
|
||||
dimension "none"
|
||||
dimension 'distribution'
|
||||
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||
}
|
||||
|
||||
internal {
|
||||
dimension 'distribution'
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
}
|
||||
|
||||
prod {
|
||||
dimension 'environment'
|
||||
|
||||
isDefault true
|
||||
}
|
||||
|
||||
staging {
|
||||
dimension 'environment'
|
||||
|
||||
applicationIdSuffix ".staging"
|
||||
|
||||
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\", " +
|
||||
"\"038c40bbbacdc873caa81ac793bb75afde6dfe436a99ab1f15e3f0cbb7434ced\", " +
|
||||
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
|
||||
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||
}
|
||||
}
|
||||
|
||||
android.applicationVariants.all { variant ->
|
||||
@@ -295,6 +339,7 @@ dependencies {
|
||||
|
||||
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 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
@@ -304,7 +349,7 @@ dependencies {
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.0.3'
|
||||
implementation 'org.signal:ringrtc-android:2.7.3'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
@@ -358,24 +403,25 @@ dependencies {
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation 'org.mockito:mockito-core:1.9.5'
|
||||
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
|
||||
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.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 'androidx.test:core:1.2.0'
|
||||
testImplementation ('org.robolectric:robolectric:4.2') {
|
||||
testImplementation ('org.robolectric:robolectric:4.4') {
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
testImplementation 'org.hamcrest:hamcrest:2.2'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
||||
|
||||
dependencyVerification {
|
||||
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
|
||||
configuration = '(play|website)(Prod|Staging)(Debug|Release)RuntimeClasspath'
|
||||
}
|
||||
|
||||
|
||||
@@ -422,28 +468,24 @@ def signProductionRelease = { variant ->
|
||||
|
||||
task signProductionPlayRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'playRelease') })
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'playProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionInternalRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'internalProdRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionWebsiteRelease {
|
||||
doLast {
|
||||
def variant = android.applicationVariants.find { (it.name == 'websiteRelease') }
|
||||
def variant = android.applicationVariants.find { (it.name == 'websiteProdRelease') }
|
||||
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
|
||||
assembleWebsiteDescriptor(variant, signedRelease)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.whenTaskAdded { task ->
|
||||
if (task.name.equals("assemblePlayRelease")) {
|
||||
task.finalizedBy signProductionPlayRelease
|
||||
}
|
||||
|
||||
if (task.name.equals("assembleWebsiteRelease")) {
|
||||
task.finalizedBy signProductionWebsiteRelease
|
||||
}
|
||||
}
|
||||
|
||||
def getLastCommitTimestamp() {
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
def result = exec {
|
||||
@@ -455,3 +497,24 @@ def getLastCommitTimestamp() {
|
||||
return os.toString() + "000"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
testLogging {
|
||||
events "failed"
|
||||
exceptionFormat "full"
|
||||
showCauses true
|
||||
showExceptions true
|
||||
showStackTraces true
|
||||
}
|
||||
}
|
||||
|
||||
def loadKeystoreProperties(filename) {
|
||||
def keystorePropertiesFile = file("${project.rootDir}/${filename}")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
return keystoreProperties;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
11
app/lint.xml
11
app/lint.xml
@@ -1,15 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
|
||||
<!-- Wont pass lint or qa with a STOPSHIP in a comment -->
|
||||
<issue id="StopShip" severity="fatal" />
|
||||
|
||||
<!-- L10N errors -->
|
||||
<!-- This is a runtime crash so we don't want to ship with this. -->
|
||||
<issue id="StringFormatMatches" severity="error" />
|
||||
|
||||
<!-- L10N warnings -->
|
||||
<issue id="MissingTranslation" severity="warning" />
|
||||
<issue id="MissingTranslation" severity="ignore" />
|
||||
<issue id="MissingQuantity" severity="warning" />
|
||||
<issue id="MissingDefaultResource" severity="error">
|
||||
<ignore path="*/res/values-*/strings.xml" /> <!-- Ignore for non-English, excludeNonTranslatables task will remove these -->
|
||||
</issue>
|
||||
<issue id="ExtraTranslation" severity="warning" />
|
||||
<issue id="ImpliedQuantity" severity="warning" />
|
||||
<issue id="TypographyDashes" severity="error" >
|
||||
<ignore path="*/res/values-*/strings.xml" /> <!-- Ignore for non-English -->
|
||||
</issue>
|
||||
|
||||
<issue id="CanvasSize" severity="error" />
|
||||
<issue id="HardcodedText" severity="error" />
|
||||
|
||||
@@ -5,5 +5,12 @@
|
||||
|
||||
<application
|
||||
android:name=".FlipperApplicationContext"
|
||||
tools:replace="android:name"/>
|
||||
tools:replace="android:name">
|
||||
|
||||
<activity
|
||||
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||
android:exported="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -15,7 +15,9 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -29,13 +31,23 @@ import java.util.Map;
|
||||
*/
|
||||
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
|
||||
|
||||
private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class);
|
||||
|
||||
public FlipperSqlCipherAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Descriptor> getDatabases() {
|
||||
return Collections.singletonList(new Descriptor(DatabaseFactory.getRawDatabase(getContext())));
|
||||
try {
|
||||
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
|
||||
databaseHelperField.setAccessible(true);
|
||||
SQLCipherOpenHelper sqlCipherOpenHelper = (SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext()));
|
||||
return Collections.singletonList(new Descriptor(sqlCipherOpenHelper));
|
||||
} catch (Exception e) {
|
||||
Log.i(TAG, "Unable to use reflection to access raw database.", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<resources>
|
||||
<string name="app_name">Signal (Flipper)</string>
|
||||
</resources>
|
||||
5
app/src/internal/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
5
app/src/internal/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/core_red_shade"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle" />
|
||||
|
||||
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
|
||||
<permission android:name="${applicationId}.ACCESS_SECRETS"
|
||||
android:label="Access to TextSecure Secrets"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
<uses-permission android:name="android.permission.WRITE_SMS"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
@@ -113,15 +114,22 @@
|
||||
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
|
||||
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
|
||||
|
||||
<activity android:name="org.thoughtcrime.securesms.WebRtcCallActivity"
|
||||
<activity android:name=".WebRtcCallActivity"
|
||||
android:theme="@style/TextSecure.LightTheme.WebRTCCall"
|
||||
android:excludeFromRecents="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||
android:taskAffinity=".calling"
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:screenOrientation="portrait"
|
||||
android:noHistory="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".InviteActivity"
|
||||
android:theme="@style/Signal.Light.NoActionBar.Invite"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
@@ -129,7 +137,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".PromptMmsActivity"
|
||||
@@ -151,7 +159,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".sharing.ShareActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
@@ -184,7 +192,7 @@
|
||||
</activity>
|
||||
|
||||
<activity android:name=".stickers.StickerPackPreviewActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
@@ -216,6 +224,23 @@
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="sgnl"
|
||||
android:host="signal.group" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true"
|
||||
tools:targetApi="23">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https"
|
||||
android:host="signal.group"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
|
||||
@@ -243,24 +268,28 @@
|
||||
android:theme="@style/TextSecure.LightTheme.Popup"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".MessageDetailsActivity"
|
||||
<activity android:name=".messagedetails.MessageDetailsActivity"
|
||||
android:label="@string/AndroidManifest__message_details"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".GroupCreateActivity"
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".groups.ui.pendingmemberinvites.PendingMemberInvitesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DatabaseMigrationActivity"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
android:launchMode="singleTask"
|
||||
@@ -274,7 +303,7 @@
|
||||
<activity android:name=".PassphraseCreateActivity"
|
||||
android:label="@string/AndroidManifest__create_passphrase"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -284,7 +313,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".NewConversationActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -294,7 +323,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".giph.ui.GiphyActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -359,7 +388,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediaoverview.MediaOverviewActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -410,10 +439,6 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name=".RecipientPreferenceActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediasend.AvatarSelectionActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
@@ -452,6 +477,9 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".contacts.TurnOffContactJoinedNotificationsActivity"
|
||||
android:theme="@style/Theme.AppCompat.Dialog.Alert" />
|
||||
|
||||
<activity android:name=".messagerequests.MessageRequestMegaphoneActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
@@ -462,50 +490,70 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".contactshare.ContactNameEditActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".contactshare.SharedContactDetailsActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ShortcutLauncherActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:exported="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity
|
||||
android:name=".maps.PlacePickerActivity"
|
||||
android:label="@string/PlacePickerActivity_title"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".MainActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".pin.PinRestoreActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.addtogroup.AddToGroupsActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.addmembers.AddMembersActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.creategroup.details.AddGroupDetailsActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||
<activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<activity android:name=".megaphone.ClientDeprecatedActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<service android:enabled="true" android:name=".service.WebRtcCallService"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
|
||||
|
||||
<service android:name=".components.voice.VoiceNotePlaybackService">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name=".service.QuickResponseService"
|
||||
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
|
||||
android:exported="true" >
|
||||
@@ -618,18 +666,25 @@
|
||||
|
||||
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
||||
|
||||
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
|
||||
|
||||
<provider android:name=".providers.PartProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
android:authorities="org.thoughtcrime.provider.securesms" />
|
||||
android:authorities="${applicationId}.part" />
|
||||
|
||||
<provider android:name=".providers.BlobContentProvider"
|
||||
android:authorities="${applicationId}.blob"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
<provider android:name=".providers.MmsBodyProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
android:authorities="org.thoughtcrime.provider.securesms.mms" />
|
||||
android:authorities="${applicationId}.mms" />
|
||||
|
||||
<provider android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="org.thoughtcrime.securesms.fileprovider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
@@ -638,23 +693,23 @@
|
||||
</provider>
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$Conversation"
|
||||
android:authorities="org.thoughtcrime.securesms.database.conversation"
|
||||
android:authorities="${applicationId}.database.conversation"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$ConversationList"
|
||||
android:authorities="org.thoughtcrime.securesms.database.conversationlist"
|
||||
android:authorities="${applicationId}.database.conversationlist"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$Attachment"
|
||||
android:authorities="org.thoughtcrime.securesms.database.attachment"
|
||||
android:authorities="${applicationId}.database.attachment"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$Sticker"
|
||||
android:authorities="org.thoughtcrime.securesms.database.sticker"
|
||||
android:authorities="${applicationId}.database.sticker"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$StickerPack"
|
||||
android:authorities="org.thoughtcrime.securesms.database.stickerpack"
|
||||
android:authorities="${applicationId}.database.stickerpack"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver android:name=".service.BootReceiver">
|
||||
@@ -700,11 +755,7 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.MessageNotifier$ReminderReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.MessageNotifier.REMINDER_ACTION"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".notifications.MessageNotifier$ReminderReceiver"/>
|
||||
|
||||
<receiver android:name=".notifications.DeleteNotificationReceiver">
|
||||
<intent-filter>
|
||||
|
||||
BIN
app/src/main/assets/sounds/state-change_confirm-down.ogg
Executable file
BIN
app/src/main/assets/sounds/state-change_confirm-down.ogg
Executable file
Binary file not shown.
BIN
app/src/main/assets/sounds/state-change_confirm-up.ogg
Executable file
BIN
app/src/main/assets/sounds/state-change_confirm-up.ogg
Executable file
Binary file not shown.
58
app/src/main/java/org/signal/glide/Log.java
Normal file
58
app/src/main/java/org/signal/glide/Log.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package org.signal.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class Log {
|
||||
|
||||
private Log() {}
|
||||
|
||||
public static void v(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().v(tag, message);
|
||||
}
|
||||
|
||||
public static void d(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().d(tag, message);
|
||||
}
|
||||
|
||||
public static void i(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().i(tag, message);
|
||||
}
|
||||
|
||||
public static void w(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().w(tag, message);
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message) {
|
||||
e(tag, message, null);
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||
SignalGlideCodecs.getLogProvider().e(tag, message, throwable);
|
||||
}
|
||||
|
||||
public interface Provider {
|
||||
void v(@NonNull String tag, @NonNull String message);
|
||||
void d(@NonNull String tag, @NonNull String message);
|
||||
void i(@NonNull String tag, @NonNull String message);
|
||||
void w(@NonNull String tag, @NonNull String message);
|
||||
void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable);
|
||||
|
||||
Provider EMPTY = new Provider() {
|
||||
@Override
|
||||
public void v(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void d(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void i(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void w(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void e(@NonNull String tag, @NonNull String message, @NonNull Throwable throwable) { }
|
||||
};
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/org/signal/glide/SignalGlideCodecs.java
Normal file
18
app/src/main/java/org/signal/glide/SignalGlideCodecs.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.signal.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class SignalGlideCodecs {
|
||||
|
||||
private static Log.Provider logProvider = Log.Provider.EMPTY;
|
||||
|
||||
private SignalGlideCodecs() {}
|
||||
|
||||
public static void setLogProvider(@NonNull Log.Provider provider) {
|
||||
logProvider = provider;
|
||||
}
|
||||
|
||||
public static @NonNull Log.Provider getLogProvider() {
|
||||
return logProvider;
|
||||
}
|
||||
}
|
||||
52
app/src/main/java/org/signal/glide/apng/APNGDrawable.java
Normal file
52
app/src/main/java/org/signal/glide/apng/APNGDrawable.java
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.signal.glide.common.FrameAnimationDrawable;
|
||||
import org.signal.glide.apng.decode.APNGDecoder;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.loader.AssetStreamLoader;
|
||||
import org.signal.glide.common.loader.FileLoader;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
import org.signal.glide.common.loader.ResourceStreamLoader;
|
||||
|
||||
/**
|
||||
* @Description: APNGDrawable
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public class APNGDrawable extends FrameAnimationDrawable<APNGDecoder> {
|
||||
public APNGDrawable(Loader provider) {
|
||||
super(provider);
|
||||
}
|
||||
|
||||
public APNGDrawable(APNGDecoder decoder) {
|
||||
super(decoder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGDecoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener) {
|
||||
return new APNGDecoder(streamLoader, listener);
|
||||
}
|
||||
|
||||
|
||||
public static APNGDrawable fromAsset(Context context, String assetPath) {
|
||||
AssetStreamLoader assetStreamLoader = new AssetStreamLoader(context, assetPath);
|
||||
return new APNGDrawable(assetStreamLoader);
|
||||
}
|
||||
|
||||
public static APNGDrawable fromFile(String filePath) {
|
||||
FileLoader fileLoader = new FileLoader(filePath);
|
||||
return new APNGDrawable(fileLoader);
|
||||
}
|
||||
|
||||
public static APNGDrawable fromResource(Context context, int resId) {
|
||||
ResourceStreamLoader resourceStreamLoader = new ResourceStreamLoader(context, resId);
|
||||
return new APNGDrawable(resourceStreamLoader);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27acTL.27:_The_Animation_Control_Chunk
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class ACTLChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("acTL");
|
||||
int num_frames;
|
||||
int num_plays;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader apngReader) throws IOException {
|
||||
num_frames = apngReader.readInt();
|
||||
num_plays = apngReader.readInt();
|
||||
}
|
||||
}
|
||||
211
app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java
Normal file
211
app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
|
||||
|
||||
private static final String TAG = APNGDecoder.class.getSimpleName();
|
||||
|
||||
private APNGWriter apngWriter;
|
||||
private int mLoopCount;
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
|
||||
private class SnapShot {
|
||||
byte dispose_op;
|
||||
Rect dstRect = new Rect();
|
||||
ByteBuffer byteBuffer;
|
||||
}
|
||||
|
||||
private SnapShot snapShot = new SnapShot();
|
||||
|
||||
/**
|
||||
* @param loader webp的reader
|
||||
* @param renderListener 渲染的回调
|
||||
*/
|
||||
public APNGDecoder(Loader loader, FrameSeqDecoder.RenderListener renderListener) {
|
||||
super(loader, renderListener);
|
||||
paint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGWriter getWriter() {
|
||||
if (apngWriter == null) {
|
||||
apngWriter = new APNGWriter();
|
||||
}
|
||||
return apngWriter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGReader getReader(Reader reader) {
|
||||
return new APNGReader(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLoopCount() {
|
||||
return mLoopCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release() {
|
||||
snapShot.byteBuffer = null;
|
||||
apngWriter = null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Rect read(APNGReader reader) throws IOException {
|
||||
List<Chunk> chunks = APNGParser.parse(reader);
|
||||
List<Chunk> otherChunks = new ArrayList<>();
|
||||
|
||||
boolean actl = false;
|
||||
APNGFrame lastFrame = null;
|
||||
byte[] ihdrData = new byte[0];
|
||||
int canvasWidth = 0, canvasHeight = 0;
|
||||
for (Chunk chunk : chunks) {
|
||||
if (chunk instanceof ACTLChunk) {
|
||||
mLoopCount = ((ACTLChunk) chunk).num_plays;
|
||||
actl = true;
|
||||
} else if (chunk instanceof FCTLChunk) {
|
||||
APNGFrame frame = new APNGFrame(reader, (FCTLChunk) chunk);
|
||||
frame.prefixChunks = otherChunks;
|
||||
frame.ihdrData = ihdrData;
|
||||
frames.add(frame);
|
||||
lastFrame = frame;
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
if (lastFrame != null) {
|
||||
lastFrame.imageChunks.add(chunk);
|
||||
}
|
||||
} else if (chunk instanceof IDATChunk) {
|
||||
if (!actl) {
|
||||
//如果为非APNG图片,则只解码PNG
|
||||
Frame frame = new StillFrame(reader);
|
||||
frame.frameWidth = canvasWidth;
|
||||
frame.frameHeight = canvasHeight;
|
||||
frames.add(frame);
|
||||
mLoopCount = 1;
|
||||
break;
|
||||
}
|
||||
if (lastFrame != null) {
|
||||
lastFrame.imageChunks.add(chunk);
|
||||
}
|
||||
|
||||
} else if (chunk instanceof IHDRChunk) {
|
||||
canvasWidth = ((IHDRChunk) chunk).width;
|
||||
canvasHeight = ((IHDRChunk) chunk).height;
|
||||
ihdrData = ((IHDRChunk) chunk).data;
|
||||
} else if (!(chunk instanceof IENDChunk)) {
|
||||
otherChunks.add(chunk);
|
||||
}
|
||||
}
|
||||
frameBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||
snapShot.byteBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||
return new Rect(0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderFrame(Frame frame) {
|
||||
if (frame == null || fullRect == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Bitmap bitmap = obtainBitmap(fullRect.width() / sampleSize, fullRect.height() / sampleSize);
|
||||
Canvas canvas = cachedCanvas.get(bitmap);
|
||||
if (canvas == null) {
|
||||
canvas = new Canvas(bitmap);
|
||||
cachedCanvas.put(bitmap, canvas);
|
||||
}
|
||||
if (frame instanceof APNGFrame) {
|
||||
// 从缓存中恢复当前帧
|
||||
frameBuffer.rewind();
|
||||
bitmap.copyPixelsFromBuffer(frameBuffer);
|
||||
// 开始绘制前,处理快照中的设定
|
||||
if (this.frameIndex == 0) {
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
} else {
|
||||
canvas.save();
|
||||
canvas.clipRect(snapShot.dstRect);
|
||||
switch (snapShot.dispose_op) {
|
||||
// 从快照中恢复上一帧之前的显示内容
|
||||
case FCTLChunk.APNG_DISPOSE_OP_PREVIOUS:
|
||||
snapShot.byteBuffer.rewind();
|
||||
bitmap.copyPixelsFromBuffer(snapShot.byteBuffer);
|
||||
break;
|
||||
// 清空上一帧所画区域
|
||||
case FCTLChunk.APNG_DISPOSE_OP_BACKGROUND:
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
break;
|
||||
// 什么都不做
|
||||
case FCTLChunk.APNG_DISPOSE_OP_NON:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
// 然后根据dispose设定传递到快照信息中
|
||||
if (((APNGFrame) frame).dispose_op == FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
|
||||
if (snapShot.dispose_op != FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
|
||||
snapShot.byteBuffer.rewind();
|
||||
bitmap.copyPixelsToBuffer(snapShot.byteBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
snapShot.dispose_op = ((APNGFrame) frame).dispose_op;
|
||||
canvas.save();
|
||||
if (((APNGFrame) frame).blend_op == FCTLChunk.APNG_BLEND_OP_SOURCE) {
|
||||
canvas.clipRect(
|
||||
frame.frameX / sampleSize,
|
||||
frame.frameY / sampleSize,
|
||||
(frame.frameX + frame.frameWidth) / sampleSize,
|
||||
(frame.frameY + frame.frameHeight) / sampleSize);
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
}
|
||||
|
||||
|
||||
snapShot.dstRect.set(frame.frameX / sampleSize,
|
||||
frame.frameY / sampleSize,
|
||||
(frame.frameX + frame.frameWidth) / sampleSize,
|
||||
(frame.frameY + frame.frameHeight) / sampleSize);
|
||||
canvas.restore();
|
||||
}
|
||||
//开始真正绘制当前帧的内容
|
||||
Bitmap inBitmap = obtainBitmap(frame.frameWidth, frame.frameHeight);
|
||||
recycleBitmap(frame.draw(canvas, paint, sampleSize, inBitmap, getWriter()));
|
||||
recycleBitmap(inBitmap);
|
||||
frameBuffer.rewind();
|
||||
bitmap.copyPixelsToBuffer(frameBuffer);
|
||||
recycleBitmap(bitmap);
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "Failed to render!", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java
Normal file
147
app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGFrame extends Frame<APNGReader, APNGWriter> {
|
||||
public final byte blend_op;
|
||||
public final byte dispose_op;
|
||||
byte[] ihdrData;
|
||||
List<Chunk> imageChunks = new ArrayList<>();
|
||||
List<Chunk> prefixChunks = new ArrayList<>();
|
||||
private static final byte[] sPNGSignatures = {(byte) 137, 80, 78, 71, 13, 10, 26, 10};
|
||||
private static final byte[] sPNGEndChunk = {0, 0, 0, 0, 0x49, 0x45, 0x4E, 0x44, (byte) 0xAE, 0x42, 0x60, (byte) 0x82};
|
||||
|
||||
private static ThreadLocal<CRC32> sCRC32 = new ThreadLocal<>();
|
||||
|
||||
private CRC32 getCRC32() {
|
||||
CRC32 crc32 = sCRC32.get();
|
||||
if (crc32 == null) {
|
||||
crc32 = new CRC32();
|
||||
sCRC32.set(crc32);
|
||||
}
|
||||
return crc32;
|
||||
}
|
||||
|
||||
public APNGFrame(APNGReader reader, FCTLChunk fctlChunk) {
|
||||
super(reader);
|
||||
blend_op = fctlChunk.blend_op;
|
||||
dispose_op = fctlChunk.dispose_op;
|
||||
frameDuration = fctlChunk.delay_num * 1000 / (fctlChunk.delay_den == 0 ? 100 : fctlChunk.delay_den);
|
||||
frameWidth = fctlChunk.width;
|
||||
frameHeight = fctlChunk.height;
|
||||
frameX = fctlChunk.x_offset;
|
||||
frameY = fctlChunk.y_offset;
|
||||
}
|
||||
|
||||
private int encode(APNGWriter apngWriter) throws IOException {
|
||||
int fileSize = 8 + 13 + 12;
|
||||
|
||||
//prefixChunks
|
||||
for (Chunk chunk : prefixChunks) {
|
||||
fileSize += chunk.length + 12;
|
||||
}
|
||||
|
||||
//imageChunks
|
||||
for (Chunk chunk : imageChunks) {
|
||||
if (chunk instanceof IDATChunk) {
|
||||
fileSize += chunk.length + 12;
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
fileSize += chunk.length + 8;
|
||||
}
|
||||
}
|
||||
fileSize += sPNGEndChunk.length;
|
||||
apngWriter.reset(fileSize);
|
||||
apngWriter.putBytes(sPNGSignatures);
|
||||
//IHDR Chunk
|
||||
apngWriter.writeInt(13);
|
||||
int start = apngWriter.position();
|
||||
apngWriter.writeFourCC(IHDRChunk.ID);
|
||||
apngWriter.writeInt(frameWidth);
|
||||
apngWriter.writeInt(frameHeight);
|
||||
apngWriter.putBytes(ihdrData);
|
||||
CRC32 crc32 = getCRC32();
|
||||
crc32.reset();
|
||||
crc32.update(apngWriter.toByteArray(), start, 17);
|
||||
apngWriter.writeInt((int) crc32.getValue());
|
||||
|
||||
//prefixChunks
|
||||
for (Chunk chunk : prefixChunks) {
|
||||
if (chunk instanceof IENDChunk) {
|
||||
continue;
|
||||
}
|
||||
reader.reset();
|
||||
reader.skip(chunk.offset);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
|
||||
apngWriter.skip(chunk.length + 12);
|
||||
}
|
||||
//imageChunks
|
||||
for (Chunk chunk : imageChunks) {
|
||||
if (chunk instanceof IDATChunk) {
|
||||
reader.reset();
|
||||
reader.skip(chunk.offset);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
|
||||
apngWriter.skip(chunk.length + 12);
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
apngWriter.writeInt(chunk.length - 4);
|
||||
start = apngWriter.position();
|
||||
apngWriter.writeFourCC(IDATChunk.ID);
|
||||
|
||||
reader.reset();
|
||||
// skip to fdat data position
|
||||
reader.skip(chunk.offset + 4 + 4 + 4);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length - 4);
|
||||
|
||||
apngWriter.skip(chunk.length - 4);
|
||||
crc32.reset();
|
||||
crc32.update(apngWriter.toByteArray(), start, chunk.length);
|
||||
apngWriter.writeInt((int) crc32.getValue());
|
||||
}
|
||||
}
|
||||
//endChunk
|
||||
apngWriter.putBytes(sPNGEndChunk);
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
|
||||
try {
|
||||
int length = encode(writer);
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inMutable = true;
|
||||
options.inBitmap = reusedBitmap;
|
||||
byte[] bytes = writer.toByteArray();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, length, options);
|
||||
assert bitmap != null;
|
||||
canvas.drawBitmap(bitmap, (float) frameX / sampleSize, (float) frameY / sampleSize, paint);
|
||||
return bitmap;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
143
app/src/main/java/org/signal/glide/apng/decode/APNGParser.java
Normal file
143
app/src/main/java/org/signal/glide/apng/decode/APNGParser.java
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.StreamReader;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @link {https://www.w3.org/TR/PNG/#5PNG-file-signature}
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGParser {
|
||||
static class FormatException extends IOException {
|
||||
FormatException() {
|
||||
super("APNG Format error");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(String filePath) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = new FileInputStream(filePath);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Context context, String assetPath) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetPath);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Context context, int resId) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getResources().openRawResource(resId);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Reader in) {
|
||||
APNGReader reader = (in instanceof APNGReader) ? (APNGReader) in : new APNGReader(in);
|
||||
try {
|
||||
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
|
||||
throw new FormatException();
|
||||
}
|
||||
while (reader.available() > 0) {
|
||||
Chunk chunk = parseChunk(reader);
|
||||
if (chunk instanceof ACTLChunk) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<Chunk> parse(APNGReader reader) throws IOException {
|
||||
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
List<Chunk> chunks = new ArrayList<>();
|
||||
while (reader.available() > 0) {
|
||||
chunks.add(parseChunk(reader));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private static Chunk parseChunk(APNGReader reader) throws IOException {
|
||||
int offset = reader.position();
|
||||
int size = reader.readInt();
|
||||
int fourCC = reader.readFourCC();
|
||||
Chunk chunk;
|
||||
if (fourCC == ACTLChunk.ID) {
|
||||
chunk = new ACTLChunk();
|
||||
} else if (fourCC == FCTLChunk.ID) {
|
||||
chunk = new FCTLChunk();
|
||||
} else if (fourCC == FDATChunk.ID) {
|
||||
chunk = new FDATChunk();
|
||||
} else if (fourCC == IDATChunk.ID) {
|
||||
chunk = new IDATChunk();
|
||||
} else if (fourCC == IENDChunk.ID) {
|
||||
chunk = new IENDChunk();
|
||||
} else if (fourCC == IHDRChunk.ID) {
|
||||
chunk = new IHDRChunk();
|
||||
} else {
|
||||
chunk = new Chunk();
|
||||
}
|
||||
chunk.offset = offset;
|
||||
chunk.fourcc = fourCC;
|
||||
chunk.length = size;
|
||||
chunk.parse(reader);
|
||||
chunk.crc = reader.readInt();
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
53
app/src/main/java/org/signal/glide/apng/decode/Chunk.java
Normal file
53
app/src/main/java/org/signal/glide/apng/decode/Chunk.java
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: Length (长度) 4字节 指定数据块中数据域的长度,其长度不超过(231-1)字节
|
||||
* Chunk Type Code (数据块类型码) 4字节 数据块类型码由ASCII字母(A-Z和a-z)组成
|
||||
* Chunk Data (数据块数据) 可变长度 存储按照Chunk Type Code指定的数据
|
||||
* CRC (循环冗余检测) 4字节 存储用来检测是否有错误的循环冗余码
|
||||
* @Link https://www.w3.org/TR/PNG
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class Chunk {
|
||||
int length;
|
||||
int fourcc;
|
||||
int crc;
|
||||
int offset;
|
||||
|
||||
static int fourCCToInt(String fourCC) {
|
||||
if (TextUtils.isEmpty(fourCC) || fourCC.length() != 4) {
|
||||
return 0xbadeffff;
|
||||
}
|
||||
return (fourCC.charAt(0) & 0xff)
|
||||
| (fourCC.charAt(1) & 0xff) << 8
|
||||
| (fourCC.charAt(2) & 0xff) << 16
|
||||
| (fourCC.charAt(3) & 0xff) << 24
|
||||
;
|
||||
}
|
||||
|
||||
void parse(APNGReader reader) throws IOException {
|
||||
int available = reader.available();
|
||||
innerParse(reader);
|
||||
int offset = available - reader.available();
|
||||
if (offset > length) {
|
||||
throw new IOException("Out of chunk area");
|
||||
} else if (offset < length) {
|
||||
reader.skip(length - offset);
|
||||
}
|
||||
}
|
||||
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
}
|
||||
}
|
||||
121
app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java
Normal file
121
app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
* @see {link=https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fcTL.27:_The_Frame_Control_Chunk}
|
||||
*/
|
||||
class FCTLChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("fcTL");
|
||||
int sequence_number;
|
||||
/**
|
||||
* x_offset >= 0
|
||||
* y_offset >= 0
|
||||
* width > 0
|
||||
* height > 0
|
||||
* x_offset + width <= 'IHDR' width
|
||||
* y_offset + height <= 'IHDR' height
|
||||
*/
|
||||
/**
|
||||
* Width of the following frame.
|
||||
*/
|
||||
int width;
|
||||
/**
|
||||
* Height of the following frame.
|
||||
*/
|
||||
int height;
|
||||
/**
|
||||
* X position at which to render the following frame.
|
||||
*/
|
||||
int x_offset;
|
||||
/**
|
||||
* Y position at which to render the following frame.
|
||||
*/
|
||||
int y_offset;
|
||||
|
||||
/**
|
||||
* The delay_num and delay_den parameters together specify a fraction indicating the time to
|
||||
* display the current frame, in seconds. If the denominator is 0, it is to be treated as if it
|
||||
* were 100 (that is, delay_num then specifies 1/100ths of a second).
|
||||
* If the the value of the numerator is 0 the decoder should render the next frame as quickly as
|
||||
* possible, though viewers may impose a reasonable lower bound.
|
||||
* <p>
|
||||
* Frame timings should be independent of the time required for decoding and display of each frame,
|
||||
* so that animations will run at the same rate regardless of the performance of the decoder implementation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Frame delay fraction numerator.
|
||||
*/
|
||||
short delay_num;
|
||||
|
||||
/**
|
||||
* Frame delay fraction denominator.
|
||||
*/
|
||||
short delay_den;
|
||||
|
||||
/**
|
||||
* Type of frame area disposal to be done after rendering this frame.
|
||||
* dispose_op specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
|
||||
* If the first 'fcTL' chunk uses a dispose_op of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
|
||||
*/
|
||||
byte dispose_op;
|
||||
|
||||
/**
|
||||
* Type of frame area rendering for this frame.
|
||||
*/
|
||||
byte blend_op;
|
||||
|
||||
/**
|
||||
* No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_NON = 0;
|
||||
|
||||
/**
|
||||
* The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_BACKGROUND = 1;
|
||||
|
||||
/**
|
||||
* The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_PREVIOUS = 2;
|
||||
|
||||
/**
|
||||
* blend_op<code> specifies whether the frame is to be alpha blended into the current output buffer content,
|
||||
* or whether it should completely replace its region in the output buffer.
|
||||
*/
|
||||
/**
|
||||
* All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
|
||||
*/
|
||||
static final int APNG_BLEND_OP_SOURCE = 0;
|
||||
|
||||
/**
|
||||
* The frame should be composited onto the output buffer based on its alpha,
|
||||
* using a simple OVER operation as described in the Alpha Channel Processing section of the Extensions
|
||||
* to the PNG Specification, Version 1.2.0. Note that the second variation of the sample code is applicable.
|
||||
*/
|
||||
static final int APNG_BLEND_OP_OVER = 1;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
sequence_number = reader.readInt();
|
||||
width = reader.readInt();
|
||||
height = reader.readInt();
|
||||
x_offset = reader.readInt();
|
||||
y_offset = reader.readInt();
|
||||
delay_num = reader.readShort();
|
||||
delay_den = reader.readShort();
|
||||
dispose_op = reader.peek();
|
||||
blend_op = reader.peek();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fdAT.27:_The_Frame_Data_Chunk
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class FDATChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("fdAT");
|
||||
int sequence_number;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
sequence_number = reader.readInt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
/**
|
||||
* @Description: 作用描述
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IDATChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("IDAT");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
/**
|
||||
* @Description: 作用描述
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IENDChunk extends Chunk {
|
||||
static final int ID = Chunk.fourCCToInt("IEND");
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* The IHDR chunk shall be the first chunk in the PNG datastream. It contains:
|
||||
* <p>
|
||||
* Width 4 bytes
|
||||
* Height 4 bytes
|
||||
* Bit depth 1 byte
|
||||
* Colour type 1 byte
|
||||
* Compression method 1 byte
|
||||
* Filter method 1 byte
|
||||
* Interlace method 1 byte
|
||||
*
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IHDRChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("IHDR");
|
||||
/**
|
||||
* 图像宽度,以像素为单位
|
||||
*/
|
||||
int width;
|
||||
/**
|
||||
* 图像高度,以像素为单位
|
||||
*/
|
||||
int height;
|
||||
|
||||
byte[] data = new byte[5];
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
width = reader.readInt();
|
||||
height = reader.readInt();
|
||||
reader.read(data, 0, data.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class StillFrame extends Frame<APNGReader, APNGWriter> {
|
||||
|
||||
public StillFrame(APNGReader reader) {
|
||||
super(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inMutable = true;
|
||||
options.inBitmap = reusedBitmap;
|
||||
Bitmap bitmap = null;
|
||||
try {
|
||||
reader.reset();
|
||||
bitmap = BitmapFactory.decodeStream(reader.toInputStream(), null, options);
|
||||
assert bitmap != null;
|
||||
paint.setXfermode(null);
|
||||
canvas.drawBitmap(bitmap, 0, 0, paint);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
74
app/src/main/java/org/signal/glide/apng/io/APNGReader.java
Normal file
74
app/src/main/java/org/signal/glide/apng/io/APNGReader.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.io;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.signal.glide.common.io.FilterReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNGReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGReader extends FilterReader {
|
||||
private static ThreadLocal<byte[]> __intBytes = new ThreadLocal<>();
|
||||
|
||||
|
||||
protected static byte[] ensureBytes() {
|
||||
byte[] bytes = __intBytes.get();
|
||||
if (bytes == null) {
|
||||
bytes = new byte[4];
|
||||
__intBytes.set(bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public APNGReader(Reader in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public int readInt() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 4);
|
||||
return buf[3] & 0xFF |
|
||||
(buf[2] & 0xFF) << 8 |
|
||||
(buf[1] & 0xFF) << 16 |
|
||||
(buf[0] & 0xFF) << 24;
|
||||
}
|
||||
|
||||
public short readShort() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 2);
|
||||
return (short) (buf[1] & 0xFF |
|
||||
(buf[0] & 0xFF) << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return read FourCC and match chars
|
||||
*/
|
||||
public boolean matchFourCC(String chars) throws IOException {
|
||||
if (TextUtils.isEmpty(chars) || chars.length() != 4) {
|
||||
return false;
|
||||
}
|
||||
int fourCC = readFourCC();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (((fourCC >> (i * 8)) & 0xff) != chars.charAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int readFourCC() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 4);
|
||||
return buf[0] & 0xff | (buf[1] & 0xff) << 8 | (buf[2] & 0xff) << 16 | (buf[3] & 0xff) << 24;
|
||||
}
|
||||
}
|
||||
41
app/src/main/java/org/signal/glide/apng/io/APNGWriter.java
Normal file
41
app/src/main/java/org/signal/glide/apng/io/APNGWriter.java
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.io;
|
||||
|
||||
import org.signal.glide.common.io.ByteBufferWriter;
|
||||
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* @Description: APNGWriter
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGWriter extends ByteBufferWriter {
|
||||
public APNGWriter() {
|
||||
super();
|
||||
}
|
||||
|
||||
public void writeFourCC(int val) {
|
||||
putByte((byte) (val & 0xff));
|
||||
putByte((byte) ((val >> 8) & 0xff));
|
||||
putByte((byte) ((val >> 16) & 0xff));
|
||||
putByte((byte) ((val >> 24) & 0xff));
|
||||
}
|
||||
|
||||
public void writeInt(int val) {
|
||||
putByte((byte) ((val >> 24) & 0xff));
|
||||
putByte((byte) ((val >> 16) & 0xff));
|
||||
putByte((byte) ((val >> 8) & 0xff));
|
||||
putByte((byte) (val & 0xff));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(int size) {
|
||||
super.reset(size);
|
||||
this.byteBuffer.order(ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.DrawFilter;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PaintFlagsDrawFilter;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @Description: Frame animation drawable
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public abstract class FrameAnimationDrawable<Decoder extends FrameSeqDecoder> extends Drawable implements Animatable2Compat, FrameSeqDecoder.RenderListener {
|
||||
private static final String TAG = FrameAnimationDrawable.class.getSimpleName();
|
||||
private final Paint paint = new Paint();
|
||||
private final Decoder frameSeqDecoder;
|
||||
private DrawFilter drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
private Matrix matrix = new Matrix();
|
||||
private Set<AnimationCallback> animationCallbacks = new HashSet<>();
|
||||
private Bitmap bitmap;
|
||||
private static final int MSG_ANIMATION_START = 1;
|
||||
private static final int MSG_ANIMATION_END = 2;
|
||||
private Handler uiHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_ANIMATION_START:
|
||||
for (AnimationCallback animationCallback : animationCallbacks) {
|
||||
animationCallback.onAnimationStart(FrameAnimationDrawable.this);
|
||||
}
|
||||
break;
|
||||
case MSG_ANIMATION_END:
|
||||
for (AnimationCallback animationCallback : animationCallbacks) {
|
||||
animationCallback.onAnimationEnd(FrameAnimationDrawable.this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
private Runnable invalidateRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
invalidateSelf();
|
||||
}
|
||||
};
|
||||
private boolean autoPlay = true;
|
||||
|
||||
public FrameAnimationDrawable(Decoder frameSeqDecoder) {
|
||||
paint.setAntiAlias(true);
|
||||
this.frameSeqDecoder = frameSeqDecoder;
|
||||
}
|
||||
|
||||
public FrameAnimationDrawable(Loader provider) {
|
||||
paint.setAntiAlias(true);
|
||||
this.frameSeqDecoder = createFrameSeqDecoder(provider, this);
|
||||
}
|
||||
|
||||
public void setAutoPlay(boolean autoPlay) {
|
||||
this.autoPlay = autoPlay;
|
||||
}
|
||||
|
||||
protected abstract Decoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener);
|
||||
|
||||
/**
|
||||
* @param loopLimit <=0为无限播放,>0为实际播放次数
|
||||
*/
|
||||
public void setLoopLimit(int loopLimit) {
|
||||
frameSeqDecoder.setLoopLimit(loopLimit);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
frameSeqDecoder.reset();
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
frameSeqDecoder.pause();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
frameSeqDecoder.resume();
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return frameSeqDecoder.isPaused();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (autoPlay) {
|
||||
frameSeqDecoder.start();
|
||||
} else {
|
||||
this.frameSeqDecoder.addRenderListener(this);
|
||||
if (!this.frameSeqDecoder.isRunning()) {
|
||||
this.frameSeqDecoder.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (autoPlay) {
|
||||
frameSeqDecoder.stop();
|
||||
} else {
|
||||
this.frameSeqDecoder.removeRenderListener(this);
|
||||
this.frameSeqDecoder.stopIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return frameSeqDecoder.isRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
if (bitmap == null || bitmap.isRecycled()) {
|
||||
return;
|
||||
}
|
||||
canvas.setDrawFilter(drawFilter);
|
||||
canvas.drawBitmap(bitmap, matrix, paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBounds(int left, int top, int right, int bottom) {
|
||||
super.setBounds(left, top, right, bottom);
|
||||
boolean sampleSizeChanged = frameSeqDecoder.setDesiredSize(getBounds().width(), getBounds().height());
|
||||
matrix.setScale(
|
||||
1.0f * getBounds().width() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().width(),
|
||||
1.0f * getBounds().height() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().height());
|
||||
|
||||
if (sampleSizeChanged)
|
||||
this.bitmap = Bitmap.createBitmap(
|
||||
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
|
||||
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
|
||||
Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
paint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
Message.obtain(uiHandler, MSG_ANIMATION_START).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRender(ByteBuffer byteBuffer) {
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
if (this.bitmap == null || this.bitmap.isRecycled()) {
|
||||
this.bitmap = Bitmap.createBitmap(
|
||||
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
|
||||
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
|
||||
Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
byteBuffer.rewind();
|
||||
if (byteBuffer.remaining() < this.bitmap.getByteCount()) {
|
||||
Log.e(TAG, "onRender:Buffer not large enough for pixels");
|
||||
return;
|
||||
}
|
||||
this.bitmap.copyPixelsFromBuffer(byteBuffer);
|
||||
uiHandler.post(invalidateRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnd() {
|
||||
Message.obtain(uiHandler, MSG_ANIMATION_END).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVisible(boolean visible, boolean restart) {
|
||||
if (this.autoPlay) {
|
||||
if (visible) {
|
||||
if (!isRunning()) {
|
||||
start();
|
||||
}
|
||||
} else if (isRunning()) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
return super.setVisible(visible, restart);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
try {
|
||||
return frameSeqDecoder.getBounds().width();
|
||||
} catch (Exception exception) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
try {
|
||||
return frameSeqDecoder.getBounds().height();
|
||||
} catch (Exception exception) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerAnimationCallback(@NonNull AnimationCallback animationCallback) {
|
||||
this.animationCallbacks.add(animationCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unregisterAnimationCallback(@NonNull AnimationCallback animationCallback) {
|
||||
return this.animationCallbacks.remove(animationCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAnimationCallbacks() {
|
||||
this.animationCallbacks.clear();
|
||||
}
|
||||
}
|
||||
33
app/src/main/java/org/signal/glide/common/decode/Frame.java
Normal file
33
app/src/main/java/org/signal/glide/common/decode/Frame.java
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.Writer;
|
||||
|
||||
/**
|
||||
* @Description: One frame in an animation
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public abstract class Frame<R extends Reader, W extends Writer> {
|
||||
protected final R reader;
|
||||
public int frameWidth;
|
||||
public int frameHeight;
|
||||
public int frameX;
|
||||
public int frameY;
|
||||
public int frameDuration;
|
||||
|
||||
public Frame(R reader) {
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
public abstract Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, W writer);
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.common.executor.FrameDecoderExecutor;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.Writer;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
/**
|
||||
* @Description: Abstract Frame Animation Decoder
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public abstract class FrameSeqDecoder<R extends Reader, W extends Writer> {
|
||||
private static final String TAG = FrameSeqDecoder.class.getSimpleName();
|
||||
private final int taskId;
|
||||
|
||||
private final Loader mLoader;
|
||||
private final Handler workerHandler;
|
||||
protected List<Frame> frames = new ArrayList<>();
|
||||
protected int frameIndex = -1;
|
||||
private int playCount;
|
||||
private Integer loopLimit = null;
|
||||
private Set<RenderListener> renderListeners = new HashSet<>();
|
||||
private AtomicBoolean paused = new AtomicBoolean(true);
|
||||
private static final Rect RECT_EMPTY = new Rect();
|
||||
private Runnable renderTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (paused.get()) {
|
||||
return;
|
||||
}
|
||||
if (canStep()) {
|
||||
long start = System.currentTimeMillis();
|
||||
long delay = step();
|
||||
long cost = System.currentTimeMillis() - start;
|
||||
workerHandler.postDelayed(this, Math.max(0, delay - cost));
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onRender(frameBuffer);
|
||||
}
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
protected int sampleSize = 1;
|
||||
|
||||
private Set<Bitmap> cacheBitmaps = new HashSet<>();
|
||||
protected Map<Bitmap, Canvas> cachedCanvas = new WeakHashMap<>();
|
||||
protected ByteBuffer frameBuffer;
|
||||
protected volatile Rect fullRect;
|
||||
private W mWriter = getWriter();
|
||||
private R mReader = null;
|
||||
|
||||
/**
|
||||
* If played all the needed
|
||||
*/
|
||||
private boolean finished = false;
|
||||
|
||||
private enum State {
|
||||
IDLE,
|
||||
RUNNING,
|
||||
INITIALIZING,
|
||||
FINISHING,
|
||||
}
|
||||
|
||||
private volatile State mState = State.IDLE;
|
||||
|
||||
public Loader getLoader() {
|
||||
return mLoader;
|
||||
}
|
||||
|
||||
protected abstract W getWriter();
|
||||
|
||||
protected abstract R getReader(Reader reader);
|
||||
|
||||
protected Bitmap obtainBitmap(int width, int height) {
|
||||
Bitmap ret = null;
|
||||
Iterator<Bitmap> iterator = cacheBitmaps.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
int reuseSize = width * height * 4;
|
||||
ret = iterator.next();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (ret != null && ret.getAllocationByteCount() >= reuseSize) {
|
||||
iterator.remove();
|
||||
if (ret.getWidth() != width || ret.getHeight() != height) {
|
||||
ret.reconfigure(width, height, Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
ret.eraseColor(0);
|
||||
return ret;
|
||||
}
|
||||
} else {
|
||||
if (ret != null && ret.getByteCount() >= reuseSize) {
|
||||
if (ret.getWidth() == width && ret.getHeight() == height) {
|
||||
iterator.remove();
|
||||
ret.eraseColor(0);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Bitmap.Config config = Bitmap.Config.ARGB_8888;
|
||||
ret = Bitmap.createBitmap(width, height, config);
|
||||
} catch (OutOfMemoryError e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected void recycleBitmap(Bitmap bitmap) {
|
||||
if (bitmap != null && !cacheBitmaps.contains(bitmap)) {
|
||||
cacheBitmaps.add(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码器的渲染回调
|
||||
*/
|
||||
public interface RenderListener {
|
||||
/**
|
||||
* 播放开始
|
||||
*/
|
||||
void onStart();
|
||||
|
||||
/**
|
||||
* 帧播放
|
||||
*/
|
||||
void onRender(ByteBuffer byteBuffer);
|
||||
|
||||
/**
|
||||
* 播放结束
|
||||
*/
|
||||
void onEnd();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param loader webp的reader
|
||||
* @param renderListener 渲染的回调
|
||||
*/
|
||||
public FrameSeqDecoder(Loader loader, @Nullable RenderListener renderListener) {
|
||||
this.mLoader = loader;
|
||||
if (renderListener != null) {
|
||||
this.renderListeners.add(renderListener);
|
||||
}
|
||||
this.taskId = FrameDecoderExecutor.getInstance().generateTaskId();
|
||||
this.workerHandler = new Handler(FrameDecoderExecutor.getInstance().getLooper(taskId));
|
||||
}
|
||||
|
||||
|
||||
public void addRenderListener(final RenderListener renderListener) {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
renderListeners.add(renderListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void removeRenderListener(final RenderListener renderListener) {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
renderListeners.remove(renderListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stopIfNeeded() {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (renderListeners.size() == 0) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Rect getBounds() {
|
||||
if (fullRect == null) {
|
||||
if (mState == State.FINISHING) {
|
||||
Log.e(TAG, "In finishing,do not interrupt");
|
||||
}
|
||||
final Thread thread = Thread.currentThread();
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (fullRect == null) {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
fullRect = RECT_EMPTY;
|
||||
} finally {
|
||||
LockSupport.unpark(thread);
|
||||
}
|
||||
}
|
||||
});
|
||||
LockSupport.park(thread);
|
||||
}
|
||||
return fullRect;
|
||||
}
|
||||
|
||||
private void initCanvasBounds(Rect rect) {
|
||||
fullRect = rect;
|
||||
frameBuffer = ByteBuffer.allocate((rect.width() * rect.height() / (sampleSize * sampleSize) + 1) * 4);
|
||||
if (mWriter == null) {
|
||||
mWriter = getWriter();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int getFrameCount() {
|
||||
return this.frames.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Loop Count defined in file
|
||||
*/
|
||||
protected abstract int getLoopCount();
|
||||
|
||||
public void start() {
|
||||
if (fullRect == RECT_EMPTY) {
|
||||
return;
|
||||
}
|
||||
if (mState == State.RUNNING || mState == State.INITIALIZING) {
|
||||
Log.i(TAG, debugInfo() + " Already started");
|
||||
return;
|
||||
}
|
||||
if (mState == State.FINISHING) {
|
||||
Log.e(TAG, debugInfo() + " Processing,wait for finish at " + mState);
|
||||
}
|
||||
mState = State.INITIALIZING;
|
||||
if (Looper.myLooper() == workerHandler.getLooper()) {
|
||||
innerStart();
|
||||
} else {
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStart();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void innerStart() {
|
||||
paused.compareAndSet(true, false);
|
||||
|
||||
final long start = System.currentTimeMillis();
|
||||
try {
|
||||
if (frames.size() == 0) {
|
||||
try {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Log.i(TAG, debugInfo() + " Set state to RUNNING,cost " + (System.currentTimeMillis() - start));
|
||||
mState = State.RUNNING;
|
||||
}
|
||||
if (getNumPlays() == 0 || !finished) {
|
||||
this.frameIndex = -1;
|
||||
renderTask.run();
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onStart();
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, debugInfo() + " No need to started");
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void innerStop() {
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
frames.clear();
|
||||
for (Bitmap bitmap : cacheBitmaps) {
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
cacheBitmaps.clear();
|
||||
if (frameBuffer != null) {
|
||||
frameBuffer = null;
|
||||
}
|
||||
cachedCanvas.clear();
|
||||
try {
|
||||
if (mReader != null) {
|
||||
mReader.close();
|
||||
mReader = null;
|
||||
}
|
||||
if (mWriter != null) {
|
||||
mWriter.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
release();
|
||||
mState = State.IDLE;
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (fullRect == RECT_EMPTY) {
|
||||
return;
|
||||
}
|
||||
if (mState == State.FINISHING || mState == State.IDLE) {
|
||||
Log.i(TAG, debugInfo() + "No need to stop");
|
||||
return;
|
||||
}
|
||||
if (mState == State.INITIALIZING) {
|
||||
Log.e(TAG, debugInfo() + "Processing,wait for finish at " + mState);
|
||||
}
|
||||
mState = State.FINISHING;
|
||||
if (Looper.myLooper() == workerHandler.getLooper()) {
|
||||
innerStop();
|
||||
} else {
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private String debugInfo() {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected abstract void release();
|
||||
|
||||
public boolean isRunning() {
|
||||
return mState == State.RUNNING || mState == State.INITIALIZING;
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return paused.get();
|
||||
}
|
||||
|
||||
public void setLoopLimit(int limit) {
|
||||
this.loopLimit = limit;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.playCount = 0;
|
||||
this.frameIndex = -1;
|
||||
this.finished = false;
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
paused.compareAndSet(false, true);
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
paused.compareAndSet(true, false);
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
workerHandler.post(renderTask);
|
||||
}
|
||||
|
||||
|
||||
public int getSampleSize() {
|
||||
return sampleSize;
|
||||
}
|
||||
|
||||
public boolean setDesiredSize(int width, int height) {
|
||||
boolean sampleSizeChanged = false;
|
||||
int sample = getDesiredSample(width, height);
|
||||
if (sample != this.sampleSize) {
|
||||
this.sampleSize = sample;
|
||||
sampleSizeChanged = true;
|
||||
final boolean tempRunning = isRunning();
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStop();
|
||||
try {
|
||||
initCanvasBounds(read(getReader(mLoader.obtain())));
|
||||
if (tempRunning) {
|
||||
innerStart();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return sampleSizeChanged;
|
||||
}
|
||||
|
||||
protected int getDesiredSample(int desiredWidth, int desiredHeight) {
|
||||
if (desiredWidth == 0 || desiredHeight == 0) {
|
||||
return 1;
|
||||
}
|
||||
int radio = Math.min(getBounds().width() / desiredWidth, getBounds().height() / desiredHeight);
|
||||
int sample = 1;
|
||||
while ((sample * 2) <= radio) {
|
||||
sample *= 2;
|
||||
}
|
||||
return sample;
|
||||
}
|
||||
|
||||
protected abstract Rect read(R reader) throws IOException;
|
||||
|
||||
private int getNumPlays() {
|
||||
return this.loopLimit != null ? this.loopLimit : this.getLoopCount();
|
||||
}
|
||||
|
||||
private boolean canStep() {
|
||||
if (!isRunning()) {
|
||||
return false;
|
||||
}
|
||||
if (frames.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
if (getNumPlays() <= 0) {
|
||||
return true;
|
||||
}
|
||||
if (this.playCount < getNumPlays() - 1) {
|
||||
return true;
|
||||
} else if (this.playCount == getNumPlays() - 1 && this.frameIndex < this.getFrameCount() - 1) {
|
||||
return true;
|
||||
}
|
||||
finished = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private long step() {
|
||||
this.frameIndex++;
|
||||
if (this.frameIndex >= this.getFrameCount()) {
|
||||
this.frameIndex = 0;
|
||||
this.playCount++;
|
||||
}
|
||||
Frame frame = getFrame(this.frameIndex);
|
||||
if (frame == null) {
|
||||
return 0;
|
||||
}
|
||||
renderFrame(frame);
|
||||
return frame.frameDuration;
|
||||
}
|
||||
|
||||
protected abstract void renderFrame(Frame frame);
|
||||
|
||||
private Frame getFrame(int index) {
|
||||
if (index < 0 || index >= frames.size()) {
|
||||
return null;
|
||||
}
|
||||
return frames.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Indexed frame
|
||||
*
|
||||
* @param index <0 means reverse from last index
|
||||
*/
|
||||
public Bitmap getFrameBitmap(int index) throws IOException {
|
||||
if (mState != State.IDLE) {
|
||||
Log.e(TAG, debugInfo() + ",stop first");
|
||||
return null;
|
||||
}
|
||||
mState = State.RUNNING;
|
||||
paused.compareAndSet(true, false);
|
||||
if (frames.size() == 0) {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
}
|
||||
if (index < 0) {
|
||||
index += this.frames.size();
|
||||
}
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
frameIndex = -1;
|
||||
while (frameIndex < index) {
|
||||
if (canStep()) {
|
||||
step();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
frameBuffer.rewind();
|
||||
Bitmap bitmap = Bitmap.createBitmap(getBounds().width() / getSampleSize(), getBounds().height() / getSampleSize(), Bitmap.Config.ARGB_8888);
|
||||
bitmap.copyPixelsFromBuffer(frameBuffer);
|
||||
innerStop();
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.executor;
|
||||
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @Description: com.github.penfeizhou.animation.executor
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-11-21
|
||||
*/
|
||||
public class FrameDecoderExecutor {
|
||||
private static int sPoolNumber = 4;
|
||||
private ArrayList<HandlerThread> mHandlerThreadGroup = new ArrayList<>();
|
||||
private AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
private FrameDecoderExecutor() {
|
||||
}
|
||||
|
||||
static class Inner {
|
||||
static final FrameDecoderExecutor sInstance = new FrameDecoderExecutor();
|
||||
}
|
||||
|
||||
public void setPoolSize(int size) {
|
||||
sPoolNumber = size;
|
||||
}
|
||||
|
||||
public static FrameDecoderExecutor getInstance() {
|
||||
return Inner.sInstance;
|
||||
}
|
||||
|
||||
public Looper getLooper(int taskId) {
|
||||
int idx = taskId % sPoolNumber;
|
||||
if (idx >= mHandlerThreadGroup.size()) {
|
||||
HandlerThread handlerThread = new HandlerThread("FrameDecoderExecutor-" + idx);
|
||||
handlerThread.start();
|
||||
|
||||
mHandlerThreadGroup.add(handlerThread);
|
||||
Looper looper = handlerThread.getLooper();
|
||||
if (looper != null) {
|
||||
return looper;
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
} else {
|
||||
if (mHandlerThreadGroup.get(idx) != null) {
|
||||
Looper looper = mHandlerThreadGroup.get(idx).getLooper();
|
||||
if (looper != null) {
|
||||
return looper;
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int generateTaskId() {
|
||||
return counter.getAndIncrement();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-14
|
||||
*/
|
||||
public class ByteBufferReader implements Reader {
|
||||
|
||||
private final ByteBuffer byteBuffer;
|
||||
|
||||
public ByteBufferReader(ByteBuffer byteBuffer) {
|
||||
this.byteBuffer = byteBuffer;
|
||||
byteBuffer.position(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long total) throws IOException {
|
||||
byteBuffer.position((int) (byteBuffer.position() + total));
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
return byteBuffer.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
byteBuffer.position(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int start, int byteCount) throws IOException {
|
||||
byteBuffer.get(buffer, start, byteCount);
|
||||
return byteCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return byteBuffer.limit() - byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
return new ByteArrayInputStream(byteBuffer.array());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* @Description: ByteBufferWriter
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-12
|
||||
*/
|
||||
public class ByteBufferWriter implements Writer {
|
||||
|
||||
protected ByteBuffer byteBuffer;
|
||||
|
||||
public ByteBufferWriter() {
|
||||
reset(10 * 1024);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putByte(byte b) {
|
||||
byteBuffer.put(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putBytes(byte[] b) {
|
||||
byteBuffer.put(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skip(int length) {
|
||||
byteBuffer.position(length + position());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toByteArray() {
|
||||
return byteBuffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(int size) {
|
||||
if (byteBuffer == null || size > byteBuffer.capacity()) {
|
||||
byteBuffer = ByteBuffer.allocate(size);
|
||||
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
byteBuffer.clear();
|
||||
}
|
||||
}
|
||||
30
app/src/main/java/org/signal/glide/common/io/FileReader.java
Normal file
30
app/src/main/java/org/signal/glide/common/io/FileReader.java
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: FileReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-23
|
||||
*/
|
||||
public class FileReader extends FilterReader {
|
||||
private final File mFile;
|
||||
|
||||
public FileReader(File file) throws IOException {
|
||||
super(new StreamReader(new FileInputStream(file)));
|
||||
mFile = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
reader.close();
|
||||
reader = new StreamReader(new FileInputStream(mFile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: FilterReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-23
|
||||
*/
|
||||
public class FilterReader implements Reader {
|
||||
protected Reader reader;
|
||||
|
||||
public FilterReader(Reader in) {
|
||||
this.reader = in;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long total) throws IOException {
|
||||
return reader.skip(total);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
return reader.peek();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
reader.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return reader.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int start, int byteCount) throws IOException {
|
||||
return reader.read(buffer, start, byteCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return reader.available();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
reader.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
reset();
|
||||
return reader.toInputStream();
|
||||
}
|
||||
}
|
||||
35
app/src/main/java/org/signal/glide/common/io/Reader.java
Normal file
35
app/src/main/java/org/signal/glide/common/io/Reader.java
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @link {https://developers.google.com/speed/webp/docs/riff_container#terminology_basics}
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-11
|
||||
*/
|
||||
public interface Reader {
|
||||
long skip(long total) throws IOException;
|
||||
|
||||
byte peek() throws IOException;
|
||||
|
||||
void reset() throws IOException;
|
||||
|
||||
int position();
|
||||
|
||||
int read(byte[] buffer, int start, int byteCount) throws IOException;
|
||||
|
||||
int available() throws IOException;
|
||||
|
||||
/**
|
||||
* close io
|
||||
*/
|
||||
void close() throws IOException;
|
||||
|
||||
InputStream toInputStream() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-11
|
||||
*/
|
||||
public class StreamReader extends FilterInputStream implements Reader {
|
||||
private int position;
|
||||
|
||||
public StreamReader(InputStream in) {
|
||||
super(in);
|
||||
try {
|
||||
in.reset();
|
||||
} catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
byte ret = (byte) read();
|
||||
position++;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int ret = super.read(b, off, len);
|
||||
position += Math.max(0, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
super.reset();
|
||||
position = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long ret = super.skip(n);
|
||||
position += ret;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
29
app/src/main/java/org/signal/glide/common/io/Writer.java
Normal file
29
app/src/main/java/org/signal/glide/common/io/Writer.java
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-12
|
||||
*/
|
||||
public interface Writer {
|
||||
void reset(int size);
|
||||
|
||||
void putByte(byte b);
|
||||
|
||||
void putBytes(byte[] b);
|
||||
|
||||
int position();
|
||||
|
||||
void skip(int length);
|
||||
|
||||
byte[] toByteArray();
|
||||
|
||||
void close() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: 从Asset中读取流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class AssetStreamLoader extends StreamLoader {
|
||||
|
||||
private final Context mContext;
|
||||
private final String mAssetName;
|
||||
|
||||
public AssetStreamLoader(Context context, String assetName) {
|
||||
mContext = context.getApplicationContext();
|
||||
mAssetName = assetName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException {
|
||||
return mContext.getAssets().open(mAssetName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.ByteBufferReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* @Description: ByteBufferLoader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-15
|
||||
*/
|
||||
public abstract class ByteBufferLoader implements Loader {
|
||||
public abstract ByteBuffer getByteBuffer();
|
||||
|
||||
@Override
|
||||
public Reader obtain() throws IOException {
|
||||
return new ByteBufferReader(getByteBuffer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.FileReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: 从文件加载流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class FileLoader implements Loader {
|
||||
|
||||
private final File mFile;
|
||||
private Reader mReader;
|
||||
|
||||
public FileLoader(String path) {
|
||||
mFile = new File(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Reader obtain() throws IOException {
|
||||
return new FileReader(mFile);
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/org/signal/glide/common/loader/Loader.java
Normal file
19
app/src/main/java/org/signal/glide/common/loader/Loader.java
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: Loader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-14
|
||||
*/
|
||||
public interface Loader {
|
||||
Reader obtain() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: 从资源加载流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class ResourceStreamLoader extends StreamLoader {
|
||||
private final Context mContext;
|
||||
private final int mResId;
|
||||
|
||||
|
||||
public ResourceStreamLoader(Context context, int resId) {
|
||||
mContext = context.getApplicationContext();
|
||||
mResId = resId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException {
|
||||
return mContext.getResources().openRawResource(mResId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.StreamReader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public abstract class StreamLoader implements Loader {
|
||||
protected abstract InputStream getInputStream() throws IOException;
|
||||
|
||||
|
||||
public final synchronized Reader obtain() throws IOException {
|
||||
return new StreamReader(getInputStream());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
|
||||
public final class AppCapabilities {
|
||||
|
||||
@@ -9,12 +9,13 @@ public final class AppCapabilities {
|
||||
}
|
||||
|
||||
private static final boolean UUID_CAPABLE = false;
|
||||
private static final boolean GV2_CAPABLE = true;
|
||||
|
||||
/**
|
||||
* @param storageCapable Whether or not the user can use storage service. This is another way of
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static SignalServiceProfile.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new SignalServiceProfile.Capabilities(UUID_CAPABLE, FeatureFlags.groupsV2(), storageCapable);
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, FeatureFlags.groupsV1AutoMigration());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
@@ -32,6 +33,7 @@ import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
@@ -42,15 +44,17 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.messages.InitialMessageRetriever;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
@@ -60,7 +64,6 @@ import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
@@ -69,6 +72,7 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
@@ -96,7 +100,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
private ViewOnceMessageManager viewOnceMessageManager;
|
||||
private TypingStatusRepository typingStatusRepository;
|
||||
private TypingStatusSender typingStatusSender;
|
||||
private IncomingMessageObserver incomingMessageObserver;
|
||||
private PersistentLogger persistentLogger;
|
||||
|
||||
private volatile boolean isAppVisible;
|
||||
@@ -107,10 +110,11 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
long startTime = System.currentTimeMillis();
|
||||
super.onCreate();
|
||||
Log.i(TAG, "onCreate()");
|
||||
initializeSecurityProvider();
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
initializeCrashHandling();
|
||||
initializeAppDependencies();
|
||||
initializeFirstEverAppLaunch();
|
||||
@@ -128,12 +132,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
initializePendingMessages();
|
||||
initializeBlobProvider();
|
||||
initializeCleanup();
|
||||
initializeGlideCodecs();
|
||||
|
||||
FeatureFlags.init();
|
||||
NotificationChannels.create(this);
|
||||
RefreshPreKeysJob.scheduleIfNecessary();
|
||||
StorageSyncHelper.scheduleRoutineSync();
|
||||
RegistrationUtil.markRegistrationPossiblyComplete();
|
||||
RegistrationUtil.maybeMarkRegistrationComplete(this);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
@@ -141,6 +146,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().beginJobLoop();
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,11 +155,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
Log.i(TAG, "App is now visible.");
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
ApplicationDependencies.getRecipientCache().warmUp();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getFrameRateTracker().begin();
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
catchUpOnMessages();
|
||||
checkBuildExpiration();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -189,6 +197,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
return persistentLogger;
|
||||
}
|
||||
|
||||
public void checkBuildExpiration() {
|
||||
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
|
||||
Log.w(TAG, "Build expired!");
|
||||
SignalStore.misc().markClientDeprecated();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSecurityProvider() {
|
||||
try {
|
||||
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
|
||||
@@ -230,7 +245,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
}
|
||||
|
||||
public void initializeMessageRetrieval() {
|
||||
this.incomingMessageObserver = new IncomingMessageObserver(this);
|
||||
ApplicationDependencies.getIncomingMessageObserver();
|
||||
}
|
||||
|
||||
private void initializeAppDependencies() {
|
||||
@@ -378,32 +393,31 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
});
|
||||
}
|
||||
|
||||
private void catchUpOnMessages() {
|
||||
InitialMessageRetriever retriever = ApplicationDependencies.getInitialMessageRetriever();
|
||||
private void initializeGlideCodecs() {
|
||||
SignalGlideCodecs.setLogProvider(new org.signal.glide.Log.Provider() {
|
||||
@Override
|
||||
public void v(@NonNull String tag, @NonNull String message) {
|
||||
Log.v(tag, message);
|
||||
}
|
||||
|
||||
if (retriever.isCaughtUp()) {
|
||||
return;
|
||||
}
|
||||
@Override
|
||||
public void d(@NonNull String tag, @NonNull String message) {
|
||||
Log.d(tag, message);
|
||||
}
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
long startTime = System.currentTimeMillis();
|
||||
@Override
|
||||
public void i(@NonNull String tag, @NonNull String message) {
|
||||
Log.i(tag, message);
|
||||
}
|
||||
|
||||
switch (retriever.begin(TimeUnit.SECONDS.toMillis(60))) {
|
||||
case SUCCESS:
|
||||
Log.i(TAG, "Successfully caught up on messages. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
break;
|
||||
case FAILURE_TIMEOUT:
|
||||
Log.w(TAG, "Did not finish catching up due to a timeout. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
break;
|
||||
case FAILURE_ERROR:
|
||||
Log.w(TAG, "Did not finish catching up due to an error. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
break;
|
||||
case SKIPPED_ALREADY_CAUGHT_UP:
|
||||
Log.i(TAG, "Already caught up. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
break;
|
||||
case SKIPPED_ALREADY_RUNNING:
|
||||
Log.i(TAG, "Already in the process of catching up. " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
break;
|
||||
@Override
|
||||
public void w(@NonNull String tag, @NonNull String message) {
|
||||
Log.w(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||
Log.e(tag, message, throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,12 +17,15 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
@@ -30,19 +33,25 @@ import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.thoughtcrime.securesms.help.HelpFragment;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
@@ -53,13 +62,16 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
*
|
||||
*/
|
||||
|
||||
public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarActivity
|
||||
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||
{
|
||||
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
|
||||
|
||||
private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile";
|
||||
private static final String PREFERENCE_CATEGORY_USERNAME = "preference_category_username";
|
||||
private static final String PREFERENCE_CATEGORY_SMS_MMS = "preference_category_sms_mms";
|
||||
private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications";
|
||||
private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
|
||||
@@ -69,6 +81,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
|
||||
private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help";
|
||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
@@ -86,6 +99,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
|
||||
if (getIntent() != null && getIntent().getCategories() != null && getIntent().getCategories().contains("android.intent.category.NOTIFICATION_PREFERENCES")) {
|
||||
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
|
||||
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_BACKUPS_FRAGMENT, false)) {
|
||||
initFragment(android.R.id.content, new BackupsPreferenceFragment());
|
||||
} else if (icicle == null) {
|
||||
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
|
||||
}
|
||||
@@ -134,6 +149,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
}
|
||||
}
|
||||
|
||||
public void pushFragment(@NonNull Fragment fragment) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||
.replace(android.R.id.content, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment {
|
||||
|
||||
@Override
|
||||
@@ -142,6 +165,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
|
||||
this.findPreference(PREFERENCE_CATEGORY_PROFILE)
|
||||
.setOnPreferenceClickListener(new ProfileClickListener());
|
||||
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
|
||||
.setOnPreferenceClickListener(new UsernameClickListener());
|
||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
|
||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||
@@ -159,7 +184,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
|
||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
this.findPreference(PREFERENCE_CATEGORY_DONATE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DONATE));
|
||||
|
||||
tintIcons();
|
||||
}
|
||||
@@ -174,6 +201,24 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
if (FeatureFlags.usernames()) {
|
||||
UsernamePreference pref = (UsernamePreference) findPreference(PREFERENCE_CATEGORY_USERNAME);
|
||||
pref.setVisible(shouldDisplayUsernameReminder());
|
||||
pref.setOnLongClickListener(v -> {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.ApplicationPreferencesActivity_hide_reminder)
|
||||
.setPositiveButton(R.string.ApplicationPreferencesActivity_hide, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
SignalStore.misc().hideUsernameReminder();
|
||||
findPreference(PREFERENCE_CATEGORY_USERNAME).setVisible(false);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, ((dialog, which) -> dialog.dismiss()))
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -188,6 +233,11 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
private void setCategorySummaries() {
|
||||
((ProfilePreference)this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh();
|
||||
|
||||
if (FeatureFlags.usernames()) {
|
||||
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
|
||||
.setVisible(shouldDisplayUsernameReminder());
|
||||
}
|
||||
|
||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
||||
.setSummary(SmsMmsPreferenceFragment.getSummary(getActivity()));
|
||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||
@@ -207,6 +257,10 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldDisplayUsernameReminder() {
|
||||
return FeatureFlags.usernames() && !Recipient.self().getUsername().isPresent() && SignalStore.misc().shouldShowUsernameReminder();
|
||||
}
|
||||
|
||||
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
|
||||
private String category;
|
||||
|
||||
@@ -247,6 +301,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
case PREFERENCE_CATEGORY_HELP:
|
||||
fragment = new HelpFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_DONATE:
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url));
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
@@ -255,14 +312,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
|
||||
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
||||
|
||||
fragmentTransaction.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end);
|
||||
|
||||
fragmentTransaction.replace(android.R.id.content, fragment);
|
||||
fragmentTransaction.addToBackStack(null);
|
||||
fragmentTransaction.commit();
|
||||
((ApplicationPreferencesActivity) requireActivity()).pushFragment(fragment);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -272,12 +322,15 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent intent = new Intent(preference.getContext(), EditProfileActivity.class);
|
||||
intent.putExtra(EditProfileActivity.EXCLUDE_SYSTEM, true);
|
||||
intent.putExtra(EditProfileActivity.DISPLAY_USERNAME, true);
|
||||
intent.putExtra(EditProfileActivity.NEXT_BUTTON_TEXT, R.string.save);
|
||||
requireActivity().startActivity(EditProfileActivity.getIntentForUserProfileEdit(preference.getContext()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
requireActivity().startActivity(intent);
|
||||
private class UsernameClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
requireActivity().startActivity(EditProfileActivity.getIntentForUsernameEdit(preference.getContext()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,28 @@ package org.thoughtcrime.securesms;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.transition.TransitionInflater;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
||||
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
@@ -29,11 +34,12 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
|
||||
/**
|
||||
* Activity for displaying avatars full screen.
|
||||
*/
|
||||
public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final String TAG = Log.tag(AvatarPreviewActivity.class);
|
||||
|
||||
@@ -58,90 +64,74 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActionBarActi
|
||||
setTheme(R.style.TextSecure_MediaPreview);
|
||||
setContentView(R.layout.contact_photo_preview_activity);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
ImageView avatar = findViewById(R.id.avatar);
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
postponeEnterTransition();
|
||||
TransitionInflater inflater = TransitionInflater.from(this);
|
||||
getWindow().setSharedElementEnterTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_enter_transition_set));
|
||||
getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set));
|
||||
}
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
ImageView avatar = findViewById(R.id.avatar);
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
|
||||
showSystemUI();
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
Context context = getApplicationContext();
|
||||
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
|
||||
|
||||
Recipient.live(recipientId).observe(this, recipient -> {
|
||||
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
|
||||
: recipient.getContactPhoto();
|
||||
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
||||
: recipient.getFallbackContactPhoto();
|
||||
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
|
||||
: recipient.getContactPhoto();
|
||||
FallbackContactPhoto fallbackPhoto = recipient.isSelf() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
||||
: recipient.getFallbackContactPhoto();
|
||||
|
||||
GlideApp.with(this).load(contactPhoto)
|
||||
.fallback(fallbackPhoto.asCallCard(this))
|
||||
.error(fallbackPhoto.asCallCard(this))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.addListener(new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
||||
Log.w(TAG, "Unable to load avatar, or avatar removed, closing");
|
||||
finish();
|
||||
return false;
|
||||
}
|
||||
Resources resources = this.getResources();
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(avatar);
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(contactPhoto)
|
||||
.fallback(fallbackPhoto.asCallCard(this))
|
||||
.error(fallbackPhoto.asCallCard(this))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.addListener(new RequestListener<Bitmap>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Bitmap> target, boolean isFirstResource) {
|
||||
Log.w(TAG, "Unable to load avatar, or avatar removed, closing");
|
||||
finish();
|
||||
return false;
|
||||
}
|
||||
|
||||
toolbar.setTitle(recipient.toShortString(context));
|
||||
@Override
|
||||
public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, DataSource dataSource, boolean isFirstResource) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
avatar.setImageDrawable(RoundedBitmapDrawableFactory.create(resources, resource));
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
startPostponedEnterTransition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
}
|
||||
});
|
||||
|
||||
toolbar.setTitle(recipient.getDisplayName(context));
|
||||
});
|
||||
|
||||
avatar.setOnClickListener(v -> toggleUiVisibility());
|
||||
FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
|
||||
|
||||
showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
|
||||
}
|
||||
findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
|
||||
|
||||
private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) {
|
||||
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
|
||||
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
|
||||
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
|
||||
|
||||
for (View view : views) {
|
||||
view.animate()
|
||||
.alpha(hide ? 0 : 1)
|
||||
.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void toggleUiVisibility() {
|
||||
int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
|
||||
if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) {
|
||||
showSystemUI();
|
||||
} else {
|
||||
hideSystemUI();
|
||||
}
|
||||
}
|
||||
|
||||
private void hideSystemUI() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN );
|
||||
}
|
||||
|
||||
private void showSystemUI() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN );
|
||||
fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
|
||||
public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (BaseActivity.isMenuWorkaroundRequired()) {
|
||||
forceOverflowMenu();
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
initializeScreenshotSecurity();
|
||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return (keyCode == KeyEvent.KEYCODE_MENU && BaseActivity.isMenuWorkaroundRequired()) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_MENU && BaseActivity.isMenuWorkaroundRequired()) {
|
||||
openOptionsMenu();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
private void initializeScreenshotSecurity() {
|
||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modified from: http://stackoverflow.com/a/13098824
|
||||
*/
|
||||
private void forceOverflowMenu() {
|
||||
try {
|
||||
ViewConfiguration config = ViewConfiguration.get(this);
|
||||
Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey");
|
||||
if(menuKeyField != null) {
|
||||
menuKeyField.setAccessible(true);
|
||||
menuKeyField.setBoolean(config, false);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.w(TAG, "Failed to force overflow menu.");
|
||||
} catch (NoSuchFieldException e) {
|
||||
Log.w(TAG, "Failed to force overflow menu.");
|
||||
}
|
||||
}
|
||||
|
||||
protected void startActivitySceneTransition(Intent intent, View sharedView, String transitionName) {
|
||||
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(this, sharedView, transitionName)
|
||||
.toBundle();
|
||||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
protected void setStatusBarColor(int color) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setStatusBarColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,97 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import android.view.KeyEvent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
public abstract class BaseActivity extends FragmentActivity {
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return (keyCode == KeyEvent.KEYCODE_MENU && isMenuWorkaroundRequired()) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Base class for all activities. The vast majority of activities shouldn't extend this directly.
|
||||
* Instead, they should extend {@link PassphraseRequiredActivity} so they're protected by
|
||||
* screen lock.
|
||||
*/
|
||||
public abstract class BaseActivity extends AppCompatActivity {
|
||||
private static final String TAG = Log.tag(BaseActivity.class);
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_MENU && isMenuWorkaroundRequired()) {
|
||||
openOptionsMenu();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
public static boolean isMenuWorkaroundRequired() {
|
||||
return VERSION.SDK_INT < VERSION_CODES.KITKAT &&
|
||||
VERSION.SDK_INT > VERSION_CODES.GINGERBREAD_MR1 &&
|
||||
("LGE".equalsIgnoreCase(Build.MANUFACTURER) || "E6710".equalsIgnoreCase(Build.DEVICE));
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
logEvent("onCreate()");
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
initializeScreenshotSecurity();
|
||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
logEvent("onStart()");
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
logEvent("onStop()");
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
logEvent("onDestroy()");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void initializeScreenshotSecurity() {
|
||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
|
||||
protected void startActivitySceneTransition(Intent intent, View sharedView, String transitionName) {
|
||||
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(this, sharedView, transitionName)
|
||||
.toBundle();
|
||||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
protected void setStatusBarColor(int color) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
getWindow().setStatusBarColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
||||
}
|
||||
|
||||
private void logEvent(@NonNull String event) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
||||
}
|
||||
|
||||
public final @NonNull ActionBar requireSupportActionBar() {
|
||||
return Objects.requireNonNull(getSupportActionBar());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -21,17 +26,18 @@ import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable {
|
||||
void bind(@NonNull MessageRecord messageRecord,
|
||||
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull ConversationMessage messageRecord,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MessageRecord> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseHighlight);
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<ConversationMessage> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
@Nullable String searchQuery,
|
||||
boolean pulseMention);
|
||||
|
||||
MessageRecord getMessageRecord();
|
||||
ConversationMessage getConversationMessage();
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
@@ -45,7 +51,17 @@ public interface BindableConversationItem extends Unbindable {
|
||||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(long messageId, boolean isMms);
|
||||
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
void onVoiceNotePause(@NonNull Uri uri);
|
||||
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
|
||||
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
|
||||
void onGroupMigrationLearnMoreClicked(@NonNull List<RecipientId> pendingRecipients);
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,7 @@ public interface BindableConversationListItem extends Unbindable {
|
||||
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull Set<Long> selectedThreads, boolean batchMode);
|
||||
|
||||
void setBatchMode(boolean batchMode);
|
||||
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
@@ -28,7 +27,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class BlockedContactsActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
@@ -14,8 +13,8 @@ import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
@@ -51,7 +50,7 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
super(context);
|
||||
|
||||
Recipient recipient = Recipient.resolved(mismatch.getRecipientId(context));
|
||||
String name = recipient.toShortString(context);
|
||||
String name = recipient.getDisplayName(context);
|
||||
String introduction = context.getString(R.string.ConfirmIdentityDialog_your_safety_number_with_s_has_changed, name, name);
|
||||
SpannableString spannableString = new SpannableString(introduction + " " +
|
||||
context.getString(R.string.ConfirmIdentityDialog_you_may_wish_to_verify_your_safety_number_with_this_contact));
|
||||
@@ -105,7 +104,6 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
}
|
||||
|
||||
processMessageRecord(messageRecord);
|
||||
processPendingMessageRecords(messageRecord.getThreadId(), mismatch);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -115,29 +113,9 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
else processIncomingMessageRecord(messageRecord);
|
||||
}
|
||||
|
||||
private void processPendingMessageRecords(long threadId, IdentityKeyMismatch mismatch) {
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(getContext());
|
||||
Cursor cursor = mmsSmsDatabase.getIdentityConflictMessagesForThread(threadId);
|
||||
MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(cursor);
|
||||
MessageRecord record;
|
||||
|
||||
try {
|
||||
while ((record = reader.getNext()) != null) {
|
||||
for (IdentityKeyMismatch recordMismatch : record.getIdentityKeyMismatches()) {
|
||||
if (mismatch.equals(recordMismatch)) {
|
||||
processMessageRecord(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (reader != null)
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
||||
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
MessageDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
@@ -160,8 +138,8 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
|
||||
private void processIncomingMessageRecord(MessageRecord messageRecord) {
|
||||
try {
|
||||
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext());
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext());
|
||||
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getRecipientId(getContext()),
|
||||
@@ -175,7 +153,9 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
messageRecord.getDateSent(),
|
||||
legacy ? Base64.decode(messageRecord.getBody()) : null,
|
||||
!legacy ? Base64.decode(messageRecord.getBody()) : null,
|
||||
0, null);
|
||||
0,
|
||||
0,
|
||||
null);
|
||||
|
||||
long pushId = pushDatabase.insert(envelope);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ import java.lang.ref.WeakReference;
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public abstract class ContactSelectionActivity extends PassphraseRequiredActionBarActivity
|
||||
public abstract class ContactSelectionActivity extends PassphraseRequiredActivity
|
||||
implements SwipeRefreshLayout.OnRefreshListener,
|
||||
ContactSelectionListFragment.OnContactSelectedListener,
|
||||
ContactSelectionListFragment.ScrollCallback
|
||||
@@ -113,7 +113,9 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {}
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {}
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.Manifest;
|
||||
import android.animation.LayoutTransition;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
@@ -29,7 +30,6 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.CycleInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.TextView;
|
||||
@@ -40,7 +40,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
@@ -57,6 +56,7 @@ import com.google.android.material.chip.ChipGroup;
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||
import org.thoughtcrime.securesms.components.emoji.WarningTextView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactChip;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||
@@ -64,6 +64,8 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -71,7 +73,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
@@ -84,7 +85,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -93,7 +93,7 @@ import java.util.Set;
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public final class ContactSelectionListFragment extends Fragment
|
||||
public final class ContactSelectionListFragment extends LoggingFragment
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
@@ -105,11 +105,11 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
public static final int NO_LIMIT = Integer.MAX_VALUE;
|
||||
|
||||
public static final String DISPLAY_MODE = "display_mode";
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String REFRESHABLE = "refreshable";
|
||||
public static final String RECENTS = "recents";
|
||||
public static final String TOTAL_CAPACITY = "total_capacity";
|
||||
public static final String SELECTION_LIMITS = "selection_limits";
|
||||
public static final String CURRENT_SELECTION = "current_selection";
|
||||
public static final String HIDE_COUNT = "hide_count";
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
private TextView emptyText;
|
||||
@@ -125,15 +125,17 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
private ChipGroup chipGroup;
|
||||
private HorizontalScrollView chipGroupScrollContainer;
|
||||
private TextView groupLimit;
|
||||
private WarningTextView groupLimit;
|
||||
|
||||
@Nullable private FixedViewsAdapter headerAdapter;
|
||||
@Nullable private FixedViewsAdapter footerAdapter;
|
||||
@Nullable private ListCallback listCallback;
|
||||
@Nullable private ScrollCallback scrollCallback;
|
||||
private GlideRequests glideRequests;
|
||||
private int selectionLimit;
|
||||
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
|
||||
private Set<RecipientId> currentSelection;
|
||||
private boolean isMulti;
|
||||
private boolean hideCount;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
@@ -208,9 +210,18 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
});
|
||||
|
||||
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
|
||||
Intent intent = requireActivity().getIntent();
|
||||
|
||||
swipeRefresh.setEnabled(intent.getBooleanExtra(REFRESHABLE, true));
|
||||
|
||||
hideCount = intent.getBooleanExtra(HIDE_COUNT, false);
|
||||
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
|
||||
isMulti = selectionLimit != null;
|
||||
|
||||
if (!isMulti) {
|
||||
selectionLimit = SelectionLimits.NO_LIMITS;
|
||||
}
|
||||
|
||||
selectionLimit = requireActivity().getIntent().getIntExtra(TOTAL_CAPACITY, NO_LIMIT);
|
||||
currentSelection = getCurrentSelection();
|
||||
|
||||
updateGroupLimit(getChipCount());
|
||||
@@ -219,12 +230,10 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
|
||||
private void updateGroupLimit(int chipCount) {
|
||||
if (selectionLimit != NO_LIMIT) {
|
||||
groupLimit.setText(String.format(Locale.getDefault(), "%d/%d", currentSelection.size() + chipCount, selectionLimit));
|
||||
groupLimit.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
groupLimit.setVisibility(View.GONE);
|
||||
}
|
||||
int members = currentSelection.size() + chipCount;
|
||||
groupLimit.setText(getResources().getQuantityString(R.plurals.ContactSelectionListFragment_d_members, members, members));
|
||||
groupLimit.setVisibility(isMulti && !hideCount ? View.VISIBLE : View.GONE);
|
||||
groupLimit.setWarning(selectionWarningLimitExceeded());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -256,7 +265,7 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
|
||||
public boolean isMulti() {
|
||||
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
||||
return isMulti;
|
||||
}
|
||||
|
||||
private void initializeCursor() {
|
||||
@@ -266,17 +275,13 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
glideRequests,
|
||||
null,
|
||||
new ListClickListener(),
|
||||
isMulti(),
|
||||
isMulti,
|
||||
currentSelection);
|
||||
|
||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||
|
||||
if (listCallback != null && FeatureFlags.newGroupUI()) {
|
||||
if (FeatureFlags.groupsV2create() && FeatureFlags.groupsV2internalTest()) {
|
||||
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback), createNewGroupsV1GroupItem(listCallback));
|
||||
} else {
|
||||
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback));
|
||||
}
|
||||
if (listCallback != null) {
|
||||
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback));
|
||||
headerAdapter.hide();
|
||||
concatenateAdapter.addAdapter(headerAdapter);
|
||||
}
|
||||
@@ -317,13 +322,6 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
return view;
|
||||
}
|
||||
|
||||
private View createNewGroupsV1GroupItem(@NonNull ListCallback listCallback) {
|
||||
View view = LayoutInflater.from(requireContext())
|
||||
.inflate(R.layout.contact_selection_new_group_v1_item, (ViewGroup) requireView(), false);
|
||||
view.setOnClickListener(v -> listCallback.onNewGroup(true));
|
||||
return view;
|
||||
}
|
||||
|
||||
private void initializeNoContactsPermission() {
|
||||
swipeRefresh.setVisibility(View.GONE);
|
||||
|
||||
@@ -463,10 +461,14 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
||||
|
||||
if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
||||
if (selectionLimitReached()) {
|
||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_the_group_is_full, Toast.LENGTH_SHORT).show();
|
||||
groupLimit.animate().scaleX(1.3f).scaleY(1.3f).setInterpolator(new CycleInterpolator(0.5f)).start();
|
||||
if (isMulti && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
|
||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
|
||||
if (selectionHardLimitReached()) {
|
||||
GroupLimitDialog.showHardLimitMessage(requireContext());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -480,26 +482,33 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
if (uuid.isPresent()) {
|
||||
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null);
|
||||
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
|
||||
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
|
||||
.setPositiveButton(R.string.ContactSelectionListFragment_okay, (dialog, which) -> dialog.dismiss())
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber());
|
||||
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
} else {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -509,16 +518,25 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean selectionLimitReached() {
|
||||
return getChipCount() >= selectionLimit;
|
||||
private boolean selectionHardLimitReached() {
|
||||
return getChipCount() + currentSelection.size() >= selectionLimit.getHardLimit();
|
||||
}
|
||||
|
||||
private boolean selectionWarningLimitReachedExactly() {
|
||||
return getChipCount() + currentSelection.size() == selectionLimit.getRecommendedLimit();
|
||||
}
|
||||
|
||||
private boolean selectionWarningLimitExceeded() {
|
||||
return getChipCount() + currentSelection.size() > selectionLimit.getRecommendedLimit();
|
||||
}
|
||||
|
||||
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
||||
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
|
||||
if (isMulti() && FeatureFlags.newGroupUI()) {
|
||||
if (isMulti) {
|
||||
addChipForSelectedContact(selectedContact);
|
||||
}
|
||||
}
|
||||
@@ -589,6 +607,9 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
private void addChip(@NonNull ContactChip chip) {
|
||||
chipGroup.addView(chip);
|
||||
updateGroupLimit(getChipCount());
|
||||
if (selectionWarningLimitReachedExactly()) {
|
||||
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
private int getChipCount() {
|
||||
@@ -631,7 +652,8 @@ public final class ContactSelectionListFragment extends Fragment
|
||||
}
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
void onContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
/** @return True if the contact is allowed to be selected, otherwise false. */
|
||||
boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
|
||||
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
|
||||
import org.thoughtcrime.securesms.service.ApplicationMigrationService.ImportState;
|
||||
|
||||
public class DatabaseMigrationActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final ImportServiceConnection serviceConnection = new ImportServiceConnection();
|
||||
private final ImportStateHandler importStateHandler = new ImportStateHandler();
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
@@ -42,7 +41,7 @@ import org.whispersystems.signalservice.internal.push.DeviceLimitExceededExcepti
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class DeviceActivity extends PassphraseRequiredActionBarActivity
|
||||
public class DeviceActivity extends PassphraseRequiredActivity
|
||||
implements Button.OnClickListener, ScanListener, DeviceLinkFragment.LinkClickedListener
|
||||
{
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
@@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.qr.ScanListener;
|
||||
import org.thoughtcrime.securesms.qr.ScanningThread;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class DeviceAddFragment extends Fragment {
|
||||
public class DeviceAddFragment extends LoggingFragment {
|
||||
|
||||
private ViewGroup container;
|
||||
private LinearLayout overlay;
|
||||
|
||||
@@ -53,7 +53,7 @@ public class DeviceListFragment extends ListFragment
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActionBarActivity.LOCALE_EXTRA);
|
||||
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActivity.LOCALE_EXTRA);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.os.Bundle;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.view.Window;
|
||||
|
||||
public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = DeviceProvisioningActivity.class.getSimpleName();
|
||||
@@ -26,7 +26,7 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActiv
|
||||
startActivity(intent);
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(R.string.DeviceProvisioningActivity_cancel, (dialog12, which) -> {
|
||||
.setNegativeButton(android.R.string.cancel, (dialog12, which) -> {
|
||||
dialog12.dismiss();
|
||||
finish();
|
||||
})
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import cn.carbswang.android.numberpickerview.library.NumberPickerView;
|
||||
|
||||
public class ExpirationDialog extends AlertDialog {
|
||||
@@ -36,7 +39,7 @@ public class ExpirationDialog extends AlertDialog {
|
||||
builder.setView(view);
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
|
||||
listener.onClick(context.getResources().getIntArray(R.array.expiration_times)[selected]);
|
||||
listener.onClick(getExpirationTimes(context, currentExpiration)[selected]);
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
@@ -47,7 +50,7 @@ public class ExpirationDialog extends AlertDialog {
|
||||
final View view = inflater.inflate(R.layout.expiration_dialog, null);
|
||||
final NumberPickerView numberPickerView = view.findViewById(R.id.expiration_number_picker);
|
||||
final TextView textView = view.findViewById(R.id.expiration_details);
|
||||
final int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
|
||||
final int[] expirationTimes = getExpirationTimes(context, currentExpiration);
|
||||
final String[] expirationDisplayValues = new String[expirationTimes.length];
|
||||
|
||||
int selectedIndex = expirationTimes.length - 1;
|
||||
@@ -80,6 +83,19 @@ public class ExpirationDialog extends AlertDialog {
|
||||
return view;
|
||||
}
|
||||
|
||||
private static int[] getExpirationTimes(Context context, int currentExpiration) {
|
||||
int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
|
||||
int location = Arrays.binarySearch(expirationTimes, currentExpiration);
|
||||
if (location < 0) {
|
||||
int[] temp = Arrays.copyOf(expirationTimes, expirationTimes.length + 1);
|
||||
temp[temp.length - 1] = currentExpiration;
|
||||
Arrays.sort(temp);
|
||||
expirationTimes = temp;
|
||||
}
|
||||
|
||||
return expirationTimes;
|
||||
}
|
||||
|
||||
public interface OnClickListener {
|
||||
public void onClick(int expirationTime);
|
||||
}
|
||||
|
||||
@@ -1,629 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.thoughtcrime.securesms.components.PushRecipientsPanel;
|
||||
import org.thoughtcrime.securesms.components.PushRecipientsPanel.RecipientsPanelChangedListener;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.RecipientsEditor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter;
|
||||
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter.OnRecipientDeletedListener;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Activity to create and update {@link GroupId.V1} groups
|
||||
*
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
implements OnRecipientDeletedListener,
|
||||
RecipientsPanelChangedListener
|
||||
{
|
||||
|
||||
private final static String TAG = GroupCreateActivity.class.getSimpleName();
|
||||
|
||||
private static final String GROUP_ID_EXTRA = "group_id";
|
||||
private static final String GROUP_THREAD_EXTRA = "group_thread";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
private static final short REQUEST_CODE_SELECT_AVATAR = 26165;
|
||||
private static final int PICK_CONTACT = 1;
|
||||
|
||||
private EditText groupName;
|
||||
private ListView listView;
|
||||
private ImageView avatar;
|
||||
private TextView creatingText;
|
||||
private Bitmap avatarBmp;
|
||||
|
||||
@NonNull private Optional<GroupData> groupToUpdate = Optional.absent();
|
||||
|
||||
public static Intent newEditGroupIntent(@NonNull Context context, @NonNull GroupId.V1 groupId) {
|
||||
Intent intent = new Intent(context, GroupCreateActivity.class);
|
||||
intent.putExtra(GroupCreateActivity.GROUP_ID_EXTRA, groupId.toString());
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
setContentView(R.layout.group_create_activity);
|
||||
//noinspection ConstantConditions
|
||||
initializeAppBar();
|
||||
initializeResources();
|
||||
initializeExistingGroup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
updateViewState();
|
||||
}
|
||||
|
||||
private boolean isSignalGroup() {
|
||||
return TextSecurePreferences.isPushRegistered(this) && !getAdapter().hasNonPushMembers();
|
||||
}
|
||||
|
||||
private void disableSignalGroupViews(int reasonResId) {
|
||||
View pushDisabled = findViewById(R.id.push_disabled);
|
||||
pushDisabled.setVisibility(View.VISIBLE);
|
||||
((TextView) findViewById(R.id.push_disabled_reason)).setText(reasonResId);
|
||||
avatar.setEnabled(false);
|
||||
groupName.setEnabled(false);
|
||||
}
|
||||
|
||||
private void enableSignalGroupViews() {
|
||||
findViewById(R.id.push_disabled).setVisibility(View.GONE);
|
||||
avatar.setEnabled(true);
|
||||
groupName.setEnabled(true);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void updateViewState() {
|
||||
if (!TextSecurePreferences.isPushRegistered(this)) {
|
||||
disableSignalGroupViews(R.string.GroupCreateActivity_youre_not_registered_for_signal);
|
||||
getSupportActionBar().setTitle(R.string.GroupCreateActivity_actionbar_mms_title);
|
||||
} else if (getAdapter().hasNonPushMembers()) {
|
||||
disableSignalGroupViews(R.string.GroupCreateActivity_contacts_dont_support_push);
|
||||
getSupportActionBar().setTitle(R.string.GroupCreateActivity_actionbar_mms_title);
|
||||
} else {
|
||||
enableSignalGroupViews();
|
||||
getSupportActionBar().setTitle(groupToUpdate.isPresent()
|
||||
? R.string.GroupCreateActivity_actionbar_edit_title
|
||||
: R.string.GroupCreateActivity_actionbar_title);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isActiveInDirectory(Recipient recipient) {
|
||||
return recipient.resolve().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED;
|
||||
}
|
||||
|
||||
private void addSelectedContacts(@NonNull Recipient... recipients) {
|
||||
new AddMembersTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipients);
|
||||
}
|
||||
|
||||
private void addSelectedContacts(@NonNull Collection<Recipient> recipients) {
|
||||
addSelectedContacts(recipients.toArray(new Recipient[recipients.size()]));
|
||||
}
|
||||
|
||||
private void initializeAppBar() {
|
||||
Drawable upIcon = ContextCompat.getDrawable(this, R.drawable.ic_arrow_left_24);
|
||||
getSupportActionBar().setHomeAsUpIndicator(upIcon);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
RecipientsEditor recipientsEditor = findViewById(R.id.recipients_text);
|
||||
PushRecipientsPanel recipientsPanel = findViewById(R.id.recipients);
|
||||
|
||||
listView = findViewById(R.id.selected_contacts_list);
|
||||
avatar = findViewById(R.id.avatar);
|
||||
groupName = findViewById(R.id.group_name);
|
||||
creatingText = findViewById(R.id.creating_group_text);
|
||||
|
||||
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(this);
|
||||
adapter.setOnRecipientDeletedListener(this);
|
||||
listView.setAdapter(adapter);
|
||||
|
||||
recipientsEditor.setHint(R.string.recipients_panel__add_members);
|
||||
recipientsPanel.setPanelChangeListener(this);
|
||||
|
||||
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
|
||||
|
||||
avatar.setImageDrawable(getDefaultGroupAvatar());
|
||||
avatar.setOnClickListener(view -> AvatarSelectionBottomSheetDialogFragment.create(avatarBmp != null, false, REQUEST_CODE_SELECT_AVATAR, true).show(getSupportFragmentManager(), null));
|
||||
}
|
||||
|
||||
private Drawable getDefaultGroupAvatar() {
|
||||
return new ResourceContactPhoto(R.drawable.ic_group_outline_34, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this));
|
||||
}
|
||||
|
||||
private void initializeExistingGroup() {
|
||||
final GroupId groupId = GroupId.parseNullableOrThrow(getIntent().getStringExtra(GROUP_ID_EXTRA));
|
||||
|
||||
if (groupId != null) {
|
||||
GroupId.V1 groupIdV1 = groupId.requireV1();
|
||||
|
||||
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupIdV1);
|
||||
|
||||
if (FeatureFlags.newGroupUI()) {
|
||||
avatar.setOnClickListener(v -> startActivity(EditProfileActivity.getIntentForGroupProfile(this, groupIdV1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
inflater.inflate(R.menu.group_create, menu);
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
case R.id.menu_create_group:
|
||||
if (groupToUpdate.isPresent()) handleGroupUpdate();
|
||||
else handleGroupCreate();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientDeleted(Recipient recipient) {
|
||||
getAdapter().remove(recipient);
|
||||
updateViewState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientsPanelUpdate(List<Recipient> recipients) {
|
||||
if (recipients != null && !recipients.isEmpty()) addSelectedContacts(recipients);
|
||||
}
|
||||
|
||||
private void handleGroupCreate() {
|
||||
if (getAdapter().getCount() < 1) {
|
||||
Log.i(TAG, getString(R.string.GroupCreateActivity_contacts_no_members));
|
||||
Toast.makeText(getApplicationContext(), R.string.GroupCreateActivity_contacts_no_members, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (isSignalGroup()) {
|
||||
new CreateSignalGroupTask(this, avatarBmp, getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
} else {
|
||||
new CreateMmsGroupTask(this, getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGroupUpdate() {
|
||||
new UpdateSignalGroupV1Task(this, groupToUpdate.get().id, avatarBmp,
|
||||
getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void handleOpenConversation(long threadId, Recipient recipient) {
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
private SelectedRecipientsAdapter getAdapter() {
|
||||
return (SelectedRecipientsAdapter) listView.getAdapter();
|
||||
}
|
||||
|
||||
private @Nullable String getGroupName() {
|
||||
return groupName.getText() != null ? groupName.getText().toString() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
|
||||
super.onActivityResult(reqCode, resultCode, data);
|
||||
|
||||
if (data == null || resultCode != Activity.RESULT_OK)
|
||||
return;
|
||||
|
||||
switch (reqCode) {
|
||||
case PICK_CONTACT:
|
||||
List<RecipientId> selected = data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS);
|
||||
|
||||
for (RecipientId contact : selected) {
|
||||
Recipient recipient = Recipient.resolved(contact);
|
||||
addSelectedContacts(recipient);
|
||||
}
|
||||
|
||||
break;
|
||||
case REQUEST_CODE_SELECT_AVATAR:
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
avatarBmp = null;
|
||||
avatar.setImageDrawable(getDefaultGroupAvatar());
|
||||
return;
|
||||
}
|
||||
|
||||
final Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
|
||||
final DecryptableUri decryptableUri = new DecryptableUri(result.getUri());
|
||||
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(decryptableUri)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerCrop()
|
||||
.override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS)
|
||||
.into(new SimpleTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
||||
setAvatar(decryptableUri, resource);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class AddRecipientButtonListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(GroupCreateActivity.this, PushContactSelectionActivity.class);
|
||||
if (groupToUpdate.isPresent()) {
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_PUSH);
|
||||
} else {
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_PUSH | DisplayMode.FLAG_SMS);
|
||||
}
|
||||
startActivityForResult(intent, PICK_CONTACT);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CreateMmsGroupTask extends AsyncTask<Void,Void,GroupActionResult> {
|
||||
private final GroupCreateActivity activity;
|
||||
private final Set<Recipient> members;
|
||||
|
||||
public CreateMmsGroupTask(GroupCreateActivity activity, Set<Recipient> members) {
|
||||
this.activity = activity;
|
||||
this.members = members;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GroupActionResult doInBackground(Void... avoid) {
|
||||
List<RecipientId> memberAddresses = new LinkedList<>();
|
||||
|
||||
for (Recipient recipient : members) {
|
||||
memberAddresses.add(recipient.getId());
|
||||
}
|
||||
memberAddresses.add(Recipient.self().getId());
|
||||
|
||||
GroupId.Mms groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateMmsGroupForMembers(memberAddresses);
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(activity).getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(GroupActionResult result) {
|
||||
activity.handleOpenConversation(result.getThreadId(), result.getGroupRecipient());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(Void... values) {
|
||||
super.onProgressUpdate(values);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class SignalGroupTask extends AsyncTask<Void,Void,Optional<GroupActionResult>> {
|
||||
|
||||
protected GroupCreateActivity activity;
|
||||
protected Bitmap avatar;
|
||||
protected Set<Recipient> members;
|
||||
protected String name;
|
||||
|
||||
public SignalGroupTask(GroupCreateActivity activity,
|
||||
Bitmap avatar,
|
||||
String name,
|
||||
Set<Recipient> members)
|
||||
{
|
||||
this.activity = activity;
|
||||
this.avatar = avatar;
|
||||
this.name = name;
|
||||
this.members = members;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
activity.findViewById(R.id.group_details_layout).setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.creating_group_layout).setVisibility(View.VISIBLE);
|
||||
activity.findViewById(R.id.menu_create_group).setVisibility(View.GONE);
|
||||
final int titleResId = activity.groupToUpdate.isPresent()
|
||||
? R.string.GroupCreateActivity_updating_group
|
||||
: R.string.GroupCreateActivity_creating_group;
|
||||
activity.creatingText.setText(activity.getString(titleResId, activity.getGroupName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Optional<GroupActionResult> groupActionResultOptional) {
|
||||
if (activity.isFinishing()) return;
|
||||
activity.findViewById(R.id.group_details_layout).setVisibility(View.VISIBLE);
|
||||
activity.findViewById(R.id.creating_group_layout).setVisibility(View.GONE);
|
||||
activity.findViewById(R.id.menu_create_group).setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CreateSignalGroupTask extends SignalGroupTask {
|
||||
public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set<Recipient> members) {
|
||||
super(activity, avatar, name, members);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
|
||||
return Optional.of(GroupManager.createGroupV1(activity, members, BitmapUtil.toByteArray(avatar), name, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Optional<GroupActionResult> result) {
|
||||
if (result.isPresent() && result.get().getThreadId() > -1) {
|
||||
if (!activity.isFinishing()) {
|
||||
activity.handleOpenConversation(result.get().getThreadId(), result.get().getGroupRecipient());
|
||||
}
|
||||
} else {
|
||||
super.onPostExecute(result);
|
||||
Toast.makeText(activity.getApplicationContext(),
|
||||
R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class UpdateSignalGroupV1Task extends SignalGroupTask {
|
||||
private final GroupId.V1 groupId;
|
||||
|
||||
UpdateSignalGroupV1Task(GroupCreateActivity activity, GroupId.V1 groupId,
|
||||
Bitmap avatar, String name, Set<Recipient> members)
|
||||
{
|
||||
super(activity, avatar, name, members);
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
|
||||
return Optional.fromNullable(GroupManager.updateGroup(activity, groupId, members, BitmapUtil.toByteArray(avatar), name));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Optional<GroupActionResult> result) {
|
||||
if (result.isPresent() && result.get().getThreadId() > -1) {
|
||||
if (!activity.isFinishing()) {
|
||||
Intent intent = activity.getIntent();
|
||||
intent.putExtra(GROUP_THREAD_EXTRA, result.get().getThreadId());
|
||||
intent.putExtra(GROUP_ID_EXTRA, result.get().getGroupRecipient().requireGroupId().toString());
|
||||
activity.setResult(RESULT_OK, intent);
|
||||
activity.finish();
|
||||
}
|
||||
} else {
|
||||
super.onPostExecute(result);
|
||||
Toast.makeText(activity.getApplicationContext(),
|
||||
R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddMembersTask extends AsyncTask<Recipient,Void,List<AddMembersTask.Result>> {
|
||||
static class Result {
|
||||
Optional<Recipient> recipient;
|
||||
boolean isPush;
|
||||
String reason;
|
||||
|
||||
public Result(@Nullable Recipient recipient, boolean isPush, @Nullable String reason) {
|
||||
this.recipient = Optional.fromNullable(recipient);
|
||||
this.isPush = isPush;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
private GroupCreateActivity activity;
|
||||
private boolean failIfNotPush;
|
||||
|
||||
public AddMembersTask(@NonNull GroupCreateActivity activity) {
|
||||
this.activity = activity;
|
||||
this.failIfNotPush = activity.groupToUpdate.isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Result> doInBackground(Recipient... recipients) {
|
||||
final List<Result> results = new LinkedList<>();
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
boolean isPush = isActiveInDirectory(recipient);
|
||||
|
||||
if (failIfNotPush && !isPush) {
|
||||
results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_cannot_add_non_push_to_existing_group,
|
||||
recipient.toShortString(activity))));
|
||||
} else if (TextUtils.equals(TextSecurePreferences.getLocalNumber(activity), recipient.getE164().or(""))) {
|
||||
results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_youre_already_in_the_group)));
|
||||
} else {
|
||||
results.add(new Result(recipient, isPush, null));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Result> results) {
|
||||
if (activity.isFinishing()) return;
|
||||
|
||||
for (Result result : results) {
|
||||
if (result.recipient.isPresent()) {
|
||||
activity.getAdapter().add(result.recipient.get(), result.isPush);
|
||||
} else {
|
||||
Toast.makeText(activity, result.reason, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
activity.updateViewState();
|
||||
}
|
||||
}
|
||||
|
||||
private static class FillExistingGroupInfoAsyncTask extends ProgressDialogAsyncTask<GroupId.V1, Void, Optional<GroupData>> {
|
||||
private GroupCreateActivity activity;
|
||||
|
||||
public FillExistingGroupInfoAsyncTask(GroupCreateActivity activity) {
|
||||
super(activity,
|
||||
R.string.GroupCreateActivity_loading_group_details,
|
||||
R.string.please_wait);
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GroupData> doInBackground(GroupId.V1... groupIds) {
|
||||
final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity);
|
||||
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
final Optional<GroupRecord> group = db.getGroup(groupIds[0]);
|
||||
final Set<Recipient> existingContacts = new HashSet<>(recipients.size());
|
||||
existingContacts.addAll(recipients);
|
||||
|
||||
if (group.isPresent()) {
|
||||
Bitmap avatar = null;
|
||||
try {
|
||||
avatar = BitmapFactory.decodeStream(AvatarHelper.getAvatar(getContext(), group.get().getRecipientId()));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to read avatar.");
|
||||
}
|
||||
return Optional.of(new GroupData(groupIds[0],
|
||||
existingContacts,
|
||||
avatar,
|
||||
BitmapUtil.toByteArray(avatar),
|
||||
group.get().getTitle()));
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Optional<GroupData> group) {
|
||||
super.onPostExecute(group);
|
||||
|
||||
if (group.isPresent() && !activity.isFinishing()) {
|
||||
activity.groupToUpdate = group;
|
||||
|
||||
activity.groupName.setText(group.get().name);
|
||||
if (group.get().avatarBmp != null) {
|
||||
activity.setAvatar(group.get().avatarBytes, group.get().avatarBmp);
|
||||
}
|
||||
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(activity, group.get().recipients);
|
||||
adapter.setOnRecipientDeletedListener(activity);
|
||||
activity.listView.setAdapter(adapter);
|
||||
activity.updateViewState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void setAvatar(T model, Bitmap bitmap) {
|
||||
avatarBmp = bitmap;
|
||||
GlideApp.with(this)
|
||||
.load(model)
|
||||
.circleCrop()
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(avatar);
|
||||
}
|
||||
|
||||
private static class GroupData {
|
||||
GroupId.V1 id;
|
||||
Set<Recipient> recipients;
|
||||
Bitmap avatarBmp;
|
||||
byte[] avatarBytes;
|
||||
String name;
|
||||
|
||||
GroupData(GroupId.V1 id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
|
||||
this.id = id;
|
||||
this.recipients = recipients;
|
||||
this.avatarBmp = avatarBmp;
|
||||
this.avatarBytes = avatarBytes;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChange
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
@@ -40,10 +41,9 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class InviteActivity extends PassphraseRequiredActionBarActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private EditText inviteText;
|
||||
@@ -63,7 +63,8 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_SMS);
|
||||
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
|
||||
getIntent().putExtra(ContactSelectionListFragment.SELECTION_LIMITS, SelectionLimits.NO_LIMITS);
|
||||
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
|
||||
setContentView(R.layout.invite_activity);
|
||||
@@ -103,7 +104,7 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
|
||||
contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
|
||||
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
|
||||
updateSmsButtonText();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
@@ -121,13 +122,14 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText();
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
}
|
||||
|
||||
private void sendSmsInvites() {
|
||||
@@ -137,12 +139,11 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
|
||||
.toArray(new SelectedContact[0]));
|
||||
}
|
||||
|
||||
private void updateSmsButtonText() {
|
||||
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
|
||||
private void updateSmsButtonText(int count) {
|
||||
smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends,
|
||||
selectedContacts.size(),
|
||||
selectedContacts.size()));
|
||||
smsSendButton.setEnabled(!selectedContacts.isEmpty());
|
||||
count,
|
||||
count));
|
||||
smsSendButton.setEnabled(count > 0);
|
||||
}
|
||||
|
||||
@Override public void onBackPressed() {
|
||||
@@ -156,7 +157,7 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
|
||||
private void cancelSmsSelection() {
|
||||
setPrimaryColorsToolbarNormal();
|
||||
contactsFragment.reset();
|
||||
updateSmsButtonText();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
|
||||
}
|
||||
|
||||
|
||||
49
app/src/main/java/org/thoughtcrime/securesms/KbsEnclave.java
Normal file
49
app/src/main/java/org/thoughtcrime/securesms/KbsEnclave.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Used in our {@link BuildConfig} to tie together the various attributes of a KBS instance. This
|
||||
* is sitting in the root directory so it can be accessed by the build config.
|
||||
*/
|
||||
public final class KbsEnclave {
|
||||
|
||||
private final String enclaveName;
|
||||
private final String serviceId;
|
||||
private final String mrEnclave;
|
||||
|
||||
public KbsEnclave(@NonNull String enclaveName, @NonNull String serviceId, @NonNull String mrEnclave) {
|
||||
this.enclaveName = enclaveName;
|
||||
this.serviceId = serviceId;
|
||||
this.mrEnclave = mrEnclave;
|
||||
}
|
||||
|
||||
public @NonNull String getMrEnclave() {
|
||||
return mrEnclave;
|
||||
}
|
||||
|
||||
public @NonNull String getEnclaveName() {
|
||||
return enclaveName;
|
||||
}
|
||||
|
||||
public @NonNull String getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
KbsEnclave enclave = (KbsEnclave) o;
|
||||
return enclaveName.equals(enclave.enclaveName) &&
|
||||
serviceId.equals(enclave.serviceId) &&
|
||||
mrEnclave.equals(enclave.mrEnclave);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(enclaveName, serviceId, mrEnclave);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
/**
|
||||
* Simply logs out lifecycle events.
|
||||
*/
|
||||
public abstract class LoggingFragment extends Fragment {
|
||||
|
||||
private static final String TAG = Log.tag(LoggingFragment.class);
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
logEvent("onCreate()");
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
logEvent("onStart()");
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
logEvent("onStop()");
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
logEvent("onDestroy()");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void logEvent(@NonNull String event) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class MainActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class MainActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final MainNavigator navigator = new MainNavigator(this);
|
||||
@@ -18,6 +21,14 @@ public class MainActivity extends PassphraseRequiredActionBarActivity {
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
navigator.onCreate(savedInstanceState);
|
||||
|
||||
handleGroupLinkInIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
handleGroupLinkInIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -42,4 +53,11 @@ public class MainActivity extends PassphraseRequiredActionBarActivity {
|
||||
public @NonNull MainNavigator getNavigator() {
|
||||
return navigator;
|
||||
}
|
||||
|
||||
private void handleGroupLinkInIntent(Intent intent) {
|
||||
Uri data = intent.getData();
|
||||
if (data != null) {
|
||||
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ package org.thoughtcrime.securesms;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
public class MainFragment extends Fragment {
|
||||
public class MainFragment extends LoggingFragment {
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
|
||||
@@ -18,25 +18,28 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.ShareCompat;
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -61,23 +64,27 @@ import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Activity for displaying media attachments in-app
|
||||
*/
|
||||
public final class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
|
||||
MediaRailAdapter.RailItemListener,
|
||||
MediaPreviewFragment.Events
|
||||
@@ -116,18 +123,22 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
private boolean cameFromAllMedia;
|
||||
private boolean showThread;
|
||||
private MediaDatabase.Sorting sorting;
|
||||
private FullscreenHelper fullscreenHelper;
|
||||
|
||||
private @Nullable Cursor cursor = null;
|
||||
|
||||
public static @NonNull Intent intentFromMediaRecord(@NonNull Context context,
|
||||
@NonNull MediaRecord mediaRecord,
|
||||
boolean leftIsRecent)
|
||||
{
|
||||
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
|
||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, mediaRecord.getThreadId());
|
||||
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, mediaRecord.getAttachment().getCaption());
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
|
||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
|
||||
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
|
||||
intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
|
||||
return intent;
|
||||
}
|
||||
|
||||
@@ -141,10 +152,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
|
||||
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
|
||||
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
|
||||
showSystemUI();
|
||||
fullscreenHelper = new FullscreenHelper(this);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
@@ -181,7 +189,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
private @NonNull String getTitleText(@NonNull MediaItem mediaItem) {
|
||||
String from;
|
||||
if (mediaItem.outgoing) from = getString(R.string.MediaPreviewActivity_you);
|
||||
else if (mediaItem.recipient != null) from = mediaItem.recipient.toShortString(this);
|
||||
else if (mediaItem.recipient != null) from = mediaItem.recipient.getDisplayName(this);
|
||||
else from = "";
|
||||
|
||||
if (showThread) {
|
||||
@@ -190,10 +198,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
|
||||
if (threadRecipient != null) {
|
||||
if (mediaItem.outgoing || threadRecipient.isGroup()) {
|
||||
if (threadRecipient.isLocalNumber()) {
|
||||
if (threadRecipient.isSelf()) {
|
||||
from = getString(R.string.note_to_self);
|
||||
} else {
|
||||
to = threadRecipient.toShortString(this);
|
||||
to = threadRecipient.getDisplayName(this);
|
||||
}
|
||||
} else {
|
||||
to = getString(R.string.MediaPreviewActivity_you);
|
||||
@@ -228,6 +236,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
restartItem = cleanupMedia();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
cursor = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
@@ -246,6 +263,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
albumRail = findViewById(R.id.media_preview_album_rail);
|
||||
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
|
||||
|
||||
albumRail.setItemAnimator(null); // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
|
||||
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
||||
albumRail.setAdapter(albumRailAdapter);
|
||||
|
||||
@@ -258,9 +276,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
|
||||
anchorMarginsToBottomInsets(detailsContainer);
|
||||
|
||||
anchorMarginsToTopInsets(toolbarLayout);
|
||||
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
|
||||
|
||||
showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
|
||||
fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
@@ -344,6 +362,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
|
||||
mediaPager.removeAllViews();
|
||||
mediaPager.setAdapter(null);
|
||||
viewModel.setCursor(this, null, leftIsRecent);
|
||||
|
||||
return restartItem;
|
||||
}
|
||||
@@ -363,6 +382,27 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
}
|
||||
}
|
||||
|
||||
private void share() {
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
|
||||
if (mediaItem != null) {
|
||||
Uri publicUri = PartAuthority.getAttachmentPublicUri(mediaItem.uri);
|
||||
String mimeType = Intent.normalizeMimeType(mediaItem.type);
|
||||
Intent shareIntent = ShareCompat.IntentBuilder.from(this)
|
||||
.setStream(publicUri)
|
||||
.setType(mimeType)
|
||||
.createChooserIntent()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
try {
|
||||
startActivity(shareIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "No activity existed to share the media.", e);
|
||||
Toast.makeText(this, R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
@SuppressLint("InlinedApi")
|
||||
private void saveToDisk() {
|
||||
@@ -370,21 +410,30 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
|
||||
if (mediaItem != null) {
|
||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||
if (StorageUtil.canWriteToMediaStore()) {
|
||||
performSavetoDisk(mediaItem);
|
||||
return;
|
||||
}
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
||||
performSavetoDisk(mediaItem);
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void performSavetoDisk(@NonNull MediaItem mediaItem) {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
|
||||
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void deleteMedia() {
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
@@ -415,36 +464,45 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.clear();
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
inflater.inflate(R.menu.media_preview, menu);
|
||||
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
if (!isMediaInDb()) {
|
||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||
menu.findItem(R.id.delete).setVisible(false);
|
||||
}
|
||||
|
||||
// Restricted to API26 because of MemoryFileUtil not supporting lower API levels well
|
||||
menu.findItem(R.id.media_preview__share).setVisible(Build.VERSION.SDK_INT >= 26);
|
||||
|
||||
if (cameFromAllMedia) {
|
||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||
}
|
||||
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.media_preview__overview: showOverview(); return true;
|
||||
case R.id.media_preview__forward: forward(); return true;
|
||||
case R.id.save: saveToDisk(); return true;
|
||||
case R.id.delete: deleteMedia(); return true;
|
||||
case android.R.id.home: finish(); return true;
|
||||
}
|
||||
int itemId = item.getItemId();
|
||||
|
||||
if (itemId == R.id.media_preview__overview) { showOverview(); return true; }
|
||||
if (itemId == R.id.media_preview__forward) { forward(); return true; }
|
||||
if (itemId == R.id.media_preview__share) { share(); return true; }
|
||||
if (itemId == R.id.save) { saveToDisk(); return true; }
|
||||
if (itemId == R.id.delete) { deleteMedia(); return true; }
|
||||
if (itemId == android.R.id.home) { finish(); return true; }
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -475,19 +533,46 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
|
||||
if (data != null) {
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(),this, data.first, data.second, leftIsRecent);
|
||||
if (data.first == cursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
cursor = Objects.requireNonNull(data.first);
|
||||
|
||||
int mediaPosition = Objects.requireNonNull(data.second);
|
||||
|
||||
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(),this, cursor, mediaPosition, leftIsRecent);
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
|
||||
viewModel.setCursor(this, data.first, leftIsRecent);
|
||||
viewModel.setCursor(this, cursor, leftIsRecent);
|
||||
|
||||
int item = restartItem >= 0 ? restartItem : data.second;
|
||||
int item = restartItem >= 0 ? restartItem : mediaPosition;
|
||||
mediaPager.setCurrentItem(item);
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,34 +583,14 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
|
||||
@Override
|
||||
public boolean singleTapOnMedia() {
|
||||
toggleUiVisibility();
|
||||
fullscreenHelper.toggleUiVisibility();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void toggleUiVisibility() {
|
||||
int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
|
||||
if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) {
|
||||
showSystemUI();
|
||||
} else {
|
||||
hideSystemUI();
|
||||
}
|
||||
}
|
||||
|
||||
private void hideSystemUI() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN );
|
||||
}
|
||||
|
||||
private void showSystemUI() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN );
|
||||
@Override
|
||||
public void mediaNotAvailable() {
|
||||
Toast.makeText(this, R.string.MediaPreviewActivity_media_no_longer_available, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
private class ViewPagerListener extends ExtendedOnPageChangedListener {
|
||||
@@ -621,6 +686,11 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
public boolean hasFragmentFor(int position) {
|
||||
return mediaPreviewFragment != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkMedia(int currentItem) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) {
|
||||
@@ -638,33 +708,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
});
|
||||
}
|
||||
|
||||
private static void anchorMarginsToTopInsets(@NonNull View viewToAnchor) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor, (view, insets) -> {
|
||||
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
|
||||
|
||||
layoutParams.setMargins(insets.getSystemWindowInsetLeft(),
|
||||
insets.getSystemWindowInsetTop(),
|
||||
insets.getSystemWindowInsetRight(),
|
||||
layoutParams.bottomMargin);
|
||||
|
||||
view.setLayoutParams(layoutParams);
|
||||
|
||||
return insets;
|
||||
});
|
||||
}
|
||||
|
||||
private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) {
|
||||
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
|
||||
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
|
||||
|
||||
for (View view : views) {
|
||||
view.animate()
|
||||
.alpha(hide ? 0 : 1)
|
||||
.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class CursorPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
|
||||
|
||||
@SuppressLint("UseSparseArrays")
|
||||
@@ -712,7 +755,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
cursor.moveToPosition(cursorPosition);
|
||||
|
||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(context, cursor);
|
||||
DatabaseAttachment attachment = mediaRecord.getAttachment();
|
||||
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
|
||||
MediaPreviewFragment fragment = MediaPreviewFragment.newInstance(attachment, autoPlay);
|
||||
|
||||
mediaFragments.put(position, fragment);
|
||||
@@ -734,16 +777,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
cursor.moveToPosition(getCursorPosition(position));
|
||||
|
||||
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
|
||||
RecipientId recipientId = mediaRecord.getRecipientId();
|
||||
RecipientId threadRecipientId = mediaRecord.getThreadRecipientId();
|
||||
|
||||
if (mediaRecord.getAttachment().getDataUri() == null) throw new AssertionError();
|
||||
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
|
||||
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
|
||||
RecipientId recipientId = mediaRecord.getRecipientId();
|
||||
RecipientId threadRecipientId = mediaRecord.getThreadRecipientId();
|
||||
|
||||
return new MediaItem(Recipient.live(recipientId).get(),
|
||||
Recipient.live(threadRecipientId).get(),
|
||||
mediaRecord.getAttachment(),
|
||||
mediaRecord.getAttachment().getDataUri(),
|
||||
attachment,
|
||||
Objects.requireNonNull(attachment.getUri()),
|
||||
mediaRecord.getContentType(),
|
||||
mediaRecord.getDate(),
|
||||
mediaRecord.isOutgoing());
|
||||
@@ -767,6 +809,14 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
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;
|
||||
@@ -805,5 +855,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
||||
void pause(int position);
|
||||
@Nullable View getPlaybackControls(int position);
|
||||
boolean hasFragmentFor(int position);
|
||||
void checkMedia(int currentItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,451 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import android.os.Parcelable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.sql.Date;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity implements LoaderCallbacks<Cursor> {
|
||||
private final static String TAG = MessageDetailsActivity.class.getSimpleName();
|
||||
|
||||
public static final String MESSAGE_ID_EXTRA = "message_id";
|
||||
public static final String THREAD_ID_EXTRA = "thread_id";
|
||||
public static final String IS_PUSH_GROUP_EXTRA = "is_push_group";
|
||||
public static final String TYPE_EXTRA = "type";
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
|
||||
private GlideRequests glideRequests;
|
||||
private long threadId;
|
||||
private boolean isPushGroup;
|
||||
private ConversationItem conversationItem;
|
||||
private ViewGroup itemParent;
|
||||
private View metadataContainer;
|
||||
private View expiresContainer;
|
||||
private TextView errorText;
|
||||
private View resendButton;
|
||||
private TextView sentDate;
|
||||
private TextView receivedDate;
|
||||
private TextView expiresInText;
|
||||
private View receivedContainer;
|
||||
private TextView transport;
|
||||
private TextView toFrom;
|
||||
private ListView recipientsList;
|
||||
private LayoutInflater inflater;
|
||||
|
||||
private DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
|
||||
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private boolean running;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle, boolean ready) {
|
||||
setContentView(R.layout.message_details_activity);
|
||||
running = true;
|
||||
|
||||
initializeResources();
|
||||
initializeActionBar();
|
||||
getSupportLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setTitle(R.string.AndroidManifest__message_details);
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
running = false;
|
||||
}
|
||||
|
||||
private void initializeActionBar() {
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
LiveRecipient recipient = Recipient.live(getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||
recipient.observe(this, r -> setActionBarColor(r.getColor()));
|
||||
|
||||
setActionBarColor(recipient.get().getColor());
|
||||
}
|
||||
|
||||
private void setActionBarColor(MaterialColor color) {
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setStatusBarColor(color.toStatusBarColor(this));
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
inflater = LayoutInflater.from(this);
|
||||
View header = inflater.inflate(R.layout.message_details_header, recipientsList, false);
|
||||
|
||||
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
|
||||
isPushGroup = getIntent().getBooleanExtra(IS_PUSH_GROUP_EXTRA, false);
|
||||
glideRequests = GlideApp.with(this);
|
||||
itemParent = header.findViewById(R.id.item_container);
|
||||
recipientsList = findViewById(R.id.recipients_list);
|
||||
metadataContainer = header.findViewById(R.id.metadata_container);
|
||||
errorText = header.findViewById(R.id.error_text);
|
||||
resendButton = header.findViewById(R.id.resend_button);
|
||||
sentDate = header.findViewById(R.id.sent_time);
|
||||
receivedContainer = header.findViewById(R.id.received_container);
|
||||
receivedDate = header.findViewById(R.id.received_time);
|
||||
transport = header.findViewById(R.id.transport);
|
||||
toFrom = header.findViewById(R.id.tofrom);
|
||||
expiresContainer = header.findViewById(R.id.expires_container);
|
||||
expiresInText = header.findViewById(R.id.expires_in);
|
||||
recipientsList.setHeaderDividersEnabled(false);
|
||||
recipientsList.addHeaderView(header, null, false);
|
||||
}
|
||||
|
||||
private void updateTransport(MessageRecord messageRecord) {
|
||||
final String transportText;
|
||||
if (messageRecord.isOutgoing() && messageRecord.isFailed()) {
|
||||
transportText = "-";
|
||||
} else if (messageRecord.isPending()) {
|
||||
transportText = getString(R.string.ConversationFragment_pending);
|
||||
} else if (messageRecord.isPush()) {
|
||||
transportText = getString(R.string.ConversationFragment_push);
|
||||
} else if (messageRecord.isMms()) {
|
||||
transportText = getString(R.string.ConversationFragment_mms);
|
||||
} else {
|
||||
transportText = getString(R.string.ConversationFragment_sms);
|
||||
}
|
||||
|
||||
transport.setText(transportText);
|
||||
}
|
||||
|
||||
private void updateTime(MessageRecord messageRecord) {
|
||||
sentDate.setOnLongClickListener(null);
|
||||
receivedDate.setOnLongClickListener(null);
|
||||
|
||||
if (messageRecord.isPending() || messageRecord.isFailed()) {
|
||||
sentDate.setText("-");
|
||||
receivedContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
Locale dateLocale = dynamicLanguage.getCurrentLocale();
|
||||
SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(this, dateLocale);
|
||||
sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent())));
|
||||
sentDate.setOnLongClickListener(v -> {
|
||||
copyToClipboard(String.valueOf(messageRecord.getDateSent()));
|
||||
return true;
|
||||
});
|
||||
|
||||
if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) {
|
||||
receivedDate.setText(dateFormatter.format(new Date(messageRecord.getDateReceived())));
|
||||
receivedDate.setOnLongClickListener(v -> {
|
||||
copyToClipboard(String.valueOf(messageRecord.getDateReceived()));
|
||||
return true;
|
||||
});
|
||||
receivedContainer.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
receivedContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateExpirationTime(final MessageRecord messageRecord) {
|
||||
if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) {
|
||||
expiresContainer.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
expiresContainer.setVisibility(View.VISIBLE);
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
long elapsed = System.currentTimeMillis() - messageRecord.getExpireStarted();
|
||||
long remaining = messageRecord.getExpiresIn() - elapsed;
|
||||
|
||||
String duration = ExpirationUtil.getExpirationDisplayValue(MessageDetailsActivity.this, Math.max((int)(remaining / 1000), 1));
|
||||
expiresInText.setText(duration);
|
||||
|
||||
if (running) {
|
||||
Util.runOnMainDelayed(this, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateRecipients(MessageRecord messageRecord, Recipient recipient, List<RecipientDeliveryStatus> recipients) {
|
||||
final int toFromRes;
|
||||
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
|
||||
toFromRes = R.string.message_details_header__with;
|
||||
} else if (messageRecord.isOutgoing()) {
|
||||
toFromRes = R.string.message_details_header__to;
|
||||
} else {
|
||||
toFromRes = R.string.message_details_header__from;
|
||||
}
|
||||
toFrom.setText(toFromRes);
|
||||
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, null, false);
|
||||
Parcelable state = recipientsList.onSaveInstanceState();
|
||||
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
|
||||
recipientsList.onRestoreInstanceState(state);
|
||||
}
|
||||
|
||||
private void inflateMessageViewIfAbsent(MessageRecord messageRecord) {
|
||||
if (conversationItem == null) {
|
||||
if (messageRecord.isGroupAction()) {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_update, itemParent, false);
|
||||
} else if (messageRecord.isOutgoing()) {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent_multimedia, itemParent, false);
|
||||
} else {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received_multimedia, itemParent, false);
|
||||
}
|
||||
itemParent.addView(conversationItem);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
|
||||
switch (type) {
|
||||
case MmsSmsDatabase.SMS_TRANSPORT:
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsDatabase.Reader reader = smsDatabase.readerFor(cursor);
|
||||
return reader.getNext();
|
||||
case MmsSmsDatabase.MMS_TRANSPORT:
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
MmsDatabase.Reader mmsReader = mmsDatabase.readerFor(cursor);
|
||||
return mmsReader.getNext();
|
||||
default:
|
||||
throw new AssertionError("no valid message type specified");
|
||||
}
|
||||
}
|
||||
|
||||
private void copyToClipboard(@NonNull String text) {
|
||||
((ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new MessageDetailsLoader(this, getIntent().getStringExtra(TYPE_EXTRA),
|
||||
getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
|
||||
MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
|
||||
|
||||
if (messageRecord == null) {
|
||||
finish();
|
||||
} else {
|
||||
new MessageRecipientAsyncTask(this, messageRecord).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
recipientsList.setAdapter(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: finish(); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,List<RecipientDeliveryStatus>> {
|
||||
|
||||
private final WeakReference<Context> weakContext;
|
||||
private final MessageRecord messageRecord;
|
||||
|
||||
MessageRecipientAsyncTask(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
||||
this.weakContext = new WeakReference<>(context);
|
||||
this.messageRecord = messageRecord;
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
return weakContext.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RecipientDeliveryStatus> doInBackground(Void... voids) {
|
||||
Context context = getContext();
|
||||
|
||||
if (context == null) {
|
||||
Log.w(TAG, "associated context is destroyed, finishing early");
|
||||
return null;
|
||||
}
|
||||
|
||||
List<RecipientDeliveryStatus> recipients = new LinkedList<>();
|
||||
|
||||
if (!messageRecord.getRecipient().isGroup()) {
|
||||
recipients.add(new RecipientDeliveryStatus(messageRecord.getRecipient(), getStatusFor(messageRecord.getDeliveryReceiptCount(), messageRecord.getReadReceiptCount(), messageRecord.isPending()), messageRecord.isUnidentified(), -1));
|
||||
} else {
|
||||
List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
|
||||
|
||||
if (receiptInfoList.isEmpty()) {
|
||||
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
|
||||
for (Recipient recipient : group) {
|
||||
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1));
|
||||
}
|
||||
} else {
|
||||
for (GroupReceiptInfo info : receiptInfoList) {
|
||||
recipients.add(new RecipientDeliveryStatus(Recipient.resolved(info.getRecipientId()),
|
||||
getStatusFor(info.getStatus(), messageRecord.isPending(), messageRecord.isFailed()),
|
||||
info.isUnidentified(),
|
||||
info.getTimestamp()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(List<RecipientDeliveryStatus> recipients) {
|
||||
if (getContext() == null) {
|
||||
Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early.");
|
||||
return;
|
||||
}
|
||||
|
||||
inflateMessageViewIfAbsent(messageRecord);
|
||||
updateRecipients(messageRecord, messageRecord.getRecipient(), recipients);
|
||||
|
||||
boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty();
|
||||
boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup && messageRecord.getIdentityKeyMismatches().isEmpty();
|
||||
|
||||
if (isGroupNetworkFailure || isIndividualNetworkFailure) {
|
||||
errorText.setVisibility(View.VISIBLE);
|
||||
resendButton.setVisibility(View.VISIBLE);
|
||||
resendButton.setOnClickListener(this::onResendClicked);
|
||||
metadataContainer.setVisibility(View.GONE);
|
||||
} else if (messageRecord.isFailed()) {
|
||||
errorText.setVisibility(View.VISIBLE);
|
||||
resendButton.setVisibility(View.GONE);
|
||||
resendButton.setOnClickListener(null);
|
||||
metadataContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
updateTransport(messageRecord);
|
||||
updateTime(messageRecord);
|
||||
updateExpirationTime(messageRecord);
|
||||
errorText.setVisibility(View.GONE);
|
||||
resendButton.setVisibility(View.GONE);
|
||||
resendButton.setOnClickListener(null);
|
||||
metadataContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private RecipientDeliveryStatus.Status getStatusFor(int deliveryReceiptCount, int readReceiptCount, boolean pending) {
|
||||
if (readReceiptCount > 0) return RecipientDeliveryStatus.Status.READ;
|
||||
else if (deliveryReceiptCount > 0) return RecipientDeliveryStatus.Status.DELIVERED;
|
||||
else if (!pending) return RecipientDeliveryStatus.Status.SENT;
|
||||
else return RecipientDeliveryStatus.Status.PENDING;
|
||||
}
|
||||
|
||||
private RecipientDeliveryStatus.Status getStatusFor(int groupStatus, boolean pending, boolean failed) {
|
||||
if (groupStatus == GroupReceiptDatabase.STATUS_READ) return RecipientDeliveryStatus.Status.READ;
|
||||
else if (groupStatus == GroupReceiptDatabase.STATUS_DELIVERED) return RecipientDeliveryStatus.Status.DELIVERED;
|
||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && failed) return RecipientDeliveryStatus.Status.UNKNOWN;
|
||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED && !pending) return RecipientDeliveryStatus.Status.SENT;
|
||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNDELIVERED) return RecipientDeliveryStatus.Status.PENDING;
|
||||
else if (groupStatus == GroupReceiptDatabase.STATUS_UNKNOWN) return RecipientDeliveryStatus.Status.UNKNOWN;
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
private void onResendClicked(View v) {
|
||||
resendButton.setVisibility(View.GONE);
|
||||
SignalExecutors.BOUNDED.execute(() -> MessageSender.resend(MessageDetailsActivity.this, messageRecord));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.BaseAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.adapter.StableIdGenerator;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
|
||||
class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
|
||||
|
||||
private final Context context;
|
||||
private final GlideRequests glideRequests;
|
||||
private final MessageRecord record;
|
||||
private final List<RecipientDeliveryStatus> members;
|
||||
private final boolean isPushGroup;
|
||||
private final StableIdGenerator<RecipientId> idGenerator;
|
||||
|
||||
MessageDetailsRecipientAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests,
|
||||
@NonNull MessageRecord record, @NonNull List<RecipientDeliveryStatus> members,
|
||||
boolean isPushGroup)
|
||||
{
|
||||
this.context = context;
|
||||
this.glideRequests = glideRequests;
|
||||
this.record = record;
|
||||
this.isPushGroup = isPushGroup;
|
||||
this.members = members;
|
||||
this.idGenerator = new StableIdGenerator<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return members.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return members.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return idGenerator.getId(members.get(position).recipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(context).inflate(R.layout.message_recipient_list_item, parent, false);
|
||||
}
|
||||
|
||||
RecipientDeliveryStatus member = members.get(position);
|
||||
|
||||
((MessageRecipientListItem)convertView).set(glideRequests, record, member, isPushGroup);
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMovedToScrapHeap(View view) {
|
||||
((MessageRecipientListItem)view).unbind();
|
||||
}
|
||||
|
||||
|
||||
static class RecipientDeliveryStatus {
|
||||
|
||||
enum Status {
|
||||
UNKNOWN, PENDING, SENT, DELIVERED, READ
|
||||
}
|
||||
|
||||
private final Recipient recipient;
|
||||
private final Status deliveryStatus;
|
||||
private final boolean isUnidentified;
|
||||
private final long timestamp;
|
||||
|
||||
RecipientDeliveryStatus(Recipient recipient, Status deliveryStatus, boolean isUnidentified, long timestamp) {
|
||||
this.recipient = recipient;
|
||||
this.deliveryStatus = deliveryStatus;
|
||||
this.isUnidentified = isUnidentified;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
Status getDeliveryStatus() {
|
||||
return deliveryStatus;
|
||||
}
|
||||
|
||||
boolean isUnidentified() {
|
||||
return isUnidentified;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
/**
|
||||
* A simple view to show the recipients of a message
|
||||
*
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class MessageRecipientListItem extends RelativeLayout
|
||||
implements RecipientForeverObserver
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
private final static String TAG = MessageRecipientListItem.class.getSimpleName();
|
||||
|
||||
private RecipientDeliveryStatus member;
|
||||
private GlideRequests glideRequests;
|
||||
private FromTextView fromView;
|
||||
private TextView errorDescription;
|
||||
private TextView actionDescription;
|
||||
private Button conflictButton;
|
||||
private AvatarImageView contactPhotoImage;
|
||||
private ImageView unidentifiedDeliveryIcon;
|
||||
private DeliveryStatusView deliveryStatusView;
|
||||
|
||||
public MessageRecipientListItem(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MessageRecipientListItem(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
this.fromView = findViewById(R.id.from);
|
||||
this.errorDescription = findViewById(R.id.error_description);
|
||||
this.actionDescription = findViewById(R.id.action_description);
|
||||
this.contactPhotoImage = findViewById(R.id.contact_photo_image);
|
||||
this.conflictButton = findViewById(R.id.conflict_button);
|
||||
this.unidentifiedDeliveryIcon = findViewById(R.id.ud_indicator);
|
||||
this.deliveryStatusView = findViewById(R.id.delivery_status);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
observeMember();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
unsubscribeFromMember();
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
public void set(final GlideRequests glideRequests,
|
||||
final MessageRecord record,
|
||||
final RecipientDeliveryStatus member,
|
||||
final boolean isPushGroup)
|
||||
{
|
||||
unsubscribeFromMember();
|
||||
|
||||
this.glideRequests = glideRequests;
|
||||
this.member = member;
|
||||
observeMember();
|
||||
|
||||
fromView.setText(member.getRecipient());
|
||||
contactPhotoImage.setAvatar(glideRequests, member.getRecipient(), false);
|
||||
setIssueIndicators(record, isPushGroup);
|
||||
unidentifiedDeliveryIcon.setVisibility(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()) && member.isUnidentified() ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
private void observeMember() {
|
||||
if (isAttachedToWindow() && member != null && member.getRecipient() != null) {
|
||||
member.getRecipient().live().observeForever(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void unsubscribeFromMember() {
|
||||
if (member != null && member.getRecipient() != null) member.getRecipient().live().removeForeverObserver(this);
|
||||
}
|
||||
|
||||
private void setIssueIndicators(final MessageRecord record,
|
||||
final boolean isPushGroup)
|
||||
{
|
||||
final NetworkFailure networkFailure = getNetworkFailure(record);
|
||||
final IdentityKeyMismatch keyMismatch = networkFailure == null ? getKeyMismatch(record) : null;
|
||||
|
||||
String errorText = "";
|
||||
|
||||
if (keyMismatch != null) {
|
||||
conflictButton.setVisibility(View.VISIBLE);
|
||||
|
||||
errorText = getContext().getString(R.string.MessageDetailsRecipient_new_safety_number);
|
||||
conflictButton.setOnClickListener(v -> new ConfirmIdentityDialog(getContext(), record, keyMismatch).show());
|
||||
} else if ((networkFailure != null && !record.isPending()) || (!isPushGroup && record.isFailed())) {
|
||||
conflictButton.setVisibility(View.GONE);
|
||||
errorText = getContext().getString(R.string.MessageDetailsRecipient_failed_to_send);
|
||||
} else {
|
||||
if (record.isOutgoing()) {
|
||||
if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.PENDING || member.getDeliveryStatus() == RecipientDeliveryStatus.Status.UNKNOWN) {
|
||||
deliveryStatusView.setVisibility(View.GONE);
|
||||
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.READ) {
|
||||
deliveryStatusView.setRead();
|
||||
deliveryStatusView.setVisibility(View.VISIBLE);
|
||||
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.DELIVERED) {
|
||||
deliveryStatusView.setDelivered();
|
||||
deliveryStatusView.setVisibility(View.VISIBLE);
|
||||
} else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.SENT) {
|
||||
deliveryStatusView.setSent();
|
||||
deliveryStatusView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
deliveryStatusView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
conflictButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
errorDescription.setText(errorText);
|
||||
errorDescription.setVisibility(TextUtils.isEmpty(errorText) ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
private NetworkFailure getNetworkFailure(final MessageRecord record) {
|
||||
if (record.hasNetworkFailures()) {
|
||||
for (final NetworkFailure failure : record.getNetworkFailures()) {
|
||||
if (failure.getRecipientId(getContext()).equals(member.getRecipient().getId())) {
|
||||
return failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private IdentityKeyMismatch getKeyMismatch(final MessageRecord record) {
|
||||
if (record.isIdentityMismatchFailure()) {
|
||||
for (final IdentityKeyMismatch mismatch : record.getIdentityKeyMismatches()) {
|
||||
if (mismatch.getRecipientId(getContext()).equals(member.getRecipient().getId())) {
|
||||
return mismatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
unsubscribeFromMember();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
if (this.member != null && this.member.getRecipient().equals(recipient)) {
|
||||
Log.d(TAG, "onRecipientChanged -- valid");
|
||||
fromView.setText(recipient);
|
||||
contactPhotoImage.setAvatar(glideRequests, recipient, false);
|
||||
} else {
|
||||
Log.d(TAG, "onRecipientChanged -- invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,25 @@ import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
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;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Activity container for starting a new conversation.
|
||||
*
|
||||
@@ -51,15 +61,41 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
Recipient recipient;
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (recipientId.isPresent()) {
|
||||
recipient = Recipient.resolved(recipientId.get());
|
||||
launch(Recipient.resolved(recipientId.get()));
|
||||
} else {
|
||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
||||
recipient = Recipient.external(this, number);
|
||||
|
||||
if (TextSecurePreferences.isPushRegistered(this) && NetworkConstraint.isMet(this)) {
|
||||
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
|
||||
|
||||
AlertDialog progress = SimpleProgressDialog.show(this);
|
||||
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
Recipient resolved = Recipient.external(this, number);
|
||||
|
||||
if (!resolved.isRegistered() || !resolved.hasUuid()) {
|
||||
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
||||
try {
|
||||
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
|
||||
resolved = Recipient.resolved(resolved.getId());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}, resolved -> {
|
||||
progress.dismiss();
|
||||
launch(resolved);
|
||||
});
|
||||
} else {
|
||||
launch(Recipient.external(this, number));
|
||||
}
|
||||
}
|
||||
launch(recipient);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void launch(Recipient recipient) {
|
||||
@@ -68,7 +104,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA));
|
||||
intent.setDataAndType(getIntent().getData(), getIntent().getType());
|
||||
|
||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
|
||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
||||
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread);
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
@@ -104,11 +140,11 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.clear();
|
||||
getMenuInflater().inflate(R.menu.new_conversation_activity, menu);
|
||||
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public abstract class PassphraseActivity extends BaseActionBarActivity {
|
||||
public abstract class PassphraseActivity extends BaseActivity {
|
||||
|
||||
private static final String TAG = PassphraseActivity.class.getSimpleName();
|
||||
|
||||
|
||||
@@ -132,12 +132,13 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
inflater.inflate(R.menu.log_submit, menu);
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity implements MasterSecretListener {
|
||||
private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName();
|
||||
public abstract class PassphraseRequiredActivity extends BaseActivity implements MasterSecretListener {
|
||||
private static final String TAG = PassphraseRequiredActivity.class.getSimpleName();
|
||||
|
||||
public static final String LOCALE_EXTRA = "locale_extra";
|
||||
public static final String NEXT_INTENT_EXTRA = "next_intent";
|
||||
@@ -49,7 +49,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
|
||||
@Override
|
||||
protected final void onCreate(Bundle savedInstanceState) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onCreate()");
|
||||
this.networkAccess = new SignalServiceNetworkAccess(this);
|
||||
onPreCreate();
|
||||
|
||||
@@ -69,7 +68,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onResume()");
|
||||
super.onResume();
|
||||
|
||||
if (networkAccess.isCensored(this)) {
|
||||
@@ -77,27 +75,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onStart()");
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onPause()");
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onStop()");
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onDestroy()");
|
||||
super.onDestroy();
|
||||
removeClearKeyReceiver(this);
|
||||
}
|
||||
@@ -185,7 +164,7 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
||||
}
|
||||
|
||||
private boolean userMustCreateSignalPin() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().lastPinCreateFailed();
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().lastPinCreateFailed() && !SignalStore.kbsValues().hasOptedOut();
|
||||
}
|
||||
|
||||
private boolean userMustSetProfileName() {
|
||||
@@ -6,7 +6,7 @@ import android.widget.Button;
|
||||
|
||||
import org.thoughtcrime.securesms.preferences.MmsPreferencesActivity;
|
||||
|
||||
public class PromptMmsActivity extends PassphraseRequiredActionBarActivity {
|
||||
public class PromptMmsActivity extends PassphraseRequiredActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
|
||||
@@ -42,7 +42,6 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
|
||||
super.onCreate(icicle, ready);
|
||||
|
||||
initializeToolbar();
|
||||
|
||||
@@ -1,770 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.preference.CheckBoxPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.color.MaterialColors;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
|
||||
import org.thoughtcrime.securesms.database.loaders.RecipientMediaLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ColorPickerPreference;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ContactPreference;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActivity implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
{
|
||||
private static final String TAG = RecipientPreferenceActivity.class.getSimpleName();
|
||||
|
||||
public static final String RECIPIENT_ID = "recipient";
|
||||
|
||||
private static final String PREFERENCE_MUTED = "pref_key_recipient_mute";
|
||||
private static final String PREFERENCE_MESSAGE_TONE = "pref_key_recipient_ringtone";
|
||||
private static final String PREFERENCE_CALL_TONE = "pref_key_recipient_call_ringtone";
|
||||
private static final String PREFERENCE_MESSAGE_VIBRATE = "pref_key_recipient_vibrate";
|
||||
private static final String PREFERENCE_CALL_VIBRATE = "pref_key_recipient_call_vibrate";
|
||||
private static final String PREFERENCE_BLOCK = "pref_key_recipient_block";
|
||||
private static final String PREFERENCE_COLOR = "pref_key_recipient_color";
|
||||
private static final String PREFERENCE_IDENTITY = "pref_key_recipient_identity";
|
||||
private static final String PREFERENCE_ABOUT = "pref_key_number";
|
||||
private static final String PREFERENCE_CUSTOM_NOTIFICATIONS = "pref_key_recipient_custom_notifications";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
|
||||
|
||||
private ImageView avatar;
|
||||
private GlideRequests glideRequests;
|
||||
private RecipientId recipientId;
|
||||
private TextView threadPhotoRailLabel;
|
||||
private ThreadPhotoRailView threadPhotoRailView;
|
||||
private CollapsingToolbarLayout toolbarLayout;
|
||||
|
||||
public static @NonNull Intent getLaunchIntent(@NonNull Context context, @NonNull RecipientId id) {
|
||||
Intent intent = new Intent(context, RecipientPreferenceActivity.class);
|
||||
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, id);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle instanceState, boolean ready) {
|
||||
setContentView(R.layout.recipient_preference_activity);
|
||||
this.glideRequests = GlideApp.with(this);
|
||||
this.recipientId = getIntent().getParcelableExtra(RECIPIENT_ID);
|
||||
|
||||
LiveRecipient recipient = Recipient.live(recipientId);
|
||||
|
||||
initializeToolbar();
|
||||
setHeader(recipient.get());
|
||||
recipient.observe(this, this::setHeader);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.preference_fragment);
|
||||
fragment.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
this.toolbarLayout = findViewById(R.id.collapsing_toolbar);
|
||||
this.avatar = findViewById(R.id.avatar);
|
||||
this.threadPhotoRailView = findViewById(R.id.recent_photos);
|
||||
this.threadPhotoRailLabel = findViewById(R.id.rail_label);
|
||||
|
||||
this.toolbarLayout.setExpandedTitleColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
||||
this.toolbarLayout.setCollapsedTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
||||
|
||||
this.threadPhotoRailView.setListener(mediaRecord ->
|
||||
startActivity(MediaPreviewActivity.intentFromMediaRecord(RecipientPreferenceActivity.this,
|
||||
mediaRecord,
|
||||
ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR)));
|
||||
|
||||
SimpleTask.run(
|
||||
() -> DatabaseFactory.getThreadDatabase(this).getThreadIdFor(recipientId),
|
||||
(threadId) -> {
|
||||
if (threadId == null) {
|
||||
Log.i(TAG, "No thread id for recipient.");
|
||||
} else {
|
||||
this.threadPhotoRailLabel.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(this, threadId)));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setLogo(null);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||
getWindow().setStatusBarColor(Color.TRANSPARENT);
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.recipient_preference_root), (v, insets) -> {
|
||||
ViewUtil.setTopMargin(toolbar, insets.getSystemWindowInsetTop());
|
||||
return insets;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setHeader(@NonNull Recipient recipient) {
|
||||
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
|
||||
: recipient.getContactPhoto();
|
||||
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
||||
: recipient.getFallbackContactPhoto();
|
||||
|
||||
glideRequests.load(contactPhoto)
|
||||
.fallback(fallbackPhoto.asCallCard(this))
|
||||
.error(fallbackPhoto.asCallCard(this))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.addListener(new RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
||||
avatar.setOnClickListener(null);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
|
||||
avatar.setOnClickListener(v -> startActivity(AvatarPreviewActivity.intentFromRecipientId(RecipientPreferenceActivity.this, recipient.getId()),
|
||||
AvatarPreviewActivity.createTransitionBundle(RecipientPreferenceActivity.this, avatar)));
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(this.avatar);
|
||||
|
||||
if (contactPhoto == null) this.avatar.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
|
||||
else this.avatar.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
|
||||
this.avatar.setBackgroundColor(recipient.getColor().toActionBarColor(this));
|
||||
this.toolbarLayout.setTitle(recipient.toShortString(this));
|
||||
this.toolbarLayout.setContentScrimColor(recipient.getColor().toActionBarColor(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new RecipientMediaLoader(this, recipientId, RecipientMediaLoader.MediaType.GALLERY, MediaDatabase.Sorting.Newest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
if (data != null && data.getCount() > 0) {
|
||||
this.threadPhotoRailLabel.setVisibility(View.VISIBLE);
|
||||
this.threadPhotoRailView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
this.threadPhotoRailLabel.setVisibility(View.GONE);
|
||||
this.threadPhotoRailView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
this.threadPhotoRailView.setCursor(glideRequests, data);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(RECIPIENT_ID, recipientId);
|
||||
initFragment(R.id.preference_fragment, new RecipientPreferenceFragment(), null, bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
this.threadPhotoRailView.setCursor(glideRequests, null);
|
||||
}
|
||||
|
||||
public static class RecipientPreferenceFragment extends CorrectedPreferenceFragment {
|
||||
private LiveRecipient recipient;
|
||||
private boolean canHaveSafetyNumber;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
Log.i(TAG, "onCreate (fragment)");
|
||||
super.onCreate(icicle);
|
||||
|
||||
initializeRecipients();
|
||||
|
||||
this.canHaveSafetyNumber = recipient.get().isRegistered() && !recipient.get().isLocalNumber();
|
||||
|
||||
Preference customNotificationsPref = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
|
||||
|
||||
if (NotificationChannels.supported()) {
|
||||
((SwitchPreferenceCompat) customNotificationsPref).setChecked(recipient.get().getNotificationChannel() != null);
|
||||
customNotificationsPref.setOnPreferenceChangeListener(new CustomNotificationsChangedListener());
|
||||
|
||||
this.findPreference(PREFERENCE_MESSAGE_TONE).setDependency(PREFERENCE_CUSTOM_NOTIFICATIONS);
|
||||
this.findPreference(PREFERENCE_MESSAGE_VIBRATE).setDependency(PREFERENCE_CUSTOM_NOTIFICATIONS);
|
||||
|
||||
if (recipient.get().getNotificationChannel() != null) {
|
||||
final Context context = requireContext();
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(getContext());
|
||||
db.setMessageRingtone(recipient.getId(), NotificationChannels.getMessageRingtone(context, recipient.get()));
|
||||
db.setMessageVibrate(recipient.getId(), NotificationChannels.getMessageVibrate(context, recipient.get()) ? VibrateState.ENABLED : VibrateState.DISABLED);
|
||||
NotificationChannels.ensureCustomChannelConsistency(context);
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
|
||||
}
|
||||
} else {
|
||||
customNotificationsPref.setVisible(false);
|
||||
}
|
||||
|
||||
this.findPreference(PREFERENCE_MESSAGE_TONE)
|
||||
.setOnPreferenceChangeListener(new RingtoneChangeListener(false));
|
||||
this.findPreference(PREFERENCE_MESSAGE_TONE)
|
||||
.setOnPreferenceClickListener(new RingtoneClickedListener(false));
|
||||
this.findPreference(PREFERENCE_CALL_TONE)
|
||||
.setOnPreferenceChangeListener(new RingtoneChangeListener(true));
|
||||
this.findPreference(PREFERENCE_CALL_TONE)
|
||||
.setOnPreferenceClickListener(new RingtoneClickedListener(true));
|
||||
this.findPreference(PREFERENCE_MESSAGE_VIBRATE)
|
||||
.setOnPreferenceChangeListener(new VibrateChangeListener(false));
|
||||
this.findPreference(PREFERENCE_CALL_VIBRATE)
|
||||
.setOnPreferenceChangeListener(new VibrateChangeListener(true));
|
||||
this.findPreference(PREFERENCE_MUTED)
|
||||
.setOnPreferenceClickListener(new MuteClickedListener());
|
||||
this.findPreference(PREFERENCE_BLOCK)
|
||||
.setOnPreferenceClickListener(new BlockClickedListener());
|
||||
this.findPreference(PREFERENCE_COLOR)
|
||||
.setOnPreferenceChangeListener(new ColorChangeListener());
|
||||
((ContactPreference)this.findPreference(PREFERENCE_ABOUT))
|
||||
.setListener(new AboutNumberClickedListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
Log.i(TAG, "onCreatePreferences...");
|
||||
addPreferencesFromResource(R.xml.recipient_preferences);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setSummaries(recipient.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == 1 && resultCode == RESULT_OK && data != null) {
|
||||
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
|
||||
|
||||
findPreference(PREFERENCE_MESSAGE_TONE).getOnPreferenceChangeListener().onPreferenceChange(findPreference(PREFERENCE_MESSAGE_TONE), uri);
|
||||
} else if (requestCode == 2 && resultCode == RESULT_OK && data != null) {
|
||||
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
|
||||
|
||||
findPreference(PREFERENCE_CALL_TONE).getOnPreferenceChangeListener().onPreferenceChange(findPreference(PREFERENCE_CALL_TONE), uri);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
|
||||
RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState);
|
||||
recyclerView.setItemAnimator(null);
|
||||
recyclerView.setLayoutAnimation(null);
|
||||
return recyclerView;
|
||||
}
|
||||
|
||||
private void initializeRecipients() {
|
||||
this.recipient = Recipient.live(getArguments().getParcelable(RECIPIENT_ID));
|
||||
this.recipient.observe(this, this::setSummaries);
|
||||
}
|
||||
|
||||
private void setSummaries(Recipient recipient) {
|
||||
CheckBoxPreference mutePreference = (CheckBoxPreference) this.findPreference(PREFERENCE_MUTED);
|
||||
Preference customPreference = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
|
||||
Preference ringtoneMessagePreference = this.findPreference(PREFERENCE_MESSAGE_TONE);
|
||||
Preference ringtoneCallPreference = this.findPreference(PREFERENCE_CALL_TONE);
|
||||
ListPreference vibrateMessagePreference = (ListPreference) this.findPreference(PREFERENCE_MESSAGE_VIBRATE);
|
||||
ListPreference vibrateCallPreference = (ListPreference) this.findPreference(PREFERENCE_CALL_VIBRATE);
|
||||
ColorPickerPreference colorPreference = (ColorPickerPreference) this.findPreference(PREFERENCE_COLOR);
|
||||
Preference blockPreference = this.findPreference(PREFERENCE_BLOCK);
|
||||
Preference identityPreference = this.findPreference(PREFERENCE_IDENTITY);
|
||||
PreferenceCategory callCategory = (PreferenceCategory)this.findPreference("call_settings");
|
||||
PreferenceCategory aboutCategory = (PreferenceCategory)this.findPreference("about");
|
||||
PreferenceCategory aboutDivider = (PreferenceCategory)this.findPreference("about_divider");
|
||||
ContactPreference aboutPreference = (ContactPreference)this.findPreference(PREFERENCE_ABOUT);
|
||||
PreferenceCategory privacyCategory = (PreferenceCategory) this.findPreference("privacy_settings");
|
||||
PreferenceCategory divider = (PreferenceCategory) this.findPreference("divider");
|
||||
|
||||
mutePreference.setChecked(recipient.isMuted());
|
||||
|
||||
ringtoneMessagePreference.setSummary(ringtoneMessagePreference.isEnabled() ? getRingtoneSummary(getContext(), recipient.getMessageRingtone()) : "");
|
||||
ringtoneCallPreference.setSummary(getRingtoneSummary(getContext(), recipient.getCallRingtone()));
|
||||
|
||||
Pair<String, Integer> vibrateMessageSummary = getVibrateSummary(getContext(), recipient.getMessageVibrate());
|
||||
Pair<String, Integer> vibrateCallSummary = getVibrateSummary(getContext(), recipient.getCallVibrate());
|
||||
|
||||
vibrateMessagePreference.setSummary(vibrateMessagePreference.isEnabled() ? vibrateMessageSummary.first : "");
|
||||
vibrateMessagePreference.setValueIndex(vibrateMessageSummary.second);
|
||||
|
||||
vibrateCallPreference.setSummary(vibrateCallSummary.first);
|
||||
vibrateCallPreference.setValueIndex(vibrateCallSummary.second);
|
||||
|
||||
blockPreference.setVisible(RecipientUtil.isBlockable(recipient));
|
||||
if (recipient.isBlocked()) blockPreference.setTitle(R.string.RecipientPreferenceActivity_unblock);
|
||||
else blockPreference.setTitle(R.string.RecipientPreferenceActivity_block);
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
mutePreference.setVisible(false);
|
||||
customPreference.setVisible(false);
|
||||
ringtoneMessagePreference.setVisible(false);
|
||||
vibrateMessagePreference.setVisible(false);
|
||||
|
||||
if (identityPreference != null) identityPreference.setVisible(false);
|
||||
if (aboutCategory != null) aboutCategory.setVisible(false);
|
||||
if (aboutDivider != null) aboutDivider.setVisible(false);
|
||||
if (privacyCategory != null) privacyCategory.setVisible(false);
|
||||
if (divider != null) divider.setVisible(false);
|
||||
if (callCategory != null) callCategory.setVisible(false);
|
||||
}
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
if (colorPreference != null) colorPreference.setVisible(false);
|
||||
if (identityPreference != null) identityPreference.setVisible(false);
|
||||
if (callCategory != null) callCategory.setVisible(false);
|
||||
if (aboutCategory != null) aboutCategory.setVisible(false);
|
||||
if (aboutDivider != null) aboutDivider.setVisible(false);
|
||||
if (divider != null) divider.setVisible(false);
|
||||
} else {
|
||||
colorPreference.setColors(MaterialColors.CONVERSATION_PALETTE.asConversationColorArray(requireActivity()));
|
||||
colorPreference.setColor(recipient.getColor().toActionBarColor(requireActivity()));
|
||||
|
||||
if (FeatureFlags.profileDisplay()) {
|
||||
aboutPreference.setTitle(recipient.getDisplayName(requireContext()));
|
||||
aboutPreference.setSummary(recipient.resolve().getE164().or(""));
|
||||
} else {
|
||||
aboutPreference.setTitle(formatRecipient(recipient));
|
||||
aboutPreference.setSummary(recipient.getCustomLabel());
|
||||
}
|
||||
|
||||
aboutPreference.setState(recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED, recipient.isBlocked());
|
||||
|
||||
IdentityUtil.getRemoteIdentityKey(getActivity(), recipient).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
|
||||
@Override
|
||||
public void onSuccess(Optional<IdentityRecord> result) {
|
||||
if (result.isPresent()) {
|
||||
if (identityPreference != null) identityPreference.setOnPreferenceClickListener(new IdentityClickedListener(result.get()));
|
||||
if (identityPreference != null) identityPreference.setEnabled(true);
|
||||
} else if (canHaveSafetyNumber) {
|
||||
if (identityPreference != null) identityPreference.setSummary(R.string.RecipientPreferenceActivity_available_once_a_message_has_been_sent_or_received);
|
||||
if (identityPreference != null) identityPreference.setEnabled(false);
|
||||
} else {
|
||||
if (identityPreference != null) getPreferenceScreen().removePreference(identityPreference);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
if (identityPreference != null) getPreferenceScreen().removePreference(identityPreference);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (recipient.isMmsGroup() && privacyCategory != null) {
|
||||
privacyCategory.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull String formatRecipient(@NonNull Recipient recipient) {
|
||||
if (recipient.getE164().isPresent()) return PhoneNumberUtils.formatNumber(recipient.requireE164());
|
||||
else if (recipient.getEmail().isPresent()) return recipient.requireEmail();
|
||||
else return "";
|
||||
}
|
||||
|
||||
private @NonNull String getRingtoneSummary(@NonNull Context context, @Nullable Uri ringtone) {
|
||||
if (ringtone == null) {
|
||||
return context.getString(R.string.preferences__default);
|
||||
} else if (ringtone.toString().isEmpty()) {
|
||||
return context.getString(R.string.preferences__silent);
|
||||
} else {
|
||||
Ringtone tone = RingtoneManager.getRingtone(getActivity(), ringtone);
|
||||
|
||||
if (tone != null) {
|
||||
return tone.getTitle(context);
|
||||
}
|
||||
}
|
||||
|
||||
return context.getString(R.string.preferences__default);
|
||||
}
|
||||
|
||||
private @NonNull Pair<String, Integer> getVibrateSummary(@NonNull Context context, @NonNull VibrateState vibrateState) {
|
||||
if (vibrateState == VibrateState.DEFAULT) {
|
||||
return new Pair<>(context.getString(R.string.preferences__default), 0);
|
||||
} else if (vibrateState == VibrateState.ENABLED) {
|
||||
return new Pair<>(context.getString(R.string.RecipientPreferenceActivity_enabled), 1);
|
||||
} else {
|
||||
return new Pair<>(context.getString(R.string.RecipientPreferenceActivity_disabled), 2);
|
||||
}
|
||||
}
|
||||
|
||||
private class RingtoneChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
private final boolean calls;
|
||||
|
||||
RingtoneChangeListener(boolean calls) {
|
||||
this.calls = calls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
final Context context = preference.getContext();
|
||||
|
||||
Uri value = (Uri)newValue;
|
||||
|
||||
Uri defaultValue;
|
||||
|
||||
if (calls) defaultValue = TextSecurePreferences.getCallNotificationRingtone(context);
|
||||
else defaultValue = TextSecurePreferences.getNotificationRingtone(context);
|
||||
|
||||
if (defaultValue.equals(value)) value = null;
|
||||
else if (value == null) value = Uri.EMPTY;
|
||||
|
||||
|
||||
new AsyncTask<Uri, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Uri... params) {
|
||||
if (calls) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setCallRingtone(recipient.getId(), params[0]);
|
||||
} else {
|
||||
DatabaseFactory.getRecipientDatabase(context).setMessageRingtone(recipient.getId(), params[0]);
|
||||
NotificationChannels.updateMessageRingtone(context, recipient.get(), params[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, value);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class RingtoneClickedListener implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private final boolean calls;
|
||||
|
||||
RingtoneClickedListener(boolean calls) {
|
||||
this.calls = calls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Uri current;
|
||||
Uri defaultUri;
|
||||
|
||||
if (calls) {
|
||||
current = recipient.get().getCallRingtone();
|
||||
defaultUri = TextSecurePreferences.getCallNotificationRingtone(getContext());
|
||||
} else {
|
||||
current = recipient.get().getMessageRingtone();
|
||||
defaultUri = TextSecurePreferences.getNotificationRingtone(getContext());
|
||||
}
|
||||
|
||||
if (current == null) current = Settings.System.DEFAULT_NOTIFICATION_URI;
|
||||
else if (current.toString().isEmpty()) current = null;
|
||||
|
||||
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultUri);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, calls ? RingtoneManager.TYPE_RINGTONE : RingtoneManager.TYPE_NOTIFICATION);
|
||||
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, current);
|
||||
|
||||
startActivityForResult(intent, calls ? 2 : 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class VibrateChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
private final boolean call;
|
||||
|
||||
VibrateChangeListener(boolean call) {
|
||||
this.call = call;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
int value = Integer.parseInt((String) newValue);
|
||||
final VibrateState vibrateState = VibrateState.fromId(value);
|
||||
final Context context = preference.getContext();
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (call) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setCallVibrate(recipient.getId(), vibrateState);
|
||||
}
|
||||
else {
|
||||
DatabaseFactory.getRecipientDatabase(context).setMessageVibrate(recipient.getId(), vibrateState);
|
||||
NotificationChannels.updateMessageVibrate(context, recipient.get(), vibrateState);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class ColorChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
final Context context = getContext();
|
||||
if (context == null) return true;
|
||||
|
||||
final int value = (Integer) newValue;
|
||||
final MaterialColor selectedColor = MaterialColors.CONVERSATION_PALETTE.getByColor(context, value);
|
||||
final MaterialColor currentColor = recipient.get().getColor();
|
||||
|
||||
if (selectedColor == null) return true;
|
||||
|
||||
if (preference.isEnabled() && !currentColor.equals(selectedColor)) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setColor(recipient.getId(), selectedColor);
|
||||
|
||||
if (recipient.get().resolve().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(recipient.getId()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class MuteClickedListener implements Preference.OnPreferenceClickListener {
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (recipient.get().isMuted()) handleUnmute(preference.getContext());
|
||||
else handleMute(preference.getContext());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleMute(@NonNull Context context) {
|
||||
MuteDialog.show(context, until -> setMuted(context, recipient.get(), until));
|
||||
|
||||
setSummaries(recipient.get());
|
||||
}
|
||||
|
||||
private void handleUnmute(@NonNull Context context) {
|
||||
setMuted(context, recipient.get(), 0);
|
||||
}
|
||||
|
||||
private void setMuted(@NonNull final Context context, final Recipient recipient, final long until) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
DatabaseFactory.getRecipientDatabase(context)
|
||||
.setMuted(recipient.getId(), until);
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
|
||||
private class IdentityClickedListener implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private final IdentityRecord identityKey;
|
||||
|
||||
private IdentityClickedListener(IdentityRecord identityKey) {
|
||||
Log.i(TAG, "Identity record: " + identityKey);
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
startActivity(VerifyIdentityActivity.newIntent(preference.getContext(), identityKey));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class BlockClickedListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Context context = preference.getContext();
|
||||
|
||||
if (recipient.get().isBlocked()) {
|
||||
BlockUnblockDialog.showUnblockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.unblock(context, recipient.get()));
|
||||
} else {
|
||||
BlockUnblockDialog.showBlockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.block(context, recipient.get()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class AboutNumberClickedListener implements ContactPreference.Listener {
|
||||
|
||||
@Override
|
||||
public void onMessageClicked() {
|
||||
CommunicationActions.startConversation(getContext(), recipient.get(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSecureCallClicked() {
|
||||
CommunicationActions.startVoiceCall(getActivity(), recipient.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSecureVideoClicked() {
|
||||
CommunicationActions.startVideoCall(getActivity(), recipient.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInSecureCallClicked() {
|
||||
CommunicationActions.startInsecureCall(requireActivity(), recipient.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongClick() {
|
||||
if (recipient.get().hasE164()) {
|
||||
Util.copyToClipboard(requireContext(), recipient.get().requireE164());
|
||||
ServiceUtil.getVibrator(requireContext()).vibrate(250);
|
||||
Toast.makeText(requireContext(), R.string.RecipientBottomSheet_copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomNotificationsChangedListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
final Context context = preference.getContext();
|
||||
final boolean enabled = (boolean) newValue;
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (enabled) {
|
||||
String channel = NotificationChannels.createChannelFor(context, recipient.get());
|
||||
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.getId(), channel);
|
||||
} else {
|
||||
NotificationChannels.deleteChannelFor(context, recipient.get());
|
||||
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.getId(), null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
||||
|
||||
nextIntent = new Intent(this, ConversationActivity.class);
|
||||
nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody());
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
@@ -56,6 +57,7 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
@@ -90,11 +92,13 @@ import org.whispersystems.libsignal.fingerprint.Fingerprint;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
|
||||
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
@@ -104,7 +108,7 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity implements ScanListener, View.OnClickListener {
|
||||
public class VerifyIdentityActivity extends PassphraseRequiredActivity implements ScanListener, View.OnClickListener {
|
||||
|
||||
private static final String TAG = Log.tag(VerifyIdentityActivity.class);
|
||||
|
||||
@@ -307,16 +311,26 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
||||
byte[] localId;
|
||||
byte[] remoteId;
|
||||
|
||||
if (FeatureFlags.uuids() && recipient.resolve().getUuid().isPresent()) {
|
||||
Recipient resolved = recipient.resolve();
|
||||
|
||||
if (FeatureFlags.verifyV2() && resolved.getUuid().isPresent()) {
|
||||
Log.i(TAG, "Using UUID (version 2).");
|
||||
version = 2;
|
||||
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
|
||||
remoteId = UuidUtil.toByteArray(recipient.resolve().getUuid().get());
|
||||
} else {
|
||||
remoteId = UuidUtil.toByteArray(resolved.getUuid().get());
|
||||
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
|
||||
Log.i(TAG, "Using E164 (version 1).");
|
||||
version = 1;
|
||||
localId = TextSecurePreferences.getLocalNumber(requireContext()).getBytes();
|
||||
remoteId = recipient.resolve().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();
|
||||
return;
|
||||
}
|
||||
|
||||
this.recipient.observe(this, this::setRecipientText);
|
||||
@@ -486,7 +500,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
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.toShortString(getContext()))));
|
||||
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.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
|
||||
@@ -19,17 +19,12 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Rational;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
@@ -37,7 +32,6 @@ import android.view.WindowManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
@@ -45,34 +39,32 @@ import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.VerifySpan;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||
|
||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
public class WebRtcCallActivity extends AppCompatActivity {
|
||||
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
|
||||
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
|
||||
|
||||
private static final int STANDARD_DELAY_FINISH = 1000;
|
||||
public static final int BUSY_SIGNAL_DELAY_FINISH = 5500;
|
||||
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
|
||||
@@ -94,6 +86,7 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.webrtc_call_activity);
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().hide();
|
||||
|
||||
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
|
||||
@@ -133,6 +126,11 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
if (!isInPipMode()) {
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -141,11 +139,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
super.onStop();
|
||||
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfiguration) {
|
||||
super.onConfigurationChanged(newConfiguration);
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -155,14 +155,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onUserLeaveHint() {
|
||||
if (deviceSupportsPipMode()) {
|
||||
PictureInPictureParams params = new PictureInPictureParams.Builder()
|
||||
.setAspectRatio(new Rational(16, 9))
|
||||
.build();
|
||||
setPictureInPictureParams(params);
|
||||
enterPipModeIfPossible();
|
||||
}
|
||||
|
||||
//noinspection deprecation
|
||||
enterPictureInPictureMode();
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (!enterPipModeIfPossible()) {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,8 +170,19 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
viewModel.setIsInPipMode(isInPictureInPictureMode);
|
||||
}
|
||||
|
||||
private boolean enterPipModeIfPossible() {
|
||||
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
|
||||
PictureInPictureParams params = new PictureInPictureParams.Builder()
|
||||
.setAspectRatio(new Rational(9, 16))
|
||||
.build();
|
||||
enterPictureInPictureMode(params);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isInPipMode() {
|
||||
return deviceSupportsPipMode() && isInPictureInPictureMode();
|
||||
return isSystemPipEnabledAndAvailable() && isInPictureInPictureMode();
|
||||
}
|
||||
|
||||
private void processIntent(@NonNull Intent intent) {
|
||||
@@ -195,21 +205,18 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
callScreen = ViewUtil.findById(this, R.id.callScreen);
|
||||
callScreen = findViewById(R.id.callScreen);
|
||||
callScreen.setControlsListener(new ControlsListener());
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
|
||||
viewModel.setIsInPipMode(isInPipMode());
|
||||
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
|
||||
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
|
||||
viewModel.getCameraDirection().observe(this, callScreen::setCameraDirection);
|
||||
viewModel.getLocalRenderState().observe(this, callScreen::setLocalRenderState);
|
||||
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
|
||||
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||
viewModel.displaySquareCallCard().observe(this, callScreen::showCallCard);
|
||||
viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
|
||||
}
|
||||
|
||||
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
|
||||
@@ -374,124 +381,92 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
}
|
||||
|
||||
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
private void handleOutgoingCall() {
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
|
||||
}
|
||||
|
||||
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
|
||||
Log.i(TAG, "handleTerminate called: " + hangupType.name());
|
||||
|
||||
callScreen.setRecipient(recipient);
|
||||
callScreen.setStatusFromHangupType(hangupType);
|
||||
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
|
||||
if (hangupType == HangupMessage.Type.NEED_PERMISSION) {
|
||||
startActivity(CalleeMustAcceptMessageRequestActivity.createIntent(this, recipient.getId()));
|
||||
}
|
||||
delayedFinish();
|
||||
}
|
||||
|
||||
private void handleCallRinging(@NonNull WebRtcViewModel event) {
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
private void handleCallRinging() {
|
||||
callScreen.setStatus(getString(R.string.RedPhone_ringing));
|
||||
}
|
||||
|
||||
private void handleCallBusy(@NonNull WebRtcViewModel event) {
|
||||
private void handleCallBusy() {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_busy));
|
||||
|
||||
delayedFinish(BUSY_SIGNAL_DELAY_FINISH);
|
||||
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
|
||||
}
|
||||
|
||||
private void handleCallConnected(@NonNull WebRtcViewModel event) {
|
||||
private void handleCallConnected() {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
}
|
||||
|
||||
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
|
||||
private void handleRecipientUnavailable() {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
|
||||
delayedFinish();
|
||||
}
|
||||
|
||||
private void handleServerFailure(@NonNull WebRtcViewModel event) {
|
||||
private void handleServerFailure() {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
|
||||
delayedFinish();
|
||||
}
|
||||
|
||||
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
|
||||
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
|
||||
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
|
||||
dialog.setTitle(R.string.RedPhone_number_not_registered);
|
||||
dialog.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
dialog.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice);
|
||||
dialog.setCancelable(true);
|
||||
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
|
||||
}
|
||||
});
|
||||
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.RedPhone_number_not_registered)
|
||||
.setIconAttribute(R.attr.dialog_alert_icon)
|
||||
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
|
||||
final IdentityKey theirKey = event.getIdentityKey();
|
||||
final Recipient recipient = event.getRecipient();
|
||||
final IdentityKey theirKey = event.getRemoteParticipants().get(0).getIdentityKey();
|
||||
final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
|
||||
|
||||
if (theirKey == null) {
|
||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||
}
|
||||
|
||||
String name = recipient.getDisplayName(this);
|
||||
String introduction = getString(R.string.WebRtcCallScreen_new_safety_numbers, name, name);
|
||||
SpannableString spannableString = new SpannableString(introduction + " " + getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
|
||||
|
||||
spannableString.setSpan(new VerifySpan(this, recipient.getId(), theirKey), introduction.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
AppCompatTextView untrustedIdentityExplanation = new AppCompatTextView(this);
|
||||
untrustedIdentityExplanation.setText(spannableString);
|
||||
untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setView(untrustedIdentityExplanation)
|
||||
.setPositiveButton(R.string.WebRtcCallScreen_accept, (d, w) -> {
|
||||
synchronized (SESSION_LOCK) {
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
|
||||
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.requireServiceId(), 1), theirKey, true);
|
||||
}
|
||||
|
||||
d.dismiss();
|
||||
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
|
||||
|
||||
startService(intent);
|
||||
})
|
||||
.setNegativeButton(R.string.WebRtcCallScreen_end_call, (d, w) -> {
|
||||
d.dismiss();
|
||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||
})
|
||||
.show();
|
||||
SafetyNumberChangeDialog.showForCall(getSupportFragmentManager(), recipient.getId());
|
||||
}
|
||||
|
||||
private boolean deviceSupportsPipMode() {
|
||||
@Override
|
||||
public void onSendAnywayAfterSafetyNumberChange() {
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
|
||||
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.AUDIO_CALL.getCode());
|
||||
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageResentAfterSafetyNumberChange() { }
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
handleTerminate(viewModel.getRecipient().get(), HangupMessage.Type.NORMAL);
|
||||
}
|
||||
|
||||
private boolean isSystemPipEnabledAndAvailable() {
|
||||
return Build.VERSION.SDK_INT >= 26 &&
|
||||
FeatureFlags.callingPip() &&
|
||||
getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
|
||||
}
|
||||
|
||||
@@ -504,33 +479,33 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEventMainThread(final WebRtcViewModel event) {
|
||||
public void onEventMainThread(@NonNull WebRtcViewModel event) {
|
||||
Log.i(TAG, "Got message from service: " + event);
|
||||
|
||||
viewModel.setRecipient(event.getRecipient());
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
|
||||
switch (event.getState()) {
|
||||
case CALL_CONNECTED: handleCallConnected(event); break;
|
||||
case NETWORK_FAILURE: handleServerFailure(event); break;
|
||||
case CALL_RINGING: handleCallRinging(event); break;
|
||||
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
||||
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
|
||||
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
|
||||
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
|
||||
case NO_SUCH_USER: handleNoSuchUser(event); break;
|
||||
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
|
||||
case CALL_INCOMING: handleIncomingCall(event); break;
|
||||
case CALL_OUTGOING: handleOutgoingCall(event); break;
|
||||
case CALL_BUSY: handleCallBusy(event); break;
|
||||
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
|
||||
case CALL_CONNECTED: handleCallConnected(); break;
|
||||
case NETWORK_FAILURE: handleServerFailure(); break;
|
||||
case CALL_RINGING: handleCallRinging(); break;
|
||||
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
||||
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
|
||||
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
|
||||
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
|
||||
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
|
||||
case NO_SUCH_USER: handleNoSuchUser(event); break;
|
||||
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
|
||||
case CALL_OUTGOING: handleOutgoingCall(); break;
|
||||
case CALL_BUSY: handleCallBusy(); break;
|
||||
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
|
||||
}
|
||||
|
||||
callScreen.setLocalRenderer(event.getLocalRenderer());
|
||||
callScreen.setRemoteRenderer(event.getRemoteRenderer());
|
||||
boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
|
||||
|
||||
viewModel.updateFromWebRtcViewModel(event);
|
||||
viewModel.updateFromWebRtcViewModel(event, enableVideo);
|
||||
|
||||
if (event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable) {
|
||||
if (enableVideo) {
|
||||
enableVideoIfAvailable = false;
|
||||
handleSetMuteVideo(false);
|
||||
}
|
||||
@@ -538,6 +513,24 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
|
||||
private final class ControlsListener implements WebRtcCallView.ControlsListener {
|
||||
|
||||
@Override
|
||||
public void onStartCall(boolean isVideoCall) {
|
||||
enableVideoIfAvailable = isVideoCall;
|
||||
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
|
||||
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, (isVideoCall ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode());
|
||||
startService(intent);
|
||||
|
||||
MessageSender.onMessageSent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelStartCall() {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onControlsFadeOut() {
|
||||
if (videoTooltip != null) {
|
||||
@@ -602,8 +595,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownCaretPressed() {
|
||||
public void onShowParticipantsList() {
|
||||
CallParticipantsListDialog.show(getSupportFragmentManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
|
||||
viewModel.setIsViewingFocusedParticipant(page);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.animation;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.Transformation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class ResizeAnimation extends Animation {
|
||||
|
||||
private final View target;
|
||||
private final int targetWidthPx;
|
||||
private final int targetHeightPx;
|
||||
|
||||
private int startWidth;
|
||||
private int startHeight;
|
||||
|
||||
public ResizeAnimation(@NonNull View target, int targetWidthPx, int targetHeightPx) {
|
||||
this.target = target;
|
||||
this.targetWidthPx = targetWidthPx;
|
||||
this.targetHeightPx = targetHeightPx;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
int newWidth = (int) (startWidth + (targetWidthPx - startWidth) * interpolatedTime);
|
||||
int newHeight = (int) (startHeight + (targetHeightPx - startHeight) * interpolatedTime);
|
||||
|
||||
ViewGroup.LayoutParams params = target.getLayoutParams();
|
||||
|
||||
params.width = newWidth;
|
||||
params.height = newHeight;
|
||||
|
||||
target.setLayoutParams(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(int width, int height, int parentWidth, int parentHeight) {
|
||||
super.initialize(width, height, parentWidth, parentHeight);
|
||||
|
||||
this.startWidth = width;
|
||||
this.startHeight = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean willChangeBounds() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.transition.Transition;
|
||||
import android.transition.TransitionValues;
|
||||
import android.util.Property;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
||||
|
||||
@TargetApi(21)
|
||||
abstract class CircleSquareImageViewTransition extends Transition {
|
||||
|
||||
private static final String CIRCLE_RATIO = "CIRCLE_RATIO";
|
||||
|
||||
private final boolean toCircle;
|
||||
|
||||
CircleSquareImageViewTransition(boolean toCircle) {
|
||||
this.toCircle = toCircle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void captureStartValues(TransitionValues transitionValues) {
|
||||
View view = transitionValues.view;
|
||||
if (view instanceof ImageView) {
|
||||
transitionValues.values.put(CIRCLE_RATIO, toCircle ? 0f : 1f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void captureEndValues(TransitionValues transitionValues) {
|
||||
View view = transitionValues.view;
|
||||
if (view instanceof ImageView) {
|
||||
transitionValues.values.put(CIRCLE_RATIO, toCircle ? 1f : 0f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
|
||||
if (startValues == null || endValues == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ImageView endImageView = (ImageView) endValues.view;
|
||||
float start = (float) startValues.values.get(CIRCLE_RATIO);
|
||||
float end = (float) endValues.values.get(CIRCLE_RATIO);
|
||||
|
||||
return ObjectAnimator.ofFloat(endImageView, new RadiusRatioProperty(), start, end);
|
||||
}
|
||||
|
||||
static final class RadiusRatioProperty extends Property<ImageView, Float> {
|
||||
|
||||
private float ratio;
|
||||
|
||||
RadiusRatioProperty() {
|
||||
super(Float.class, "circle_ratio");
|
||||
}
|
||||
|
||||
@Override
|
||||
final public void set(ImageView imageView, Float ratio) {
|
||||
this.ratio = ratio;
|
||||
Drawable imageViewDrawable = imageView.getDrawable();
|
||||
if (imageViewDrawable instanceof RoundedBitmapDrawable) {
|
||||
RoundedBitmapDrawable drawable = (RoundedBitmapDrawable) imageViewDrawable;
|
||||
if (ratio > 0.95) {
|
||||
drawable.setCircular(true);
|
||||
} else {
|
||||
drawable.setCornerRadius(Math.min(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()) * ratio * 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float get(ImageView object) {
|
||||
return ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* Will only transition {@link android.widget.ImageView}s that contain a {@link androidx.core.graphics.drawable.RoundedBitmapDrawable}.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public final class CircleToSquareImageViewTransition extends CircleSquareImageViewTransition {
|
||||
public CircleToSquareImageViewTransition(Context context, AttributeSet attrs) {
|
||||
super(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.thoughtcrime.securesms.animation.transitions;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* Will only transition {@link android.widget.ImageView}s that contain a {@link androidx.core.graphics.drawable.RoundedBitmapDrawable}.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public final class SquareToCircleImageViewTransition extends CircleSquareImageViewTransition {
|
||||
public SquareToCircleImageViewTransition(Context context, AttributeSet attrs) {
|
||||
super(true);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ public abstract class Attachment {
|
||||
private final String fastPreflightId;
|
||||
|
||||
private final boolean voiceNote;
|
||||
private final boolean borderless;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final boolean quote;
|
||||
@@ -59,11 +60,26 @@ public abstract class Attachment {
|
||||
@NonNull
|
||||
private final TransformProperties transformProperties;
|
||||
|
||||
public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName,
|
||||
int cdnNumber, @Nullable String location, @Nullable String key, @Nullable String relay,
|
||||
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
|
||||
int width, int height, boolean quote, long uploadTimestamp, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
|
||||
public Attachment(@NonNull String contentType,
|
||||
int transferState,
|
||||
long size,
|
||||
@Nullable String fileName,
|
||||
int cdnNumber,
|
||||
@Nullable String location,
|
||||
@Nullable String key,
|
||||
@Nullable String relay,
|
||||
@Nullable byte[] digest,
|
||||
@Nullable String fastPreflightId,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
int width,
|
||||
int height,
|
||||
boolean quote,
|
||||
long uploadTimestamp,
|
||||
@Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash,
|
||||
@Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
@@ -77,6 +93,7 @@ public abstract class Attachment {
|
||||
this.digest = digest;
|
||||
this.fastPreflightId = fastPreflightId;
|
||||
this.voiceNote = voiceNote;
|
||||
this.borderless = borderless;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.quote = quote;
|
||||
@@ -89,10 +106,7 @@ public abstract class Attachment {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public abstract Uri getDataUri();
|
||||
|
||||
@Nullable
|
||||
public abstract Uri getThumbnailUri();
|
||||
public abstract Uri getUri();
|
||||
|
||||
public int getTransferState() {
|
||||
return transferState;
|
||||
@@ -150,6 +164,10 @@ public abstract class Attachment {
|
||||
return voiceNote;
|
||||
}
|
||||
|
||||
public boolean isBorderless() {
|
||||
return borderless;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
@@ -20,17 +20,34 @@ public class DatabaseAttachment extends Attachment {
|
||||
private final boolean hasThumbnail;
|
||||
private final int displayOrder;
|
||||
|
||||
public DatabaseAttachment(AttachmentId attachmentId, long mmsId,
|
||||
boolean hasData, boolean hasThumbnail,
|
||||
String contentType, int transferProgress, long size,
|
||||
String fileName, int cdnNumber, String location, String key, String relay,
|
||||
byte[] digest, String fastPreflightId, boolean voiceNote,
|
||||
int width, int height, boolean quote, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties, int displayOrder,
|
||||
public DatabaseAttachment(AttachmentId attachmentId,
|
||||
long mmsId,
|
||||
boolean hasData,
|
||||
boolean hasThumbnail,
|
||||
String contentType,
|
||||
int transferProgress,
|
||||
long size,
|
||||
String fileName,
|
||||
int cdnNumber,
|
||||
String location,
|
||||
String key,
|
||||
String relay,
|
||||
byte[] digest,
|
||||
String fastPreflightId,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
int width,
|
||||
int height,
|
||||
boolean quote,
|
||||
@Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash,
|
||||
@Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties,
|
||||
int displayOrder,
|
||||
long uploadTimestamp)
|
||||
{
|
||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this.attachmentId = attachmentId;
|
||||
this.hasData = hasData;
|
||||
this.hasThumbnail = hasThumbnail;
|
||||
@@ -40,7 +57,7 @@ public class DatabaseAttachment extends Attachment {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Uri getDataUri() {
|
||||
public Uri getUri() {
|
||||
if (hasData) {
|
||||
return PartAuthority.getAttachmentDataUri(attachmentId);
|
||||
} else {
|
||||
@@ -48,16 +65,6 @@ public class DatabaseAttachment extends Attachment {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Uri getThumbnailUri() {
|
||||
if (hasThumbnail) {
|
||||
return PartAuthority.getAttachmentThumbnailUri(attachmentId);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public AttachmentId getAttachmentId() {
|
||||
return attachmentId;
|
||||
}
|
||||
|
||||
@@ -10,18 +10,12 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
public class MmsNotificationAttachment extends Attachment {
|
||||
|
||||
public MmsNotificationAttachment(int status, long size) {
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null, null);
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, 0, 0, false, 0, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getDataUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getThumbnailUri() {
|
||||
public Uri getUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,29 +18,34 @@ import java.util.List;
|
||||
|
||||
public class PointerAttachment extends Attachment {
|
||||
|
||||
private PointerAttachment(@NonNull String contentType, int transferState, long size,
|
||||
@Nullable String fileName, int cdnNumber, @NonNull String location,
|
||||
@Nullable String key, @Nullable String relay,
|
||||
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
|
||||
int width, int height, long uploadTimestamp, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
||||
private PointerAttachment(@NonNull String contentType,
|
||||
int transferState,
|
||||
long size,
|
||||
@Nullable String fileName,
|
||||
int cdnNumber,
|
||||
@NonNull String location,
|
||||
@Nullable String key,
|
||||
@Nullable String relay,
|
||||
@Nullable byte[] digest,
|
||||
@Nullable String fastPreflightId,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
int width,
|
||||
int height,
|
||||
long uploadTimestamp,
|
||||
@Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
|
||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getDataUri() {
|
||||
public Uri getUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getThumbnailUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static List<Attachment> forPointers(Optional<List<SignalServiceAttachment>> pointers) {
|
||||
List<Attachment> results = new LinkedList<>();
|
||||
|
||||
@@ -91,21 +96,22 @@ public class PointerAttachment extends Attachment {
|
||||
}
|
||||
|
||||
return Optional.of(new PointerAttachment(pointer.get().getContentType(),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||
pointer.get().asPointer().getSize().or(0),
|
||||
pointer.get().asPointer().getFileName().orNull(),
|
||||
pointer.get().asPointer().getCdnNumber(),
|
||||
pointer.get().asPointer().getRemoteId().toString(),
|
||||
encodedKey, null,
|
||||
pointer.get().asPointer().getDigest().orNull(),
|
||||
fastPreflightId,
|
||||
pointer.get().asPointer().getVoiceNote(),
|
||||
pointer.get().asPointer().getWidth(),
|
||||
pointer.get().asPointer().getHeight(),
|
||||
pointer.get().asPointer().getUploadTimestamp(),
|
||||
pointer.get().asPointer().getCaption().orNull(),
|
||||
stickerLocator,
|
||||
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||
pointer.get().asPointer().getSize().or(0),
|
||||
pointer.get().asPointer().getFileName().orNull(),
|
||||
pointer.get().asPointer().getCdnNumber(),
|
||||
pointer.get().asPointer().getRemoteId().toString(),
|
||||
encodedKey, null,
|
||||
pointer.get().asPointer().getDigest().orNull(),
|
||||
fastPreflightId,
|
||||
pointer.get().asPointer().getVoiceNote(),
|
||||
pointer.get().asPointer().isBorderless(),
|
||||
pointer.get().asPointer().getWidth(),
|
||||
pointer.get().asPointer().getHeight(),
|
||||
pointer.get().asPointer().getUploadTimestamp(),
|
||||
pointer.get().asPointer().getCaption().orNull(),
|
||||
stickerLocator,
|
||||
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
|
||||
|
||||
}
|
||||
|
||||
@@ -123,6 +129,7 @@ public class PointerAttachment extends Attachment {
|
||||
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
|
||||
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
|
||||
|
||||
@@ -16,16 +16,11 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
public class TombstoneAttachment extends Attachment {
|
||||
|
||||
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
|
||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null, null);
|
||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, 0, 0, quote, 0, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getDataUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getThumbnailUri() {
|
||||
public @Nullable Uri getUri() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,38 +13,51 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
public class UriAttachment extends Attachment {
|
||||
|
||||
private final @NonNull Uri dataUri;
|
||||
private final @Nullable Uri thumbnailUri;
|
||||
|
||||
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
|
||||
@Nullable String fileName, boolean voiceNote, boolean quote, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
|
||||
public UriAttachment(@NonNull Uri uri,
|
||||
@NonNull String contentType,
|
||||
int transferState,
|
||||
long size,
|
||||
@Nullable String fileName,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
boolean quote,
|
||||
@Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash,
|
||||
@Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties)
|
||||
{
|
||||
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
}
|
||||
|
||||
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
|
||||
@NonNull String contentType, int transferState, long size, int width, int height,
|
||||
@Nullable String fileName, @Nullable String fastPreflightId,
|
||||
boolean voiceNote, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
|
||||
public UriAttachment(@NonNull Uri dataUri,
|
||||
@NonNull String contentType,
|
||||
int transferState,
|
||||
long size,
|
||||
int width,
|
||||
int height,
|
||||
@Nullable String fileName,
|
||||
@Nullable String fastPreflightId,
|
||||
boolean voiceNote,
|
||||
boolean borderless,
|
||||
boolean quote,
|
||||
@Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash,
|
||||
@Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this.dataUri = dataUri;
|
||||
this.thumbnailUri = thumbnailUri;
|
||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this.dataUri = dataUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Uri getDataUri() {
|
||||
public Uri getUri() {
|
||||
return dataUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Uri getThumbnailUri() {
|
||||
return thumbnailUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri);
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.Pair;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.LoadControl;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class AudioSlidePlayer implements SensorEventListener {
|
||||
|
||||
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
|
||||
|
||||
private static @NonNull Optional<AudioSlidePlayer> playing = Optional.absent();
|
||||
|
||||
private final @NonNull Context context;
|
||||
private final @NonNull AudioSlide slide;
|
||||
private final @NonNull Handler progressEventHandler;
|
||||
private final @NonNull AudioManager audioManager;
|
||||
private final @NonNull SensorManager sensorManager;
|
||||
private final @NonNull Sensor proximitySensor;
|
||||
private final @Nullable WakeLock wakeLock;
|
||||
|
||||
private @NonNull WeakReference<Listener> listener;
|
||||
private @Nullable SimpleExoPlayer mediaPlayer;
|
||||
private long startTime;
|
||||
|
||||
public synchronized static AudioSlidePlayer createFor(@NonNull Context context,
|
||||
@NonNull AudioSlide slide,
|
||||
@NonNull Listener listener)
|
||||
{
|
||||
if (playing.isPresent() && playing.get().getAudioSlide().equals(slide)) {
|
||||
playing.get().setListener(listener);
|
||||
return playing.get();
|
||||
} else {
|
||||
return new AudioSlidePlayer(context, slide, listener);
|
||||
}
|
||||
}
|
||||
|
||||
private AudioSlidePlayer(@NonNull Context context,
|
||||
@NonNull AudioSlide slide,
|
||||
@NonNull Listener listener)
|
||||
{
|
||||
this.context = context;
|
||||
this.slide = slide;
|
||||
this.listener = new WeakReference<>(listener);
|
||||
this.progressEventHandler = new ProgressEventHandler(this);
|
||||
this.audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
|
||||
this.sensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||
this.proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.wakeLock = ServiceUtil.getPowerManager(context).newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
|
||||
} else {
|
||||
this.wakeLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void play(final double progress) throws IOException {
|
||||
play(progress, false);
|
||||
}
|
||||
|
||||
private void play(final double progress, boolean earpiece) throws IOException {
|
||||
if (this.mediaPlayer != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (slide.getUri() == null) {
|
||||
throw new IOException("Slide has no URI!");
|
||||
}
|
||||
|
||||
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl();
|
||||
this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl);
|
||||
this.startTime = System.currentTimeMillis();
|
||||
|
||||
mediaPlayer.prepare(createMediaSource(slide.getUri()));
|
||||
mediaPlayer.setPlayWhenReady(true);
|
||||
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setContentType(earpiece ? C.CONTENT_TYPE_SPEECH : C.CONTENT_TYPE_MUSIC)
|
||||
.setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA)
|
||||
.build());
|
||||
mediaPlayer.addListener(new Player.EventListener() {
|
||||
|
||||
boolean started = false;
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")");
|
||||
switch (playbackState) {
|
||||
case Player.STATE_READY:
|
||||
Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered");
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
if (mediaPlayer == null) return;
|
||||
|
||||
if (started) {
|
||||
Log.d(TAG, "Already started. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
started = true;
|
||||
|
||||
if (progress > 0) {
|
||||
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
|
||||
}
|
||||
|
||||
sensorManager.registerListener(AudioSlidePlayer.this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
|
||||
|
||||
setPlaying(AudioSlidePlayer.this);
|
||||
}
|
||||
|
||||
notifyOnStart();
|
||||
progressEventHandler.sendEmptyMessage(0);
|
||||
break;
|
||||
|
||||
case Player.STATE_ENDED:
|
||||
Log.i(TAG, "onComplete");
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
mediaPlayer = null;
|
||||
|
||||
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
||||
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyOnStop();
|
||||
progressEventHandler.removeMessages(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
Log.w(TAG, "MediaPlayer Error: " + error);
|
||||
|
||||
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
|
||||
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
mediaPlayer = null;
|
||||
|
||||
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
||||
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyOnStop();
|
||||
progressEventHandler.removeMessages(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private MediaSource createMediaSource(@NonNull Uri uri) {
|
||||
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
|
||||
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(context, defaultDataSourceFactory, null);
|
||||
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
|
||||
|
||||
return new ExtractorMediaSource.Factory(attachmentDataSourceFactory)
|
||||
.setExtractorsFactory(extractorsFactory)
|
||||
.createMediaSource(uri);
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
Log.i(TAG, "Stop called!");
|
||||
|
||||
removePlaying(this);
|
||||
|
||||
if (this.mediaPlayer != null) {
|
||||
this.mediaPlayer.stop();
|
||||
this.mediaPlayer.release();
|
||||
}
|
||||
|
||||
sensorManager.unregisterListener(AudioSlidePlayer.this);
|
||||
|
||||
this.mediaPlayer = null;
|
||||
}
|
||||
|
||||
public synchronized static void stopAll() {
|
||||
if (playing.isPresent()) {
|
||||
playing.get().stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(@NonNull Listener listener) {
|
||||
this.listener = new WeakReference<>(listener);
|
||||
|
||||
if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
|
||||
notifyOnStart();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull AudioSlide getAudioSlide() {
|
||||
return slide;
|
||||
}
|
||||
|
||||
|
||||
private Pair<Double, Integer> getProgress() {
|
||||
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
|
||||
return new Pair<>(0D, 0);
|
||||
} else {
|
||||
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
|
||||
(int) mediaPlayer.getCurrentPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnStart() {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onStart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyOnStop() {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onStop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyOnProgress(final double progress, final long millis) {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onProgress(progress, millis);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull Listener getListener() {
|
||||
Listener listener = this.listener.get();
|
||||
|
||||
if (listener != null) return listener;
|
||||
else return new Listener() {
|
||||
@Override
|
||||
public void onStart() {}
|
||||
@Override
|
||||
public void onStop() {}
|
||||
@Override
|
||||
public void onProgress(double progress, long millis) {}
|
||||
};
|
||||
}
|
||||
|
||||
private synchronized static void setPlaying(@NonNull AudioSlidePlayer player) {
|
||||
if (playing.isPresent() && playing.get() != player) {
|
||||
playing.get().notifyOnStop();
|
||||
playing.get().stop();
|
||||
}
|
||||
|
||||
playing = Optional.of(player);
|
||||
}
|
||||
|
||||
private synchronized static void removePlaying(@NonNull AudioSlidePlayer player) {
|
||||
if (playing.isPresent() && playing.get() == player) {
|
||||
playing = Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) return;
|
||||
if (mediaPlayer == null || mediaPlayer.getPlaybackState() != Player.STATE_READY) return;
|
||||
|
||||
int streamType;
|
||||
|
||||
if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) {
|
||||
streamType = AudioManager.STREAM_VOICE_CALL;
|
||||
} else {
|
||||
streamType = AudioManager.STREAM_MUSIC;
|
||||
}
|
||||
|
||||
if (streamType == AudioManager.STREAM_VOICE_CALL &&
|
||||
mediaPlayer.getAudioStreamType() != streamType &&
|
||||
!audioManager.isWiredHeadsetOn())
|
||||
{
|
||||
double position = mediaPlayer.getCurrentPosition();
|
||||
double duration = mediaPlayer.getDuration();
|
||||
double progress = position / duration;
|
||||
|
||||
if (wakeLock != null) wakeLock.acquire();
|
||||
stop();
|
||||
try {
|
||||
play(progress, true);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
} else if (streamType == AudioManager.STREAM_MUSIC &&
|
||||
mediaPlayer.getAudioStreamType() != streamType &&
|
||||
System.currentTimeMillis() - startTime > 500)
|
||||
{
|
||||
if (wakeLock != null) wakeLock.release();
|
||||
stop();
|
||||
notifyOnStop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onStart();
|
||||
void onStop();
|
||||
void onProgress(double progress, long millis);
|
||||
}
|
||||
|
||||
private static class ProgressEventHandler extends Handler {
|
||||
|
||||
private final WeakReference<AudioSlidePlayer> playerReference;
|
||||
|
||||
private ProgressEventHandler(@NonNull AudioSlidePlayer player) {
|
||||
this.playerReference = new WeakReference<>(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
AudioSlidePlayer player = playerReference.get();
|
||||
|
||||
if (player == null || player.mediaPlayer == null || !isPlayerActive(player.mediaPlayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Pair<Double, Integer> progress = player.getProgress();
|
||||
player.notifyOnProgress(progress.first, progress.second);
|
||||
sendEmptyMessageDelayed(0, 50);
|
||||
}
|
||||
|
||||
private boolean isPlayerActive(@NonNull SimpleExoPlayer player) {
|
||||
return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import com.google.protobuf.ByteString;
|
||||
|
||||
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.model.databaseprotos.AudioWaveFormData;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -89,7 +90,11 @@ public final class AudioWaveForm {
|
||||
AudioHash audioHash = attachment.getAudioHash();
|
||||
if (audioHash != null) {
|
||||
AudioFileInfo audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioHash.getAudioWaveForm());
|
||||
if (audioFileInfo.waveForm.length != BAR_COUNT) {
|
||||
if (audioFileInfo.waveForm.length == 0) {
|
||||
Log.w(TAG, "Recovering from a wave form generation error " + cacheKey);
|
||||
Util.runOnMain(onFailure);
|
||||
return;
|
||||
} else if (audioFileInfo.waveForm.length != BAR_COUNT) {
|
||||
Log.w(TAG, "Wave form from database does not match bar count, regenerating " + cacheKey);
|
||||
} else {
|
||||
WAVE_FORM_CACHE.put(cacheKey, audioFileInfo);
|
||||
@@ -100,13 +105,19 @@ public final class AudioWaveForm {
|
||||
}
|
||||
|
||||
try {
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
|
||||
|
||||
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
|
||||
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||
|
||||
DatabaseFactory.getAttachmentDatabase(context).writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
|
||||
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||
Util.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||
|
||||
@@ -4,6 +4,10 @@ package org.thoughtcrime.securesms.backup;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@@ -13,10 +17,14 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
@@ -26,25 +34,49 @@ import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
public class BackupDialog {
|
||||
|
||||
public static void showEnableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
||||
private static final String TAG = Log.tag(BackupDialog.class);
|
||||
|
||||
public static void showEnableBackupDialog(@NonNull Context context,
|
||||
@Nullable Intent backupDirectorySelectionIntent,
|
||||
@Nullable String backupDirectoryDisplayName,
|
||||
@NonNull Runnable onBackupsEnabled)
|
||||
{
|
||||
String[] password = BackupUtil.generateBackupPassphrase();
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.BackupDialog_enable_local_backups)
|
||||
.setView(R.layout.backup_enable_dialog)
|
||||
.setView(backupDirectorySelectionIntent != null ? R.layout.backup_enable_dialog_v29 : R.layout.backup_enable_dialog)
|
||||
.setPositiveButton(R.string.BackupDialog_enable_backups, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
dialog.setOnShowListener(created -> {
|
||||
if (backupDirectoryDisplayName != null) {
|
||||
TextView folderName = dialog.findViewById(R.id.backup_enable_dialog_folder_name);
|
||||
if (folderName != null) {
|
||||
folderName.setText(backupDirectoryDisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
Button button = ((AlertDialog) created).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setOnClickListener(v -> {
|
||||
CheckBox confirmationCheckBox = dialog.findViewById(R.id.confirmation_check);
|
||||
if (confirmationCheckBox.isChecked()) {
|
||||
if (backupDirectorySelectionIntent != null && backupDirectorySelectionIntent.getData() != null) {
|
||||
Uri backupDirectoryUri = backupDirectorySelectionIntent.getData();
|
||||
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
|
||||
SignalStore.settings().setSignalBackupDirectory(backupDirectoryUri);
|
||||
context.getContentResolver()
|
||||
.takePersistableUriPermission(backupDirectoryUri, takeFlags);
|
||||
}
|
||||
|
||||
BackupPassphrase.set(context, Util.join(password, " "));
|
||||
TextSecurePreferences.setNextBackupTime(context, 0);
|
||||
TextSecurePreferences.setBackupEnabled(context, true);
|
||||
LocalBackupListener.schedule(context);
|
||||
|
||||
preference.setChecked(true);
|
||||
onBackupsEnabled.run();
|
||||
created.dismiss();
|
||||
} else {
|
||||
Toast.makeText(context, R.string.BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box, Toast.LENGTH_LONG).show();
|
||||
@@ -75,16 +107,42 @@ public class BackupDialog {
|
||||
|
||||
}
|
||||
|
||||
public static void showDisableBackupDialog(@NonNull Context context, @NonNull SwitchPreferenceCompat preference) {
|
||||
@RequiresApi(29)
|
||||
public static void showChooseBackupLocationDialog(@NonNull Fragment fragment, int requestCode) {
|
||||
new AlertDialog.Builder(fragment.requireContext())
|
||||
.setView(R.layout.backup_choose_location_dialog)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setPositiveButton(R.string.BackupDialog_choose_folder, ((dialog, which) -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, SignalStore.settings().getLatestSignalBackupDirectory());
|
||||
}
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
fragment.startActivityForResult(intent, requestCode);
|
||||
|
||||
dialog.dismiss();
|
||||
}))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void showDisableBackupDialog(@NonNull Context context, @NonNull Runnable onBackupsDisabled) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.BackupDialog_delete_backups)
|
||||
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.BackupDialog_delete_backups_statement, (dialog, which) -> {
|
||||
BackupPassphrase.set(context, null);
|
||||
TextSecurePreferences.setBackupEnabled(context, false);
|
||||
BackupUtil.deleteAllBackups();
|
||||
preference.setChecked(false);
|
||||
BackupUtil.disableBackups(context);
|
||||
|
||||
onBackupsDisabled.run();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.thoughtcrime.securesms.backup;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public enum BackupFileIOError {
|
||||
ACCESS_ERROR(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_directory_has_been_deleted_or_moved),
|
||||
FILE_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_file_is_too_large),
|
||||
NOT_ENOUGH_SPACE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_there_is_not_enough_space),
|
||||
UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_tap_to_manage_backups);
|
||||
|
||||
private static final short BACKUP_FAILED_ID = 31321;
|
||||
|
||||
private final @StringRes int titleId;
|
||||
private final @StringRes int messageId;
|
||||
|
||||
BackupFileIOError(@StringRes int titleId, @StringRes int messageId) {
|
||||
this.titleId = titleId;
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public static void clearNotification(@NonNull Context context) {
|
||||
NotificationManagerCompat.from(context).cancel(BACKUP_FAILED_ID);
|
||||
}
|
||||
|
||||
public void postNotification(@NonNull Context context) {
|
||||
Intent intent = new Intent(context, ApplicationPreferencesActivity.class);
|
||||
|
||||
intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_BACKUPS_FRAGMENT, true);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, intent, 0);
|
||||
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES)
|
||||
.setSmallIcon(R.drawable.ic_signal_backup)
|
||||
.setContentTitle(context.getString(titleId))
|
||||
.setContentText(context.getString(messageId))
|
||||
.setContentIntent(pendingIntent)
|
||||
.build();
|
||||
|
||||
NotificationManagerCompat.from(context)
|
||||
.notify(BACKUP_FAILED_ID, backupFailedNotification);
|
||||
}
|
||||
|
||||
public static void postNotificationForException(@NonNull Context context, @NonNull IOException e, int runAttempt) {
|
||||
BackupFileIOError error = getFromException(e);
|
||||
|
||||
if (error != null) {
|
||||
error.postNotification(context);
|
||||
}
|
||||
|
||||
if (error == null && runAttempt > 0) {
|
||||
UNKNOWN.postNotification(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable BackupFileIOError getFromException(@NonNull IOException e) {
|
||||
if (e.getMessage() != null) {
|
||||
if (e.getMessage().contains("EFBIG")) return FILE_TOO_LARGE;
|
||||
else if (e.getMessage().contains("ENOSPC")) return NOT_ENOUGH_SPACE;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,12 @@ package org.thoughtcrime.securesms.backup;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import com.annimon.stream.function.Consumer;
|
||||
import com.annimon.stream.function.Predicate;
|
||||
@@ -30,6 +33,7 @@ import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SearchDatabase;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
@@ -40,7 +44,6 @@ import org.whispersystems.libsignal.kdf.HKDFv3;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -50,6 +53,7 @@ import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
@@ -84,7 +88,32 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, passphrase);
|
||||
try (OutputStream outputStream = new FileOutputStream(output)) {
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
public static void export(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull DocumentFile output,
|
||||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
try (OutputStream outputStream = Objects.requireNonNull(context.getContentResolver().openOutputStream(output.getUri()))) {
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase);
|
||||
}
|
||||
}
|
||||
|
||||
private static void internalExport(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull OutputStream fileOutputStream,
|
||||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase);
|
||||
int count = 0;
|
||||
|
||||
try {
|
||||
@@ -96,7 +125,9 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
|
||||
for (String table : tables) {
|
||||
if (table.equals(MmsDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMessage, null, count);
|
||||
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count);
|
||||
} else if (table.equals(SmsDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count);
|
||||
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
|
||||
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count);
|
||||
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
|
||||
@@ -283,11 +314,15 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean isNonExpiringMessage(@NonNull Cursor cursor) {
|
||||
private static boolean isNonExpiringMmsMessage(@NonNull Cursor cursor) {
|
||||
return cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0 &&
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) <= 0;
|
||||
}
|
||||
|
||||
private static boolean isNonExpiringSmsMessage(@NonNull Cursor cursor) {
|
||||
return cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0;
|
||||
}
|
||||
|
||||
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long mmsId) {
|
||||
String[] columns = new String[] { MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE};
|
||||
String where = MmsDatabase.ID + " = ?";
|
||||
@@ -316,7 +351,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
private byte[] iv;
|
||||
private int counter;
|
||||
|
||||
private BackupFrameOutputStream(@NonNull File output, @NonNull String passphrase) throws IOException {
|
||||
private BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String passphrase) throws IOException {
|
||||
try {
|
||||
byte[] salt = Util.getSecretBytes(32);
|
||||
byte[] key = getBackupKey(passphrase, salt);
|
||||
@@ -328,7 +363,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
|
||||
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
this.mac = Mac.getInstance("HmacSHA256");
|
||||
this.outputStream = new FileOutputStream(output);
|
||||
this.outputStream = output;
|
||||
this.iv = Util.getSecretBytes(16);
|
||||
this.counter = Conversions.byteArrayToInt(iv);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user