Compare commits
926 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c8a99c79c | ||
|
|
c6b9855198 | ||
|
|
c142928fad | ||
|
|
fc0cfd5188 | ||
|
|
d9c78e5c3e | ||
|
|
b449fceca0 | ||
|
|
f0d15c0bce | ||
|
|
8f031f61ea | ||
|
|
502e8559f0 | ||
|
|
8cf6f7e936 | ||
|
|
0ef01cc620 | ||
|
|
930828ef86 | ||
|
|
9ebe920195 | ||
|
|
0df36047e7 | ||
|
|
94604921f9 | ||
|
|
284fe294ac | ||
|
|
ffa01d491a | ||
|
|
1d9513e743 | ||
|
|
277c9e22f1 | ||
|
|
4e7b4da941 | ||
|
|
7e72c9c33b | ||
|
|
e0e2c3a3f5 | ||
|
|
3b5e444e76 | ||
|
|
02006e3ff5 | ||
|
|
8083596f19 | ||
|
|
6551689a0c | ||
|
|
3fbf21a34e | ||
|
|
b598431237 | ||
|
|
3b5d9a2cae | ||
|
|
3bd8aa8a86 | ||
|
|
fe5fca8eaf | ||
|
|
848101a783 | ||
|
|
d7c350f987 | ||
|
|
4c526f0b3c | ||
|
|
876ffb5b13 | ||
|
|
0b14cf3d6a | ||
|
|
c2044b36b1 | ||
|
|
f970b4acfa | ||
|
|
e69c0af613 | ||
|
|
42e2dd8b30 | ||
|
|
4fd6e7b033 | ||
|
|
3580816eac | ||
|
|
f1c2e5f194 | ||
|
|
b6d59f1d46 | ||
|
|
a3521681e7 | ||
|
|
ca10e1136c | ||
|
|
282fb2af0e | ||
|
|
1f5a597d50 | ||
|
|
a32666817c | ||
|
|
b9bd3f2b4c | ||
|
|
4173efbe5a | ||
|
|
8121c8bd41 | ||
|
|
6ca8218d55 | ||
|
|
653afbdd46 | ||
|
|
4453d1752f | ||
|
|
5782c8a58b | ||
|
|
3e041befc8 | ||
|
|
6d3847c26f | ||
|
|
4bdfc37bc1 | ||
|
|
28afe0d829 | ||
|
|
04dddd3378 | ||
|
|
23a14583fe | ||
|
|
19c83de510 | ||
|
|
e94d4b64cf | ||
|
|
7380c99f3a | ||
|
|
2961a372c3 | ||
|
|
5e2a4fb058 | ||
|
|
a16242b9f8 | ||
|
|
acfba9ac96 | ||
|
|
006343460e | ||
|
|
1cc8634cc7 | ||
|
|
9f8a110428 | ||
|
|
07b4279d0b | ||
|
|
6a33b231e3 | ||
|
|
b38d02061d | ||
|
|
f832a36a5e | ||
|
|
911ca7c29d | ||
|
|
544b75a2a7 | ||
|
|
56e8d4fb06 | ||
|
|
36a2278aef | ||
|
|
0c785b85b8 | ||
|
|
a0ecba147e | ||
|
|
977591ac82 | ||
|
|
fe1838d3fe | ||
|
|
ede06cf97d | ||
|
|
93deee6824 | ||
|
|
db19077834 | ||
|
|
6f91f62db2 | ||
|
|
1fc63b7597 | ||
|
|
a079e479ec | ||
|
|
e90705b459 | ||
|
|
244db437cb | ||
|
|
6558eae032 | ||
|
|
88b54a262b | ||
|
|
4fa8b8a4bd | ||
|
|
cc0ced9a81 | ||
|
|
52447f5e97 | ||
|
|
bceb69b284 | ||
|
|
a8d826020d | ||
|
|
2ada7f87f2 | ||
|
|
7f8ca58762 | ||
|
|
058c25808b | ||
|
|
c1e6b6b086 | ||
|
|
b0a6bb79f6 | ||
|
|
de52bf50a2 | ||
|
|
207dd23c86 | ||
|
|
7e0de29dd7 | ||
|
|
eaa8f1ee8f | ||
|
|
fb494c1151 | ||
|
|
49ecd9ef5d | ||
|
|
8772214fd4 | ||
|
|
5b74d0cbeb | ||
|
|
1e7c93007d | ||
|
|
608815a69b | ||
|
|
fb49efa34d | ||
|
|
b20e8616ec | ||
|
|
acf78b6b63 | ||
|
|
d68fe928b8 | ||
|
|
db4d561d9e | ||
|
|
6933ca50a7 | ||
|
|
88c0e6f8ab | ||
|
|
6cd4728e3c | ||
|
|
97cc82837c | ||
|
|
5f0b0e8495 | ||
|
|
c837d590ab | ||
|
|
6108a32631 | ||
|
|
6d78e1760d | ||
|
|
c7b7242eff | ||
|
|
77c687efcf | ||
|
|
274af2f010 | ||
|
|
6cd5100530 | ||
|
|
115a408b0b | ||
|
|
d983016137 | ||
|
|
a8309b6b2b | ||
|
|
e241589c92 | ||
|
|
dcb6240a6a | ||
|
|
599bb5ab0f | ||
|
|
739fe584c6 | ||
|
|
920d1c6207 | ||
|
|
c1d3d26a15 | ||
|
|
e3b66dc7e8 | ||
|
|
dfa08d1356 | ||
|
|
6da734c2f9 | ||
|
|
a9f3141a77 | ||
|
|
ee267d4702 | ||
|
|
c39174ed8a | ||
|
|
9c872b6da6 | ||
|
|
2924a09936 | ||
|
|
b9da012cc4 | ||
|
|
8626d41787 | ||
|
|
c58705722a | ||
|
|
c702ff676a | ||
|
|
995569dd50 | ||
|
|
a0dfd42527 | ||
|
|
a7dd78cce6 | ||
|
|
e2e9cd40b3 | ||
|
|
e4fc6f41b8 | ||
|
|
778b2a0d27 | ||
|
|
64cd15ca0b | ||
|
|
1e46c3c0ba | ||
|
|
91772b4e11 | ||
|
|
af5c7cb7ca | ||
|
|
e760474fa9 | ||
|
|
28bb88e347 | ||
|
|
17a1fe97ca | ||
|
|
7348237862 | ||
|
|
4e66db5dc1 | ||
|
|
ffa6c9acd1 | ||
|
|
b3e66a9259 | ||
|
|
5c0cf8c1f0 | ||
|
|
89c716fca4 | ||
|
|
aae9830dfa | ||
|
|
48c72697c1 | ||
|
|
aac7e9ea53 | ||
|
|
a1b10b3222 | ||
|
|
2ab8db33e3 | ||
|
|
2b1386232f | ||
|
|
7bb1caa22e | ||
|
|
72c14b8651 | ||
|
|
f85c3bb1e6 | ||
|
|
d8714fe3b4 | ||
|
|
c54c1907b6 | ||
|
|
55257155dd | ||
|
|
55a8bd86de | ||
|
|
c60909272b | ||
|
|
3dcc2d8171 | ||
|
|
0ccbb22e4c | ||
|
|
0ffa10eaea | ||
|
|
9824073023 | ||
|
|
d2adc11a2d | ||
|
|
4d0b870c88 | ||
|
|
840997cb7d | ||
|
|
e4406621ed | ||
|
|
7f96ee0b62 | ||
|
|
17f850b31a | ||
|
|
5d87ad0301 | ||
|
|
a33eeed6d3 | ||
|
|
554ce29337 | ||
|
|
6054d0b36f | ||
|
|
8ba15cf3b4 | ||
|
|
b15e5b4867 | ||
|
|
544511905a | ||
|
|
02e3f511bd | ||
|
|
0539b071e7 | ||
|
|
36e400650c | ||
|
|
d8930daf4b | ||
|
|
17b5816d5b | ||
|
|
e574c81ea2 | ||
|
|
6799a2f841 | ||
|
|
61fb20f412 | ||
|
|
47f648e7bc | ||
|
|
064c0ddb82 | ||
|
|
b42c42007d | ||
|
|
807cdfce2e | ||
|
|
43dc3aeebd | ||
|
|
0369c5ee16 | ||
|
|
3df2390fe4 | ||
|
|
9ec4a7af0f | ||
|
|
70636fb4a7 | ||
|
|
3c4efdd8f9 | ||
|
|
80deb301e5 | ||
|
|
5eabfdc34c | ||
|
|
5e96832666 | ||
|
|
1ccce24cf8 | ||
|
|
d59e4f2da7 | ||
|
|
a254c1a7b2 | ||
|
|
82cc610938 | ||
|
|
becf7c40e8 | ||
|
|
ed21c3ca03 | ||
|
|
631005e565 | ||
|
|
b57fab8c75 | ||
|
|
4260a8436b | ||
|
|
23d478191f | ||
|
|
d075a33d4e | ||
|
|
7507dadbe7 | ||
|
|
bfd0363390 | ||
|
|
cfb22825f4 | ||
|
|
d37fafbfe7 | ||
|
|
2dc2eb5835 | ||
|
|
019d036f69 | ||
|
|
f1ca5fc8e2 | ||
|
|
9089fc4001 | ||
|
|
b281b817ba | ||
|
|
cee6736656 | ||
|
|
350ca059b9 | ||
|
|
a7cc5bdc5e | ||
|
|
2893dfff60 | ||
|
|
1c14f06734 | ||
|
|
ff053437e3 | ||
|
|
03dc220cea | ||
|
|
097f97b5e4 | ||
|
|
95d3db3260 | ||
|
|
0485ae603b | ||
|
|
2f93da6f1a | ||
|
|
78f3e28974 | ||
|
|
fecd8300c3 | ||
|
|
262f90dbe7 | ||
|
|
ed271c6f3e | ||
|
|
3fa5843c93 | ||
|
|
7f544cb3e5 | ||
|
|
9f4d40a364 | ||
|
|
09f0c5b63f | ||
|
|
53e0dc5dee | ||
|
|
8b06051e7c | ||
|
|
660ec88202 | ||
|
|
b9057a1c11 | ||
|
|
1f85b1f3d2 | ||
|
|
71f78f03f4 | ||
|
|
4b3d129097 | ||
|
|
daeb823399 | ||
|
|
c7f76c5d1c | ||
|
|
9ba1391a1e | ||
|
|
5d137465e8 | ||
|
|
78cbb3c073 | ||
|
|
097d95428a | ||
|
|
2cfb6ede7f | ||
|
|
695663a5b1 | ||
|
|
a2204c8370 | ||
|
|
8c4757ea02 | ||
|
|
85821274fa | ||
|
|
a8b1ec8e52 | ||
|
|
adbdaebd69 | ||
|
|
c2da4fcd7d | ||
|
|
46ebff3659 | ||
|
|
452ccd0350 | ||
|
|
6e6c809690 | ||
|
|
7b5c1904cf | ||
|
|
c0aa9d7587 | ||
|
|
5d03e3d516 | ||
|
|
2bc3a4417f | ||
|
|
59d03cbeb2 | ||
|
|
f9f9ae68f5 | ||
|
|
88f2b67984 | ||
|
|
44dba4bd98 | ||
|
|
ef585eba42 | ||
|
|
42967dab13 | ||
|
|
4f75d1a5db | ||
|
|
debacbdf58 | ||
|
|
12c4283c30 | ||
|
|
096c9e0ff7 | ||
|
|
2f23a13a6f | ||
|
|
bdadbaaac4 | ||
|
|
16f41555ba | ||
|
|
8c037456e7 | ||
|
|
feccee557e | ||
|
|
67f5605bc4 | ||
|
|
ccb18cd46c | ||
|
|
5b1d91016c | ||
|
|
27ab04ab41 | ||
|
|
9432a45b39 | ||
|
|
9e3475ed94 | ||
|
|
86d088bce2 | ||
|
|
a135e7efa2 | ||
|
|
c247935f1a | ||
|
|
31657f5c15 | ||
|
|
bcecc30d33 | ||
|
|
7193252d77 | ||
|
|
5b682a3a3d | ||
|
|
6b8659a393 | ||
|
|
14557d3dc1 | ||
|
|
03cbee0277 | ||
|
|
adc0907906 | ||
|
|
f14a71a076 | ||
|
|
12c2b53f7c | ||
|
|
ff60b5b731 | ||
|
|
43954a176a | ||
|
|
d698d3bd6f | ||
|
|
50a81c0e60 | ||
|
|
b0b8377a8e | ||
|
|
7d02bb8487 | ||
|
|
8449d75684 | ||
|
|
b89163bb14 | ||
|
|
b7ce220600 | ||
|
|
948079a42e | ||
|
|
de1c6cdd0c | ||
|
|
37147fb0a5 | ||
|
|
33334f80c3 | ||
|
|
36286da9bd | ||
|
|
2248acb9f2 | ||
|
|
3632a2cc95 | ||
|
|
9447ea12cb | ||
|
|
89c2329fdf | ||
|
|
84012c7adb | ||
|
|
d7395af774 | ||
|
|
be4daff0f3 | ||
|
|
c2459d0a31 | ||
|
|
5f993ed0f7 | ||
|
|
acea24c19c | ||
|
|
82b5d58d04 | ||
|
|
af2990fa08 | ||
|
|
0fa48540e1 | ||
|
|
0f06b96832 | ||
|
|
4f423be6f9 | ||
|
|
bea21ed5ff | ||
|
|
a9cff032f5 | ||
|
|
b2d02d4ace | ||
|
|
066df77abf | ||
|
|
b38a3e6259 | ||
|
|
d78919acf8 | ||
|
|
af96d11188 | ||
|
|
5e1bef26ed | ||
|
|
95333eccd4 | ||
|
|
89e075c56e | ||
|
|
d026498a8c | ||
|
|
bae381e6f8 | ||
|
|
050f7a24c9 | ||
|
|
77ad1ea729 | ||
|
|
bf667c0cfc | ||
|
|
ccb8ef98b4 | ||
|
|
a1f0660339 | ||
|
|
447236ee38 | ||
|
|
ebf3b0dfe1 | ||
|
|
ee9216df8a | ||
|
|
3e34668232 | ||
|
|
8f8f41f184 | ||
|
|
4f7cba8d7c | ||
|
|
00b719c783 | ||
|
|
b03eccec33 | ||
|
|
5805539deb | ||
|
|
1b48fd07a3 | ||
|
|
1e375ec494 | ||
|
|
ee9acf2687 | ||
|
|
1d8e85fcad | ||
|
|
4e2afa7362 | ||
|
|
d06564c7b9 | ||
|
|
ced48d0788 | ||
|
|
e273593343 | ||
|
|
c5767b07a7 | ||
|
|
709ce7e9de | ||
|
|
a8f7908ed4 | ||
|
|
8230786638 | ||
|
|
df4ecc4e32 | ||
|
|
5b755b9501 | ||
|
|
b3c7c8db5c | ||
|
|
a21c537428 | ||
|
|
6d339cd023 | ||
|
|
840c493265 | ||
|
|
14999800e2 | ||
|
|
8e332d0798 | ||
|
|
cd007a20d7 | ||
|
|
8b99af3eef | ||
|
|
d354de806e | ||
|
|
c7ef0c06f8 | ||
|
|
9d14bcb808 | ||
|
|
de03706d8d | ||
|
|
a3caabcafd | ||
|
|
faafa40122 | ||
|
|
d1a6582ad7 | ||
|
|
f81c0b448e | ||
|
|
70347e754c | ||
|
|
a1245baf61 | ||
|
|
007ea43dc8 | ||
|
|
582028f2c2 | ||
|
|
0e2d52026e | ||
|
|
233cc7ecce | ||
|
|
1baf03f51a | ||
|
|
f066a9cab2 | ||
|
|
825d1a02ab | ||
|
|
97cc8b7777 | ||
|
|
72662b5b52 | ||
|
|
ea4e0b6d6f | ||
|
|
6df93c0bb5 | ||
|
|
23a0bb3ce0 | ||
|
|
4cd5256267 | ||
|
|
5362e07a5c | ||
|
|
936bd327bd | ||
|
|
db89619a4a | ||
|
|
b5ce7325fc | ||
|
|
ccaeb089ab | ||
|
|
3240ba3a55 | ||
|
|
bb7be66efe | ||
|
|
8814a0d949 | ||
|
|
9aa488223f | ||
|
|
30d9233365 | ||
|
|
3ac540c687 | ||
|
|
8d561ead21 | ||
|
|
15b650382e | ||
|
|
110a40592b | ||
|
|
d0ce4ff032 | ||
|
|
85c9a9050a | ||
|
|
af42d5b671 | ||
|
|
9580bb0a38 | ||
|
|
9abb167874 | ||
|
|
cd13676a21 | ||
|
|
1dd59bee36 | ||
|
|
59bcbe592b | ||
|
|
0917e17c69 | ||
|
|
eb249ca69a | ||
|
|
97d1175915 | ||
|
|
2141f1073e | ||
|
|
c6287547a3 | ||
|
|
9257c6ddf3 | ||
|
|
480748e1aa | ||
|
|
c015687951 | ||
|
|
1bd1e9cc65 | ||
|
|
c4a4374465 | ||
|
|
90681d47f8 | ||
|
|
936be693ce | ||
|
|
a32b875587 | ||
|
|
4e6798e38e | ||
|
|
6352f7baf4 | ||
|
|
aac9725adc | ||
|
|
900371bb30 | ||
|
|
a58f564d1e | ||
|
|
942154a61f | ||
|
|
7f0a7b0c13 | ||
|
|
37bcac40bb | ||
|
|
02ea99254a | ||
|
|
3849b46f0a | ||
|
|
116bd41c63 | ||
|
|
457ad4c607 | ||
|
|
d0a9bd4c6d | ||
|
|
d3bed549f2 | ||
|
|
fe1aa016b9 | ||
|
|
af55cb0c03 | ||
|
|
714eaa62a8 | ||
|
|
5038f49487 | ||
|
|
57835dc8f1 | ||
|
|
3439eb4536 | ||
|
|
929ee04814 | ||
|
|
9d98a779a8 | ||
|
|
5a23ddeaf4 | ||
|
|
7ae5159194 | ||
|
|
bdf93af3db | ||
|
|
dcc147d994 | ||
|
|
19b2658414 | ||
|
|
1c4833f3b4 | ||
|
|
e8ca673bf8 | ||
|
|
5a614faee1 | ||
|
|
af8042c5f4 | ||
|
|
a46e7541d0 | ||
|
|
a6890fc8dd | ||
|
|
0c0d7aeead | ||
|
|
874697f6e5 | ||
|
|
e0d1987445 | ||
|
|
17400020b7 | ||
|
|
e8e80e5d05 | ||
|
|
f9946083dd | ||
|
|
453f93a84f | ||
|
|
a8c47b5091 | ||
|
|
78a818eba6 | ||
|
|
4ca90374b9 | ||
|
|
a5fbcffa14 | ||
|
|
a21ec2f166 | ||
|
|
cdfb88ea18 | ||
|
|
1ec45fe364 | ||
|
|
60b5c82da8 | ||
|
|
f0af5743c4 | ||
|
|
81930a6833 | ||
|
|
278ee79df0 | ||
|
|
7f2a758400 | ||
|
|
fc1c092cf0 | ||
|
|
f42a8cf962 | ||
|
|
27db9d06e4 | ||
|
|
3d5cfb3c74 | ||
|
|
df9186827c | ||
|
|
e0137706b2 | ||
|
|
99bcda8709 | ||
|
|
eddff07eb8 | ||
|
|
4e859a84ce | ||
|
|
8665dad867 | ||
|
|
c0996ed116 | ||
|
|
830e651fef | ||
|
|
acb48752ce | ||
|
|
ba8597900a | ||
|
|
c1f0253aa3 | ||
|
|
d70d82c5ea | ||
|
|
29b9d3f902 | ||
|
|
1a85a9cb31 | ||
|
|
ead6e6b2f3 | ||
|
|
03b1eb4bd5 | ||
|
|
5c870ca8ea | ||
|
|
965de16de1 | ||
|
|
a210ef3136 | ||
|
|
beaa86389d | ||
|
|
4fbb87b5b7 | ||
|
|
76d1382d9a | ||
|
|
79a142c1be | ||
|
|
dd66e22443 | ||
|
|
c77809fa90 | ||
|
|
9f7bb69341 | ||
|
|
b58faf4fd1 | ||
|
|
dbeb2b5330 | ||
|
|
9574a19ec2 | ||
|
|
73bb7873e1 | ||
|
|
475c54213d | ||
|
|
8d6f1341f1 | ||
|
|
80d0ba31ca | ||
|
|
c7bfede74c | ||
|
|
c902d17f98 | ||
|
|
6c31d656dd | ||
|
|
34e8d5ac57 | ||
|
|
d636f37132 | ||
|
|
150a21bfa3 | ||
|
|
5b61c8ac18 | ||
|
|
d72d4c4c41 | ||
|
|
5a1464c069 | ||
|
|
c2ec09f079 | ||
|
|
8a8817f8d3 | ||
|
|
a5368b7ea9 | ||
|
|
d8a75d599d | ||
|
|
f92e2bae4a | ||
|
|
19d15cb3e5 | ||
|
|
b66e0e7e32 | ||
|
|
4a1f39f4be | ||
|
|
a17d3e1b47 | ||
|
|
f580128366 | ||
|
|
18252712a5 | ||
|
|
ed5f5adc9b | ||
|
|
4c30aa9f13 | ||
|
|
3cdf17ccaa | ||
|
|
a8dbfd812d | ||
|
|
ff3890cc12 | ||
|
|
9c196bd2d5 | ||
|
|
4508aa7c35 | ||
|
|
f15a629731 | ||
|
|
857fda42c8 | ||
|
|
a5eb823a17 | ||
|
|
31435128f4 | ||
|
|
6656077edc | ||
|
|
9974f6edf1 | ||
|
|
6715a89a25 | ||
|
|
23b4a9b191 | ||
|
|
666cdeae6b | ||
|
|
5d7ac81c4b | ||
|
|
93856ed8cf | ||
|
|
84fd1a9140 | ||
|
|
9949e5e3a5 | ||
|
|
c0a44c7fc3 | ||
|
|
d9df1ec39e | ||
|
|
abcd599ad8 | ||
|
|
8a3d0dde91 | ||
|
|
0f3de6c979 | ||
|
|
c089d6cd43 | ||
|
|
cfcb9a8cdb | ||
|
|
83479d11b7 | ||
|
|
73b8f11b5a | ||
|
|
4593014d00 | ||
|
|
2cf5d57454 | ||
|
|
048d859881 | ||
|
|
1df28c6564 | ||
|
|
f59e937006 | ||
|
|
5948b46ac7 | ||
|
|
75b232bfdc | ||
|
|
9e6594cc0b | ||
|
|
7f85b61e89 | ||
|
|
1a32bc8232 | ||
|
|
e603162ee7 | ||
|
|
08eca5d844 | ||
|
|
1d1dbcf9cd | ||
|
|
e6b107fa78 | ||
|
|
284cca3e25 | ||
|
|
1e0b0d926a | ||
|
|
cb86be578b | ||
|
|
b8bb2b78bd | ||
|
|
6fceb25121 | ||
|
|
0484047b4e | ||
|
|
b9a10653f1 | ||
|
|
42a5504f0d | ||
|
|
ae784db80d | ||
|
|
f5cbf64ccf | ||
|
|
cecf16c595 | ||
|
|
fb4c9d3bf1 | ||
|
|
b5aa46bb67 | ||
|
|
2dc68ed053 | ||
|
|
58d818923d | ||
|
|
f4a6cd9c68 | ||
|
|
ed04535537 | ||
|
|
7d4de3d4cb | ||
|
|
bc7cc306cb | ||
|
|
5fa07e7094 | ||
|
|
2b069fa63f | ||
|
|
99848f98d3 | ||
|
|
967e9dd9a7 | ||
|
|
4a720289e2 | ||
|
|
86d58e97cf | ||
|
|
bc6b7d15f1 | ||
|
|
37f3b1f1ba | ||
|
|
262012887c | ||
|
|
d2cb6098fc | ||
|
|
1973fbf376 | ||
|
|
bd63b9bec9 | ||
|
|
faa8c78f8d | ||
|
|
923016f12c | ||
|
|
f469ce049d | ||
|
|
922f6d89e9 | ||
|
|
4741a76f37 | ||
|
|
d197c57c55 | ||
|
|
4ee60bf867 | ||
|
|
4351578838 | ||
|
|
56c17e32f1 | ||
|
|
48698381fc | ||
|
|
5ad02f724c | ||
|
|
132c81b142 | ||
|
|
77e3cc40e0 | ||
|
|
2a644437fb | ||
|
|
d5fffb0132 | ||
|
|
c8c152fe60 | ||
|
|
350d1f47d3 | ||
|
|
2ae42cb095 | ||
|
|
dae0d30367 | ||
|
|
e5f70bdbda | ||
|
|
156fe37a60 | ||
|
|
56848fb83d | ||
|
|
3cba8ab58a | ||
|
|
88dac70087 | ||
|
|
9445555d66 | ||
|
|
7db1588578 | ||
|
|
0a7970ad0c | ||
|
|
10ad3fbf82 | ||
|
|
95858898d7 | ||
|
|
16c8cc88d7 | ||
|
|
c0c051bb66 | ||
|
|
bd0d1e842f | ||
|
|
7f0c998b24 | ||
|
|
5a4c2fc7b0 | ||
|
|
456ba5fa02 | ||
|
|
9de420fde6 | ||
|
|
401e3687de | ||
|
|
6777b3e0e6 | ||
|
|
b5d37702f9 | ||
|
|
320ea9eb4e | ||
|
|
86d8cde9b4 | ||
|
|
bf759711ef | ||
|
|
068ffc2167 | ||
|
|
95304fe001 | ||
|
|
2de64fca02 | ||
|
|
3211dd2a8f | ||
|
|
b6dc25a368 | ||
|
|
4e64242883 | ||
|
|
fcd3b501eb | ||
|
|
62ed098687 | ||
|
|
2a93ddfb99 | ||
|
|
387392f38b | ||
|
|
5b298b4a04 | ||
|
|
cb78684282 | ||
|
|
67704612df | ||
|
|
f3c8b51520 | ||
|
|
b1057d63a1 | ||
|
|
2ccdf0e396 | ||
|
|
93e6ccb9e4 | ||
|
|
196ef60a82 | ||
|
|
478e5667b4 | ||
|
|
06ea000f42 | ||
|
|
d1b8e77fdc | ||
|
|
8cf2654c5b | ||
|
|
18531146f7 | ||
|
|
c274c1bb28 | ||
|
|
42a8522e98 | ||
|
|
960e165c7d | ||
|
|
eab23a9e66 | ||
|
|
c7b626082c | ||
|
|
59f362495a | ||
|
|
6c44437c6f | ||
|
|
fed8ae68e9 | ||
|
|
934a2a67bc | ||
|
|
05345b8582 | ||
|
|
cef5de2be4 | ||
|
|
7b4299d5da | ||
|
|
ec20b0e0e3 | ||
|
|
ff1531b836 | ||
|
|
1675c8a79a | ||
|
|
bb90987e7c | ||
|
|
ecea6abeb6 | ||
|
|
4a2f3136c6 | ||
|
|
d12b02fac5 | ||
|
|
1e564b6ad1 | ||
|
|
c77daa8226 | ||
|
|
fa35814344 | ||
|
|
48efcaa785 | ||
|
|
f3f6cc87d9 | ||
|
|
29cdb5290b | ||
|
|
77524ae1f2 | ||
|
|
30ba9d7e27 | ||
|
|
9652fd2844 | ||
|
|
d2ece1c1f2 | ||
|
|
8bdc257963 | ||
|
|
88f9ec313f | ||
|
|
8ad5126408 | ||
|
|
1e27847015 | ||
|
|
e67eca77ff | ||
|
|
ba46a9d81a | ||
|
|
49cccc6927 | ||
|
|
5977e9141d | ||
|
|
c0982293bf | ||
|
|
ae6ef62160 | ||
|
|
d95b08d4fd | ||
|
|
128da6db04 | ||
|
|
2701607810 | ||
|
|
4055fe183b | ||
|
|
1c47812877 | ||
|
|
060bed8559 | ||
|
|
4a3c173adb | ||
|
|
8cf3ba424a | ||
|
|
9c40de5bf1 | ||
|
|
11a2ed0743 | ||
|
|
01a9931d92 | ||
|
|
38bcc6c293 | ||
|
|
bceb9b4972 | ||
|
|
ecdc285378 | ||
|
|
6d111e5f68 | ||
|
|
9aed2343c1 | ||
|
|
733d54e339 | ||
|
|
267bc32e23 | ||
|
|
7acb4973d8 | ||
|
|
39ba8c2ad3 | ||
|
|
621ac62c7e | ||
|
|
652306edd0 | ||
|
|
b9b4dccff4 | ||
|
|
ce6d2d9c69 | ||
|
|
e842f78457 | ||
|
|
cab3657ab0 | ||
|
|
a7aa980e58 | ||
|
|
6a0a419f0c | ||
|
|
94e8303022 | ||
|
|
ccfcfa71df | ||
|
|
cfdcd61e51 | ||
|
|
b89c20ff40 | ||
|
|
8caaf057e8 | ||
|
|
a52c295a38 | ||
|
|
8aa185070b | ||
|
|
de60d4d37f | ||
|
|
6e5658431b | ||
|
|
6df5457305 | ||
|
|
fd50b38630 | ||
|
|
d41b24f9ae | ||
|
|
aa5e32f0ee | ||
|
|
749d096931 | ||
|
|
8e86c7d81a | ||
|
|
148cfd1b53 | ||
|
|
93c1277fd0 | ||
|
|
23e069ffa8 | ||
|
|
6e7fab40ac | ||
|
|
8a7cac7c03 | ||
|
|
932e7b4af5 | ||
|
|
2f8a7fa296 | ||
|
|
5e6f71cd32 | ||
|
|
ce0058864f | ||
|
|
5a8753de85 | ||
|
|
c646316a97 | ||
|
|
6df8988f54 | ||
|
|
5b534c8b1a | ||
|
|
ab2e85f6c7 | ||
|
|
975a121c55 | ||
|
|
64cf032181 | ||
|
|
d8a56be5e8 | ||
|
|
286b64274c | ||
|
|
0cb2404735 | ||
|
|
0a8bbf14a6 | ||
|
|
eb1dd58a0b | ||
|
|
a79df7d815 | ||
|
|
e0c11998c3 | ||
|
|
de72eceecf | ||
|
|
c46e53ab24 | ||
|
|
2c28fa6a57 | ||
|
|
f010a3ec0d | ||
|
|
e390fb4fc5 | ||
|
|
a4ce77cbcc | ||
|
|
18613e3b6f | ||
|
|
278fdebf43 | ||
|
|
a122bb4899 | ||
|
|
22ed8caed3 | ||
|
|
8b6ecd1d2e | ||
|
|
f2703f0b7b | ||
|
|
1efb92b913 | ||
|
|
5ccf84f7a2 | ||
|
|
c3368f6de6 | ||
|
|
77e971cb9b | ||
|
|
003fa1b059 | ||
|
|
03aa9e9712 | ||
|
|
55699e27bc | ||
|
|
bf28e109d3 | ||
|
|
6b476876d9 | ||
|
|
fd862e575b | ||
|
|
7fd6f5b3ff | ||
|
|
42e94d8f92 | ||
|
|
b572fce658 | ||
|
|
276e867f9a | ||
|
|
b2d4608cdb | ||
|
|
9d21c36ddf | ||
|
|
983290aa5b | ||
|
|
88b9fc25d2 | ||
|
|
60c7fb0056 | ||
|
|
fa6da1902f | ||
|
|
5cc3ac00c7 | ||
|
|
33daa21ad9 | ||
|
|
c4d1bdc44d | ||
|
|
ca99c732f8 | ||
|
|
1f79808cf0 | ||
|
|
5c0e1100ed | ||
|
|
d0b763c16e | ||
|
|
b962751c96 | ||
|
|
94e8553b73 | ||
|
|
351b625975 | ||
|
|
a2b6dbda14 | ||
|
|
a6564f8f84 | ||
|
|
4dbe165c18 | ||
|
|
f29a42411e | ||
|
|
02b0800b22 | ||
|
|
2cfa431cad | ||
|
|
fe4068afce | ||
|
|
1c23603c25 | ||
|
|
c2a86fcc74 | ||
|
|
d42c9b5dbc | ||
|
|
3b6429c163 | ||
|
|
6896f8ea15 | ||
|
|
a3768c7d74 | ||
|
|
c9a0a66f18 | ||
|
|
db1ad39c6b | ||
|
|
9f04c28bfd | ||
|
|
10631d7e71 | ||
|
|
cfff10622a | ||
|
|
b769c7d9b6 | ||
|
|
1e0f691a56 | ||
|
|
f0852d1d39 | ||
|
|
1ee422a012 | ||
|
|
ca87820dd5 | ||
|
|
45ddb7e1ad | ||
|
|
fd46777f04 | ||
|
|
5bb36c15d5 | ||
|
|
c5571e8a8d | ||
|
|
b8ab1bc3b2 | ||
|
|
3683e6a9e2 | ||
|
|
c364345e1d | ||
|
|
7da73bbc30 | ||
|
|
177322eca4 | ||
|
|
506491d13d | ||
|
|
e884911b60 | ||
|
|
7b20fca1ac | ||
|
|
e2c2e59442 | ||
|
|
be66db898c | ||
|
|
5d9d6ac12b | ||
|
|
c6d3bed8da | ||
|
|
e74c429695 | ||
|
|
90a37852cc | ||
|
|
1763be2956 | ||
|
|
9797c54a4d | ||
|
|
c5114e2cb3 | ||
|
|
e3b22dabce | ||
|
|
ba67796992 | ||
|
|
d482c60a98 | ||
|
|
ebe8d38a91 | ||
|
|
c76081d99c | ||
|
|
bef9beff16 | ||
|
|
25e82ff5e4 | ||
|
|
13ffba1c99 | ||
|
|
a0c1446e9f | ||
|
|
657b520908 | ||
|
|
51e8e8c2c8 | ||
|
|
1e534a2a10 | ||
|
|
9fe30524b2 | ||
|
|
e6b4249cf3 | ||
|
|
57ef0e9024 | ||
|
|
9ed290b8f6 | ||
|
|
d2be554e1b | ||
|
|
e7a807ab5b | ||
|
|
9ff8f8587b | ||
|
|
a6f31c60bd | ||
|
|
4643dea2ad | ||
|
|
f70bf9c5bd | ||
|
|
557e6a800f | ||
|
|
17a391d3cf | ||
|
|
fc989f3820 | ||
|
|
2be382afab | ||
|
|
b08f81a8dc | ||
|
|
535e00c6d0 |
17
.github/workflows/android.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Android CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew qa
|
||||
2
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.classpath
|
||||
captures/
|
||||
project.properties
|
||||
.project
|
||||
.settings
|
||||
@@ -8,7 +9,6 @@ gen/
|
||||
*.iml
|
||||
out
|
||||
tests
|
||||
lint.xml
|
||||
local.properties
|
||||
ant.properties
|
||||
.DS_Store
|
||||
|
||||
10
.tx/config
@@ -1,10 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = da_DK:da-rDK,he:iw,hi_IN:hi-rIN,id:in,kn_IN:kn-rIN,pt_BR:pt-rBR,pt_PT:pt,qu_EC:qu-rEC,sv_SE:sv-rSE,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW
|
||||
|
||||
[signal-android.master]
|
||||
file_filter = res/values-<lang>/strings.xml
|
||||
source_file = res/values/strings.xml
|
||||
source_lang = en
|
||||
type = ANDROID
|
||||
|
||||
@@ -12,7 +12,7 @@ RUN dpkg --add-architecture i386 && \
|
||||
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 27.0.3
|
||||
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/ && \
|
||||
|
||||
@@ -62,7 +62,7 @@ The form and manner of this distribution makes it eligible for export under the
|
||||
|
||||
Copyright 2011 Whisper Systems
|
||||
|
||||
Copyright 2013-2017 Open Whisper Systems
|
||||
Copyright 2013-2020 Open Whisper Systems
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
||||
287
ReproducibleBuilds.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Reproducible Builds
|
||||
|
||||
|
||||
## TL;DR
|
||||
|
||||
You can just use these [instructions](https://signal.org/blog/reproducible-android/) from the official announcement at Open Whisper Systems's blog:
|
||||
```
|
||||
# Clone the Signal Android source repository
|
||||
$ git clone https://github.com/signalapp/Signal-Android.git && cd Signal-Android
|
||||
|
||||
# Check out the release tag for the version you'd like to compare
|
||||
$ git checkout v[the version number]
|
||||
|
||||
# Build using the Docker environment
|
||||
$ docker run --rm -v $(pwd):/project -w /project whispersystems/signal-android:1.3 ./gradlew clean assembleRelease
|
||||
|
||||
# Verify the APKs
|
||||
$ python3 apkdiff/apkdiff.py build/outputs/apks/project-release-unsigned.apk path/to/SignalFromPlay.apk
|
||||
```
|
||||
|
||||
Note that the instructions above use a pre-built Signal Docker image from [Docker Hub](https://hub.docker.com/u/whispersystems/). If you wish to compile the image yourself, continue reading the longer version below.
|
||||
|
||||
|
||||
***
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
Since version 3.15.0 Signal for Android has supported reproducible builds. This is achieved by replicating the build environment as a Docker image. You'll need to build the image, run a container instance of it, compile Signal inside the container and finally compare the resulted APK to the APK that is distributed in the Google Play Store.
|
||||
|
||||
The command line parts in this guide are written for Linux but with some little modifications you can adapt them to macOS (OS X) and Windows. In the following sections we will use `3.15.2` as an example Signal version. You'll just need to replace all occurrences of `3.15.2` with the version number you are about to verify.
|
||||
|
||||
|
||||
## Setting up directories
|
||||
|
||||
First let's create a new directory for this whole reproducible builds project. In your home folder (`~`), create a new directory called `reproducible-signal`.
|
||||
```
|
||||
user@host:$ mkdir ~/reproducible-signal
|
||||
```
|
||||
|
||||
Next create another directory inside `reproducible-signal` called `apk-from-google-play-store`.
|
||||
```
|
||||
user@host:$ mkdir ~/reproducible-signal/apk-from-google-play-store
|
||||
```
|
||||
|
||||
We will use this directory to share APKs between the host OS and the Docker container.
|
||||
|
||||
Finally create one more directory inside `reproducible-signal` called `image-build-context`.
|
||||
```
|
||||
user@host:$ mkdir ~/reproducible-signal/image-build-context
|
||||
```
|
||||
|
||||
This directory will be used later to build our Docker image.
|
||||
|
||||
|
||||
## Getting the Google Play Store version of Signal APK
|
||||
|
||||
To compare the APKs we of course need a version of Signal from the Google Play Store.
|
||||
|
||||
First make sure that the Signal version you want to verify is installed on your Android device. You'll need `adb` for this part.
|
||||
|
||||
Plug your device to your computer and run this command to pull the APK from the device:
|
||||
|
||||
```
|
||||
user@host:$ adb pull $(adb shell pm path org.thoughtcrime.securesms | grep /base.apk | awk -F':' '{print $2}') ~/reproducible-signal/apk-from-google-play-store/Signal-$(adb shell dumpsys package org.thoughtcrime.securesms | grep versionName | awk -F'=' '{print $2}').apk
|
||||
```
|
||||
|
||||
This will pull a file into `~/reproducible-signal/apk-from-google-play-store/` with the name `Signal-<version>.apk`
|
||||
|
||||
Alternatively, you can do this step-by-step:
|
||||
|
||||
```
|
||||
user@host:$ adb shell pm path org.thoughtcrime.securesms
|
||||
```
|
||||
This will output something like:
|
||||
```
|
||||
package:/data/app/org.thoughtcrime.securesms-aWRzcGlzcG9wZA==/base.apk
|
||||
```
|
||||
|
||||
The output will tell you where the Signal APK is located in your device. (In this example the path is `/data/app/org.thoughtcrime.securesms-aWRzcGlzcG9wZA==/base.apk`)
|
||||
|
||||
Now using this information, pull the APK from your device to the `reproducible-signal/apk-from-google-play-store` directory you created before:
|
||||
```
|
||||
user@host:$ adb pull \
|
||||
/data/app/org.thoughtcrime.securesms-aWRzcGlzcG9wZA==/base.apk \
|
||||
~/reproducible-signal/apk-from-google-play-store/Signal-3.15.2.apk
|
||||
```
|
||||
|
||||
We will use this APK in the final part when we compare it with the self-built APK from GitHub.
|
||||
|
||||
## Identifying the ABI
|
||||
|
||||
Since v4.37.0, the APKs have been split by ABI, the CPU architecture of the target device. Google play will serve the correct one to you for your device.
|
||||
|
||||
To identify which ABIs the google play APK supports, we can look inside the APK, which is just a zip file:
|
||||
|
||||
```
|
||||
user@host:$ unzip -l ~/reproducible-signal/apk-from-google-play-store/Signal-*.apk | grep lib/
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
1214348 00-00-1980 00:00 lib/armeabi-v7a/libconscrypt_jni.so
|
||||
151980 00-00-1980 00:00 lib/armeabi-v7a/libcurve25519.so
|
||||
4164320 00-00-1980 00:00 lib/armeabi-v7a/libjingle_peerconnection_so.so
|
||||
13948 00-00-1980 00:00 lib/armeabi-v7a/libnative-utils.so
|
||||
2357812 00-00-1980 00:00 lib/armeabi-v7a/libsqlcipher.so
|
||||
```
|
||||
|
||||
As there is just one sub directory of `lib/` called `armeabi-v7a`, that is your ABI. Make a note of that for later. If you see more than one subdirectory of `lib/`:
|
||||
|
||||
```
|
||||
1214348 00-00-1980 00:00 lib/armeabi-v7a/libconscrypt_jni.so
|
||||
151980 00-00-1980 00:00 lib/armeabi-v7a/libcurve25519.so
|
||||
4164320 00-00-1980 00:00 lib/armeabi-v7a/libjingle_peerconnection_so.so
|
||||
13948 00-00-1980 00:00 lib/armeabi-v7a/libnative-utils.so
|
||||
2357812 00-00-1980 00:00 lib/armeabi-v7a/libsqlcipher.so
|
||||
2111376 00-00-1980 00:00 lib/x86/libconscrypt_jni.so
|
||||
201056 00-00-1980 00:00 lib/x86/libcurve25519.so
|
||||
7303888 00-00-1980 00:00 lib/x86/libjingle_peerconnection_so.so
|
||||
5596 00-00-1980 00:00 lib/x86/libnative-utils.so
|
||||
3977636 00-00-1980 00:00 lib/x86/libsqlcipher.so
|
||||
```
|
||||
|
||||
Then that means you have the `universal` APK.
|
||||
|
||||
## Installing Docker
|
||||
|
||||
Install Docker by following the instructions for your platform at https://docs.docker.com/engine/installation/
|
||||
|
||||
Your platform might also have its own preferred way of installing Docker. E.g. Ubuntu has its own Docker package (`docker.io`) if you do not want to follow Docker's instructions.
|
||||
|
||||
In the following sections we will assume that your Docker installation works without issues. So after installing, please make sure that everything is running smoothly before continuing.
|
||||
|
||||
|
||||
## Building a Docker image for Signal
|
||||
|
||||
#### Grabbing the `Dockerfile`
|
||||
|
||||
First you will need the `Dockerfile` for Signal Android. It comes bundled with Signal's source code. The `Dockerfile` contains instructions on how to automatically build a Docker image for Signal. You just need to run it and it builds itself.
|
||||
|
||||
Download the `Dockerfile` to the `image-build-context` directory.
|
||||
|
||||
```
|
||||
user@host:$ wget -O ~/reproducible-signal/image-build-context/Dockerfile_v3.15.2 \
|
||||
https://raw.githubusercontent.com/signalapp/Signal-Android/v3.15.2/Dockerfile
|
||||
```
|
||||
|
||||
Note that the `Dockerfile` is specific to the Signal version you want to compare to. Again you have to adjust the URL above to match the right version. (Though sometimes the file might not be up to date, see the [Troubleshooting section](#troubleshooting))
|
||||
|
||||
|
||||
#### Building the image
|
||||
|
||||
Now we have everything we need to build the Docker image for Signal. Go to the `image-build-context` directory:
|
||||
|
||||
```
|
||||
user@host:$ cd ~/reproducible-signal/image-build-context
|
||||
```
|
||||
|
||||
And list the contents.
|
||||
```
|
||||
user@host:$ ls
|
||||
```
|
||||
The output should look like this:
|
||||
```
|
||||
Dockerfile_v3.15.2
|
||||
```
|
||||
|
||||
Now in this directory build the image using `Dockerfile_v3.15.2`:
|
||||
```
|
||||
user@host:$ docker build --file Dockerfile_v3.15.2 --tag signal-android .
|
||||
```
|
||||
|
||||
(Note that there is a dot at the end of that command!)
|
||||
|
||||
Wait a few years for the build to finish... :construction_worker:
|
||||
|
||||
(Depending on your computer and network connection, this may take several minutes.)
|
||||
|
||||
:calendar: :sleeping:
|
||||
|
||||
After the build has finished, you may wish to list all your Docker images to see that it's really there:
|
||||
```
|
||||
user@host:$ docker images
|
||||
```
|
||||
Output should look something like this:
|
||||
```
|
||||
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
|
||||
signal-android latest c6b84450b896 46 seconds ago 2.94 GB
|
||||
ubuntu 14.04.3 8693db7e8a00 9 weeks ago 187.9 MB
|
||||
```
|
||||
|
||||
|
||||
## Compiling Signal inside a container
|
||||
|
||||
Next we will run a container of the image we just built, grab Signal's source code and compile Signal.
|
||||
|
||||
First go to the `reproducible-signal` directory:
|
||||
|
||||
```
|
||||
user@host:$ cd ~/reproducible-signal/
|
||||
```
|
||||
|
||||
To run a new ephemeral container with an interactive terminal session execute the following long command:
|
||||
```
|
||||
user@host:$ docker run \
|
||||
--name signal \
|
||||
--rm \
|
||||
--interactive \
|
||||
--tty \
|
||||
--volume $(pwd)/apk-from-google-play-store:/signal-build/apk-from-google-play-store \
|
||||
--workdir /signal-build \
|
||||
signal-android
|
||||
```
|
||||
Now you are inside the container.
|
||||
|
||||
Grab Signal's source code from GitHub and go to the repository directory:
|
||||
```
|
||||
root@container:# git clone https://github.com/signalapp/Signal-Android.git
|
||||
root@container:# cd Signal-Android
|
||||
```
|
||||
|
||||
Before you can compile, you **must** ensure that you are at the right commit. In other words you **must** checkout the version you wish to verify (here we are verifying 3.15.2):
|
||||
```
|
||||
root@container:# git checkout --quiet v3.15.2
|
||||
```
|
||||
|
||||
Now you may compile the release APK by running:
|
||||
```
|
||||
root@container:# ./gradlew clean assemblePlayRelease --exclude-task signProductionPlayRelease
|
||||
```
|
||||
This will take a few minutes :sleeping:
|
||||
|
||||
|
||||
#### Checking if the APKs match
|
||||
|
||||
After the build has completed successfully we can finally compare if the APKs match. For the comparison we need of course the Google Play Store version of Signal APK which you copied to the `apk-from-google-play-store` directory in the beginning of this guide. Because we used that directory as a `--volume` parameter for our container, we can see all the files in that directory within our container.
|
||||
|
||||
So now we can compare the APKs using the `apkdiff.py` tool.
|
||||
|
||||
The above build step produced several APKs, one for each supported ABI and one universal one. You will need to determine the correct APK to compare.
|
||||
|
||||
Currently, the most common ABI is `armeabi-v7a`. Other options at this time include `x86` and `universal`. In the future it will also include 64-bit options, such as `x86_64` and `arm64-v8a`.
|
||||
|
||||
See [Identifying the ABI](#identifying-the-abi) above if you don't know the ABI of your play store APK.
|
||||
|
||||
Once you have determined the ABI, add an `abi` environment variable. For example, suppose we determine that `armeabi-v7a` is the ABI google play has served:
|
||||
|
||||
```
|
||||
root@container:# export abi=armeabi-v7a
|
||||
```
|
||||
|
||||
And the diff script to compare:
|
||||
```
|
||||
root@container:# python3 apkdiff/apkdiff.py \
|
||||
build/outputs/apk/play/release/*play-$abi-release-unsigned*.apk \
|
||||
../apk-from-google-play-store/Signal-3.15.2.apk
|
||||
```
|
||||
Output:
|
||||
```
|
||||
APKs match!
|
||||
```
|
||||
|
||||
If you get `APKs match!`, you have successfully verified that the Google Play release matches with your own self-built version of Signal. Congratulations! Your APKs are a match made in heaven! :sparkles:
|
||||
|
||||
If you get `APKs don't match!`, you did something wrong in the previous steps. See the [Troubleshooting section](#troubleshooting) for more info.
|
||||
|
||||
|
||||
## Comparing next time
|
||||
|
||||
If the build environment (i.e. `Dockerfile`) has not changed, you don't need to build the image again to verify a newer APK. You can just [run the container again](#compiling-signal-inside-a-container).
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you cannot get things to work, please do not open an issue or comment on an existing issue at GitHub. Instead, ask for help at https://community.signalusers.org/c/development
|
||||
|
||||
Some common issues why things may not work:
|
||||
- some pinned packages in the `Dockerfile` are not available anymore and building of the Docker image fails
|
||||
- the Android packages in the Docker image are outdated and compiling Signal fails
|
||||
- you built the Docker image with a wrong version of the `Dockerfile`
|
||||
- you didn't checkout the correct Signal version tag with Git before compiling
|
||||
- the ABI you selected is not the correct ABI, particularly if you see an error along the lines of `Sorted manifests don't match, lib/x86/libcurve25519.so vs lib/armeabi-v7a/libcurve25519.so`.
|
||||
- this guide is outdated
|
||||
- you are in a dream
|
||||
- if you run into this issue: https://issuetracker.google.com/issues/110237303 try to add `resources.arsc` to the list of ignored files and compare again
|
||||
@@ -1,20 +1,21 @@
|
||||
#! /usr/bin/env python
|
||||
#! /usr/bin/env python3
|
||||
|
||||
import sys
|
||||
from zipfile import ZipFile
|
||||
|
||||
class ApkDiff:
|
||||
|
||||
IGNORE_FILES = ["META-INF/CERT.RSA", "META-INF/CERT.SF", "META-INF/MANIFEST.MF"]
|
||||
# resources.arsc is ignored due to https://issuetracker.google.com/issues/110237303
|
||||
# May be fixed in Android Gradle Plugin 3.4
|
||||
IGNORE_FILES = ["META-INF/MANIFEST.MF", "META-INF/SIGNAL_S.RSA", "META-INF/SIGNAL_S.SF", "resources.arsc"]
|
||||
|
||||
def compare(self, sourceApk, destinationApk):
|
||||
sourceZip = ZipFile(sourceApk, 'r')
|
||||
destinationZip = ZipFile(destinationApk, 'r')
|
||||
|
||||
if self.compareManifests(sourceZip, destinationZip) and self.compareEntries(sourceZip, destinationZip) == True:
|
||||
print "APKs match!"
|
||||
print("APKs match!")
|
||||
else:
|
||||
print "APKs don't match!"
|
||||
print("APKs don't match!")
|
||||
|
||||
def compareManifests(self, sourceZip, destinationZip):
|
||||
sourceEntrySortedList = sorted(sourceZip.namelist())
|
||||
@@ -23,23 +24,23 @@ class ApkDiff:
|
||||
for ignoreFile in self.IGNORE_FILES:
|
||||
while ignoreFile in sourceEntrySortedList: sourceEntrySortedList.remove(ignoreFile)
|
||||
while ignoreFile in destinationEntrySortedList: destinationEntrySortedList.remove(ignoreFile)
|
||||
|
||||
|
||||
if len(sourceEntrySortedList) != len(destinationEntrySortedList):
|
||||
print "Manifest lengths differ!"
|
||||
|
||||
print("Manifest lengths differ!")
|
||||
|
||||
for (sourceEntryName, destinationEntryName) in zip(sourceEntrySortedList, destinationEntrySortedList):
|
||||
if sourceEntryName != destinationEntryName:
|
||||
print "Sorted manifests don't match, %s vs %s" % (sourceEntryName, destinationEntryName)
|
||||
print("Sorted manifests don't match, %s vs %s" % (sourceEntryName, destinationEntryName))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def compareEntries(self, sourceZip, destinationZip):
|
||||
sourceInfoList = filter(lambda sourceInfo: sourceInfo.filename not in self.IGNORE_FILES, sourceZip.infolist())
|
||||
destinationInfoList = filter(lambda destinationInfo: destinationInfo.filename not in self.IGNORE_FILES, destinationZip.infolist())
|
||||
|
||||
sourceInfoList = list(filter(lambda sourceInfo: sourceInfo.filename not in self.IGNORE_FILES, sourceZip.infolist()))
|
||||
destinationInfoList = list(filter(lambda destinationInfo: destinationInfo.filename not in self.IGNORE_FILES, destinationZip.infolist()))
|
||||
|
||||
if len(sourceInfoList) != len(destinationInfoList):
|
||||
print "APK info lists of different length!"
|
||||
print("APK info lists of different length!")
|
||||
return False
|
||||
|
||||
for sourceEntryInfo in sourceInfoList:
|
||||
@@ -49,19 +50,19 @@ class ApkDiff:
|
||||
destinationEntry = destinationZip.open(destinationEntryInfo, 'r')
|
||||
|
||||
if self.compareFiles(sourceEntry, destinationEntry) != True:
|
||||
print "APK entry %s does not match %s!" % (sourceEntryInfo.filename, destinationEntryInfo.filename)
|
||||
print("APK entry %s does not match %s!" % (sourceEntryInfo.filename, destinationEntryInfo.filename))
|
||||
return False
|
||||
|
||||
destinationInfoList.remove(destinationEntryInfo)
|
||||
break
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def compareFiles(self, sourceFile, destinationFile):
|
||||
sourceChunk = sourceFile.read(1024)
|
||||
destinationChunk = destinationFile.read(1024)
|
||||
|
||||
while sourceChunk != "" or destinationChunk != "":
|
||||
while sourceChunk != b"" or destinationChunk != b"":
|
||||
if sourceChunk != destinationChunk:
|
||||
return False
|
||||
|
||||
@@ -72,7 +73,7 @@ class ApkDiff:
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
print "Usage: apkdiff <pathToFirstApk> <pathToSecondApk>"
|
||||
print("Usage: apkdiff <pathToFirstApk> <pathToSecondApk>")
|
||||
sys.exit(1)
|
||||
|
||||
ApkDiff().compare(sys.argv[1], sys.argv[2])
|
||||
|
||||
10
app/.tx/config
Normal file
@@ -0,0 +1,10 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = da_DK:da-rDK,he:iw,id:in,kn_IN:kn-rIN,pt_BR:pt-rBR,pt_PT:pt,qu_EC:qu-rEC,sv_SE:sv-rSE,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW
|
||||
|
||||
[signal-android.master]
|
||||
file_filter = src/main/res/values-<lang>/strings.xml
|
||||
source_file = src/main/res/values/strings.xml
|
||||
source_lang = en
|
||||
type = ANDROID
|
||||
|
||||
436
app/build.gradle
Normal file
@@ -0,0 +1,436 @@
|
||||
import org.signal.signing.ApkSignerUtil
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
maven {
|
||||
url "https://repo1.maven.org/maven2"
|
||||
}
|
||||
jcenter {
|
||||
content {
|
||||
includeVersion 'org.jetbrains.trove4j', 'trove4j', '20160824'
|
||||
}
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.1'
|
||||
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
|
||||
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
apply plugin: 'androidx.navigation.safeargs'
|
||||
apply plugin: 'witness'
|
||||
apply from: 'translations.gradle'
|
||||
apply from: 'witness-verifications.gradle'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
|
||||
content {
|
||||
includeGroupByRegex "com\\.github\\.chrisbanes.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/shortcutbadger/releases/"
|
||||
content {
|
||||
includeGroupByRegex "me\\.leolin.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
|
||||
content {
|
||||
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
|
||||
content {
|
||||
includeGroupByRegex "org\\.signal.*"
|
||||
}
|
||||
}
|
||||
maven { // textdrawable
|
||||
url 'https://dl.bintray.com/amulyakhare/maven'
|
||||
content {
|
||||
includeGroupByRegex "com\\.amulyakhare.*"
|
||||
}
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.10.0'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.navigation:navigation-fragment:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-ui:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||
implementation "androidx.camera:camera-core:1.0.0-alpha06"
|
||||
implementation "androidx.camera:camera-camera2:1.0.0-alpha06"
|
||||
|
||||
implementation('com.google.firebase:firebase-messaging:17.3.4') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
}
|
||||
|
||||
implementation 'com.google.android.gms:play-services-maps:16.1.0'
|
||||
implementation 'com.google.android.gms:play-services-auth:16.0.1'
|
||||
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
|
||||
|
||||
implementation 'org.conscrypt:conscrypt-android:2.0.0'
|
||||
implementation 'org.signal:aesgcmprovider:0.0.3'
|
||||
|
||||
implementation project(':libsignal-service')
|
||||
|
||||
implementation 'org.signal:ringrtc-android:0.3.1'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
|
||||
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
annotationProcessor 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
implementation 'pl.tajchert:waitingdots:0.1.0'
|
||||
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.google.zxing:core:3.2.1'
|
||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp'
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
||||
}
|
||||
implementation 'com.annimon:stream:1.1.8'
|
||||
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
exclude group: 'com.android.support', module: 'recyclerview-v7'
|
||||
}
|
||||
|
||||
implementation 'com.airbnb.android:lottie:3.0.7'
|
||||
|
||||
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
|
||||
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
|
||||
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
|
||||
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
|
||||
exclude group: 'com.fasterxml.jackson.core'
|
||||
exclude group: 'org.freemarker'
|
||||
}
|
||||
|
||||
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 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.multidex:multidex:2.0.1'
|
||||
androidTestImplementation 'androidx.multidex:multidex-instrumentation:2.0.0'
|
||||
androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
|
||||
androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
|
||||
androidTestImplementation ('org.assertj:assertj-core:1.7.1') {
|
||||
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
||||
}
|
||||
androidTestImplementation ('com.squareup.assertj:assertj-android:1.1.1') {
|
||||
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
testImplementation 'org.robolectric:robolectric:4.2'
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
}
|
||||
|
||||
dependencyVerification {
|
||||
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 587
|
||||
def canonicalVersionName = "4.53.2"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
'armeabi-v7a' : 1,
|
||||
'arm64-v8a' : 2,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4]
|
||||
|
||||
android {
|
||||
flavorDimensions "none"
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
dexOptions {
|
||||
javaMaxHeapSize "4g"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
multiDexEnabled true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
project.ext.set("archivesBaseName", "Signal");
|
||||
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "USER_AGENT", "\"OWA\""
|
||||
buildConfigField "boolean", "DEV_BUILD", "false"
|
||||
buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
|
||||
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"f2e2a5004794a6c1bac5c4949eadbc243dd02e02d1a93f10fe24584fb70815d8\""
|
||||
buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"f51f435802ada769e67aaf5744372bb7e7d519eecf996d335eb5b46b872b5789\""
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
}
|
||||
|
||||
resConfigs autoResConfig()
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'LICENSE.txt'
|
||||
exclude 'LICENSE'
|
||||
exclude 'NOTICE'
|
||||
exclude 'asm-license.txt'
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||
'proguard/proguard-firebase-messaging.pro',
|
||||
'proguard/proguard-google-play-services.pro',
|
||||
'proguard/proguard-jackson.pro',
|
||||
'proguard/proguard-sqlite.pro',
|
||||
'proguard/proguard-appcompat-v7.pro',
|
||||
'proguard/proguard-square-okhttp.pro',
|
||||
'proguard/proguard-square-okio.pro',
|
||||
'proguard/proguard-spongycastle.pro',
|
||||
'proguard/proguard-rounded-image-view.pro',
|
||||
'proguard/proguard-glide.pro',
|
||||
'proguard/proguard-shortcutbadger.pro',
|
||||
'proguard/proguard-retrofit.pro',
|
||||
'proguard/proguard-webrtc.pro',
|
||||
'proguard/proguard-klinker.pro',
|
||||
'proguard/proguard-retrolambda.pro',
|
||||
'proguard/proguard-okhttp.pro',
|
||||
'proguard/proguard-ez-vcard.pro',
|
||||
'proguard/proguard.cfg'
|
||||
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_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
|
||||
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"b5a865941f95887018c86725cc92308d34a3084dc2b4e7bd2de5e5e1690b50c6\""
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles = buildTypes.debug.proguardFiles
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
play {
|
||||
dimension "none"
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
}
|
||||
|
||||
website {
|
||||
dimension "none"
|
||||
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||
}
|
||||
}
|
||||
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
|
||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||
def postFix = abiPostFix.get(abiName, 0)
|
||||
|
||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
||||
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError true
|
||||
baseline file("lint-baseline.xml")
|
||||
disable "LintError"
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def assembleWebsiteDescriptor = { variant, file ->
|
||||
if (file.exists()) {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
file.eachByte 4096, {bytes, size ->
|
||||
md.update(bytes, 0, size);
|
||||
}
|
||||
|
||||
String digest = md.digest().collect {String.format "%02x", it}.join();
|
||||
String url = variant.productFlavors.get(0).ext.websiteUpdateUrl
|
||||
String apkName = file.getName()
|
||||
|
||||
String descriptor = "{" +
|
||||
"\"versionCode\" : ${canonicalVersionCode * postFixSize + abiPostFix['universal']}," +
|
||||
"\"versionName\" : \"$canonicalVersionName\"," +
|
||||
"\"sha256sum\" : \"$digest\"," +
|
||||
"\"url\" : \"$url/$apkName\"" +
|
||||
"}"
|
||||
|
||||
File descriptorFile = new File(file.getParent(), apkName.replace(".apk", ".json"))
|
||||
|
||||
descriptorFile.write(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
def signProductionRelease = { variant ->
|
||||
variant.outputs.collect { output ->
|
||||
String apkName = output.outputFile.name
|
||||
File inputFile = new File(output.outputFile.path)
|
||||
File outputFile = new File(output.outputFile.parent, apkName.replace('-unsigned', ''))
|
||||
|
||||
new ApkSignerUtil('sun.security.pkcs11.SunPKCS11',
|
||||
'pkcs11.config',
|
||||
'PKCS11',
|
||||
'file:pkcs11.password').calculateSignature(inputFile.getAbsolutePath(),
|
||||
outputFile.getAbsolutePath())
|
||||
|
||||
inputFile.delete()
|
||||
outputFile
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionPlayRelease {
|
||||
doLast {
|
||||
signProductionRelease(android.applicationVariants.find { (it.name == 'playRelease') })
|
||||
}
|
||||
}
|
||||
|
||||
task signProductionWebsiteRelease {
|
||||
doLast {
|
||||
def variant = android.applicationVariants.find { (it.name == 'websiteRelease') }
|
||||
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 {
|
||||
executable = 'git'
|
||||
args = ['log', '-1', '--pretty=format:%ct']
|
||||
standardOutput = os
|
||||
}
|
||||
|
||||
return os.toString() + "000"
|
||||
}
|
||||
}
|
||||
6
app/jni/Application.mk
Normal file
@@ -0,0 +1,6 @@
|
||||
# Built with NDK 19.2.5345600
|
||||
APP_ABI := armeabi-v7a x86 arm64-v8a x86_64
|
||||
APP_PLATFORM := android-19
|
||||
APP_STL := c++_static
|
||||
APP_CPPFLAGS += -fexceptions
|
||||
APP_OPTIM := debug
|
||||
45
app/jni/utils/org_thoughtcrime_securesms_util_FileUtils.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include "org_thoughtcrime_securesms_util_FileUtils.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <linux/memfd.h>
|
||||
#include <syscall.h>
|
||||
|
||||
jint JNICALL Java_org_thoughtcrime_securesms_util_FileUtils_getFileDescriptorOwner
|
||||
(JNIEnv *env, jclass clazz, jobject fileDescriptor)
|
||||
{
|
||||
jclass fdClass = env->GetObjectClass(fileDescriptor);
|
||||
|
||||
if (fdClass == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
jfieldID fdFieldId = env->GetFieldID(fdClass, "descriptor", "I");
|
||||
|
||||
if (fdFieldId == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int fd = env->GetIntField(fileDescriptor, fdFieldId);
|
||||
|
||||
struct stat stat_struct;
|
||||
|
||||
if (fstat(fd, &stat_struct) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return stat_struct.st_uid;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_org_thoughtcrime_securesms_util_FileUtils_createMemoryFileDescriptor
|
||||
(JNIEnv *env, jclass clazz, jstring jname)
|
||||
{
|
||||
const char *name = env->GetStringUTFChars(jname, NULL);
|
||||
|
||||
int fd = syscall(SYS_memfd_create, name, MFD_CLOEXEC);
|
||||
|
||||
env->ReleaseStringUTFChars(jname, name);
|
||||
|
||||
return fd;
|
||||
}
|
||||
29
app/jni/utils/org_thoughtcrime_securesms_util_FileUtils.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/* DO NOT EDIT THIS FILE - it is machine generated */
|
||||
#include <jni.h>
|
||||
/* Header for class org_thoughtcrime_securesms_util_FileUtils */
|
||||
|
||||
#ifndef _Included_org_thoughtcrime_securesms_util_FileUtils
|
||||
#define _Included_org_thoughtcrime_securesms_util_FileUtils
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/*
|
||||
* Class: org_thoughtcrime_securesms_util_FileUtils
|
||||
* Method: getFileDescriptorOwner
|
||||
* Signature: (Ljava/io/FileDescriptor;)I
|
||||
*/
|
||||
JNIEXPORT jint JNICALL Java_org_thoughtcrime_securesms_util_FileUtils_getFileDescriptorOwner
|
||||
(JNIEnv *, jclass, jobject);
|
||||
|
||||
/*
|
||||
* Class: org_thoughtcrime_securesms_util_FileUtils
|
||||
* Method: createMemoryFileDescriptor
|
||||
* Signature: (Ljava/lang/String;)I
|
||||
*/
|
||||
JNIEXPORT jint JNICALL Java_org_thoughtcrime_securesms_util_FileUtils_createMemoryFileDescriptor
|
||||
(JNIEnv *, jclass, jstring);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
48
app/lint-baseline.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="5" by="lint 3.3.2" client="gradle" variant="playRelease" version="3.3.2">
|
||||
|
||||
<issue
|
||||
id="MissingPermission"
|
||||
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
|
||||
errorLine1=" List<SubscriptionInfo> list = subscriptionManager.getActiveSubscriptionInfoList();"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/org/thoughtcrime/securesms/util/dualsim/SubscriptionManagerCompat.java"
|
||||
line="101"
|
||||
column="35"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ResourceType"
|
||||
message="Expected resource of type styleable"
|
||||
errorLine1=" drawables.getColor(1, 0xff000000);"
|
||||
errorLine2=" ~">
|
||||
<location
|
||||
file="src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java"
|
||||
line="187"
|
||||
column="36"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="AppLinkUrlError"
|
||||
message="Missing URL"
|
||||
errorLine1=" <intent-filter>"
|
||||
errorLine2=" ^">
|
||||
<location
|
||||
file="AndroidManifest.xml"
|
||||
line="368"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="AppLinkUrlError"
|
||||
message="Missing URL"
|
||||
errorLine1=" <intent-filter>"
|
||||
errorLine2=" ^">
|
||||
<location
|
||||
file="AndroidManifest.xml"
|
||||
line="381"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
||||
25
app/lint.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
|
||||
<!-- 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="MissingQuantity" severity="warning" />
|
||||
<issue id="ExtraTranslation" severity="warning" />
|
||||
<issue id="ImpliedQuantity" severity="warning" />
|
||||
|
||||
<issue id="CanvasSize" severity="error" />
|
||||
<issue id="HardcodedText" severity="error" />
|
||||
<issue id="VectorRaster" severity="error" />
|
||||
<issue id="ButtonOrder" severity="error" />
|
||||
<issue id="ExtraTranslation" severity="warning" />
|
||||
|
||||
<issue id="RestrictedApi" severity="error">
|
||||
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java" />
|
||||
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java" />
|
||||
</issue>
|
||||
|
||||
</lint>
|
||||
1
app/proguard/proguard-firebase-messaging.pro
Normal file
@@ -0,0 +1 @@
|
||||
-dontwarn com.google.firebase.analytics.connector.AnalyticsConnector
|
||||
11
app/proguard/proguard.cfg
Normal file
@@ -0,0 +1,11 @@
|
||||
-dontoptimize
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep class org.whispersystems.** { *; }
|
||||
-keep class org.thoughtcrime.securesms.** { *; }
|
||||
-keepclassmembers class ** {
|
||||
public void onEvent*(**);
|
||||
}
|
||||
|
||||
# Protobuf lite
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.thoughtcrime.securesms">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference"/>
|
||||
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2"/>
|
||||
|
||||
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
|
||||
android:label="Access to TextSecure Secrets"
|
||||
@@ -27,6 +27,7 @@
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
|
||||
@@ -58,6 +59,7 @@
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||
@@ -86,10 +88,7 @@
|
||||
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
||||
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE"
|
||||
android:protectionLevel="signature" />
|
||||
<uses-permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE" />
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
|
||||
|
||||
<application android:name=".ApplicationContext"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -110,23 +109,25 @@
|
||||
<meta-data android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
|
||||
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
|
||||
<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"
|
||||
android:theme="@style/TextSecure.LightTheme.WebRTCCall"
|
||||
android:excludeFromRecents="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|fontScale"
|
||||
android:launchMode="singleTask"/>
|
||||
|
||||
<activity android:name=".CountrySelectionActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".InviteActivity"
|
||||
android:theme="@style/TextSecure.HighlightTheme"
|
||||
android:theme="@style/Signal.Light.NoActionBar.Invite"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:parentActivityName=".ConversationListActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
|
||||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".PromptMmsActivity"
|
||||
@@ -174,15 +175,31 @@
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ConversationListActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
<activity android:name=".stickers.StickerPackPreviewActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="true" />
|
||||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<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="addstickers" />
|
||||
</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="https"
|
||||
android:host="signal.art"
|
||||
android:pathPrefix="/addstickers"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias android:name=".RoutingActivity"
|
||||
android:targetActivity=".ConversationListActivity"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
@@ -198,27 +215,19 @@
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity android:name=".ConversationListArchiveActivity"
|
||||
android:label="@string/AndroidManifest_archived_conversations"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:parentActivityName=".ConversationListActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ConversationActivity"
|
||||
<activity android:name=".conversation.ConversationActivity"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:parentActivityName=".ConversationListActivity">
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
|
||||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ConversationPopupActivity"
|
||||
<activity android:name=".longmessage.LongMessageActivity" />
|
||||
|
||||
<activity android:name=".conversation.ConversationPopupActivity"
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
@@ -241,13 +250,13 @@
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DatabaseUpgradeActivity"
|
||||
<activity android:name=".migrations.ApplicationMigrationActivity"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ExperienceUpgradeActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -278,10 +287,11 @@
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediasend.MediaSendActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
<activity android:name=".mediasend.MediaSendActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphraseChangeActivity"
|
||||
android:label="@string/AndroidManifest__change_passphrase"
|
||||
@@ -298,9 +308,22 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".RegistrationActivity"
|
||||
<activity android:name=".registration.RegistrationNavigationActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".revealable.ViewOnceMessageActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:excludeFromRecents="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".stickers.StickerManagementActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -319,10 +342,9 @@
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".MediaOverviewActivity"
|
||||
<activity android:name=".mediaoverview.MediaOverviewActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DummyActivity"
|
||||
@@ -380,19 +402,15 @@
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".scribbles.ScribbleActivity"
|
||||
android:theme="@style/TextSecure.ScribbleTheme"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
<activity android:name=".scribbles.ImageEditorStickerSelectActivity"
|
||||
android:theme="@style/TextSecure.DarkTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".scribbles.StickerSelectActivity"
|
||||
android:theme="@style/TextSecure.ScribbleTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
|
||||
<activity android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
||||
android:theme="@style/TextSecure.DarkTheme"/>
|
||||
|
||||
<activity android:name=".CreateProfileActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
@@ -425,10 +443,21 @@
|
||||
android:exported="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".camera.CameraActivity"
|
||||
android:theme="@style/TextSecure.ScribbleTheme"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
<activity
|
||||
android:name=".maps.PlacePickerActivity"
|
||||
android:label="@string/PlacePickerActivity_title"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity
|
||||
android:name=".usernames.ProfileEditActivityV2"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
|
||||
<activity android:name=".MainActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
@@ -472,12 +501,11 @@
|
||||
|
||||
<service android:name=".service.GenericForegroundService"/>
|
||||
|
||||
<receiver android:name=".gcm.GcmBroadcastReceiver" android:permission="com.google.android.c2dm.permission.SEND" >
|
||||
<service android:name=".gcm.FcmService">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
|
||||
<category android:name="org.thoughtcrime.securesms" />
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".service.SmsListener"
|
||||
android:permission="android.permission.BROADCAST_SMS"
|
||||
@@ -544,6 +572,8 @@
|
||||
|
||||
<receiver android:name=".service.ExpirationListener" />
|
||||
|
||||
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
||||
|
||||
<provider android:name=".providers.PartProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
@@ -554,7 +584,7 @@
|
||||
android:exported="false"
|
||||
android:authorities="org.thoughtcrime.provider.securesms.mms" />
|
||||
|
||||
<provider android:name="android.support.v4.content.FileProvider"
|
||||
<provider android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="org.thoughtcrime.securesms.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
@@ -575,11 +605,18 @@
|
||||
android:authorities="org.thoughtcrime.securesms.database.attachment"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
<provider android:name=".database.DatabaseContentProviders$Sticker"
|
||||
android:authorities="org.thoughtcrime.securesms.database.sticker"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$StickerPack"
|
||||
android:authorities="org.thoughtcrime.securesms.database.stickerpack"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name="androidx.camera.camera2.impl.Camera2Initializer"
|
||||
android:authorities="${applicationId}.camerax-init"
|
||||
android:exported="false"
|
||||
tools:node="remove" />
|
||||
android:enabled="false" />
|
||||
|
||||
<receiver android:name=".service.BootReceiver">
|
||||
<intent-filter>
|
||||
@@ -655,6 +692,37 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".gcm.FcmJobService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:enabled="@bool/enable_job_service"
|
||||
tools:targetApi="26" />
|
||||
|
||||
<service
|
||||
android:name=".jobmanager.JobSchedulerScheduler$SystemService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:enabled="@bool/enable_job_service"
|
||||
tools:targetApi="26" />
|
||||
|
||||
<service
|
||||
android:name=".jobmanager.KeepAliveService"
|
||||
android:enabled="@bool/enable_alarm_manager" />
|
||||
|
||||
<receiver
|
||||
android:name=".jobmanager.AlarmManagerScheduler$RetryReceiver"
|
||||
android:enabled="@bool/enable_alarm_manager" />
|
||||
|
||||
<!-- Probably don't need this one -->
|
||||
<receiver
|
||||
android:name=".jobmanager.BootReceiver"
|
||||
android:enabled="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
|
||||
|
||||
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false"/>
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_W" android:value="632.0dip" />
|
||||
BIN
app/src/main/assets/emoji/Activity.webp
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
app/src/main/assets/emoji/Flags.webp
Normal file
|
After Width: | Height: | Size: 447 KiB |
BIN
app/src/main/assets/emoji/Foods.webp
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
app/src/main/assets/emoji/Nature.webp
Normal file
|
After Width: | Height: | Size: 387 KiB |
BIN
app/src/main/assets/emoji/Objects.webp
Normal file
|
After Width: | Height: | Size: 494 KiB |
BIN
app/src/main/assets/emoji/People_0.webp
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
app/src/main/assets/emoji/People_1.webp
Normal file
|
After Width: | Height: | Size: 952 KiB |
BIN
app/src/main/assets/emoji/People_2.webp
Normal file
|
After Width: | Height: | Size: 862 KiB |
BIN
app/src/main/assets/emoji/People_3.webp
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
app/src/main/assets/emoji/Places.webp
Normal file
|
After Width: | Height: | Size: 567 KiB |
BIN
app/src/main/assets/emoji/Symbols.webp
Normal file
|
After Width: | Height: | Size: 376 KiB |
@@ -0,0 +1,395 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.camera.camera2.Camera2AppConfig;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
|
||||
import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.ringrtc.CallConnectionFactory;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||
import org.thoughtcrime.securesms.insights.InsightsOptOut;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
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.UncaughtExceptionLogger;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
||||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
||||
|
||||
import java.security.Security;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Will be called once when the TextSecure process is created.
|
||||
*
|
||||
* We're using this as an insertion point to patch up the Android PRNG disaster,
|
||||
* to initialize the job manager, and to check for GCM registration freshness.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class ApplicationContext extends MultiDexApplication implements DefaultLifecycleObserver {
|
||||
|
||||
private static final String TAG = ApplicationContext.class.getSimpleName();
|
||||
|
||||
private ExpiringMessageManager expiringMessageManager;
|
||||
private ViewOnceMessageManager viewOnceMessageManager;
|
||||
private TypingStatusRepository typingStatusRepository;
|
||||
private TypingStatusSender typingStatusSender;
|
||||
private IncomingMessageObserver incomingMessageObserver;
|
||||
private PersistentLogger persistentLogger;
|
||||
|
||||
private volatile boolean isAppVisible;
|
||||
|
||||
public static ApplicationContext getInstance(Context context) {
|
||||
return (ApplicationContext)context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.i(TAG, "onCreate()");
|
||||
initializeSecurityProvider();
|
||||
initializeLogging();
|
||||
initializeCrashHandling();
|
||||
initializeAppDependencies();
|
||||
initializeFirstEverAppLaunch();
|
||||
initializeApplicationMigrations();
|
||||
initializeMessageRetrieval();
|
||||
initializeExpiringMessageManager();
|
||||
initializeRevealableMessageManager();
|
||||
initializeTypingStatusRepository();
|
||||
initializeTypingStatusSender();
|
||||
initializeGcmCheck();
|
||||
initializeSignedPreKeyCheck();
|
||||
initializePeriodicTasks();
|
||||
initializeCircumvention();
|
||||
initializeRingRtc();
|
||||
initializePendingMessages();
|
||||
initializeBlobProvider();
|
||||
initializeCameraX();
|
||||
NotificationChannels.create(this);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().beginJobLoop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
isAppVisible = true;
|
||||
Log.i(TAG, "App is now visible.");
|
||||
ApplicationDependencies.getRecipientCache().warmUp();
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getFrameRateTracker().begin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(@NonNull LifecycleOwner owner) {
|
||||
isAppVisible = false;
|
||||
Log.i(TAG, "App is no longer visible.");
|
||||
KeyCachingService.onAppBackgrounded(this);
|
||||
MessageNotifier.setVisibleThread(-1);
|
||||
ApplicationDependencies.getFrameRateTracker().end();
|
||||
}
|
||||
|
||||
public ExpiringMessageManager getExpiringMessageManager() {
|
||||
return expiringMessageManager;
|
||||
}
|
||||
|
||||
public ViewOnceMessageManager getViewOnceMessageManager() {
|
||||
return viewOnceMessageManager;
|
||||
}
|
||||
|
||||
public TypingStatusRepository getTypingStatusRepository() {
|
||||
return typingStatusRepository;
|
||||
}
|
||||
|
||||
public TypingStatusSender getTypingStatusSender() {
|
||||
return typingStatusSender;
|
||||
}
|
||||
|
||||
public boolean isAppVisible() {
|
||||
return isAppVisible;
|
||||
}
|
||||
|
||||
public PersistentLogger getPersistentLogger() {
|
||||
return persistentLogger;
|
||||
}
|
||||
|
||||
private void initializeSecurityProvider() {
|
||||
try {
|
||||
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
|
||||
} catch (ClassNotFoundException e) {
|
||||
Log.e(TAG, "Failed to find AesGcmCipher class");
|
||||
throw new ProviderInitializationException();
|
||||
}
|
||||
|
||||
int aesPosition = Security.insertProviderAt(new AesGcmProvider(), 1);
|
||||
Log.i(TAG, "Installed AesGcmProvider: " + aesPosition);
|
||||
|
||||
if (aesPosition < 0) {
|
||||
Log.e(TAG, "Failed to install AesGcmProvider()");
|
||||
throw new ProviderInitializationException();
|
||||
}
|
||||
|
||||
int conscryptPosition = Security.insertProviderAt(Conscrypt.newProvider(), 2);
|
||||
Log.i(TAG, "Installed Conscrypt provider: " + conscryptPosition);
|
||||
|
||||
if (conscryptPosition < 0) {
|
||||
Log.w(TAG, "Did not install Conscrypt provider. May already be present.");
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeLogging() {
|
||||
persistentLogger = new PersistentLogger(this);
|
||||
org.thoughtcrime.securesms.logging.Log.initialize(new AndroidLogger(), persistentLogger);
|
||||
|
||||
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
|
||||
}
|
||||
|
||||
private void initializeCrashHandling() {
|
||||
final Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(originalHandler));
|
||||
}
|
||||
|
||||
private void initializeApplicationMigrations() {
|
||||
ApplicationMigrations.onApplicationCreate(this, ApplicationDependencies.getJobManager());
|
||||
}
|
||||
|
||||
public void initializeMessageRetrieval() {
|
||||
this.incomingMessageObserver = new IncomingMessageObserver(this);
|
||||
}
|
||||
|
||||
private void initializeAppDependencies() {
|
||||
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this, new SignalServiceNetworkAccess(this)));
|
||||
}
|
||||
|
||||
private void initializeFirstEverAppLaunch() {
|
||||
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
|
||||
if (!SQLCipherOpenHelper.databaseFileExists(this)) {
|
||||
Log.i(TAG, "First ever app launch!");
|
||||
|
||||
InsightsOptOut.userRequestedOptOut(this);
|
||||
TextSecurePreferences.setAppMigrationVersion(this, ApplicationMigrations.CURRENT_VERSION);
|
||||
TextSecurePreferences.setJobManagerVersion(this, JobManager.CURRENT_VERSION);
|
||||
TextSecurePreferences.setLastExperienceVersionCode(this, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(this, true);
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
}
|
||||
|
||||
Log.i(TAG, "Setting first install version to " + BuildConfig.CANONICAL_VERSION_CODE);
|
||||
TextSecurePreferences.setFirstInstallVersion(this, BuildConfig.CANONICAL_VERSION_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeGcmCheck() {
|
||||
if (TextSecurePreferences.isPushRegistered(this)) {
|
||||
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6);
|
||||
|
||||
if (TextSecurePreferences.getFcmToken(this) == null || nextSetTime <= System.currentTimeMillis()) {
|
||||
ApplicationDependencies.getJobManager().add(new FcmRefreshJob());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSignedPreKeyCheck() {
|
||||
if (!TextSecurePreferences.isSignedPreKeyRegistered(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new CreateSignedPreKeyJob(this));
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeExpiringMessageManager() {
|
||||
this.expiringMessageManager = new ExpiringMessageManager(this);
|
||||
}
|
||||
|
||||
private void initializeRevealableMessageManager() {
|
||||
this.viewOnceMessageManager = new ViewOnceMessageManager(this);
|
||||
}
|
||||
|
||||
private void initializeTypingStatusRepository() {
|
||||
this.typingStatusRepository = new TypingStatusRepository();
|
||||
}
|
||||
|
||||
private void initializeTypingStatusSender() {
|
||||
this.typingStatusSender = new TypingStatusSender(this);
|
||||
}
|
||||
|
||||
private void initializePeriodicTasks() {
|
||||
RotateSignedPreKeyListener.schedule(this);
|
||||
DirectoryRefreshListener.schedule(this);
|
||||
LocalBackupListener.schedule(this);
|
||||
RotateSenderCertificateListener.schedule(this);
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
UpdateApkRefreshListener.schedule(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeRingRtc() {
|
||||
try {
|
||||
Set<String> HARDWARE_AEC_BLACKLIST = new HashSet<String>() {{
|
||||
add("Pixel");
|
||||
add("Pixel XL");
|
||||
add("Moto G5");
|
||||
add("Moto G (5S) Plus");
|
||||
add("Moto G4");
|
||||
add("TA-1053");
|
||||
add("Mi A1");
|
||||
add("E5823"); // Sony z5 compact
|
||||
add("Redmi Note 5");
|
||||
add("FP2"); // Fairphone FP2
|
||||
add("MI 5");
|
||||
}};
|
||||
|
||||
Set<String> OPEN_SL_ES_WHITELIST = new HashSet<String>() {{
|
||||
add("Pixel");
|
||||
add("Pixel XL");
|
||||
}};
|
||||
|
||||
if (HARDWARE_AEC_BLACKLIST.contains(Build.MODEL)) {
|
||||
WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
|
||||
}
|
||||
|
||||
if (!OPEN_SL_ES_WHITELIST.contains(Build.MODEL)) {
|
||||
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
|
||||
}
|
||||
|
||||
CallConnectionFactory.initialize(this, new RingRtcLogger());
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void initializeCircumvention() {
|
||||
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(ApplicationContext.this);
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, t);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void executePendingContactSync() {
|
||||
if (TextSecurePreferences.needsFullContactSync(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
|
||||
}
|
||||
}
|
||||
|
||||
private void initializePendingMessages() {
|
||||
if (TextSecurePreferences.getNeedsMessagePull(this)) {
|
||||
Log.i(TAG, "Scheduling a message fetch.");
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
FcmJobService.schedule(this);
|
||||
} else {
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob(this));
|
||||
}
|
||||
TextSecurePreferences.setNeedsMessagePull(this, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBlobProvider() {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
BlobProvider.getInstance().onSessionStart(this);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private void initializeCameraX() {
|
||||
if (CameraXUtil.isSupported()) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
CameraX.init(this, Camera2AppConfig.create(this));
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "Failed to initialize CameraX.");
|
||||
}
|
||||
}, "signal-camerax-initialization").start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
||||
}
|
||||
|
||||
private static class ProviderInitializationException extends RuntimeException {
|
||||
}
|
||||
}
|
||||
@@ -17,22 +17,17 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.graphics.drawable.DrawableCompat;
|
||||
import android.support.v7.preference.Preference;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
@@ -41,11 +36,15 @@ 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.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.usernames.ProfileEditActivityV2;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The Activity for application preference display and management.
|
||||
@@ -66,6 +65,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
|
||||
private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance";
|
||||
private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats";
|
||||
private static final String PREFERENCE_CATEGORY_STORAGE = "preference_category_storage";
|
||||
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
|
||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
|
||||
@@ -111,7 +111,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
if (fragmentManager.getBackStackEntryCount() > 0) {
|
||||
fragmentManager.popBackStack();
|
||||
} else {
|
||||
Intent intent = new Intent(this, ConversationListActivity.class);
|
||||
// TODO [greyson] Navigation
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
@@ -150,14 +151,21 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
|
||||
this.findPreference(PREFERENCE_CATEGORY_CHATS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS));
|
||||
this.findPreference(PREFERENCE_CATEGORY_STORAGE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_STORAGE));
|
||||
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
|
||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
|
||||
if (VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
tintIcons(getActivity());
|
||||
}
|
||||
tintIcons();
|
||||
}
|
||||
|
||||
private void tintIcons() {
|
||||
if (Build.VERSION.SDK_INT >= 21) return;
|
||||
|
||||
Preference preference = this.findPreference(PREFERENCE_CATEGORY_SMS_MMS);
|
||||
preference.getIcon().setColorFilter(ThemeUtil.getThemedColor(requireContext(), R.attr.icon_tint), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -196,38 +204,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(11)
|
||||
private void tintIcons(Context context) {
|
||||
Drawable sms = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.ic_textsms_white_24dp));
|
||||
Drawable notifications = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.ic_notifications_white_24dp));
|
||||
Drawable privacy = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.ic_security_white_24dp));
|
||||
Drawable appearance = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.ic_brightness_6_white_24dp));
|
||||
Drawable chats = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.ic_forum_white_24dp));
|
||||
Drawable devices = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.ic_laptop_white_24dp));
|
||||
Drawable advanced = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.ic_advanced_white_24dp));
|
||||
|
||||
int[] tintAttr = new int[]{R.attr.pref_icon_tint};
|
||||
TypedArray typedArray = context.obtainStyledAttributes(tintAttr);
|
||||
int color = typedArray.getColor(0, 0x0);
|
||||
typedArray.recycle();
|
||||
|
||||
DrawableCompat.setTint(sms, color);
|
||||
DrawableCompat.setTint(notifications, color);
|
||||
DrawableCompat.setTint(privacy, color);
|
||||
DrawableCompat.setTint(appearance, color);
|
||||
DrawableCompat.setTint(chats, color);
|
||||
DrawableCompat.setTint(devices, color);
|
||||
DrawableCompat.setTint(advanced, color);
|
||||
|
||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS).setIcon(sms);
|
||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS).setIcon(notifications);
|
||||
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION).setIcon(privacy);
|
||||
this.findPreference(PREFERENCE_CATEGORY_APPEARANCE).setIcon(appearance);
|
||||
this.findPreference(PREFERENCE_CATEGORY_CHATS).setIcon(chats);
|
||||
this.findPreference(PREFERENCE_CATEGORY_DEVICES).setIcon(devices);
|
||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED).setIcon(advanced);
|
||||
}
|
||||
|
||||
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
|
||||
private String category;
|
||||
|
||||
@@ -255,6 +231,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
case PREFERENCE_CATEGORY_CHATS:
|
||||
fragment = new ChatsPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_STORAGE:
|
||||
fragment = new StoragePreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_DEVICES:
|
||||
Intent intent = new Intent(getActivity(), DeviceActivity.class);
|
||||
startActivity(intent);
|
||||
@@ -272,6 +251,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
|
||||
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();
|
||||
@@ -284,11 +266,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent intent = new Intent(preference.getContext(), CreateProfileActivity.class);
|
||||
intent.putExtra(CreateProfileActivity.EXCLUDE_SYSTEM, true);
|
||||
if (FeatureFlags.USERNAMES) {
|
||||
requireActivity().startActivity(ProfileEditActivityV2.getLaunchIntent(requireContext()));
|
||||
} else {
|
||||
Intent intent = new Intent(preference.getContext(), CreateProfileActivity.class);
|
||||
intent.putExtra(CreateProfileActivity.EXCLUDE_SYSTEM, true);
|
||||
|
||||
getActivity().startActivity(intent);
|
||||
// ((BaseActionBarActivity)getActivity()).startActivitySceneTransition(intent, getActivity().findViewById(R.id.avatar), "avatar");
|
||||
requireActivity().startActivity(intent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
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 android.support.annotation.NonNull;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
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;
|
||||
@@ -16,6 +17,8 @@ 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;
|
||||
|
||||
@@ -35,6 +38,7 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
initializeScreenshotSecurity();
|
||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,9 +56,7 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void initializeScreenshotSecurity() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH &&
|
||||
TextSecurePreferences.isScreenSecurityEnabled(this))
|
||||
{
|
||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
@@ -92,4 +94,8 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
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 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);
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -41,7 +42,7 @@ public class BasicIntroFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.color_fragment, container, false);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.View;
|
||||
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
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.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable {
|
||||
void bind(@NonNull MessageRecord 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);
|
||||
|
||||
MessageRecord getMessageRecord();
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
interface EventListener {
|
||||
void onQuoteClicked(MmsMessageRecord messageRecord);
|
||||
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
|
||||
void onMoreTextClicked(@NonNull RecipientId conversationRecipientId, long messageId, boolean isMms);
|
||||
void onStickerClicked(@NonNull StickerLocator stickerLocator);
|
||||
void onViewOnceMessageClicked(@NonNull MmsMessageRecord messageRecord);
|
||||
void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView);
|
||||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(long messageId, boolean isMms);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
@@ -4,12 +4,12 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.cursoradapter.widget.CursorAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
@@ -17,12 +17,14 @@ import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.BlockedContactsLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.preferences.BlockedContactListItem;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
@@ -78,6 +80,12 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
getLoaderManager().restartLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle bundle) {
|
||||
super.onActivityCreated(bundle);
|
||||
@@ -85,19 +93,19 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new BlockedContactsLoader(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
if (getListAdapter() != null) {
|
||||
((CursorAdapter) getListAdapter()).changeCursor(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
if (getListAdapter() != null) {
|
||||
((CursorAdapter) getListAdapter()).changeCursor(null);
|
||||
}
|
||||
@@ -107,7 +115,7 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
Recipient recipient = ((BlockedContactListItem)view).getRecipient();
|
||||
Intent intent = new Intent(getActivity(), RecipientPreferenceActivity.class);
|
||||
intent.putExtra(RecipientPreferenceActivity.ADDRESS_EXTRA, recipient.getAddress());
|
||||
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, recipient.getId());
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
@@ -129,8 +137,8 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
String address = cursor.getString(1);
|
||||
Recipient recipient = Recipient.from(context, Address.fromSerialized(address), true);
|
||||
RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RecipientDatabase.ID)));
|
||||
LiveRecipient recipient = Recipient.live(recipientId);
|
||||
|
||||
((BlockedContactListItem) view).set(glideRequests, recipient);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
public class ClearProfileAvatarActivity extends Activity {
|
||||
|
||||
@@ -5,14 +5,14 @@ import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
@@ -20,12 +20,16 @@ import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.VerifySpan;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
@@ -46,9 +50,9 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
{
|
||||
super(context);
|
||||
|
||||
Recipient recipient = Recipient.from(context, mismatch.getAddress(), false);
|
||||
String name = recipient.toShortString();
|
||||
String introduction = String.format(context.getString(R.string.ConfirmIdentityDialog_your_safety_number_with_s_has_changed), name, name);
|
||||
Recipient recipient = Recipient.resolved(mismatch.getRecipientId(context));
|
||||
String name = recipient.toShortString(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));
|
||||
|
||||
@@ -59,7 +63,7 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
setTitle(name);
|
||||
setMessage(spannableString);
|
||||
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.ConfirmIdentityDialog_accept), new AcceptListener(messageRecord, mismatch, recipient.getAddress()));
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.ConfirmIdentityDialog_accept), new AcceptListener(messageRecord, mismatch, recipient.getId()));
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(android.R.string.cancel), new CancelListener());
|
||||
}
|
||||
|
||||
@@ -78,12 +82,12 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
|
||||
private final MessageRecord messageRecord;
|
||||
private final IdentityKeyMismatch mismatch;
|
||||
private final Address address;
|
||||
private final RecipientId recipientId;
|
||||
|
||||
private AcceptListener(MessageRecord messageRecord, IdentityKeyMismatch mismatch, Address address) {
|
||||
private AcceptListener(MessageRecord messageRecord, IdentityKeyMismatch mismatch, RecipientId recipientId) {
|
||||
this.messageRecord = messageRecord;
|
||||
this.mismatch = mismatch;
|
||||
this.address = address;
|
||||
this.recipientId = recipientId;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@@ -94,7 +98,7 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(address.toPhoneString(), 1);
|
||||
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(Recipient.resolved(recipientId).requireServiceId(), 1);
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(getContext());
|
||||
|
||||
identityKeyStore.saveIdentity(mismatchAddress, mismatch.getIdentityKey(), true);
|
||||
@@ -137,17 +141,17 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getAddress(),
|
||||
mismatch.getRecipientId(getContext()),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
if (messageRecord.getRecipient().isPushGroupRecipient()) {
|
||||
MessageSender.resendGroupMessage(getContext(), messageRecord, mismatch.getAddress());
|
||||
if (messageRecord.getRecipient().isPushGroup()) {
|
||||
MessageSender.resendGroupMessage(getContext(), messageRecord, Recipient.resolved(mismatch.getRecipientId(getContext())).getId());
|
||||
} else {
|
||||
MessageSender.resend(getContext(), messageRecord);
|
||||
}
|
||||
} else {
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getAddress(),
|
||||
mismatch.getRecipientId(getContext()),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
MessageSender.resend(getContext(), messageRecord);
|
||||
@@ -160,13 +164,13 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getAddress(),
|
||||
mismatch.getRecipientId(getContext()),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
boolean legacy = !messageRecord.isContentBundleKeyExchange();
|
||||
|
||||
SignalServiceEnvelope envelope = new SignalServiceEnvelope(SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE,
|
||||
messageRecord.getIndividualRecipient().getAddress().toPhoneString(),
|
||||
Optional.of(RecipientUtil.toSignalServiceAddress(getContext(), messageRecord.getIndividualRecipient())),
|
||||
messageRecord.getRecipientDeviceId(),
|
||||
messageRecord.getDateSent(),
|
||||
legacy ? Base64.decode(messageRecord.getBody()) : null,
|
||||
@@ -175,9 +179,7 @@ public class ConfirmIdentityDialog extends AlertDialog {
|
||||
|
||||
long pushId = pushDatabase.insert(envelope);
|
||||
|
||||
ApplicationContext.getInstance(getContext())
|
||||
.getJobManager()
|
||||
.add(new PushDecryptJob(getContext(), pushId, messageRecord.getId()));
|
||||
ApplicationDependencies.getJobManager().add(new PushDecryptMessageJob(getContext(), pushId, messageRecord.getId()));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
@@ -19,17 +19,21 @@ package org.thoughtcrime.securesms;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.util.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
@@ -63,7 +67,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
int displayMode = TextSecurePreferences.isSmsEnabled(this) ? DisplayMode.FLAG_ALL
|
||||
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_GROUPS;
|
||||
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS;
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
}
|
||||
|
||||
@@ -112,10 +116,10 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(String number) {}
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(String number) {}
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {}
|
||||
|
||||
private static class RefreshDirectoryTask extends AsyncTask<Context, Void, Void> {
|
||||
|
||||
@@ -127,7 +131,6 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Context... params) {
|
||||
|
||||
try {
|
||||
DirectoryHelper.refreshDirectory(params[0], true);
|
||||
} catch (IOException e) {
|
||||
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* 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.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Fragment for selecting a one or more contacts from a list.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public final class ContactSelectionListFragment extends Fragment
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
|
||||
|
||||
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";
|
||||
|
||||
private TextView emptyText;
|
||||
private Set<SelectedContact> selectedContacts;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private View showContactsLayout;
|
||||
private Button showContactsButton;
|
||||
private TextView showContactsDescription;
|
||||
private ProgressWheel showContactsProgress;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerViewFastScroller fastScroller;
|
||||
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
|
||||
|
||||
@Nullable private FixedViewsAdapter footerAdapter;
|
||||
@Nullable private InviteCallback inviteCallback;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (context instanceof InviteCallback) {
|
||||
inviteCallback = (InviteCallback) context;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle icicle) {
|
||||
super.onActivityCreated(icicle);
|
||||
|
||||
initializeCursor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
|
||||
handleContactPermissionGranted();
|
||||
} else {
|
||||
this.getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
})
|
||||
.onAnyDenied(() -> {
|
||||
getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||
|
||||
if (getActivity().getIntent().getBooleanExtra(RECENTS, false)) {
|
||||
getLoaderManager().initLoader(0, null, ContactSelectionListFragment.this);
|
||||
} else {
|
||||
initializeNoContactsPermission();
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
|
||||
|
||||
emptyText = ViewUtil.findById(view, android.R.id.empty);
|
||||
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
|
||||
swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh);
|
||||
fastScroller = ViewUtil.findById(view, R.id.fast_scroller);
|
||||
showContactsLayout = view.findViewById(R.id.show_contacts_container);
|
||||
showContactsButton = view.findViewById(R.id.show_contacts_button);
|
||||
showContactsDescription = view.findViewById(R.id.show_contacts_description);
|
||||
showContactsProgress = view.findViewById(R.id.progress);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
|
||||
swipeRefresh.setEnabled(getActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
public @NonNull List<SelectedContact> getSelectedContacts() {
|
||||
List<SelectedContact> selected = new LinkedList<>();
|
||||
if (selectedContacts != null) {
|
||||
selected.addAll(selectedContacts);
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
private boolean isMulti() {
|
||||
return getActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
|
||||
}
|
||||
|
||||
private void initializeCursor() {
|
||||
cursorRecyclerViewAdapter = new ContactSelectionListAdapter(requireContext(),
|
||||
GlideApp.with(this),
|
||||
null,
|
||||
new ListClickListener(),
|
||||
isMulti());
|
||||
selectedContacts = cursorRecyclerViewAdapter.getSelectedContacts();
|
||||
|
||||
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
|
||||
|
||||
concatenateAdapter.addAdapter(cursorRecyclerViewAdapter);
|
||||
if (inviteCallback != null) {
|
||||
footerAdapter = new FixedViewsAdapter(createInviteActionView(inviteCallback));
|
||||
footerAdapter.hide();
|
||||
concatenateAdapter.addAdapter(footerAdapter);
|
||||
}
|
||||
|
||||
recyclerView.setAdapter(concatenateAdapter);
|
||||
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true));
|
||||
}
|
||||
|
||||
private View createInviteActionView(@NonNull InviteCallback inviteCallback) {
|
||||
View view = LayoutInflater.from(requireContext())
|
||||
.inflate(R.layout.contact_selection_invite_action_item, (ViewGroup) requireView(), false);
|
||||
view.setOnClickListener(v -> inviteCallback.onInvite());
|
||||
return view;
|
||||
}
|
||||
|
||||
private void initializeNoContactsPermission() {
|
||||
swipeRefresh.setVisibility(View.GONE);
|
||||
|
||||
showContactsLayout.setVisibility(View.VISIBLE);
|
||||
showContactsProgress.setVisibility(View.INVISIBLE);
|
||||
showContactsDescription.setText(R.string.contact_selection_list_fragment__signal_needs_access_to_your_contacts_in_order_to_display_them);
|
||||
showContactsButton.setVisibility(View.VISIBLE);
|
||||
|
||||
showContactsButton.setOnClickListener(v -> {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts))
|
||||
.onSomeGranted(permissions -> {
|
||||
if (permissions.contains(Manifest.permission.WRITE_CONTACTS)) {
|
||||
handleContactPermissionGranted();
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
public void setQueryFilter(String filter) {
|
||||
this.cursorFilter = filter;
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
}
|
||||
|
||||
public void resetQueryFilter() {
|
||||
setQueryFilter(null);
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void setRefreshing(boolean refreshing) {
|
||||
swipeRefresh.setRefreshing(refreshing);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
selectedContacts.clear();
|
||||
|
||||
if (!isDetached() && !isRemoving() && getActivity() != null && !getActivity().isFinishing()) {
|
||||
getLoaderManager().restartLoader(0, null, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new ContactsCursorLoader(getActivity(),
|
||||
getActivity().getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL),
|
||||
cursorFilter, getActivity().getIntent().getBooleanExtra(RECENTS, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, @Nullable Cursor data) {
|
||||
swipeRefresh.setVisibility(View.VISIBLE);
|
||||
showContactsLayout.setVisibility(View.GONE);
|
||||
|
||||
cursorRecyclerViewAdapter.changeCursor(data);
|
||||
|
||||
if (footerAdapter != null) {
|
||||
footerAdapter.show();
|
||||
}
|
||||
|
||||
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
|
||||
boolean useFastScroller = data != null && data.getCount() > 20;
|
||||
recyclerView.setVerticalScrollBarEnabled(!useFastScroller);
|
||||
if (useFastScroller) {
|
||||
fastScroller.setVisibility(View.VISIBLE);
|
||||
fastScroller.setRecyclerView(recyclerView);
|
||||
} else {
|
||||
fastScroller.setRecyclerView(null);
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
cursorRecyclerViewAdapter.changeCursor(null);
|
||||
fastScroller.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleContactPermissionGranted() {
|
||||
final Context context = requireContext();
|
||||
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
swipeRefresh.setVisibility(View.GONE);
|
||||
showContactsLayout.setVisibility(View.VISIBLE);
|
||||
showContactsButton.setVisibility(View.INVISIBLE);
|
||||
showContactsDescription.setText(R.string.ConversationListFragment_loading);
|
||||
showContactsProgress.setVisibility(View.VISIBLE);
|
||||
showContactsProgress.spin();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... voids) {
|
||||
try {
|
||||
DirectoryHelper.refreshDirectory(context, false);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
if (result) {
|
||||
showContactsLayout.setVisibility(View.GONE);
|
||||
swipeRefresh.setVisibility(View.VISIBLE);
|
||||
reset();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
|
||||
initializeNoContactsPermission();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
|
||||
@Override
|
||||
public void onItemClick(ContactSelectionListItem contact) {
|
||||
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
|
||||
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
|
||||
|
||||
if (!isMulti() || !selectedContacts.contains(selectedContact)) {
|
||||
if (contact.isUsernameType()) {
|
||||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
return UsernameUtil.fetchUuidForUsername(requireContext(), contact.getNumber());
|
||||
}, uuid -> {
|
||||
loadingDialog.dismiss();
|
||||
if (uuid.isPresent()) {
|
||||
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
|
||||
selectedContacts.add(SelectedContact.forUsername(recipient.getId(), contact.getNumber()));
|
||||
contact.setChecked(true);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null);
|
||||
}
|
||||
} 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())
|
||||
.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
selectedContacts.add(selectedContact);
|
||||
contact.setChecked(true);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedContacts.remove(selectedContact);
|
||||
contact.setChecked(false);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
|
||||
this.onContactSelectedListener = onContactSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener onRefreshListener) {
|
||||
this.swipeRefresh.setOnRefreshListener(onRefreshListener);
|
||||
}
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
void onContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
||||
}
|
||||
|
||||
public interface InviteCallback {
|
||||
void onInvite();
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,10 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -54,12 +55,12 @@ public class CountrySelectionFragment extends ListFragment implements LoaderMana
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<ArrayList<Map<String, String>>> onCreateLoader(int arg0, Bundle arg1) {
|
||||
public @NonNull Loader<ArrayList<Map<String, String>>> onCreateLoader(int arg0, Bundle arg1) {
|
||||
return new CountryListLoader(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<ArrayList<Map<String, String>>> loader,
|
||||
public void onLoadFinished(@NonNull Loader<ArrayList<Map<String, String>>> loader,
|
||||
ArrayList<Map<String, String>> results)
|
||||
{
|
||||
String[] from = {"country_name", "country_code"};
|
||||
@@ -72,7 +73,7 @@ public class CountrySelectionFragment extends ListFragment implements LoaderMana
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<ArrayList<Map<String, String>>> arg0) {
|
||||
public void onLoaderReset(@NonNull Loader<ArrayList<Map<String, String>>> arg0) {
|
||||
this.setListAdapter(null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.Animator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||
import org.thoughtcrime.securesms.components.LabeledEditText;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
|
||||
import org.thoughtcrime.securesms.profiles.SystemProfileUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class CreateProfileActivity extends BaseActionBarActivity {
|
||||
|
||||
private static final String TAG = CreateProfileActivity.class.getSimpleName();
|
||||
|
||||
public static final String NEXT_INTENT = "next_intent";
|
||||
public static final String EXCLUDE_SYSTEM = "exclude_system";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private InputAwareLayout container;
|
||||
private ImageView avatar;
|
||||
private CircularProgressButton finishButton;
|
||||
private LabeledEditText name;
|
||||
private EmojiToggle emojiToggle;
|
||||
private MediaKeyboard mediaKeyboard;
|
||||
private View reveal;
|
||||
|
||||
private Intent nextIntent;
|
||||
private byte[] avatarBytes;
|
||||
private File captureFile;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
|
||||
setContentView(R.layout.profile_create_activity);
|
||||
|
||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
|
||||
initializeResources();
|
||||
initializeEmojiInput();
|
||||
initializeProfileName(getIntent().getBooleanExtra(EXCLUDE_SYSTEM, false));
|
||||
initializeProfileAvatar(getIntent().getBooleanExtra(EXCLUDE_SYSTEM, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (container.isInputOpen()) container.hideCurrentInput(name.getInput());
|
||||
else super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (container.getCurrentInput() == mediaKeyboard) {
|
||||
container.hideAttachedInput(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
switch (requestCode) {
|
||||
case AvatarSelection.REQUEST_CODE_AVATAR:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
Uri outputFile = Uri.fromFile(new File(getCacheDir(), "cropped"));
|
||||
Uri inputFile = (data != null ? data.getData() : null);
|
||||
|
||||
if (inputFile == null && captureFile != null) {
|
||||
inputFile = Uri.fromFile(captureFile);
|
||||
}
|
||||
|
||||
if (data != null && data.getBooleanExtra("delete", false)) {
|
||||
avatarBytes = null;
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_solid_white_24).asDrawable(this, getResources().getColor(R.color.grey_400)));
|
||||
} else {
|
||||
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
new AsyncTask<Void, Void, byte[]>() {
|
||||
@Override
|
||||
protected byte[] doInBackground(Void... params) {
|
||||
try {
|
||||
BitmapUtil.ScaleResult result = BitmapUtil.createScaledBytes(CreateProfileActivity.this, AvatarSelection.getResultUri(data), new ProfileMediaConstraints());
|
||||
return result.getBitmap();
|
||||
} catch (BitmapDecodingException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(byte[] result) {
|
||||
if (result != null) {
|
||||
avatarBytes = result;
|
||||
GlideApp.with(CreateProfileActivity.this)
|
||||
.load(avatarBytes)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
} else {
|
||||
Toast.makeText(CreateProfileActivity.this, R.string.CreateProfileActivity_error_setting_profile_photo, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
TextView skipButton = ViewUtil.findById(this, R.id.skip_button);
|
||||
|
||||
this.avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
this.name = ViewUtil.findById(this, R.id.name);
|
||||
this.emojiToggle = ViewUtil.findById(this, R.id.emoji_toggle);
|
||||
this.mediaKeyboard = ViewUtil.findById(this, R.id.emoji_drawer);
|
||||
this.container = ViewUtil.findById(this, R.id.container);
|
||||
this.finishButton = ViewUtil.findById(this, R.id.finish_button);
|
||||
this.reveal = ViewUtil.findById(this, R.id.reveal);
|
||||
this.nextIntent = getIntent().getParcelableExtra(NEXT_INTENT);
|
||||
|
||||
this.avatar.setOnClickListener(view -> Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.onAnyResult(this::startAvatarSelection)
|
||||
.execute());
|
||||
|
||||
this.name.getInput().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (s.toString().getBytes().length > ProfileCipher.NAME_PADDED_LENGTH) {
|
||||
name.getInput().setError(getString(R.string.CreateProfileActivity_too_long));
|
||||
finishButton.setEnabled(false);
|
||||
} else if (name.getInput().getError() != null || !finishButton.isEnabled()) {
|
||||
name.getInput().setError(null);
|
||||
finishButton.setEnabled(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.finishButton.setOnClickListener(view -> {
|
||||
this.finishButton.setIndeterminateProgressMode(true);
|
||||
this.finishButton.setProgress(50);
|
||||
handleUpload();
|
||||
});
|
||||
|
||||
skipButton.setOnClickListener(view -> {
|
||||
if (nextIntent != null) startActivity(nextIntent);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeProfileName(boolean excludeSystem) {
|
||||
if (!TextUtils.isEmpty(TextSecurePreferences.getProfileName(this))) {
|
||||
String profileName = TextSecurePreferences.getProfileName(this);
|
||||
|
||||
name.setText(profileName);
|
||||
name.getInput().setSelection(profileName.length(), profileName.length());
|
||||
} else if (!excludeSystem) {
|
||||
SystemProfileUtil.getSystemProfileName(this).addListener(new ListenableFuture.Listener<String>() {
|
||||
@Override
|
||||
public void onSuccess(String result) {
|
||||
if (!TextUtils.isEmpty(result)) {
|
||||
name.setText(result);
|
||||
name.getInput().setSelection(result.length(), result.length());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeProfileAvatar(boolean excludeSystem) {
|
||||
RecipientId selfId = Recipient.self().getId();
|
||||
|
||||
if (AvatarHelper.getAvatarFile(this, selfId).exists() && AvatarHelper.getAvatarFile(this, selfId).length() > 0) {
|
||||
new AsyncTask<Void, Void, byte[]>() {
|
||||
@Override
|
||||
protected byte[] doInBackground(Void... params) {
|
||||
try {
|
||||
return Util.readFully(AvatarHelper.getInputStreamFor(CreateProfileActivity.this, selfId));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(byte[] result) {
|
||||
if (result != null) {
|
||||
avatarBytes = result;
|
||||
GlideApp.with(CreateProfileActivity.this)
|
||||
.load(result)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
} else if (!excludeSystem) {
|
||||
SystemProfileUtil.getSystemProfileAvatar(this, new ProfileMediaConstraints()).addListener(new ListenableFuture.Listener<byte[]>() {
|
||||
@Override
|
||||
public void onSuccess(byte[] result) {
|
||||
if (result != null) {
|
||||
avatarBytes = result;
|
||||
GlideApp.with(CreateProfileActivity.this)
|
||||
.load(result)
|
||||
.circleCrop()
|
||||
.into(avatar);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeEmojiInput() {
|
||||
this.emojiToggle.attach(mediaKeyboard);
|
||||
|
||||
this.emojiToggle.setOnClickListener(v -> {
|
||||
if (container.getCurrentInput() == mediaKeyboard) {
|
||||
container.showSoftkey(name.getInput());
|
||||
} else {
|
||||
container.show(name.getInput(), mediaKeyboard);
|
||||
}
|
||||
});
|
||||
|
||||
this.mediaKeyboard.setProviders(0, new EmojiKeyboardProvider(this, new EmojiKeyboardProvider.EmojiEventListener() {
|
||||
@Override
|
||||
public void onKeyEvent(KeyEvent keyEvent) {
|
||||
name.dispatchKeyEvent(keyEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
final int start = name.getInput().getSelectionStart();
|
||||
final int end = name.getInput().getSelectionEnd();
|
||||
|
||||
name.getText().replace(Math.min(start, end), Math.max(start, end), emoji);
|
||||
name.getInput().setSelection(start + emoji.length());
|
||||
}
|
||||
}));
|
||||
|
||||
this.container.addOnKeyboardShownListener(() -> emojiToggle.setToMedia());
|
||||
this.name.setOnClickListener(v -> container.showSoftkey(name.getInput()));
|
||||
}
|
||||
|
||||
private void startAvatarSelection() {
|
||||
captureFile = AvatarSelection.startAvatarSelection(this, avatarBytes != null, true);
|
||||
}
|
||||
|
||||
private void handleUpload() {
|
||||
final String name;
|
||||
final StreamDetails avatar;
|
||||
|
||||
if (TextUtils.isEmpty(this.name.getText().toString())) name = null;
|
||||
else name = this.name.getText().toString();
|
||||
|
||||
if (avatarBytes == null || avatarBytes.length == 0) avatar = null;
|
||||
else avatar = new StreamDetails(new ByteArrayInputStream(avatarBytes),
|
||||
"image/jpeg", avatarBytes.length);
|
||||
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
Context context = CreateProfileActivity.this;
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(CreateProfileActivity.this);
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
|
||||
try {
|
||||
accountManager.setProfileName(profileKey, name);
|
||||
TextSecurePreferences.setProfileName(context, name);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), name);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
accountManager.setProfileAvatar(profileKey, avatar);
|
||||
AvatarHelper.setAvatar(CreateProfileActivity.this, Recipient.self().getId(), avatarBytes);
|
||||
TextSecurePreferences.setProfileAvatarId(CreateProfileActivity.this, new SecureRandom().nextInt());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(Boolean result) {
|
||||
super.onPostExecute(result);
|
||||
|
||||
if (result) {
|
||||
if (captureFile != null) captureFile.delete();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) handleFinishedLollipop();
|
||||
else handleFinishedLegacy();
|
||||
} else {
|
||||
Toast.makeText(CreateProfileActivity.this, R.string.CreateProfileActivity_problem_setting_profile, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void handleFinishedLegacy() {
|
||||
finishButton.setProgress(0);
|
||||
if (nextIntent != null) startActivity(nextIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
private void handleFinishedLollipop() {
|
||||
int[] finishButtonLocation = new int[2];
|
||||
int[] revealLocation = new int[2];
|
||||
|
||||
finishButton.getLocationInWindow(finishButtonLocation);
|
||||
reveal.getLocationInWindow(revealLocation);
|
||||
|
||||
int finishX = finishButtonLocation[0] - revealLocation[0];
|
||||
int finishY = finishButtonLocation[1] - revealLocation[1];
|
||||
|
||||
finishX += finishButton.getWidth() / 2;
|
||||
finishY += finishButton.getHeight() / 2;
|
||||
|
||||
Animator animation = ViewAnimationUtils.createCircularReveal(reveal, finishX, finishY, 0f, (float) Math.max(reveal.getWidth(), reveal.getHeight()));
|
||||
animation.setDuration(500);
|
||||
animation.addListener(new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
finishButton.setProgress(0);
|
||||
if (nextIntent != null) startActivity(nextIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {}
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {}
|
||||
});
|
||||
|
||||
reveal.setVisibility(View.VISIBLE);
|
||||
animation.start();
|
||||
}
|
||||
}
|
||||
@@ -149,7 +149,8 @@ public class DatabaseMigrationActivity extends PassphraseRequiredActionBarActivi
|
||||
if (getIntent().hasExtra("next_intent")) {
|
||||
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
|
||||
} else {
|
||||
startActivity(new Intent(this, ConversationListActivity.class));
|
||||
// TODO [greyson] Navigation
|
||||
startActivity(new Intent(this, MainActivity.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,20 @@ import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.transition.TransitionInflater;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
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;
|
||||
@@ -61,6 +63,7 @@ public class DeviceActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle, boolean ready) {
|
||||
getSupportActionBar().setHomeAsUpIndicator(ContextCompat.getDrawable(this, R.drawable.ic_arrow_left_24));
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.AndroidManifest__linked_devices);
|
||||
this.deviceAddFragment = new DeviceAddFragment();
|
||||
@@ -75,6 +78,16 @@ public class DeviceActivity extends PassphraseRequiredActionBarActivity
|
||||
} else {
|
||||
initFragment(android.R.id.content, deviceListFragment, dynamicLanguage.getCurrentLocale());
|
||||
}
|
||||
|
||||
overridePendingTransition(R.anim.slide_from_end, R.anim.slide_to_start);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
if (isFinishing()) {
|
||||
overridePendingTransition(R.anim.slide_from_start, R.anim.slide_to_end);
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -165,7 +178,7 @@ public class DeviceActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
try {
|
||||
Context context = DeviceActivity.this;
|
||||
SignalServiceAccountManager accountManager = AccountManagerFactory.createManager(context);
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
String verificationCode = accountManager.getNewDeviceVerificationCode();
|
||||
String ephemeralId = uri.getQueryParameter("uuid");
|
||||
String publicKeyEncoded = uri.getQueryParameter("pub_key");
|
||||
@@ -5,7 +5,8 @@ import android.annotation.TargetApi;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
@@ -29,7 +30,7 @@ public class DeviceAddFragment extends Fragment {
|
||||
private ScanListener scanListener;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
|
||||
this.overlay = ViewUtil.findById(this.container, R.id.overlay);
|
||||
this.scannerView = ViewUtil.findById(this.container, R.id.scanner);
|
||||
@@ -3,7 +3,8 @@ package org.thoughtcrime.securesms;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -16,7 +17,7 @@ public class DeviceLinkFragment extends Fragment implements View.OnClickListener
|
||||
private Uri uri;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
|
||||
this.container = (LinearLayout) inflater.inflate(R.layout.device_link_fragment, container, false);
|
||||
this.container.findViewById(R.id.link_device).setOnClickListener(this);
|
||||
|
||||
@@ -31,6 +32,7 @@ public class DeviceLinkFragment extends Fragment implements View.OnClickListener
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfiguration) {
|
||||
super.onConfigurationChanged(newConfiguration);
|
||||
if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
container.setOrientation(LinearLayout.HORIZONTAL);
|
||||
} else {
|
||||
@@ -51,6 +53,6 @@ public class DeviceLinkFragment extends Fragment implements View.OnClickListener
|
||||
}
|
||||
|
||||
public interface LinkClickedListener {
|
||||
public void onLink(Uri uri);
|
||||
void onLink(Uri uri);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.devicelist.Device;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -25,7 +27,6 @@ import android.widget.Toast;
|
||||
import com.melnykov.fab.FloatingActionButton;
|
||||
|
||||
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -35,23 +36,19 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class DeviceListFragment extends ListFragment
|
||||
implements LoaderManager.LoaderCallbacks<List<Device>>,
|
||||
ListView.OnItemClickListener, InjectableType, Button.OnClickListener
|
||||
ListView.OnItemClickListener, Button.OnClickListener
|
||||
{
|
||||
|
||||
private static final String TAG = DeviceListFragment.class.getSimpleName();
|
||||
|
||||
@Inject
|
||||
SignalServiceAccountManager accountManager;
|
||||
|
||||
private Locale locale;
|
||||
private View empty;
|
||||
private View progressContainer;
|
||||
private FloatingActionButton addDeviceButton;
|
||||
private Button.OnClickListener addDeviceButtonListener;
|
||||
private SignalServiceAccountManager accountManager;
|
||||
private Locale locale;
|
||||
private View empty;
|
||||
private View progressContainer;
|
||||
private FloatingActionButton addDeviceButton;
|
||||
private Button.OnClickListener addDeviceButtonListener;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
@@ -62,7 +59,7 @@ public class DeviceListFragment extends ListFragment
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
ApplicationContext.getInstance(activity).injectDependencies(this);
|
||||
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,7 +86,7 @@ public class DeviceListFragment extends ListFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<List<Device>> onCreateLoader(int id, Bundle args) {
|
||||
public @NonNull Loader<List<Device>> onCreateLoader(int id, Bundle args) {
|
||||
empty.setVisibility(View.GONE);
|
||||
progressContainer.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -97,7 +94,7 @@ public class DeviceListFragment extends ListFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<List<Device>> loader, List<Device> data) {
|
||||
public void onLoadFinished(@NonNull Loader<List<Device>> loader, List<Device> data) {
|
||||
progressContainer.setVisibility(View.GONE);
|
||||
|
||||
if (data == null) {
|
||||
@@ -116,7 +113,7 @@ public class DeviceListFragment extends ListFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<List<Device>> loader) {
|
||||
public void onLoaderReset(@NonNull Loader<List<Device>> loader) {
|
||||
setListAdapter(null);
|
||||
}
|
||||
|
||||
@@ -165,6 +162,7 @@ public class DeviceListFragment extends ListFragment
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleDisconnectDevice(final long deviceId) {
|
||||
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
|
||||
R.string.DeviceListActivity_unlinking_device_no_ellipsis,
|
||||
@@ -174,10 +172,6 @@ public class DeviceListFragment extends ListFragment
|
||||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
accountManager.removeDevice(deviceId);
|
||||
|
||||
ApplicationContext.getInstance(getContext())
|
||||
.getJobManager()
|
||||
.add(new RefreshUnidentifiedDeliveryAbilityJob(getContext()));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
|
||||
@@ -210,7 +204,7 @@ public class DeviceListFragment extends ListFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
public @NonNull View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = ((Activity)getContext()).getLayoutInflater().inflate(resource, parent, false);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.view.Window;
|
||||
|
||||
public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActivity {
|
||||
@@ -7,19 +7,23 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import android.view.View;
|
||||
|
||||
import com.melnykov.fab.FloatingActionButton;
|
||||
import com.nineoldandroids.animation.ArgbEvaluator;
|
||||
|
||||
import org.thoughtcrime.securesms.IntroPagerAdapter.IntroPage;
|
||||
import org.thoughtcrime.securesms.database.model.Sticker;
|
||||
import org.thoughtcrime.securesms.experienceupgrades.StickersIntroFragment;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -29,11 +33,17 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ExperienceUpgradeActivity extends BaseActionBarActivity implements TypingIndicatorIntroFragment.Controller {
|
||||
public class ExperienceUpgradeActivity extends BaseActionBarActivity
|
||||
implements TypingIndicatorIntroFragment.Controller,
|
||||
LinkPreviewsIntroFragment.Controller,
|
||||
StickersIntroFragment.Controller
|
||||
{
|
||||
private static final String TAG = ExperienceUpgradeActivity.class.getSimpleName();
|
||||
private static final String DISMISS_ACTION = "org.thoughtcrime.securesms.ExperienceUpgradeActivity.DISMISS_ACTION";
|
||||
private static final int NOTIFICATION_ID = 1339;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
private enum ExperienceUpgrade {
|
||||
SIGNAL_REBRANDING(157,
|
||||
new IntroPage(0xFF2090EA,
|
||||
@@ -80,7 +90,21 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity implements
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
|
||||
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
|
||||
null,
|
||||
true);
|
||||
true),
|
||||
LINK_PREVIEWS(449,
|
||||
new IntroPage(0xFF2090EA, LinkPreviewsIntroFragment.newInstance()),
|
||||
R.string.ExperienceUpgradeActivity_introducing_link_previews,
|
||||
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
|
||||
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
|
||||
null,
|
||||
true),
|
||||
STICKERS(580,
|
||||
new IntroPage(0xFF2090EA, StickersIntroFragment.newInstance()),
|
||||
R.string.ExperienceUpgradeActivity_introducing_stickers,
|
||||
R.string.ExperienceUpgradeActivity_why_use_words_when_you_can_use_stickers,
|
||||
R.string.ExperienceUpgradeActivity_why_use_words_when_you_can_use_stickers,
|
||||
null,
|
||||
true);
|
||||
|
||||
private int version;
|
||||
private List<IntroPage> pages;
|
||||
@@ -150,7 +174,7 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity implements
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setStatusBarColor(getResources().getColor(R.color.signal_primary_dark));
|
||||
dynamicTheme.onCreate(this);
|
||||
|
||||
final Optional<ExperienceUpgrade> upgrade = getExperienceUpgrade(this);
|
||||
if (!upgrade.isPresent()) {
|
||||
@@ -175,14 +199,21 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity implements
|
||||
ServiceUtil.getNotificationManager(this).cancel(NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private void onContinue(Optional<ExperienceUpgrade> seenUpgrade) {
|
||||
ServiceUtil.getNotificationManager(this).cancel(NOTIFICATION_ID);
|
||||
int latestVersion = seenUpgrade.isPresent() ? seenUpgrade.get().getVersion()
|
||||
: Util.getCurrentApkReleaseVersion(this);
|
||||
: Util.getCanonicalVersionCode();
|
||||
TextSecurePreferences.setLastExperienceVersionCode(this, latestVersion);
|
||||
if (seenUpgrade.isPresent() && seenUpgrade.get().nextIntent != null) {
|
||||
Intent intent = new Intent(this, seenUpgrade.get().nextIntent);
|
||||
Intent nextIntent = new Intent(this, ConversationListActivity.class);
|
||||
// TODO [greyson] Navigation
|
||||
Intent nextIntent = new Intent(this, MainActivity.class);
|
||||
intent.putExtra("next_intent", nextIntent);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
@@ -197,7 +228,7 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity implements
|
||||
}
|
||||
|
||||
public static Optional<ExperienceUpgrade> getExperienceUpgrade(Context context) {
|
||||
final int currentVersionCode = Util.getCurrentApkReleaseVersion(context);
|
||||
final int currentVersionCode = Util.getCanonicalVersionCode();
|
||||
final int lastSeenVersion = TextSecurePreferences.getLastExperienceVersionCode(context);
|
||||
Log.i(TAG, "getExperienceUpgrade(" + lastSeenVersion + ")");
|
||||
|
||||
@@ -215,33 +246,18 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished() {
|
||||
public void onTypingIndicatorsFinished() {
|
||||
onContinue(Optional.of(ExperienceUpgrade.TYPING_INDICATORS));
|
||||
}
|
||||
|
||||
private final class OnPageChangeListener implements ViewPager.OnPageChangeListener {
|
||||
private final ArgbEvaluator evaluator = new ArgbEvaluator();
|
||||
private final ExperienceUpgrade upgrade;
|
||||
@Override
|
||||
public void onLinkPreviewsFinished() {
|
||||
onContinue(Optional.of(ExperienceUpgrade.LINK_PREVIEWS));
|
||||
}
|
||||
|
||||
public OnPageChangeListener(ExperienceUpgrade upgrade) {
|
||||
this.upgrade = upgrade;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
final int nextPosition = (position + 1) % upgrade.getPages().size();
|
||||
|
||||
final int color = (Integer)evaluator.evaluate(positionOffset,
|
||||
upgrade.getPage(position).backgroundColor,
|
||||
upgrade.getPage(nextPosition).backgroundColor);
|
||||
getWindow().setBackgroundDrawable(new ColorDrawable(color));
|
||||
}
|
||||
@Override
|
||||
public void onStickersFinished() {
|
||||
onContinue(Optional.of(ExperienceUpgrade.STICKERS));
|
||||
}
|
||||
|
||||
public static class AppUpgradeReceiver extends BroadcastReceiver {
|
||||
@@ -299,7 +315,7 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity implements
|
||||
.build();
|
||||
ServiceUtil.getNotificationManager(context).notify(NOTIFICATION_ID, notification);
|
||||
} else if (DISMISS_ACTION.equals(intent.getAction())) {
|
||||
TextSecurePreferences.setExperienceDismissedVersionCode(context, Util.getCurrentApkReleaseVersion(context));
|
||||
TextSecurePreferences.setExperienceDismissedVersionCode(context, Util.getCanonicalVersionCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
@@ -20,12 +20,17 @@ package org.thoughtcrime.securesms;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -40,7 +45,6 @@ import android.widget.Toast;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.soundcloud.android.crop.Crop;
|
||||
|
||||
import org.thoughtcrime.securesms.components.PushRecipientsPanel;
|
||||
import org.thoughtcrime.securesms.components.PushRecipientsPanel.RecipientsPanelChangedListener;
|
||||
@@ -48,7 +52,6 @@ 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.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
@@ -58,6 +61,7 @@ import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
@@ -88,7 +92,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private final static String TAG = GroupCreateActivity.class.getSimpleName();
|
||||
|
||||
public static final String GROUP_ADDRESS_EXTRA = "group_recipient";
|
||||
public static final String GROUP_ID_EXTRA = "group_id";
|
||||
public static final String GROUP_THREAD_EXTRA = "group_thread";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
@@ -115,7 +119,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
setContentView(R.layout.group_create_activity);
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
initializeAppBar();
|
||||
initializeResources();
|
||||
initializeExistingGroup();
|
||||
}
|
||||
@@ -174,6 +178,12 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
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 = ViewUtil.findById(this, R.id.recipients_text);
|
||||
PushRecipientsPanel recipientsPanel = ViewUtil.findById(this, R.id.recipients);
|
||||
@@ -187,15 +197,15 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
recipientsEditor.setHint(R.string.recipients_panel__add_members);
|
||||
recipientsPanel.setPanelChangeListener(this);
|
||||
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_group_white_24dp).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this)));
|
||||
avatar.setOnClickListener(view -> Crop.pickImage(GroupCreateActivity.this));
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_group_outline_40, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this)));
|
||||
avatar.setOnClickListener(view -> AvatarSelection.startAvatarSelection(this, false, false));
|
||||
}
|
||||
|
||||
private void initializeExistingGroup() {
|
||||
final Address groupAddress = getIntent().getParcelableExtra(GROUP_ADDRESS_EXTRA);
|
||||
final String groupId = getIntent().getStringExtra(GROUP_ID_EXTRA);
|
||||
|
||||
if (groupAddress != null) {
|
||||
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupAddress.toGroupString());
|
||||
if (groupId != null) {
|
||||
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +268,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
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.ADDRESS_EXTRA, recipient.getAddress());
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
@@ -281,23 +291,23 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
switch (reqCode) {
|
||||
case PICK_CONTACT:
|
||||
List<String> selected = data.getStringArrayListExtra("contacts");
|
||||
|
||||
for (String contact : selected) {
|
||||
Address address = Address.fromExternal(this, contact);
|
||||
Recipient recipient = Recipient.from(this, address, false);
|
||||
List<RecipientId> selected = data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS);
|
||||
|
||||
for (RecipientId contact : selected) {
|
||||
Recipient recipient = Recipient.resolved(contact);
|
||||
addSelectedContacts(recipient);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Crop.REQUEST_PICK:
|
||||
new Crop(data.getData()).output(outputFile).asSquare().start(this);
|
||||
case AvatarSelection.REQUEST_CODE_AVATAR:
|
||||
AvatarSelection.circularCropImage(this, data.getData(), outputFile, R.string.CropImageActivity_group_avatar);
|
||||
break;
|
||||
case Crop.REQUEST_CROP:
|
||||
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
|
||||
final Uri resultUri = AvatarSelection.getResultUri(data);
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(Crop.getOutput(data))
|
||||
.load(resultUri)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerCrop()
|
||||
@@ -305,7 +315,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
.into(new SimpleTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
||||
setAvatar(Crop.getOutput(data), resource);
|
||||
setAvatar(resultUri, resource);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -335,15 +345,17 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
protected GroupActionResult doInBackground(Void... avoid) {
|
||||
List<Address> memberAddresses = new LinkedList<>();
|
||||
List<RecipientId> memberAddresses = new LinkedList<>();
|
||||
|
||||
for (Recipient recipient : members) {
|
||||
memberAddresses.add(recipient.getAddress());
|
||||
memberAddresses.add(recipient.getId());
|
||||
}
|
||||
memberAddresses.add(Recipient.self().getId());
|
||||
|
||||
String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true);
|
||||
Recipient groupRecipient = Recipient.from(activity, Address.fromSerialized(groupId), true);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true);
|
||||
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);
|
||||
}
|
||||
@@ -446,7 +458,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
if (!activity.isFinishing()) {
|
||||
Intent intent = activity.getIntent();
|
||||
intent.putExtra(GROUP_THREAD_EXTRA, result.get().getThreadId());
|
||||
intent.putExtra(GROUP_ADDRESS_EXTRA, result.get().getGroupRecipient().getAddress());
|
||||
intent.putExtra(GROUP_ID_EXTRA, result.get().getGroupRecipient().requireGroupId());
|
||||
activity.setResult(RESULT_OK, intent);
|
||||
activity.finish();
|
||||
}
|
||||
@@ -488,8 +500,8 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
if (failIfNotPush && !isPush) {
|
||||
results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_cannot_add_non_push_to_existing_group,
|
||||
recipient.toShortString())));
|
||||
} else if (TextUtils.equals(TextSecurePreferences.getLocalNumber(activity), recipient.getAddress().serialize())) {
|
||||
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));
|
||||
@@ -4,12 +4,13 @@ import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.provider.ContactsContract;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.LinkedList;
|
||||
@@ -32,7 +33,7 @@ public class GroupMembersDialog extends AsyncTask<Void, Void, List<Recipient>> {
|
||||
|
||||
@Override
|
||||
protected List<Recipient> doInBackground(Void... params) {
|
||||
return DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.getAddress().toGroupString(), true);
|
||||
return DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,18 +67,11 @@ public class GroupMembersDialog extends AsyncTask<Void, Void, List<Recipient>> {
|
||||
|
||||
if (recipient.getContactUri() != null) {
|
||||
Intent intent = new Intent(context, RecipientPreferenceActivity.class);
|
||||
intent.putExtra(RecipientPreferenceActivity.ADDRESS_EXTRA, recipient.getAddress());
|
||||
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, recipient.getId());
|
||||
|
||||
context.startActivity(intent);
|
||||
} else {
|
||||
final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
|
||||
if (recipient.getAddress().isEmail()) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.EMAIL, recipient.getAddress().toEmailString());
|
||||
} else {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.PHONE, recipient.getAddress().toPhoneString());
|
||||
}
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
context.startActivity(intent);
|
||||
context.startActivity(RecipientExporter.export(recipient).asAddContactIntent());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +91,7 @@ public class GroupMembersDialog extends AsyncTask<Void, Void, List<Recipient>> {
|
||||
|
||||
public GroupMembers(List<Recipient> recipients) {
|
||||
for (Recipient recipient : recipients) {
|
||||
if (isLocalNumber(recipient)) {
|
||||
if (recipient.isLocalNumber()) {
|
||||
members.push(recipient);
|
||||
} else {
|
||||
members.add(recipient);
|
||||
@@ -109,15 +103,10 @@ public class GroupMembersDialog extends AsyncTask<Void, Void, List<Recipient>> {
|
||||
List<String> recipientStrings = new LinkedList<>();
|
||||
|
||||
for (Recipient recipient : members) {
|
||||
if (isLocalNumber(recipient)) {
|
||||
if (recipient.isLocalNumber()) {
|
||||
recipientStrings.add(context.getString(R.string.GroupMembersDialog_me));
|
||||
} else {
|
||||
String name = recipient.toShortString();
|
||||
|
||||
if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) {
|
||||
name += " ~" + recipient.getProfileName();
|
||||
}
|
||||
|
||||
String name = getRecipientName(recipient);
|
||||
recipientStrings.add(name);
|
||||
}
|
||||
}
|
||||
@@ -125,12 +114,20 @@ public class GroupMembersDialog extends AsyncTask<Void, Void, List<Recipient>> {
|
||||
return recipientStrings.toArray(new String[members.size()]);
|
||||
}
|
||||
|
||||
private String getRecipientName(Recipient recipient) {
|
||||
if (FeatureFlags.PROFILE_DISPLAY) return recipient.getDisplayName(context);
|
||||
|
||||
String name = recipient.toShortString(context);
|
||||
|
||||
if (recipient.getName(context) == null && !TextUtils.isEmpty(recipient.getProfileName())) {
|
||||
name += " ~" + recipient.getProfileName();
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
public Recipient get(int index) {
|
||||
return members.get(index);
|
||||
}
|
||||
|
||||
private boolean isLocalNumber(Recipient recipient) {
|
||||
return Util.isOwnNumber(context, recipient.getAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* The central entry point for all envelopes that have been retrieved. Envelopes must be processed
|
||||
* here to guarantee proper ordering.
|
||||
*/
|
||||
public class IncomingMessageProcessor {
|
||||
|
||||
private static final String TAG = Log.tag(IncomingMessageProcessor.class);
|
||||
|
||||
private final Context context;
|
||||
private final ReentrantLock lock;
|
||||
|
||||
public IncomingMessageProcessor(@NonNull Context context) {
|
||||
this.context = context;
|
||||
this.lock = new ReentrantLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An instance of a Processor that will allow you to process messages in a thread safe
|
||||
* way. Must be closed.
|
||||
*/
|
||||
public Processor acquire() {
|
||||
lock.lock();
|
||||
|
||||
Thread current = Thread.currentThread();
|
||||
Log.d(TAG, "Lock acquired by thread " + current.getId() + " (" + current.getName() + ")");
|
||||
|
||||
return new Processor(context);
|
||||
}
|
||||
|
||||
private void release() {
|
||||
Thread current = Thread.currentThread();
|
||||
Log.d(TAG, "Lock about to be released by thread " + current.getId() + " (" + current.getName() + ")");
|
||||
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
public class Processor implements Closeable {
|
||||
|
||||
private final Context context;
|
||||
private final PushDatabase pushDatabase;
|
||||
private final MmsSmsDatabase mmsSmsDatabase;
|
||||
private final JobManager jobManager;
|
||||
|
||||
private Processor(@NonNull Context context) {
|
||||
this.context = context;
|
||||
this.pushDatabase = DatabaseFactory.getPushDatabase(context);
|
||||
this.mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
this.jobManager = ApplicationDependencies.getJobManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The id of the {@link PushDecryptMessageJob} that was scheduled to process the message, if
|
||||
* one was created. Otherwise null.
|
||||
*/
|
||||
public @Nullable String processEnvelope(@NonNull SignalServiceEnvelope envelope) {
|
||||
if (envelope.hasSource()) {
|
||||
Recipient.externalPush(context, envelope.getSourceAddress());
|
||||
}
|
||||
|
||||
if (envelope.isReceipt()) {
|
||||
processReceipt(envelope);
|
||||
return null;
|
||||
} else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender()) {
|
||||
return processMessage(envelope);
|
||||
} else {
|
||||
Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull String processMessage(@NonNull SignalServiceEnvelope envelope) {
|
||||
Log.i(TAG, "Received message. Inserting in PushDatabase.");
|
||||
|
||||
long id = pushDatabase.insert(envelope);
|
||||
PushDecryptMessageJob job = new PushDecryptMessageJob(context, id);
|
||||
|
||||
jobManager.add(job);
|
||||
|
||||
return job.getId();
|
||||
}
|
||||
|
||||
private void processReceipt(@NonNull SignalServiceEnvelope envelope) {
|
||||
Log.i(TAG, String.format(Locale.ENGLISH, "Received receipt: (XXXXX, %d)", envelope.getTimestamp()));
|
||||
mmsSmsDatabase.incrementDeliveryReceiptCount(new SyncMessageId(Recipient.externalPush(context, envelope.getSourceAddress()).getId(), envelope.getTimestamp()),
|
||||
System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
291
app/src/main/java/org/thoughtcrime/securesms/InviteActivity.java
Normal file
@@ -0,0 +1,291 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AnimRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChangedListener;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
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.concurrent.ExecutionException;
|
||||
|
||||
public class InviteActivity extends PassphraseRequiredActionBarActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private EditText inviteText;
|
||||
private ViewGroup smsSendFrame;
|
||||
private Button smsSendButton;
|
||||
private Animation slideInAnimation;
|
||||
private Animation slideOutAnimation;
|
||||
private DynamicTheme dynamicTheme = new DynamicNoActionBarInviteTheme();
|
||||
private Toolbar primaryToolbar;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
super.onPreCreate();
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@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.REFRESHABLE, false);
|
||||
|
||||
setContentView(R.layout.invite_activity);
|
||||
|
||||
initializeAppBar();
|
||||
initializeResources();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private void initializeAppBar() {
|
||||
primaryToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(primaryToolbar);
|
||||
|
||||
assert getSupportActionBar() != null;
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.AndroidManifest__invite_friends);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
slideInAnimation = loadAnimation(R.anim.slide_from_bottom);
|
||||
slideOutAnimation = loadAnimation(R.anim.slide_to_bottom);
|
||||
|
||||
View shareButton = ViewUtil.findById(this, R.id.share_button);
|
||||
View smsButton = ViewUtil.findById(this, R.id.sms_button);
|
||||
Button smsCancelButton = ViewUtil.findById(this, R.id.cancel_sms_button);
|
||||
ContactFilterToolbar contactFilter = ViewUtil.findById(this, R.id.contact_filter);
|
||||
|
||||
inviteText = ViewUtil.findById(this, R.id.invite_text);
|
||||
smsSendFrame = ViewUtil.findById(this, R.id.sms_send_frame);
|
||||
smsSendButton = ViewUtil.findById(this, R.id.send_sms_button);
|
||||
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();
|
||||
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
smsButton.setOnClickListener(new SmsClickListener());
|
||||
smsCancelButton.setOnClickListener(new SmsCancelClickListener());
|
||||
smsSendButton.setOnClickListener(new SmsSendClickListener());
|
||||
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
|
||||
contactFilter.setNavigationIcon(R.drawable.ic_search_conversation_24);
|
||||
}
|
||||
|
||||
private Animation loadAnimation(@AnimRes int animResId) {
|
||||
final Animation animation = AnimationUtils.loadAnimation(this, animResId);
|
||||
animation.setInterpolator(new FastOutSlowInInterpolator());
|
||||
return animation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText();
|
||||
}
|
||||
|
||||
private void sendSmsInvites() {
|
||||
new SendSmsInvitesAsyncTask(this, inviteText.getText().toString())
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
contactsFragment.getSelectedContacts()
|
||||
.toArray(new SelectedContact[contactsFragment.getSelectedContacts().size()]));
|
||||
}
|
||||
|
||||
private void updateSmsButtonText() {
|
||||
smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends,
|
||||
contactsFragment.getSelectedContacts().size(),
|
||||
contactsFragment.getSelectedContacts().size()));
|
||||
smsSendButton.setEnabled(!contactsFragment.getSelectedContacts().isEmpty());
|
||||
}
|
||||
|
||||
@Override public void onBackPressed() {
|
||||
if (smsSendFrame.getVisibility() == View.VISIBLE) {
|
||||
cancelSmsSelection();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelSmsSelection() {
|
||||
setPrimaryColorsToolbarNormal();
|
||||
contactsFragment.reset();
|
||||
updateSmsButtonText();
|
||||
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
|
||||
}
|
||||
|
||||
private void setPrimaryColorsToolbarNormal() {
|
||||
primaryToolbar.setBackgroundColor(0);
|
||||
primaryToolbar.getNavigationIcon().setColorFilter(null);
|
||||
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.title_text_color_primary));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
getWindow().setStatusBarColor(ThemeUtil.getThemedColor(this, android.R.attr.statusBarColor));
|
||||
getWindow().setNavigationBarColor(ThemeUtil.getThemedColor(this, android.R.attr.navigationBarColor));
|
||||
WindowUtil.setLightStatusBarFromTheme(this);
|
||||
}
|
||||
|
||||
WindowUtil.setLightNavigationBarFromTheme(this);
|
||||
}
|
||||
|
||||
private void setPrimaryColorsToolbarForSms() {
|
||||
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.signal_primary));
|
||||
primaryToolbar.getNavigationIcon().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.conversation_subtitle_color), PorterDuff.Mode.SRC_IN);
|
||||
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.signal_primary));
|
||||
WindowUtil.clearLightStatusBar(getWindow());
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 27) {
|
||||
getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.signal_primary));
|
||||
WindowUtil.clearLightNavigationBar(getWindow());
|
||||
}
|
||||
}
|
||||
|
||||
private class ShareClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, inviteText.getText().toString());
|
||||
sendIntent.setType("text/plain");
|
||||
if (sendIntent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(Intent.createChooser(sendIntent, getString(R.string.InviteActivity_invite_to_signal)));
|
||||
} else {
|
||||
Toast.makeText(InviteActivity.this, R.string.InviteActivity_no_app_to_share_to, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SmsClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
setPrimaryColorsToolbarForSms();
|
||||
ViewUtil.animateIn(smsSendFrame, slideInAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
private class SmsCancelClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
cancelSmsSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private class SmsSendClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
new AlertDialog.Builder(InviteActivity.this)
|
||||
.setTitle(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_invites,
|
||||
contactsFragment.getSelectedContacts().size(),
|
||||
contactsFragment.getSelectedContacts().size()))
|
||||
.setMessage(inviteText.getText().toString())
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> sendSmsInvites())
|
||||
.setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private class ContactFilterChangedListener implements OnFilterChangedListener {
|
||||
@Override
|
||||
public void onFilterChanged(String filter) {
|
||||
contactsFragment.setQueryFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class SendSmsInvitesAsyncTask extends ProgressDialogAsyncTask<SelectedContact,Void,Void> {
|
||||
private final String message;
|
||||
|
||||
SendSmsInvitesAsyncTask(Context context, String message) {
|
||||
super(context, R.string.InviteActivity_sending, R.string.InviteActivity_sending);
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(SelectedContact... contacts) {
|
||||
final Context context = getContext();
|
||||
if (context == null) return null;
|
||||
|
||||
for (SelectedContact contact : contacts) {
|
||||
RecipientId recipientId = contact.getOrCreateRecipientId(context);
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
|
||||
|
||||
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null);
|
||||
|
||||
if (recipient.getContactUri() != null) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setHasSentInvite(recipient.getId());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
final Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE).addListener(new Listener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
contactsFragment.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {}
|
||||
});
|
||||
Toast.makeText(context, R.string.InviteActivity_invitations_sent, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class LinkPreviewsIntroFragment extends Fragment {
|
||||
|
||||
private Controller controller;
|
||||
|
||||
public static LinkPreviewsIntroFragment newInstance() {
|
||||
LinkPreviewsIntroFragment fragment = new LinkPreviewsIntroFragment();
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public LinkPreviewsIntroFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
||||
}
|
||||
|
||||
controller = (Controller) getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.experience_upgrade_link_previews_fragment, container, false);
|
||||
|
||||
view.findViewById(R.id.experience_ok_button).setOnClickListener(v -> {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(requireContext())));
|
||||
controller.onLinkPreviewsFinished();
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onLinkPreviewsFinished();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class MainActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final MainNavigator navigator = new MainNavigator(this);
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
setContentView(R.layout.main_activity);
|
||||
|
||||
navigator.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
super.onPreCreate();
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (!navigator.onBackPressed()) {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull MainNavigator getNavigator() {
|
||||
return navigator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
public class MainFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(requireActivity() instanceof MainActivity)) {
|
||||
throw new IllegalStateException("Can only be used inside of MainActivity!");
|
||||
}
|
||||
}
|
||||
|
||||
protected @NonNull MainNavigator getNavigator() {
|
||||
return MainNavigator.get(requireActivity());
|
||||
}
|
||||
}
|
||||
104
app/src/main/java/org/thoughtcrime/securesms/MainNavigator.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
public class MainNavigator {
|
||||
|
||||
private final MainActivity activity;
|
||||
|
||||
public MainNavigator(@NonNull MainActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
public static MainNavigator get(@NonNull Activity activity) {
|
||||
if (!(activity instanceof MainActivity)) {
|
||||
throw new IllegalArgumentException("Activity must be an instance of MainActivity!");
|
||||
}
|
||||
|
||||
return ((MainActivity) activity).getNavigator();
|
||||
}
|
||||
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
if (savedInstanceState != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.add(R.id.fragment_container, ConversationListFragment.newInstance())
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the back pressed was handled in our own custom way, false if it should be given
|
||||
* to the system to do the default behavior.
|
||||
*/
|
||||
public boolean onBackPressed() {
|
||||
Fragment fragment = getFragmentManager().findFragmentById(R.id.fragment_container);
|
||||
|
||||
if (fragment instanceof BackHandler) {
|
||||
return ((BackHandler) fragment).onBackPressed();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, long lastSeen, int startingPosition) {
|
||||
Intent intent = ConversationActivity.buildIntent(activity, recipientId, threadId, distributionType, lastSeen, startingPosition);
|
||||
|
||||
activity.startActivity(intent);
|
||||
activity.overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);
|
||||
}
|
||||
|
||||
public void goToAppSettings() {
|
||||
Intent intent = new Intent(activity, ApplicationPreferencesActivity.class);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
|
||||
public void goToArchiveList() {
|
||||
getFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||
.replace(R.id.fragment_container, ConversationListArchiveFragment.newInstance())
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public void goToGroupCreation() {
|
||||
Intent intent = new Intent(activity, GroupCreateActivity.class);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
public void goToInvite() {
|
||||
Intent intent = new Intent(activity, InviteActivity.class);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
public void goToInsights() {
|
||||
InsightsLauncher.showInsightsDashboard(activity.getSupportFragmentManager());
|
||||
}
|
||||
|
||||
private @NonNull FragmentManager getFragmentManager() {
|
||||
return activity.getSupportFragmentManager();
|
||||
}
|
||||
|
||||
public interface BackHandler {
|
||||
/**
|
||||
* @return True if the back pressed was handled in our own custom way, false if it should be given
|
||||
* to the system to do the default behavior.
|
||||
*/
|
||||
boolean onBackPressed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,794 @@
|
||||
/*
|
||||
* 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.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
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.util.Pair;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import org.thoughtcrime.securesms.animation.DepthPageTransformer;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
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.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Activity for displaying media attachments in-app
|
||||
*/
|
||||
public final class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
|
||||
MediaRailAdapter.RailItemListener,
|
||||
MediaPreviewFragment.Events
|
||||
{
|
||||
|
||||
private final static String TAG = MediaPreviewActivity.class.getSimpleName();
|
||||
|
||||
private static final int NOT_IN_A_THREAD = -2;
|
||||
|
||||
public static final String THREAD_ID_EXTRA = "thread_id";
|
||||
public static final String DATE_EXTRA = "date";
|
||||
public static final String SIZE_EXTRA = "size";
|
||||
public static final String CAPTION_EXTRA = "caption";
|
||||
public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent";
|
||||
public static final String HIDE_ALL_MEDIA_EXTRA = "came_from_all_media";
|
||||
public static final String SHOW_THREAD_EXTRA = "show_thread";
|
||||
public static final String SORTING_EXTRA = "sorting";
|
||||
|
||||
private ViewPager mediaPager;
|
||||
private View detailsContainer;
|
||||
private TextView caption;
|
||||
private View captionContainer;
|
||||
private RecyclerView albumRail;
|
||||
private MediaRailAdapter albumRailAdapter;
|
||||
private ViewGroup playbackControlsContainer;
|
||||
private Uri initialMediaUri;
|
||||
private String initialMediaType;
|
||||
private long initialMediaSize;
|
||||
private String initialCaption;
|
||||
private boolean leftIsRecent;
|
||||
private MediaPreviewViewModel viewModel;
|
||||
private ViewPagerListener viewPagerListener;
|
||||
|
||||
private int restartItem = -1;
|
||||
private long threadId = NOT_IN_A_THREAD;
|
||||
private boolean cameFromAllMedia;
|
||||
private boolean showThread;
|
||||
private MediaDatabase.Sorting sorting;
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
this.setTheme(R.style.TextSecure_MediaPreview);
|
||||
setContentView(R.layout.media_preview_activity);
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
|
||||
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
|
||||
showSystemUI();
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
initializeViews();
|
||||
initializeResources();
|
||||
initializeObservers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRailItemClicked(int distanceFromActive) {
|
||||
mediaPager.setCurrentItem(mediaPager.getCurrentItem() + distanceFromActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRailItemDeleteClicked(int distanceFromActive) {
|
||||
throw new UnsupportedOperationException("Callback unsupported.");
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void initializeActionBar() {
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
|
||||
if (mediaItem != null) {
|
||||
getSupportActionBar().setTitle(getTitleText(mediaItem));
|
||||
getSupportActionBar().setSubtitle(getSubTitleText(mediaItem));
|
||||
}
|
||||
}
|
||||
|
||||
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 from = "";
|
||||
|
||||
if (showThread) {
|
||||
String to = null;
|
||||
Recipient threadRecipient = mediaItem.threadRecipient;
|
||||
|
||||
if (threadRecipient != null) {
|
||||
if (mediaItem.outgoing || threadRecipient.isGroup()) {
|
||||
if (threadRecipient.isLocalNumber()) {
|
||||
from = getString(R.string.note_to_self);
|
||||
} else {
|
||||
to = threadRecipient.toShortString(this);
|
||||
}
|
||||
} else {
|
||||
to = getString(R.string.MediaPreviewActivity_you);
|
||||
}
|
||||
}
|
||||
|
||||
return to != null ? getString(R.string.MediaPreviewActivity_s_to_s, from, to)
|
||||
: from;
|
||||
} else {
|
||||
return from;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull String getSubTitleText(@NonNull MediaItem mediaItem) {
|
||||
if (mediaItem.date > 0) {
|
||||
return DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), mediaItem.date);
|
||||
} else {
|
||||
return getString(R.string.MediaPreviewActivity_draft);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
initializeMedia();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
restartItem = cleanupMedia();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
initializeResources();
|
||||
}
|
||||
|
||||
private void initializeViews() {
|
||||
mediaPager = findViewById(R.id.media_pager);
|
||||
mediaPager.setOffscreenPageLimit(1);
|
||||
mediaPager.setPageTransformer(true, new DepthPageTransformer());
|
||||
|
||||
viewPagerListener = new ViewPagerListener();
|
||||
mediaPager.addOnPageChangeListener(viewPagerListener);
|
||||
|
||||
albumRail = findViewById(R.id.media_preview_album_rail);
|
||||
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
|
||||
|
||||
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
||||
albumRail.setAdapter(albumRailAdapter);
|
||||
|
||||
detailsContainer = findViewById(R.id.media_preview_details_container);
|
||||
caption = findViewById(R.id.media_preview_caption);
|
||||
captionContainer = findViewById(R.id.media_preview_caption_container);
|
||||
playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_container);
|
||||
|
||||
View toolbarLayout = findViewById(R.id.toolbar_layout);
|
||||
|
||||
anchorMarginsToBottomInsets(detailsContainer);
|
||||
|
||||
anchorMarginsToTopInsets(toolbarLayout);
|
||||
|
||||
showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
Intent intent = getIntent();
|
||||
|
||||
threadId = intent.getLongExtra(THREAD_ID_EXTRA, NOT_IN_A_THREAD);
|
||||
cameFromAllMedia = intent.getBooleanExtra(HIDE_ALL_MEDIA_EXTRA, false);
|
||||
showThread = intent.getBooleanExtra(SHOW_THREAD_EXTRA, false);
|
||||
sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(SORTING_EXTRA, 0)];
|
||||
|
||||
initialMediaUri = intent.getData();
|
||||
initialMediaType = intent.getType();
|
||||
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
|
||||
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
|
||||
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
restartItem = -1;
|
||||
}
|
||||
|
||||
private void initializeObservers() {
|
||||
viewModel.getPreviewData().observe(this, previewData -> {
|
||||
if (previewData == null || mediaPager == null || mediaPager.getAdapter() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!((MediaItemAdapter) mediaPager.getAdapter()).hasFragmentFor(mediaPager.getCurrentItem())) {
|
||||
Log.d(TAG, "MediaItemAdapter wasn't ready. Posting again...");
|
||||
viewModel.resubmitPreviewData();
|
||||
}
|
||||
|
||||
View playbackControls = ((MediaItemAdapter) mediaPager.getAdapter()).getPlaybackControls(mediaPager.getCurrentItem());
|
||||
|
||||
if (previewData.getAlbumThumbnails().isEmpty() && previewData.getCaption() == null && playbackControls == null) {
|
||||
detailsContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
detailsContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
albumRail.setVisibility(previewData.getAlbumThumbnails().isEmpty() ? View.GONE : View.VISIBLE);
|
||||
albumRailAdapter.setMedia(previewData.getAlbumThumbnails(), previewData.getActivePosition());
|
||||
albumRail.smoothScrollToPosition(previewData.getActivePosition());
|
||||
|
||||
captionContainer.setVisibility(previewData.getCaption() == null ? View.GONE : View.VISIBLE);
|
||||
caption.setText(previewData.getCaption());
|
||||
|
||||
if (playbackControls != null) {
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
playbackControls.setLayoutParams(params);
|
||||
|
||||
playbackControlsContainer.removeAllViews();
|
||||
playbackControlsContainer.addView(playbackControls);
|
||||
} else {
|
||||
playbackControlsContainer.removeAllViews();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeMedia() {
|
||||
if (!isContentTypeSupported(initialMediaType)) {
|
||||
Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing.");
|
||||
Toast.makeText(getApplicationContext(), R.string.MediaPreviewActivity_unssuported_media_type, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Loading Part URI: " + initialMediaUri);
|
||||
|
||||
if (isMediaInDb()) {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
} else {
|
||||
mediaPager.setAdapter(new SingleItemPagerAdapter(getSupportFragmentManager(), initialMediaUri, initialMediaType, initialMediaSize));
|
||||
|
||||
if (initialCaption != null) {
|
||||
detailsContainer.setVisibility(View.VISIBLE);
|
||||
captionContainer.setVisibility(View.VISIBLE);
|
||||
caption.setText(initialCaption);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int cleanupMedia() {
|
||||
int restartItem = mediaPager.getCurrentItem();
|
||||
|
||||
mediaPager.removeAllViews();
|
||||
mediaPager.setAdapter(null);
|
||||
|
||||
return restartItem;
|
||||
}
|
||||
|
||||
private void showOverview() {
|
||||
startActivity(MediaOverviewActivity.forThread(this, threadId));
|
||||
}
|
||||
|
||||
private void forward() {
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
|
||||
if (mediaItem != null) {
|
||||
Intent composeIntent = new Intent(this, ShareActivity.class);
|
||||
composeIntent.putExtra(Intent.EXTRA_STREAM, mediaItem.uri);
|
||||
composeIntent.setType(mediaItem.type);
|
||||
startActivity(composeIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
@SuppressLint("InlinedApi")
|
||||
private void saveToDisk() {
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
|
||||
if (mediaItem != null) {
|
||||
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_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));
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void deleteMedia() {
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
if (mediaItem == null || mediaItem.attachment == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title);
|
||||
builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message);
|
||||
builder.setCancelable(true);
|
||||
|
||||
builder.setPositiveButton(R.string.delete, (dialogInterface, which) -> {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
AttachmentUtil.deleteAttachment(MediaPreviewActivity.this.getApplicationContext(),
|
||||
mediaItem.attachment);
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
|
||||
finish();
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
|
||||
menu.clear();
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
inflater.inflate(R.menu.media_preview, menu);
|
||||
|
||||
if (!isMediaInDb()) {
|
||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||
menu.findItem(R.id.delete).setVisible(false);
|
||||
}
|
||||
|
||||
if (cameFromAllMedia) {
|
||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(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;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isMediaInDb() {
|
||||
return threadId != NOT_IN_A_THREAD;
|
||||
}
|
||||
|
||||
private @Nullable MediaItem getCurrentMediaItem() {
|
||||
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
|
||||
|
||||
if (adapter != null) {
|
||||
return adapter.getMediaItemFor(mediaPager.getCurrentItem());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isContentTypeSupported(final String contentType) {
|
||||
return contentType != null && (contentType.startsWith("image/") || contentType.startsWith("video/"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Pair<Cursor, Integer>> onCreateLoader(int id, Bundle args) {
|
||||
return new PagingMediaLoader(this, threadId, initialMediaUri, leftIsRecent, sorting);
|
||||
}
|
||||
|
||||
@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);
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
|
||||
viewModel.setCursor(this, data.first, leftIsRecent);
|
||||
|
||||
int item = restartItem >= 0 ? restartItem : data.second;
|
||||
mediaPager.setCurrentItem(item);
|
||||
|
||||
if (item == 0) {
|
||||
viewPagerListener.onPageSelected(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean singleTapOnMedia() {
|
||||
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 );
|
||||
}
|
||||
|
||||
private class ViewPagerListener extends ExtendedOnPageChangedListener {
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
super.onPageSelected(position);
|
||||
|
||||
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
|
||||
|
||||
if (adapter != null) {
|
||||
MediaItem item = adapter.getMediaItemFor(position);
|
||||
if (item.recipient != null) item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar());
|
||||
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
|
||||
initializeActionBar();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onPageUnselected(int position) {
|
||||
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
|
||||
|
||||
if (adapter != null) {
|
||||
MediaItem item = adapter.getMediaItemFor(position);
|
||||
if (item.recipient != null) item.recipient.live().removeObservers(MediaPreviewActivity.this);
|
||||
|
||||
adapter.pause(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class SingleItemPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
|
||||
|
||||
private final Uri uri;
|
||||
private final String mediaType;
|
||||
private final long size;
|
||||
|
||||
private MediaPreviewFragment mediaPreviewFragment;
|
||||
|
||||
SingleItemPagerAdapter(@NonNull FragmentManager fragmentManager,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String mediaType,
|
||||
long size)
|
||||
{
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
this.uri = uri;
|
||||
this.mediaType = mediaType;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
mediaPreviewFragment = MediaPreviewFragment.newInstance(uri, mediaType, size, true);
|
||||
return mediaPreviewFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
if (mediaPreviewFragment != null) {
|
||||
mediaPreviewFragment.cleanUp();
|
||||
mediaPreviewFragment = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
return new MediaItem(null, null, null, uri, mediaType, -1, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause(int position) {
|
||||
if (mediaPreviewFragment != null) {
|
||||
mediaPreviewFragment.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View getPlaybackControls(int position) {
|
||||
if (mediaPreviewFragment != null) {
|
||||
return mediaPreviewFragment.getPlaybackControls();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFragmentFor(int position) {
|
||||
return mediaPreviewFragment != null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor, (view, insets) -> {
|
||||
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
|
||||
|
||||
layoutParams.setMargins(insets.getSystemWindowInsetLeft(),
|
||||
layoutParams.topMargin,
|
||||
insets.getSystemWindowInsetRight(),
|
||||
insets.getSystemWindowInsetBottom());
|
||||
|
||||
view.setLayoutParams(layoutParams);
|
||||
|
||||
return insets;
|
||||
});
|
||||
}
|
||||
|
||||
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")
|
||||
private final Map<Integer, MediaPreviewFragment> mediaFragments = new HashMap<>();
|
||||
|
||||
private final Context context;
|
||||
private final Cursor cursor;
|
||||
private final boolean leftIsRecent;
|
||||
|
||||
private boolean active;
|
||||
private int autoPlayPosition;
|
||||
|
||||
CursorPagerAdapter(@NonNull FragmentManager fragmentManager,
|
||||
@NonNull Context context,
|
||||
@NonNull Cursor cursor,
|
||||
int autoPlayPosition,
|
||||
boolean leftIsRecent)
|
||||
{
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
this.context = context.getApplicationContext();
|
||||
this.cursor = cursor;
|
||||
this.autoPlayPosition = autoPlayPosition;
|
||||
this.leftIsRecent = leftIsRecent;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
if (!active) return 0;
|
||||
else return cursor.getCount();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
boolean autoPlay = autoPlayPosition == position;
|
||||
int cursorPosition = getCursorPosition(position);
|
||||
|
||||
autoPlayPosition = -1;
|
||||
|
||||
cursor.moveToPosition(cursorPosition);
|
||||
|
||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(context, cursor);
|
||||
DatabaseAttachment attachment = mediaRecord.getAttachment();
|
||||
MediaPreviewFragment fragment = MediaPreviewFragment.newInstance(attachment, autoPlay);
|
||||
|
||||
mediaFragments.put(position, fragment);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
MediaPreviewFragment removed = mediaFragments.remove(position);
|
||||
|
||||
if (removed != null) {
|
||||
removed.cleanUp();
|
||||
}
|
||||
|
||||
super.destroyItem(container, position, object);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
return new MediaItem(Recipient.live(recipientId).get(),
|
||||
Recipient.live(threadRecipientId).get(),
|
||||
mediaRecord.getAttachment(),
|
||||
mediaRecord.getAttachment().getDataUri(),
|
||||
mediaRecord.getContentType(),
|
||||
mediaRecord.getDate(),
|
||||
mediaRecord.isOutgoing());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause(int position) {
|
||||
MediaPreviewFragment mediaView = mediaFragments.get(position);
|
||||
if (mediaView != null) mediaView.pause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View getPlaybackControls(int position) {
|
||||
MediaPreviewFragment mediaView = mediaFragments.get(position);
|
||||
if (mediaView != null) return mediaView.getPlaybackControls();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFragmentFor(int position) {
|
||||
return mediaFragments.containsKey(position);
|
||||
}
|
||||
|
||||
private int getCursorPosition(int position) {
|
||||
if (leftIsRecent) return position;
|
||||
else return cursor.getCount() - 1 - position;
|
||||
}
|
||||
}
|
||||
|
||||
private static class MediaItem {
|
||||
private final @Nullable Recipient recipient;
|
||||
private final @Nullable Recipient threadRecipient;
|
||||
private final @Nullable DatabaseAttachment attachment;
|
||||
private final @NonNull Uri uri;
|
||||
private final @NonNull String type;
|
||||
private final long date;
|
||||
private final boolean outgoing;
|
||||
|
||||
private MediaItem(@Nullable Recipient recipient,
|
||||
@Nullable Recipient threadRecipient,
|
||||
@Nullable DatabaseAttachment attachment,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String type,
|
||||
long date,
|
||||
boolean outgoing)
|
||||
{
|
||||
this.recipient = recipient;
|
||||
this.threadRecipient = threadRecipient;
|
||||
this.attachment = attachment;
|
||||
this.uri = uri;
|
||||
this.type = type;
|
||||
this.date = date;
|
||||
this.outgoing = outgoing;
|
||||
}
|
||||
}
|
||||
|
||||
interface MediaItemAdapter {
|
||||
MediaItem getMediaItemFor(int position);
|
||||
void pause(int position);
|
||||
@Nullable View getPlaybackControls(int position);
|
||||
boolean hasFragmentFor(int position);
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,12 @@ import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.Loader;
|
||||
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.logging.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -50,10 +52,11 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
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;
|
||||
@@ -71,14 +74,14 @@ import java.util.Locale;
|
||||
/**
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity implements LoaderCallbacks<Cursor>, RecipientModifiedListener {
|
||||
public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity implements LoaderCallbacks<Cursor> {
|
||||
private final static String TAG = MessageDetailsActivity.class.getSimpleName();
|
||||
|
||||
public final static String MESSAGE_ID_EXTRA = "message_id";
|
||||
public final static String THREAD_ID_EXTRA = "thread_id";
|
||||
public final static String IS_PUSH_GROUP_EXTRA = "is_push_group";
|
||||
public final static String TYPE_EXTRA = "type";
|
||||
public final static String ADDRESS_EXTRA = "address";
|
||||
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;
|
||||
@@ -98,7 +101,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
private ListView recipientsList;
|
||||
private LayoutInflater inflater;
|
||||
|
||||
private DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
|
||||
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private boolean running;
|
||||
@@ -147,10 +150,10 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
Recipient recipient = Recipient.from(this, getIntent().getParcelableExtra(ADDRESS_EXTRA), true);
|
||||
recipient.addListener(this);
|
||||
LiveRecipient recipient = Recipient.live(getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||
recipient.observe(this, r -> setActionBarColor(r.getColor()));
|
||||
|
||||
setActionBarColor(recipient.getColor());
|
||||
setActionBarColor(recipient.get().getColor());
|
||||
}
|
||||
|
||||
private void setActionBarColor(MaterialColor color) {
|
||||
@@ -162,11 +165,6 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(final Recipient recipient) {
|
||||
Util.runOnMain(() -> setActionBarColor(recipient.getColor()));
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
inflater = LayoutInflater.from(this);
|
||||
View header = inflater.inflate(R.layout.message_details_header, recipientsList, false);
|
||||
@@ -269,7 +267,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
toFromRes = R.string.message_details_header__from;
|
||||
}
|
||||
toFrom.setText(toFromRes);
|
||||
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, false);
|
||||
conversationItem.bind(messageRecord, Optional.absent(), Optional.absent(), glideRequests, dynamicLanguage.getCurrentLocale(), new HashSet<>(), recipient, null, false);
|
||||
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, glideRequests, messageRecord, recipients, isPushGroup));
|
||||
}
|
||||
|
||||
@@ -364,20 +362,20 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
List<RecipientDeliveryStatus> recipients = new LinkedList<>();
|
||||
|
||||
if (!messageRecord.getRecipient().isGroupRecipient()) {
|
||||
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().getAddress().toGroupString(), false);
|
||||
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), false);
|
||||
|
||||
for (Recipient recipient : group) {
|
||||
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1));
|
||||
}
|
||||
} else {
|
||||
for (GroupReceiptInfo info : receiptInfoList) {
|
||||
recipients.add(new RecipientDeliveryStatus(Recipient.from(context, info.getAddress(), true),
|
||||
recipients.add(new RecipientDeliveryStatus(Recipient.resolved(info.getRecipientId()),
|
||||
getStatusFor(info.getStatus(), messageRecord.isPending(), messageRecord.isFailed()),
|
||||
info.isUnidentified(),
|
||||
info.getTimestamp()));
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -11,7 +11,9 @@ 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;
|
||||
@@ -19,11 +21,12 @@ 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 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,
|
||||
@@ -34,6 +37,7 @@ class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.
|
||||
this.record = record;
|
||||
this.isPushGroup = isPushGroup;
|
||||
this.members = members;
|
||||
this.idGenerator = new StableIdGenerator<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -48,11 +52,7 @@ class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
try {
|
||||
return Conversions.byteArrayToLong(MessageDigest.getInstance("SHA1").digest(members.get(position).recipient.getAddress().serialize().getBytes()));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return idGenerator.getId(members.get(position).recipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -25,6 +25,8 @@ 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;
|
||||
@@ -32,11 +34,11 @@ 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.RecipientModifiedListener;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
/**
|
||||
* A simple view to show the recipients of a message
|
||||
@@ -44,7 +46,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class MessageRecipientListItem extends RelativeLayout
|
||||
implements RecipientModifiedListener
|
||||
implements RecipientForeverObserver
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
private final static String TAG = MessageRecipientListItem.class.getSimpleName();
|
||||
@@ -84,10 +86,12 @@ public class MessageRecipientListItem extends RelativeLayout
|
||||
final RecipientDeliveryStatus member,
|
||||
final boolean isPushGroup)
|
||||
{
|
||||
if (this.member != null) this.member.getRecipient().live().removeForeverObserver(this);
|
||||
|
||||
this.glideRequests = glideRequests;
|
||||
this.member = member;
|
||||
|
||||
member.getRecipient().addListener(this);
|
||||
member.getRecipient().live().observeForever(this);
|
||||
fromView.setText(member.getRecipient());
|
||||
contactPhotoImage.setAvatar(glideRequests, member.getRecipient(), false);
|
||||
setIssueIndicators(record, isPushGroup);
|
||||
@@ -138,7 +142,7 @@ public class MessageRecipientListItem extends RelativeLayout
|
||||
private NetworkFailure getNetworkFailure(final MessageRecord record) {
|
||||
if (record.hasNetworkFailures()) {
|
||||
for (final NetworkFailure failure : record.getNetworkFailures()) {
|
||||
if (failure.getAddress().equals(member.getRecipient().getAddress())) {
|
||||
if (failure.getRecipientId(getContext()).equals(member.getRecipient().getId())) {
|
||||
return failure;
|
||||
}
|
||||
}
|
||||
@@ -149,7 +153,7 @@ public class MessageRecipientListItem extends RelativeLayout
|
||||
private IdentityKeyMismatch getKeyMismatch(final MessageRecord record) {
|
||||
if (record.isIdentityMismatchFailure()) {
|
||||
for (final IdentityKeyMismatch mismatch : record.getIdentityKeyMismatches()) {
|
||||
if (mismatch.getAddress().equals(member.getRecipient().getAddress())) {
|
||||
if (mismatch.getRecipientId(getContext()).equals(member.getRecipient().getId())) {
|
||||
return mismatch;
|
||||
}
|
||||
}
|
||||
@@ -158,14 +162,17 @@ public class MessageRecipientListItem extends RelativeLayout
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
if (this.member != null && this.member.getRecipient() != null) this.member.getRecipient().removeListener(this);
|
||||
if (this.member != null && this.member.getRecipient() != null) this.member.getRecipient().live().removeForeverObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(final Recipient recipient) {
|
||||
Util.runOnMain(() -> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Activity container for starting a new conversation.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public class NewConversationActivity extends ContactSelectionActivity
|
||||
implements ContactSelectionListFragment.InviteCallback
|
||||
{
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = NewConversationActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle, boolean ready) {
|
||||
super.onCreate(bundle, ready);
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
Recipient recipient;
|
||||
if (recipientId.isPresent()) {
|
||||
recipient = Recipient.resolved(recipientId.get());
|
||||
} else {
|
||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
||||
recipient = Recipient.external(this, number);
|
||||
}
|
||||
launch(recipient);
|
||||
}
|
||||
|
||||
private void launch(Recipient recipient) {
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA));
|
||||
intent.setDataAndType(getIntent().getData(), getIntent().getType());
|
||||
|
||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
|
||||
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread);
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: super.onBackPressed(); return true;
|
||||
case R.id.menu_refresh: handleManualRefresh(); return true;
|
||||
case R.id.menu_new_group: handleCreateGroup(); return true;
|
||||
case R.id.menu_invite: handleInvite(); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleManualRefresh() {
|
||||
contactsFragment.setRefreshing(true);
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
private void handleCreateGroup() {
|
||||
startActivity(new Intent(this, GroupCreateActivity.class));
|
||||
}
|
||||
|
||||
private void handleInvite() {
|
||||
startActivity(new Intent(this, InviteActivity.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onPrepareOptionsPanel(View view, Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
inflater.inflate(R.menu.new_conversation_activity, menu);
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvite() {
|
||||
handleInvite();
|
||||
}
|
||||
}
|
||||
@@ -66,10 +66,11 @@ public class PassphraseCreateActivity extends PassphraseActivity {
|
||||
IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this);
|
||||
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
|
||||
|
||||
TextSecurePreferences.setLastExperienceVersionCode(PassphraseCreateActivity.this, Util.getCurrentApkReleaseVersion(PassphraseCreateActivity.this));
|
||||
TextSecurePreferences.setLastExperienceVersionCode(PassphraseCreateActivity.this, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setPasswordDisabled(PassphraseCreateActivity.this, true);
|
||||
TextSecurePreferences.setReadReceiptsEnabled(PassphraseCreateActivity.this, true);
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(PassphraseCreateActivity.this, true);
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(PassphraseCreateActivity.this, false);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -17,15 +17,16 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
|
||||
import androidx.core.os.CancellationSignal;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableString;
|
||||
@@ -149,6 +150,8 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("MissingSuperCall") // no fragments to dispatch to
|
||||
public void onActivityResult(int requestCode, int resultcode, Intent data) {
|
||||
if (requestCode != 1) return;
|
||||
|
||||
@@ -262,19 +265,19 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
|
||||
assert keyguardManager != null;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && !keyguardManager.isKeyguardSecure()) {
|
||||
if (!keyguardManager.isKeyguardSecure()) {
|
||||
Log.w(TAG ,"Keyguard not secure...");
|
||||
handleAuthenticated();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 16 && fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) {
|
||||
if (fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) {
|
||||
Log.i(TAG, "Listening for fingerprints...");
|
||||
fingerprintCancellationSignal = new CancellationSignal();
|
||||
fingerprintManager.authenticate(null, 0, fingerprintCancellationSignal, fingerprintListener, null);
|
||||
} else if (Build.VERSION.SDK_INT >= 21){
|
||||
Log.i(TAG, "firing intent...");
|
||||
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock Signal", "");
|
||||
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.PassphrasePromptActivity_unlock_signal), "");
|
||||
startActivityForResult(intent, 1);
|
||||
} else {
|
||||
Log.w(TAG, "Not compatible...");
|
||||
@@ -283,7 +286,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
}
|
||||
|
||||
private void pauseScreenLock() {
|
||||
if (Build.VERSION.SDK_INT >= 16 && fingerprintCancellationSignal != null) {
|
||||
if (fingerprintCancellationSignal != null) {
|
||||
fingerprintCancellationSignal.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
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 static final String LOCALE_EXTRA = "locale_extra";
|
||||
|
||||
private static final int STATE_NORMAL = 0;
|
||||
private static final int STATE_CREATE_PASSPHRASE = 1;
|
||||
private static final int STATE_PROMPT_PASSPHRASE = 2;
|
||||
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
|
||||
private static final int STATE_EXPERIENCE_UPGRADE = 4;
|
||||
private static final int STATE_WELCOME_PUSH_SCREEN = 5;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
|
||||
@Override
|
||||
protected final void onCreate(Bundle savedInstanceState) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onCreate()");
|
||||
this.networkAccess = new SignalServiceNetworkAccess(this);
|
||||
onPreCreate();
|
||||
|
||||
final boolean locked = KeyCachingService.isLocked(this);
|
||||
routeApplicationState(locked);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (!isFinishing()) {
|
||||
initializeClearKeyReceiver();
|
||||
onCreate(savedInstanceState, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onPreCreate() {}
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] onResume()");
|
||||
super.onResume();
|
||||
|
||||
if (networkAccess.isCensored(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob(this));
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMasterSecretCleared() {
|
||||
Log.d(TAG, "onMasterSecretCleared()");
|
||||
if (ApplicationContext.getInstance(this).isAppVisible()) routeApplicationState(true);
|
||||
else finish();
|
||||
}
|
||||
|
||||
protected <T extends Fragment> T initFragment(@IdRes int target,
|
||||
@NonNull T fragment)
|
||||
{
|
||||
return initFragment(target, fragment, null);
|
||||
}
|
||||
|
||||
protected <T extends Fragment> T initFragment(@IdRes int target,
|
||||
@NonNull T fragment,
|
||||
@Nullable Locale locale)
|
||||
{
|
||||
return initFragment(target, fragment, locale, null);
|
||||
}
|
||||
|
||||
protected <T extends Fragment> T initFragment(@IdRes int target,
|
||||
@NonNull T fragment,
|
||||
@Nullable Locale locale,
|
||||
@Nullable Bundle extras)
|
||||
{
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable(LOCALE_EXTRA, locale);
|
||||
|
||||
if (extras != null) {
|
||||
args.putAll(extras);
|
||||
}
|
||||
|
||||
fragment.setArguments(args);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(target, fragment)
|
||||
.commitAllowingStateLoss();
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private void routeApplicationState(boolean locked) {
|
||||
Intent intent = getIntentForState(getApplicationState(locked));
|
||||
if (intent != null) {
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private Intent getIntentForState(int state) {
|
||||
Log.d(TAG, "routeApplicationState(), state: " + state);
|
||||
|
||||
switch (state) {
|
||||
case STATE_CREATE_PASSPHRASE: return getCreatePassphraseIntent();
|
||||
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
|
||||
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
|
||||
case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent();
|
||||
case STATE_EXPERIENCE_UPGRADE: return getExperienceUpgradeIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
private int getApplicationState(boolean locked) {
|
||||
if (!MasterSecretUtil.isPassphraseInitialized(this)) {
|
||||
return STATE_CREATE_PASSPHRASE;
|
||||
} else if (locked) {
|
||||
return STATE_PROMPT_PASSPHRASE;
|
||||
} else if (ApplicationMigrations.isUpdate(this) && ApplicationMigrations.isUiBlockingMigrationRunning()) {
|
||||
return STATE_UI_BLOCKING_UPGRADE;
|
||||
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
||||
return STATE_WELCOME_PUSH_SCREEN;
|
||||
} else if (ExperienceUpgradeActivity.isUpdate(this)) {
|
||||
return STATE_EXPERIENCE_UPGRADE;
|
||||
} else {
|
||||
return STATE_NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
private Intent getCreatePassphraseIntent() {
|
||||
return getRoutedIntent(PassphraseCreateActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getPromptPassphraseIntent() {
|
||||
return getRoutedIntent(PassphrasePromptActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getUiBlockingUpgradeIntent() {
|
||||
return getRoutedIntent(ApplicationMigrationActivity.class,
|
||||
TextSecurePreferences.hasPromptedPushRegistration(this)
|
||||
? getConversationListIntent()
|
||||
: getPushRegistrationIntent());
|
||||
}
|
||||
|
||||
private Intent getExperienceUpgradeIntent() {
|
||||
return getRoutedIntent(ExperienceUpgradeActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getPushRegistrationIntent() {
|
||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(this, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private Intent getConversationListIntent() {
|
||||
// TODO [greyson] Navigation
|
||||
return new Intent(this, MainActivity.class);
|
||||
}
|
||||
|
||||
private void initializeClearKeyReceiver() {
|
||||
this.clearKeyReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "onReceive() for clear key event");
|
||||
onMasterSecretCleared();
|
||||
}
|
||||
};
|
||||
|
||||
IntentFilter filter = new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT);
|
||||
registerReceiver(clearKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null);
|
||||
}
|
||||
|
||||
private void removeClearKeyReceiver(Context context) {
|
||||
if (clearKeyReceiver != null) {
|
||||
context.unregisterReceiver(clearKeyReceiver);
|
||||
clearKeyReceiver = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
public class PlayServicesProblemActivity extends FragmentActivity {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
|
||||
public class PlayServicesProblemFragment extends DialogFragment {
|
||||
|
||||
@Override
|
||||
public @NonNull Dialog onCreateDialog(@Nullable Bundle bundle) {
|
||||
int code = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getActivity());
|
||||
Dialog dialog = GoogleApiAvailability.getInstance().getErrorDialog(getActivity(), code, 9111);
|
||||
|
||||
if (dialog == null) {
|
||||
return new AlertDialog.Builder(requireActivity())
|
||||
.setNegativeButton(android.R.string.ok, null)
|
||||
.setMessage(R.string.PlayServicesProblemFragment_the_version_of_google_play_services_you_have_installed_is_not_functioning)
|
||||
.create();
|
||||
} else {
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
super.onCancel(dialog);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
Activity activity = getActivity();
|
||||
if (activity != null) activity.finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Activity container for selecting a list of contacts.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public class PushContactSelectionActivity extends ContactSelectionActivity {
|
||||
|
||||
public static final String KEY_SELECTED_RECIPIENTS = "recipients";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private final static String TAG = PushContactSelectionActivity.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
|
||||
super.onCreate(icicle, ready);
|
||||
|
||||
getToolbar().setNavigationIcon(R.drawable.ic_check_24);
|
||||
getToolbar().setNavigationOnClickListener(v -> {
|
||||
Intent resultIntent = getIntent();
|
||||
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
|
||||
List<RecipientId> recipients = Stream.of(selectedContacts).map(sc -> sc.getOrCreateRecipientId(this)).toList();
|
||||
|
||||
resultIntent.putParcelableArrayListExtra(KEY_SELECTED_RECIPIENTS, new ArrayList<>(recipients));
|
||||
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class ReadReceiptsIntroFragment extends Fragment {
|
||||
|
||||
public static ReadReceiptsIntroFragment newInstance() {
|
||||
ReadReceiptsIntroFragment fragment = new ReadReceiptsIntroFragment();
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public ReadReceiptsIntroFragment() {}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.experience_upgrade_preference_fragment, container, false);
|
||||
SwitchCompat preference = ViewUtil.findById(v, R.id.preference);
|
||||
|
||||
preference.setChecked(TextSecurePreferences.isReadReceiptsEnabled(getContext()));
|
||||
preference.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
TextSecurePreferences.setReadReceiptsEnabled(getContext(), isChecked);
|
||||
ApplicationDependencies.getJobManager()
|
||||
.add(new MultiDeviceConfigurationUpdateJob(isChecked,
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
});
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,826 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
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.engine.DiskCacheStrategy;
|
||||
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.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
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.Dialogs;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
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_address";
|
||||
public static final String CAN_HAVE_SAFETY_NUMBER_EXTRA = "can_have_safety_number";
|
||||
|
||||
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 final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private ImageView avatar;
|
||||
private GlideRequests glideRequests;
|
||||
private RecipientId recipientId;
|
||||
private TextView threadPhotoRailLabel;
|
||||
private ThreadPhotoRailView threadPhotoRailView;
|
||||
private CollapsingToolbarLayout toolbarLayout;
|
||||
|
||||
@Override
|
||||
public void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.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);
|
||||
|
||||
getSupportLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
finish();
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
this.toolbarLayout = ViewUtil.findById(this, R.id.collapsing_toolbar);
|
||||
this.avatar = ViewUtil.findById(this, R.id.avatar);
|
||||
this.threadPhotoRailView = ViewUtil.findById(this, R.id.recent_photos);
|
||||
this.threadPhotoRailLabel = ViewUtil.findById(this, 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 -> {
|
||||
Intent intent = new Intent(RecipientPreferenceActivity.this, 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.LEFT_IS_RECENT_EXTRA, ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR);
|
||||
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
|
||||
startActivity(intent);
|
||||
});
|
||||
|
||||
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 = ViewUtil.findById(this, 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.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(this)))
|
||||
: 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)
|
||||
.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 = getActivity().getIntent()
|
||||
.getBooleanExtra(RecipientPreferenceActivity.CAN_HAVE_SAFETY_NUMBER_EXTRA, false);
|
||||
|
||||
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.PROFILE_DISPLAY) {
|
||||
aboutPreference.setTitle(recipient.getDisplayName(requireContext()));
|
||||
aboutPreference.setSummary(recipient.resolve().getE164().or(""));
|
||||
} else {
|
||||
aboutPreference.setTitle(formatRecipient(recipient));
|
||||
aboutPreference.setSummary(recipient.getCustomLabel());
|
||||
}
|
||||
|
||||
aboutPreference.setSecure(recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED);
|
||||
|
||||
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) {
|
||||
Intent verifyIdentityIntent = new Intent(preference.getContext(), VerifyIdentityActivity.class);
|
||||
verifyIdentityIntent.putExtra(VerifyIdentityActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
verifyIdentityIntent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey.getIdentityKey()));
|
||||
verifyIdentityIntent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, identityKey.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
|
||||
startActivity(verifyIdentityIntent);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class BlockClickedListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (recipient.get().isBlocked()) handleUnblock(preference.getContext());
|
||||
else handleBlock(preference.getContext());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleBlock(@NonNull final Context context) {
|
||||
new AsyncTask<Void, Void, Pair<Integer, Integer>>() {
|
||||
|
||||
@Override
|
||||
protected Pair<Integer, Integer> doInBackground(Void... voids) {
|
||||
int titleRes = R.string.RecipientPreferenceActivity_block_this_contact_question;
|
||||
int bodyRes = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact;
|
||||
|
||||
if (recipient.get().isGroup()) {
|
||||
bodyRes = R.string.RecipientPreferenceActivity_block_and_leave_group_description;
|
||||
|
||||
if (recipient.get().isGroup() && DatabaseFactory.getGroupDatabase(context).isActive(recipient.get().requireGroupId())) {
|
||||
titleRes = R.string.RecipientPreferenceActivity_block_and_leave_group;
|
||||
} else {
|
||||
titleRes = R.string.RecipientPreferenceActivity_block_group;
|
||||
}
|
||||
}
|
||||
|
||||
return new Pair<>(titleRes, bodyRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Pair<Integer, Integer> titleAndBody) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(titleAndBody.first)
|
||||
.setMessage(titleAndBody.second)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_block, (dialog, which) -> {
|
||||
setBlocked(context, recipient.get(), true);
|
||||
}).show();
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private void handleUnblock(@NonNull Context context) {
|
||||
int titleRes = R.string.RecipientPreferenceActivity_unblock_this_contact_question;
|
||||
int bodyRes = R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact;
|
||||
|
||||
if (recipient.resolve().isGroup()) {
|
||||
titleRes = R.string.RecipientPreferenceActivity_unblock_this_group_question;
|
||||
bodyRes = R.string.RecipientPreferenceActivity_unblock_this_group_description;
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(titleRes)
|
||||
.setMessage(bodyRes)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, (dialog, which) -> setBlocked(context, recipient.get(), false)).show();
|
||||
}
|
||||
|
||||
private void setBlocked(@NonNull final Context context, final Recipient recipient, final boolean blocked) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
if (blocked) {
|
||||
RecipientUtil.block(context, recipient);
|
||||
} else {
|
||||
RecipientUtil.unblock(context, recipient);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
try {
|
||||
Intent dialIntent = new Intent(Intent.ACTION_DIAL,
|
||||
Uri.parse("tel:" + recipient.get().requireE164()));
|
||||
startActivity(dialIntent);
|
||||
} catch (ActivityNotFoundException anfe) {
|
||||
Log.w(TAG, anfe);
|
||||
Dialogs.showAlertDialog(getContext(),
|
||||
getString(R.string.ConversationActivity_calls_not_supported),
|
||||
getString(R.string.ConversationActivity_this_device_does_not_appear_to_support_dial_actions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
379
app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java
Normal file
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
* Copyright (C) 2014-2017 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.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Process;
|
||||
import android.provider.OpenableColumns;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FileUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* An activity to quickly share content with contacts
|
||||
*
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class ShareActivity extends PassphraseRequiredActionBarActivity
|
||||
implements ContactSelectionListFragment.OnContactSelectedListener, SwipeRefreshLayout.OnRefreshListener
|
||||
{
|
||||
private static final String TAG = ShareActivity.class.getSimpleName();
|
||||
|
||||
public static final String EXTRA_THREAD_ID = "thread_id";
|
||||
public static final String EXTRA_RECIPIENT_ID = "recipient_id";
|
||||
public static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private SearchToolbar searchToolbar;
|
||||
private ImageView searchAction;
|
||||
private View progressWheel;
|
||||
private Uri resolvedExtra;
|
||||
private String mimeType;
|
||||
private boolean isPassingAlongMedia;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
int mode = DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS;
|
||||
|
||||
if (TextSecurePreferences.isSmsEnabled(this)) {
|
||||
mode |= DisplayMode.FLAG_SMS;
|
||||
|
||||
}
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, mode);
|
||||
}
|
||||
|
||||
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
getIntent().putExtra(ContactSelectionListFragment.RECENTS, true);
|
||||
|
||||
setContentView(R.layout.share_activity);
|
||||
|
||||
initializeToolbar();
|
||||
initializeResources();
|
||||
initializeSearch();
|
||||
initializeMedia();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
Log.i(TAG, "onNewIntent()");
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
initializeMedia();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
Log.i(TAG, "onResume()");
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (!isPassingAlongMedia && resolvedExtra != null) {
|
||||
BlobProvider.getInstance().delete(this, resolvedExtra);
|
||||
|
||||
if (!isFinishing()) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (searchToolbar.isVisible()) searchToolbar.collapse();
|
||||
else super.onBackPressed();
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
progressWheel = findViewById(R.id.progress_wheel);
|
||||
searchToolbar = findViewById(R.id.search_toolbar);
|
||||
searchAction = findViewById(R.id.search_action);
|
||||
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
contactsFragment.setOnRefreshListener(this);
|
||||
}
|
||||
|
||||
private void initializeSearch() {
|
||||
searchAction.setOnClickListener(v -> searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2),
|
||||
searchAction.getY() + (searchAction.getHeight() / 2)));
|
||||
|
||||
searchToolbar.setListener(new SearchToolbar.SearchListener() {
|
||||
@Override
|
||||
public void onSearchTextChange(String text) {
|
||||
if (contactsFragment != null) {
|
||||
contactsFragment.setQueryFilter(text);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchClosed() {
|
||||
if (contactsFragment != null) {
|
||||
contactsFragment.resetQueryFilter();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeMedia() {
|
||||
final Context context = this;
|
||||
isPassingAlongMedia = false;
|
||||
|
||||
Uri streamExtra = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
mimeType = getMimeType(streamExtra);
|
||||
|
||||
if (streamExtra != null && PartAuthority.isLocalUri(streamExtra)) {
|
||||
isPassingAlongMedia = true;
|
||||
resolvedExtra = streamExtra;
|
||||
handleResolvedMedia(getIntent(), false);
|
||||
} else {
|
||||
contactsFragment.getView().setVisibility(View.GONE);
|
||||
progressWheel.setVisibility(View.VISIBLE);
|
||||
new ResolveMediaTask(context).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, streamExtra);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleResolvedMedia(Intent intent, boolean animate) {
|
||||
long threadId = intent.getLongExtra(EXTRA_THREAD_ID, -1);
|
||||
int distributionType = intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, -1);
|
||||
RecipientId recipientId = null;
|
||||
|
||||
if (intent.hasExtra(EXTRA_RECIPIENT_ID)) {
|
||||
recipientId = RecipientId.from(intent.getStringExtra(EXTRA_RECIPIENT_ID));
|
||||
}
|
||||
|
||||
boolean hasResolvedDestination = threadId != -1 && recipientId != null && distributionType != -1;
|
||||
|
||||
if (hasResolvedDestination) {
|
||||
createConversation(threadId, recipientId, distributionType);
|
||||
} else if (animate) {
|
||||
ViewUtil.fadeIn(contactsFragment.requireView(), 300);
|
||||
ViewUtil.fadeOut(progressWheel, 300);
|
||||
} else {
|
||||
contactsFragment.requireView().setVisibility(View.VISIBLE);
|
||||
progressWheel.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void createConversation(long threadId, @NonNull RecipientId recipientId, int distributionType) {
|
||||
final Intent intent = getBaseShareIntent(ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
|
||||
|
||||
isPassingAlongMedia = true;
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private Intent getBaseShareIntent(final @NonNull Class<?> target) {
|
||||
final Intent intent = new Intent(this, target);
|
||||
final String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
final ArrayList<Media> mediaExtra = getIntent().getParcelableArrayListExtra(ConversationActivity.MEDIA_EXTRA);
|
||||
final StickerLocator stickerExtra = getIntent().getParcelableExtra(ConversationActivity.STICKER_EXTRA);
|
||||
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, textExtra);
|
||||
intent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaExtra);
|
||||
intent.putExtra(ConversationActivity.STICKER_EXTRA, stickerExtra);
|
||||
|
||||
if (resolvedExtra != null) intent.setDataAndType(resolvedExtra, mimeType);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
private String getMimeType(@Nullable Uri uri) {
|
||||
if (uri != null) {
|
||||
final String mimeType = MediaUtil.getMimeType(getApplicationContext(), uri);
|
||||
if (mimeType != null) return mimeType;
|
||||
}
|
||||
return MediaUtil.getCorrectedMimeType(getIntent().getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
SimpleTask.run(this.getLifecycle(), () -> {
|
||||
Recipient recipient;
|
||||
if (recipientId.isPresent()) {
|
||||
recipient = Recipient.resolved(recipientId.get());
|
||||
} else {
|
||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
||||
recipient = Recipient.external(this, number);
|
||||
}
|
||||
|
||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
|
||||
return new Pair<>(existingThread, recipient);
|
||||
}, result -> {
|
||||
createConversation(result.first(), result.second().getId(), ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private class ResolveMediaTask extends AsyncTask<Uri, Void, Uri> {
|
||||
private final Context context;
|
||||
|
||||
ResolveMediaTask(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Uri doInBackground(Uri... uris) {
|
||||
try {
|
||||
if (uris.length != 1 || uris[0] == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InputStream inputStream;
|
||||
|
||||
if ("file".equals(uris[0].getScheme())) {
|
||||
inputStream = openFileUri(uris[0]);
|
||||
} else {
|
||||
inputStream = context.getContentResolver().openInputStream(uris[0]);
|
||||
}
|
||||
|
||||
if (inputStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Cursor cursor = getContentResolver().query(uris[0], new String[] {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, null, null, null);
|
||||
String fileName = null;
|
||||
Long fileSize = null;
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
try {
|
||||
fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
|
||||
fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
|
||||
return BlobProvider.getInstance()
|
||||
.forData(inputStream, fileSize == null ? 0 : fileSize)
|
||||
.withMimeType(mimeType)
|
||||
.withFileName(fileName)
|
||||
.createForMultipleSessionsOnDisk(context);
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Uri uri) {
|
||||
resolvedExtra = uri;
|
||||
handleResolvedMedia(getIntent(), true);
|
||||
}
|
||||
|
||||
private InputStream openFileUri(Uri uri) throws IOException {
|
||||
FileInputStream fin = new FileInputStream(uri.getPath());
|
||||
int owner = FileUtils.getFileDescriptorOwner(fin.getFD());
|
||||
|
||||
if (owner == -1 || owner == Process.myUid()) {
|
||||
fin.close();
|
||||
throw new IOException("File owned by application");
|
||||
}
|
||||
|
||||
return fin;
|
||||
}
|
||||
}
|
||||
}
|
||||