mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Compare commits
2497 commits
4.7.0-beta
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6f897cb39 | ||
|
|
97606578a4 | ||
|
|
48c9b4d7af | ||
|
|
7d0d1a22ee | ||
|
|
d467699388 | ||
|
|
5ad7b5da14 | ||
|
|
50c922b581 | ||
|
|
b574bf420c | ||
|
|
3df3c8d741 | ||
|
|
4039da9c8a | ||
|
|
3cec19126d | ||
|
|
a816a956b8 | ||
|
|
cec3639b73 | ||
|
|
07cae7eb12 | ||
|
|
3b561275a4 | ||
|
|
3f868e02fe | ||
|
|
57644a34de | ||
|
|
e306c8c7fc | ||
|
|
dd9190df07 | ||
|
|
d90861b5f3 | ||
|
|
9151898a4d | ||
|
|
fe788caf0e | ||
|
|
7e0353cc91 | ||
|
|
a897c127e5 | ||
|
|
50aa053c19 | ||
|
|
b88b6a8093 | ||
|
|
24d808b1a7 | ||
|
|
6f09853424 | ||
|
|
a7593e07fc | ||
|
|
844b182df2 | ||
|
|
d299b0b129 | ||
|
|
965b159139 | ||
|
|
00b8e59ade | ||
|
|
be47deeb40 | ||
|
|
e8c67fdd6f | ||
|
|
ff98c15840 | ||
|
|
3711fd749e | ||
|
|
dce7095f74 | ||
|
|
d2b12159af | ||
|
|
618be9ee7c | ||
|
|
e1abcc6dca | ||
|
|
2a9ef440b7 | ||
|
|
77b933c5a8 | ||
|
|
6b56165d4f | ||
|
|
61c79a86f7 | ||
|
|
40d195e06b | ||
|
|
e173e402c2 | ||
|
|
7817e6603c | ||
|
|
bf4b5a51f5 | ||
|
|
3ffda24b82 | ||
|
|
c99acbb5e1 | ||
|
|
cc1cc7d929 | ||
|
|
6bcce4ddbf | ||
|
|
696a593cbc | ||
|
|
88e474533e | ||
|
|
8e76c60a38 | ||
|
|
85aa50d8d8 | ||
|
|
c496545023 | ||
|
|
1183a9e1c2 | ||
|
|
170cd6fccc | ||
|
|
bc7ac8be64 | ||
|
|
5ed68e0171 | ||
|
|
41e6776b32 | ||
|
|
e290a8c4ea | ||
|
|
93e26f6c10 | ||
|
|
3f22a596db | ||
|
|
d5c836b8b5 | ||
|
|
9afcb6db15 | ||
|
|
a6f568497d | ||
|
|
209c0df091 | ||
|
|
7b0de4185c | ||
|
|
89458ed826 | ||
|
|
a3f86fbac0 | ||
|
|
28cee7f539 | ||
|
|
daa2f10f7b | ||
|
|
e14ea0ac68 | ||
|
|
c3ad96cd1f | ||
|
|
c6a0f25041 | ||
|
|
7ab7136a5b | ||
|
|
3698e1673e | ||
|
|
bdd5c8766b | ||
|
|
ce2b794936 | ||
|
|
e267f46fd7 | ||
|
|
ab6911dd11 | ||
|
|
b0283043ee | ||
|
|
0e71a726c1 | ||
|
|
d74ccb523e | ||
|
|
4dc1b9a903 | ||
|
|
45c756cfd6 | ||
|
|
069997d780 | ||
|
|
e2c9e1196f | ||
|
|
e8c642b9c6 | ||
|
|
d75c48cd34 | ||
|
|
d9ab840570 | ||
|
|
5ee3ba4ea9 | ||
|
|
d694789d4b | ||
|
|
b71249ea36 | ||
|
|
7855d4e1db | ||
|
|
7e2527c46c | ||
|
|
d16dbcf0fd | ||
|
|
1d28ce1846 | ||
|
|
2ea38abdfe | ||
|
|
416cc6ea7f | ||
|
|
6dc4790597 | ||
|
|
f8556aa46b | ||
|
|
df09bcad76 | ||
|
|
0ca4eba63b | ||
|
|
c556d14fb0 | ||
|
|
6cb78c8c59 | ||
|
|
61517461dd | ||
|
|
1fdc2bcc58 | ||
|
|
8f3415f6fa | ||
|
|
ae7a3c5bce | ||
|
|
31e15ddfca | ||
|
|
808dc92cd7 | ||
|
|
99936e8f75 | ||
|
|
2ce07b5e89 | ||
|
|
9d3ef9e8a5 | ||
|
|
5f17dd8534 | ||
|
|
719b28f0ab | ||
|
|
6f1439756e | ||
|
|
7fdbaf5fd6 | ||
|
|
4639e054bb | ||
|
|
504f6e2a2c | ||
|
|
1e6f501dee | ||
|
|
633aee829a | ||
|
|
38ffac31b4 | ||
|
|
6e40e3f75f | ||
|
|
7d7900e081 | ||
|
|
f0e899bb95 | ||
|
|
db7ca6793b | ||
|
|
ac521557ce | ||
|
|
b5babae39a | ||
|
|
ce13d4c7d4 | ||
|
|
e9cc03891b | ||
|
|
98bf3daed8 | ||
|
|
a5cee98a57 | ||
|
|
26e391cbf8 | ||
|
|
0add60c628 | ||
|
|
fc90a95e94 | ||
|
|
c153b2d928 | ||
|
|
881e2c217b | ||
|
|
77d744d020 | ||
|
|
5a16761fdf | ||
|
|
e0ff593f3d | ||
|
|
71bb569fde | ||
|
|
9096225b45 | ||
|
|
ad0037fe4c | ||
|
|
7eed9c06d3 | ||
|
|
e2dabf5448 | ||
|
|
08c72dbb8c | ||
|
|
f99b51d572 | ||
|
|
8f0f6581b2 | ||
|
|
fac6e42c22 | ||
|
|
e8d3c8750a | ||
|
|
dee932da42 | ||
|
|
2f4fd3da18 | ||
|
|
22ae4e372f | ||
|
|
95bd14bdd4 | ||
|
|
ffabd02f31 | ||
|
|
0bb7761db9 | ||
|
|
50418f5dbb | ||
|
|
e800249445 | ||
|
|
ec5b6e5707 | ||
|
|
97c6c0b553 | ||
|
|
95ce77e0e4 | ||
|
|
243a6d8cb2 | ||
|
|
e98318a23d | ||
|
|
ace0a3f61e | ||
|
|
b3ac16052f | ||
|
|
865216d717 | ||
|
|
670eecf0d6 | ||
|
|
8dcb18d059 | ||
|
|
92672bde0a | ||
|
|
ada6f35d92 | ||
|
|
332828dc7c | ||
|
|
595ff96d50 | ||
|
|
6cdcdec373 | ||
|
|
3098c3e68e | ||
|
|
2c9d627794 | ||
|
|
70df098ee4 | ||
|
|
c32bac7b07 | ||
|
|
3d41a4d221 | ||
|
|
dfa87e4088 | ||
|
|
d6b43c474b | ||
|
|
9d0f2cafc9 | ||
|
|
98cc173d2e | ||
|
|
4ae046a166 | ||
|
|
62180140b7 | ||
|
|
899129d4bc | ||
|
|
c4618702ab | ||
|
|
8c7c7b40c3 | ||
|
|
856f3e7f94 | ||
|
|
f7be887984 | ||
|
|
028ece407c | ||
|
|
bb81957aab | ||
|
|
da581c3737 | ||
|
|
461537aa9c | ||
|
|
1a54746a80 | ||
|
|
be24224f4c | ||
|
|
f2cdb92858 | ||
|
|
aa0255bcfd | ||
|
|
ff425089c7 | ||
|
|
b4c2a52bf7 | ||
|
|
f397456879 | ||
|
|
5337ab6413 | ||
|
|
998f969c0f | ||
|
|
58410ee112 | ||
|
|
6c97ee9176 | ||
|
|
187946bf34 | ||
|
|
3c40bf3d6f | ||
|
|
b7a9f4ba8e | ||
|
|
4a1c5304b1 | ||
|
|
67e3c51a84 | ||
|
|
4d8ab32da7 | ||
|
|
62ff36e7a7 | ||
|
|
6f80409086 | ||
|
|
fd3f746e3d | ||
|
|
42fbbc51fd | ||
|
|
60c74ee5b2 | ||
|
|
79212a8757 | ||
|
|
1307ec5471 | ||
|
|
c62f549521 | ||
|
|
9ba2684f31 | ||
|
|
4b631a19ef | ||
|
|
496279d724 | ||
|
|
c25ed404dc | ||
|
|
a81973e4cf | ||
|
|
654e790a6d | ||
|
|
6602c7692b | ||
|
|
cc57244b56 | ||
|
|
19df3b07dc | ||
|
|
4ef9a2bdf3 | ||
|
|
61be1d21d5 | ||
|
|
8148354901 | ||
|
|
ae39d79420 | ||
|
|
2bd0de4af1 | ||
|
|
cb27b35984 | ||
|
|
316bc6698a | ||
|
|
fea1dbe5ca | ||
|
|
85679dcc43 | ||
|
|
32060f6830 | ||
|
|
dfdc26a575 | ||
|
|
9e1d358f4e | ||
|
|
90922568b5 | ||
|
|
d212b7b06e | ||
|
|
4cb83980ba | ||
|
|
def52f69ad | ||
|
|
17ce34aba7 | ||
|
|
1833b1985d | ||
|
|
5256ee79c6 | ||
|
|
27e59a5f8b | ||
|
|
cea2d49778 | ||
|
|
c0f67d01fe | ||
|
|
b6279b03c0 | ||
|
|
21398c7b37 | ||
|
|
4cb7ea1965 | ||
|
|
7aae03f1f9 | ||
|
|
25d13f44c7 | ||
|
|
81d0da4241 | ||
|
|
1556abc79e | ||
|
|
28b6bd7e90 | ||
|
|
1c3173b871 | ||
|
|
17588de5a9 | ||
|
|
502c6413ee | ||
|
|
02cbb45de9 | ||
|
|
f5852a7b3e | ||
|
|
cfec621787 | ||
|
|
6847227f1a | ||
|
|
f1fdb186ec | ||
|
|
d822cbc827 | ||
|
|
627f881364 | ||
|
|
244061c0b1 | ||
|
|
dc2b94ca4d | ||
|
|
7c78b021db | ||
|
|
d113797dfb | ||
|
|
926b8d4dc1 | ||
|
|
85e24e25bf | ||
|
|
1c1729f3f0 | ||
|
|
bcce9a9ba1 | ||
|
|
a496e2bf56 | ||
|
|
73237ee335 | ||
|
|
b293bf7f2f | ||
|
|
4689b7c7da | ||
|
|
e38040428b | ||
|
|
b740409642 | ||
|
|
99a5ed23f6 | ||
|
|
c9a3a01733 | ||
|
|
966f713f19 | ||
|
|
344afdfcfa | ||
|
|
2634945b8d | ||
|
|
2713c82ca3 | ||
|
|
1a813ee11e | ||
|
|
e5cec2d45c | ||
|
|
6e9c6d1b33 | ||
|
|
056abd629f | ||
|
|
6c86af747b | ||
|
|
f7790fbed7 | ||
|
|
90524da610 | ||
|
|
616b7bb70f | ||
|
|
985a304df9 | ||
|
|
cd35f213c1 | ||
|
|
dcbc837106 | ||
|
|
5ef7eab0c5 | ||
|
|
c64bd5bc1c | ||
|
|
afa041baf6 | ||
|
|
94b6db6a08 | ||
|
|
6ba8760be7 | ||
|
|
b1b1ab0d8a | ||
|
|
518ecc1823 | ||
|
|
e2dfd95857 | ||
|
|
51d725c757 | ||
|
|
af3b1fa418 | ||
|
|
bc9a6581b1 | ||
|
|
26df085df3 | ||
|
|
c08157b659 | ||
|
|
910527ef1b | ||
|
|
8577571e67 | ||
|
|
dbca62bea9 | ||
|
|
5e9be7d10b | ||
|
|
836deaae99 | ||
|
|
a2680028ce | ||
|
|
c8ff7262d4 | ||
|
|
06d8e903fc | ||
|
|
a5872ef8de | ||
|
|
9255830fe2 | ||
|
|
80eaf08fbf | ||
|
|
903aaad6fe | ||
|
|
bdb2615300 | ||
|
|
bab2acb75c | ||
|
|
bd52960749 | ||
|
|
23810e41e5 | ||
|
|
3f3a229844 | ||
|
|
0eb659b633 | ||
|
|
c35a44b1a0 | ||
|
|
1cccf7d26b | ||
|
|
0fab732e89 | ||
|
|
317a7c4417 | ||
|
|
689665c475 | ||
|
|
18e15b60a4 | ||
|
|
7bead679ad | ||
|
|
f0ad67fb29 | ||
|
|
2621eb306e | ||
|
|
90bf20e50e | ||
|
|
1f45ba8bd0 | ||
|
|
c528f0cdb8 | ||
|
|
a0108776dd | ||
|
|
a503ef06ee | ||
|
|
fbc19c7053 | ||
|
|
9c8c5f309e | ||
|
|
b40fbcad77 | ||
|
|
8dda38a925 | ||
|
|
d150027c24 | ||
|
|
7018cd3442 | ||
|
|
d6494cd27c | ||
|
|
10f2d7cd78 | ||
|
|
6767bc09f9 | ||
|
|
b22ab7024e | ||
|
|
c6fa645f94 | ||
|
|
fb3feb0bc3 | ||
|
|
77f61c1cfa | ||
|
|
9ce803667b | ||
|
|
50bd8f67d5 | ||
|
|
faac4111d9 | ||
|
|
6121cb41bf | ||
|
|
2aed404167 | ||
|
|
2f9eb2f0ab | ||
|
|
1255d626af | ||
|
|
a83f9d4424 | ||
|
|
fecf067b50 | ||
|
|
b194272f91 | ||
|
|
cad90752db | ||
|
|
2abad0ab9a | ||
|
|
a0d74c8036 | ||
|
|
2eb376fd2d | ||
|
|
1942ee8f85 | ||
|
|
488a0fd98c | ||
|
|
08412ef99a | ||
|
|
e16e767d5a | ||
|
|
886be9e038 | ||
|
|
2a5b5d368c | ||
|
|
be5428aa08 | ||
|
|
d6c6de2b5e | ||
|
|
915a847083 | ||
|
|
0d8397b914 | ||
|
|
b5a1e21f40 | ||
|
|
9837a834d4 | ||
|
|
8a4956e7c1 | ||
|
|
052d7cc522 | ||
|
|
6c6fb9eff3 | ||
|
|
b23f52adec | ||
|
|
8769a47ed0 | ||
|
|
ebb7201701 | ||
|
|
6e83b794b3 | ||
|
|
3045378eb0 | ||
|
|
dc4619a7d7 | ||
|
|
87b6c2deef | ||
|
|
614ac7f9cf | ||
|
|
71e1734ca0 | ||
|
|
71b1cf8e7a | ||
|
|
dee684b364 | ||
|
|
0b6805a73c | ||
|
|
11795cded8 | ||
|
|
cc5bfcf14d | ||
|
|
b3ab9601b2 | ||
|
|
0e6d91a467 | ||
|
|
fbf68db2dd | ||
|
|
0bf50f1495 | ||
|
|
8363d41441 | ||
|
|
c1e20af56d | ||
|
|
5d14d8bb7b | ||
|
|
a99067f701 | ||
|
|
2ceebdcdda | ||
|
|
dca10c9b2a | ||
|
|
82e341ea8c | ||
|
|
508f1154f5 | ||
|
|
07cb09128e | ||
|
|
d6d705a975 | ||
|
|
c709c720e4 | ||
|
|
96a07fa8c6 | ||
|
|
141d7b08a6 | ||
|
|
3c21044cf0 | ||
|
|
e0e7032827 | ||
|
|
4cca59a39f | ||
|
|
4e852601fc | ||
|
|
30364c48b0 | ||
|
|
354f39d76d | ||
|
|
64a2c5f455 | ||
|
|
9ffe3b4d7f | ||
|
|
fb323a4606 | ||
|
|
7d6c50cf29 | ||
|
|
8b9ceef6da | ||
|
|
12e6de52a8 | ||
|
|
990549eb24 | ||
|
|
4afa2ebc93 | ||
|
|
e05d4cf94a | ||
|
|
01c079440d | ||
|
|
8eda500dae | ||
|
|
a392cf3a6b | ||
|
|
977fb63693 | ||
|
|
fcd365ad81 | ||
|
|
f1b23337e0 | ||
|
|
296a324ba3 | ||
|
|
7e2a4c124d | ||
|
|
b8a6177f97 | ||
|
|
f3991fb29d | ||
|
|
0bf6d60570 | ||
|
|
ef79475525 | ||
|
|
ec9c7bd070 | ||
|
|
875198164d | ||
|
|
17511a4c26 | ||
|
|
ca08ed68be | ||
|
|
e292b0d7e8 | ||
|
|
8c1889b181 | ||
|
|
037cd71814 | ||
|
|
40e9dfc522 | ||
|
|
7e0c6a23a9 | ||
|
|
7ccd42580d | ||
|
|
a238ae8db0 | ||
|
|
a210ea67c1 | ||
|
|
dd454113e8 | ||
|
|
690140c2b8 | ||
|
|
ff323cea68 | ||
|
|
fede808df1 | ||
|
|
ba5786fa0a | ||
|
|
22b447a67f | ||
|
|
ea79e9243d | ||
|
|
709f7dd3c5 | ||
|
|
9381b459a0 | ||
|
|
7bf9eb8394 | ||
|
|
30fc60c0ef | ||
|
|
3e5a0c22f8 | ||
|
|
2b75ecdaca | ||
|
|
6afd711539 | ||
|
|
bfc435c350 | ||
|
|
0f59e1a381 | ||
|
|
dd7546484b | ||
|
|
86b35354c3 | ||
|
|
3e91f3e5ff | ||
|
|
74394445c9 | ||
|
|
536667cfe1 | ||
|
|
b057163b43 | ||
|
|
63051ae58e | ||
|
|
00a7d34509 | ||
|
|
da385ee6e1 | ||
|
|
2952b2db01 | ||
|
|
2ed4f44e50 | ||
|
|
1c09388266 | ||
|
|
e5795ea05f | ||
|
|
0625239477 | ||
|
|
5e6186d115 | ||
|
|
28998d4463 | ||
|
|
6121040bda | ||
|
|
0f3aea191f | ||
|
|
e9385b5c07 | ||
|
|
ec68e931c4 | ||
|
|
37c23066f0 | ||
|
|
a5b8a8a683 | ||
|
|
6d08625168 | ||
|
|
b4329af83a | ||
|
|
de5f44ba04 | ||
|
|
e686ed90e9 | ||
|
|
f5ba48b9f0 | ||
|
|
4dd92cddf0 | ||
|
|
1ead4d9218 | ||
|
|
6a41f7f67d | ||
|
|
febc6a85d3 | ||
|
|
e1bcae703c | ||
|
|
d05da148e7 | ||
|
|
ec9984c86b | ||
|
|
456181609b | ||
|
|
f7d65b102e | ||
|
|
06d1ae81b5 | ||
|
|
8477980011 | ||
|
|
e283b7b48e | ||
|
|
fa9bcd3475 | ||
|
|
7f49a7756c | ||
|
|
e7a4a24eaf | ||
|
|
e938105db0 | ||
|
|
ca7035d84a | ||
|
|
27b4fa63f9 | ||
|
|
e464a49176 | ||
|
|
15f9d04747 | ||
|
|
9504c6d1ca | ||
|
|
d0c733e81b | ||
|
|
e85c97837f | ||
|
|
375c020b9b | ||
|
|
fee595cfdc | ||
|
|
dbb1793ea0 | ||
|
|
4b0fcd38d7 | ||
|
|
3a3518b2a5 | ||
|
|
788dd338fb | ||
|
|
2e89f4d101 | ||
|
|
8b183fd347 | ||
|
|
815a2ed854 | ||
|
|
2ba54f085d | ||
|
|
ae80e45e1c | ||
|
|
7967a97b01 | ||
|
|
d5e20c02fe | ||
|
|
00fbfcc490 | ||
|
|
17e3622bdc | ||
|
|
6b7591c971 | ||
|
|
633fa07164 | ||
|
|
fbe5885d08 | ||
|
|
099350244a | ||
|
|
eac72a0e42 | ||
|
|
de29fbc125 | ||
|
|
864677dab1 | ||
|
|
c500761940 | ||
|
|
e7f888ad78 | ||
|
|
32a3feef42 | ||
|
|
e379833e18 | ||
|
|
486706299d | ||
|
|
b1a03db96f | ||
|
|
5cee11c779 | ||
|
|
a9736df3eb | ||
|
|
da848ecc61 | ||
|
|
942e78eede | ||
|
|
b084807d18 | ||
|
|
d0dc42c67b | ||
|
|
83f249b1d9 | ||
|
|
8f52182959 | ||
|
|
2c9a15a007 | ||
|
|
5d96ea3b56 | ||
|
|
e1c4fc2525 | ||
|
|
049f63b61e | ||
|
|
eebc1bc91e | ||
|
|
0d5bf5ed3e | ||
|
|
bad1621126 | ||
|
|
8e93fefda3 | ||
|
|
ea92ca2220 | ||
|
|
3734c8ca90 | ||
|
|
aad50669c4 | ||
|
|
679f125870 | ||
|
|
a90dd53c9c | ||
|
|
e664e1802c | ||
|
|
20fc177c7a | ||
|
|
7c755bd080 | ||
|
|
372a16d3ab | ||
|
|
d4b0356bda | ||
|
|
31b9836cc0 | ||
|
|
0571862142 | ||
|
|
0497237630 | ||
|
|
75905c1321 | ||
|
|
676a2c7710 | ||
|
|
967fb0563d | ||
|
|
3fb5d8a97b | ||
|
|
4682a75049 | ||
|
|
466395a34b | ||
|
|
2380c0fa6d | ||
|
|
137e8941cb | ||
|
|
409baab7c4 | ||
|
|
f068b5c9ce | ||
|
|
3cdcc4bb77 | ||
|
|
a3ed13bc79 | ||
|
|
3a23c8813e | ||
|
|
9d58ced715 | ||
|
|
86e2c731da | ||
|
|
5593e06cf5 | ||
|
|
7bfed48b23 | ||
|
|
821986b6a9 | ||
|
|
b9065d3c7c | ||
|
|
47a308dcfc | ||
|
|
9f6fb13e80 | ||
|
|
6d3ef66995 | ||
|
|
e57ff021e9 | ||
|
|
d5ca4c29d3 | ||
|
|
b3da6ed347 | ||
|
|
b6e36272f7 | ||
|
|
41081f565a | ||
|
|
b172396f0d | ||
|
|
95b6a8e7a5 | ||
|
|
d6ce9250fc | ||
|
|
0e057d8aa4 | ||
|
|
e0c58c0ac5 | ||
|
|
4304552bc8 | ||
|
|
ab1d271b76 | ||
|
|
b5b57405c6 | ||
|
|
6476bb518d | ||
|
|
f9cf90fecd | ||
|
|
e5f227ba35 | ||
|
|
d718cff486 | ||
|
|
3167544873 | ||
|
|
af4ff25310 | ||
|
|
75bb28cb2f | ||
|
|
935d463896 | ||
|
|
c988504319 | ||
|
|
5bcf0e8ddb | ||
|
|
a5f846a26d | ||
|
|
877565e516 | ||
|
|
3a7265295e | ||
|
|
46dc7355b2 | ||
|
|
d80b023918 | ||
|
|
c4eb74ff0d | ||
|
|
f48da167ab | ||
|
|
6b11e37b14 | ||
|
|
5a1487a691 | ||
|
|
badbbde183 | ||
|
|
713ca4c1d5 | ||
|
|
d312141eda | ||
|
|
4f0ca4adca | ||
|
|
a4c897b47d | ||
|
|
711f2f4200 | ||
|
|
49d8381705 | ||
|
|
0ce886cb56 | ||
|
|
40541d3316 | ||
|
|
11e44d1fc4 | ||
|
|
d864b5efa5 | ||
|
|
0b0d7ce85a | ||
|
|
363ce834fa | ||
|
|
8bb6b61edc | ||
|
|
2d2a5e26f6 | ||
|
|
213e62d125 | ||
|
|
8d78f2b698 | ||
|
|
f3a2020466 | ||
|
|
167b42810f | ||
|
|
d71966e77d | ||
|
|
cf901a9c2b | ||
|
|
a743a0d2c6 | ||
|
|
601aaf0b5c | ||
|
|
f65666a996 | ||
|
|
4f5ea3b5a4 | ||
|
|
90971224d5 | ||
|
|
1f08682340 | ||
|
|
1e4066163f | ||
|
|
b7e7e08bbe | ||
|
|
eb80a16202 | ||
|
|
a7fb2ccfec | ||
|
|
4afb10d82a | ||
|
|
f74976f563 | ||
|
|
a96940b94a | ||
|
|
3fcbc9bf28 | ||
|
|
a2bf235656 | ||
|
|
40093d77cf | ||
|
|
e205d0ef00 | ||
|
|
416f4ffef0 | ||
|
|
68fe26e05f | ||
|
|
cda5deb18d | ||
|
|
5c8e4bcc22 | ||
|
|
70b4a500d2 | ||
|
|
135ca527ee | ||
|
|
c88917ac68 | ||
|
|
d02ca882bd | ||
|
|
807a36b54c | ||
|
|
5f6c02e2ca | ||
|
|
9a33def8a4 | ||
|
|
88b4227bbc | ||
|
|
5da87f598a | ||
|
|
612c8b3301 | ||
|
|
6ad5f7573b | ||
|
|
2be4f691f2 | ||
|
|
eeb19846cc | ||
|
|
467e029599 | ||
|
|
4ff8c7c7eb | ||
|
|
cc6ec98846 | ||
|
|
257352927d | ||
|
|
50cb162bd3 | ||
|
|
e562f1505d | ||
|
|
d5d3cc0bc2 | ||
|
|
d03f94d52a | ||
|
|
6edd20e214 | ||
|
|
0217ca78a0 | ||
|
|
c435852b32 | ||
|
|
c6fe442b09 | ||
|
|
6d23402001 | ||
|
|
e3d356765d | ||
|
|
d446e6d998 | ||
|
|
157ea2c847 | ||
|
|
97f12b150a | ||
|
|
6826a51307 | ||
|
|
8a39529fd1 | ||
|
|
865f3b9692 | ||
|
|
b0dbfbcc3d | ||
|
|
7d9a3edf31 | ||
|
|
d0ded694f8 | ||
|
|
899a3ea374 | ||
|
|
27b1cf90c6 | ||
|
|
6d959d489b | ||
|
|
9e08a5a506 | ||
|
|
86e09b5dce | ||
|
|
248a06c8be | ||
|
|
e8100e58da | ||
|
|
10c68a2c28 | ||
|
|
cfe00d1b61 | ||
|
|
20daa67ccf | ||
|
|
42cf8fd89e | ||
|
|
bb4e9fdeb5 | ||
|
|
cd8785855d | ||
|
|
fe2a074d0b | ||
|
|
63cb7d6630 | ||
|
|
eaa498f1ad | ||
|
|
f6d4f56bbc | ||
|
|
31d92abcdf | ||
|
|
92ec1940e5 | ||
|
|
5ebb4ee6ac | ||
|
|
a9d11543d8 | ||
|
|
0e451dcc8a | ||
|
|
e74ebb0d81 | ||
|
|
4f10a2d1fc | ||
|
|
690f6fa4c2 | ||
|
|
45cfac3ac6 | ||
|
|
db2ce910b9 | ||
|
|
0baceddf27 | ||
|
|
fa7486ff36 | ||
|
|
b2b55305d2 | ||
|
|
5eece24d68 | ||
|
|
51dd246971 | ||
|
|
3190a1869d | ||
|
|
2c2beb5725 | ||
|
|
7b8b92706b | ||
|
|
8fc3185278 | ||
|
|
96d85027c3 | ||
|
|
41721e3994 | ||
|
|
1a35a7048d | ||
|
|
49059d6b3c | ||
|
|
c0e8bb6c1a | ||
|
|
1ef4e57d9e | ||
|
|
85a61839bc | ||
|
|
30ab0fa827 | ||
|
|
439e115338 | ||
|
|
722840f1c5 | ||
|
|
db751efa91 | ||
|
|
6c22b1f66d | ||
|
|
9fc9574369 | ||
|
|
ce4fed2197 | ||
|
|
e03dcf3f88 | ||
|
|
d434ab7298 | ||
|
|
4673db9e4e | ||
|
|
6893d35a93 | ||
|
|
4cbad5308c | ||
|
|
7fb3a6ada3 | ||
|
|
ace4caca3f | ||
|
|
95c4106bd0 | ||
|
|
c2e1333be1 | ||
|
|
b4a754db53 | ||
|
|
49c3e68f84 | ||
|
|
3359fbcbd7 | ||
|
|
6308d66eb6 | ||
|
|
4628560411 | ||
|
|
84af9437bc | ||
|
|
8a1d88c4b5 | ||
|
|
5a7872222d | ||
|
|
5d7addb8d8 | ||
|
|
bed9288f21 | ||
|
|
ed1cf58fd4 | ||
|
|
a29777eebc | ||
|
|
1f604d54f2 | ||
|
|
245b848c91 | ||
|
|
34e1f0070e | ||
|
|
fb9d1c5c0d | ||
|
|
b301a5227a | ||
|
|
94f2c1cc98 | ||
|
|
cbaf7673f5 | ||
|
|
4d190cabdd | ||
|
|
5c77b58154 | ||
|
|
ce1c3dad65 | ||
|
|
4a75315240 | ||
|
|
317bbb470b | ||
|
|
d8d424d446 | ||
|
|
ff81a1c615 | ||
|
|
769006b043 | ||
|
|
c40c15b66f | ||
|
|
7c3bed7dd4 | ||
|
|
674fa1f41b | ||
|
|
ab1ea3392d | ||
|
|
2821d4b72f | ||
|
|
9ed2415d1b | ||
|
|
859e32e655 | ||
|
|
f325c5ebbd | ||
|
|
be849e0c80 | ||
|
|
de9a2318af | ||
|
|
9ec927c0c4 | ||
|
|
453b986f82 | ||
|
|
b123082559 | ||
|
|
27298639c3 | ||
|
|
2c09158977 | ||
|
|
bf4ab1b412 | ||
|
|
ce7779f720 | ||
|
|
8c8f15b02d | ||
|
|
ded00052b5 | ||
|
|
e0dc53564e | ||
|
|
349167868f | ||
|
|
00d14feded | ||
|
|
48fd2ba1f8 | ||
|
|
dced2dae7e | ||
|
|
00dd62553b | ||
|
|
a855c569fb | ||
|
|
2021c5e102 | ||
|
|
bd93f0ed71 | ||
|
|
5889389866 | ||
|
|
2104d79e1c | ||
|
|
415da6f03d | ||
|
|
1d0b5a5d4d | ||
|
|
e5c7fa07cc | ||
|
|
9c40709666 | ||
|
|
4978c9a16d | ||
|
|
196a010f36 | ||
|
|
b58a23b60d | ||
|
|
12c112fa39 | ||
|
|
3d8bc10499 | ||
|
|
fdee7e618e | ||
|
|
5ce7a5524a | ||
|
|
ef32dac910 | ||
|
|
ea49d3a411 | ||
|
|
6280ed5f3d | ||
|
|
51d9b18c1c | ||
|
|
6746e71197 | ||
|
|
57f3b0c78b | ||
|
|
455db9b9eb | ||
|
|
c8d9248e0c | ||
|
|
a7f868fe15 | ||
|
|
59aa036875 | ||
|
|
1c8a376c7f | ||
|
|
1ee5993624 | ||
|
|
247788c64c | ||
|
|
505fa3b66c | ||
|
|
a676c51401 | ||
|
|
8e588bf800 | ||
|
|
f29f5f9bc7 | ||
|
|
a136b7da8b | ||
|
|
bc38ff19b6 | ||
|
|
9e3a7055fe | ||
|
|
36cc74956e | ||
|
|
6c20ac8d40 | ||
|
|
59ebff6d15 | ||
|
|
b55d7070b0 | ||
|
|
55cd29e710 | ||
|
|
8a410fc77f | ||
|
|
5cc491335c | ||
|
|
e14a142fb7 | ||
|
|
f3b6627635 | ||
|
|
3ec942c475 | ||
|
|
ce5a5c5ddc | ||
|
|
a53542c092 | ||
|
|
ae59911f0a | ||
|
|
e29d64a02b | ||
|
|
4c66012372 | ||
|
|
9465593aa6 | ||
|
|
2394c701cf | ||
|
|
0bea45054b | ||
|
|
756a83797f | ||
|
|
87f4ebbd4c | ||
|
|
5689557487 | ||
|
|
e85488cf65 | ||
|
|
83df46ca8b | ||
|
|
1d328f98e3 | ||
|
|
b5b37bd74d | ||
|
|
a6b510e536 | ||
|
|
22795a5284 | ||
|
|
4c9bdec61e | ||
|
|
a73e483478 | ||
|
|
8ce91d1300 | ||
|
|
79ca1d6505 | ||
|
|
7d40ad5ad1 | ||
|
|
681c0f22c3 | ||
|
|
2086fbad66 | ||
|
|
fb86cd9bcb | ||
|
|
b9addcf683 | ||
|
|
6c757a9637 | ||
|
|
6cabf0bdf7 | ||
|
|
179a6c39ca | ||
|
|
8a92624254 | ||
|
|
7aebfafde3 | ||
|
|
1cd79b5086 | ||
|
|
25be5e1814 | ||
|
|
c7032b6000 | ||
|
|
bf1140fb38 | ||
|
|
e012d45c10 | ||
|
|
11eb0199f8 | ||
|
|
949d870d7e | ||
|
|
0cfdedc09a | ||
|
|
d8c406320c | ||
|
|
a278333eb4 | ||
|
|
0d5b189978 | ||
|
|
e1852f4ae4 | ||
|
|
964a597aa1 | ||
|
|
5ae345e794 | ||
|
|
5289dc4824 | ||
|
|
f94b57d304 | ||
|
|
72fd952a55 | ||
|
|
811a0466d4 | ||
|
|
f449763c4e | ||
|
|
c4fa68858c | ||
|
|
77e99fbfd3 | ||
|
|
af3cab475b | ||
|
|
57f8ff3341 | ||
|
|
54ee456f8e | ||
|
|
f3eb821946 | ||
|
|
a788191e50 | ||
|
|
94e6b28f4f | ||
|
|
ce3c37ad15 | ||
|
|
af6cfdfc18 | ||
|
|
515b645b89 | ||
|
|
c6550b6256 | ||
|
|
e3f1611a0f | ||
|
|
08052d64fd | ||
|
|
8f34c3ea5c | ||
|
|
ceb3679975 | ||
|
|
72f8574a1e | ||
|
|
ec6316f6e5 | ||
|
|
4bd6dc4e0f | ||
|
|
cad05a0c83 | ||
|
|
c68db48de9 | ||
|
|
e80c0e6068 | ||
|
|
b3464433ed | ||
|
|
5cd0a741ab | ||
|
|
60a3752fe8 | ||
|
|
217f116324 | ||
|
|
1b644741c6 | ||
|
|
c7586feebc | ||
|
|
5b80833f30 | ||
|
|
a858cffc82 | ||
|
|
974e0cffc6 | ||
|
|
9f36ec950f | ||
|
|
791e209d62 | ||
|
|
cb589b95c8 | ||
|
|
5e727b4e42 | ||
|
|
058a88424c | ||
|
|
3ccf4dc3a6 | ||
|
|
b7996b9e82 | ||
|
|
274ed49f16 | ||
|
|
fb75ea344c | ||
|
|
483e88a02d | ||
|
|
ef9339e912 | ||
|
|
896b621545 | ||
|
|
12e7041c57 | ||
|
|
01f6ac29e9 | ||
|
|
a71ba2096b | ||
|
|
ee4e332330 | ||
|
|
f9f25b2b15 | ||
|
|
87bfe5b6c4 | ||
|
|
88c230f136 | ||
|
|
cfe7a8ed38 | ||
|
|
a3489f4064 | ||
|
|
ab9aedf5ee | ||
|
|
aab5863b57 | ||
|
|
0238de5b3b | ||
|
|
b8e2541cda | ||
|
|
87153679a3 | ||
|
|
91df15c022 | ||
|
|
8f4c5bdc61 | ||
|
|
84b39df26c | ||
|
|
1741ed5fe5 | ||
|
|
56d8e64762 | ||
|
|
78f1a1e645 | ||
|
|
37786a0b83 | ||
|
|
824b225549 | ||
|
|
4c127344c1 | ||
|
|
61c1079e7c | ||
|
|
dcbd978184 | ||
|
|
fa6d93076c | ||
|
|
a95c6fb287 | ||
|
|
e039a562fe | ||
|
|
69d149a284 | ||
|
|
6d099a3075 | ||
|
|
eea978d8e8 | ||
|
|
46a6751df6 | ||
|
|
1594252aeb | ||
|
|
ec207af81d | ||
|
|
cda23c44c0 | ||
|
|
144fc5d728 | ||
|
|
a424f05ab9 | ||
|
|
481e8db0aa | ||
|
|
f6e6914656 | ||
|
|
75ce3a9c05 | ||
|
|
e83afdf436 | ||
|
|
5d58b2a0fd | ||
|
|
133f533b87 | ||
|
|
c927a83aa5 | ||
|
|
3a2757d962 | ||
|
|
2e8b258b90 | ||
|
|
37df7d83e3 | ||
|
|
dddfdd8b78 | ||
|
|
76716503e9 | ||
|
|
26e1332421 | ||
|
|
6cfec04424 | ||
|
|
bf64b496c9 | ||
|
|
4f6c5c7f48 | ||
|
|
d48f7697df | ||
|
|
99e771898a | ||
|
|
7cf51f51a1 | ||
|
|
fc8ac5fc56 | ||
|
|
7cff514c3a | ||
|
|
3060af3940 | ||
|
|
458f817142 | ||
|
|
701117474e | ||
|
|
e93e6ca5cc | ||
|
|
f8d60c1284 | ||
|
|
01d721d477 | ||
|
|
6ecc0839ea | ||
|
|
1d5e496f3f | ||
|
|
3adbd83259 | ||
|
|
376af91e88 | ||
|
|
26e30c6060 | ||
|
|
f6545f5641 | ||
|
|
31580e6291 | ||
|
|
4f848b182a | ||
|
|
d5f43323a2 | ||
|
|
f90b518f43 | ||
|
|
8f1eeebd66 | ||
|
|
9a97a45448 | ||
|
|
1ea32e7544 | ||
|
|
80da408930 | ||
|
|
8303b356da | ||
|
|
a557875ce8 | ||
|
|
4d561a4635 | ||
|
|
fa7d4bc267 | ||
|
|
876029cbc9 | ||
|
|
e9f7e4dcf5 | ||
|
|
82071985bc | ||
|
|
10b7545807 | ||
|
|
ccd53b74db | ||
|
|
eb0748df7f | ||
|
|
ecad8fbdce | ||
|
|
655cc8c291 | ||
|
|
889b98db1e | ||
|
|
465201010d | ||
|
|
b3ef701661 | ||
|
|
d062910133 | ||
|
|
405596d291 | ||
|
|
63b06ed1fa | ||
|
|
bb844739c1 | ||
|
|
4fe0487eaf | ||
|
|
121f400732 | ||
|
|
1a5bc838b9 | ||
|
|
552e158979 | ||
|
|
d8f0338f7c | ||
|
|
54a775e0fd | ||
|
|
9c7739cf6c | ||
|
|
22254b7846 | ||
|
|
fde0ca60a1 | ||
|
|
faf20eb369 | ||
|
|
68f2535072 | ||
|
|
9df97d7594 | ||
|
|
efbcfee316 | ||
|
|
c9c362d570 | ||
|
|
65a3d0520f | ||
|
|
86beb60507 | ||
|
|
78052cae12 | ||
|
|
65595a1d51 | ||
|
|
2b315cd0e4 | ||
|
|
b880578f99 | ||
|
|
3343b728fa | ||
|
|
3b588f467a | ||
|
|
fec0766501 | ||
|
|
ffa9a909a9 | ||
|
|
507fb8a3ce | ||
|
|
ebf9fa9145 | ||
|
|
9ca6978b7c | ||
|
|
d55c60cd98 | ||
|
|
a9c28aa1aa | ||
|
|
2cef97bce1 | ||
|
|
ed541bf5c0 | ||
|
|
107388584b | ||
|
|
1719a57cdc | ||
|
|
b4d25b0e6e | ||
|
|
b4baddcc5b | ||
|
|
cf7dbb7f61 | ||
|
|
7633af198a | ||
|
|
20f4a072c4 | ||
|
|
0c7a6bb3b3 | ||
|
|
ca475c0ab8 | ||
|
|
7e06c5d85a | ||
|
|
0239d3a3a6 | ||
|
|
0dbd403d5b | ||
|
|
39fba60066 | ||
|
|
8e2fc8b6cd | ||
|
|
74ad52e0f5 | ||
|
|
42e2a1c040 | ||
|
|
f532214e1a | ||
|
|
e9f0bed2d2 | ||
|
|
00b92a61b4 | ||
|
|
41de644945 | ||
|
|
fb7c3a3cdc | ||
|
|
9daa433c44 | ||
|
|
177eb186a5 | ||
|
|
d848622ace | ||
|
|
81af7a8bc0 | ||
|
|
6856a399c5 | ||
|
|
84f7af8d13 | ||
|
|
2070f8fb08 | ||
|
|
fbf2d39640 | ||
|
|
6b0bae9c3d | ||
|
|
bd197bd219 | ||
|
|
5300cf698d | ||
|
|
f048b895b5 | ||
|
|
fdafcfd7a4 | ||
|
|
79961739e0 | ||
|
|
33865b469c | ||
|
|
502c7f9fc1 | ||
|
|
9c855ef923 | ||
|
|
6ceee7fdb7 | ||
|
|
498b8435bf | ||
|
|
1578e76700 | ||
|
|
fbb59cbc2a | ||
|
|
23c3a63aed | ||
|
|
cacaf29771 | ||
|
|
559397d420 | ||
|
|
a64db777d9 | ||
|
|
aa36129053 | ||
|
|
1aeb917d62 | ||
|
|
486f905d65 | ||
|
|
7f1dc95cfc | ||
|
|
ace9e02133 | ||
|
|
770e816468 | ||
|
|
bbe26ec35b | ||
|
|
c002ea3205 | ||
|
|
0fe03e2eec | ||
|
|
5aa949b42c | ||
|
|
11cab8c4a4 | ||
|
|
7c94bd8b67 | ||
|
|
4f25dfb33c | ||
|
|
b96e5e8121 | ||
|
|
2d1479a64f | ||
|
|
a13b46bd2c | ||
|
|
c4358f20f5 | ||
|
|
fd7700a819 | ||
|
|
48ec21def4 | ||
|
|
4ce7c90577 | ||
|
|
896ede0b88 | ||
|
|
ed05b648d1 | ||
|
|
b6001544f1 | ||
|
|
7607607857 | ||
|
|
0c261ca4cb | ||
|
|
4eb11a05bc | ||
|
|
e616582162 | ||
|
|
7b13bb3bd2 | ||
|
|
b16ca7325e | ||
|
|
81bb7156dc | ||
|
|
54818c51b4 | ||
|
|
9e3c679665 | ||
|
|
637d0b9cfb | ||
|
|
61fe57628f | ||
|
|
8c7c4bee0d | ||
|
|
7e0208f8e8 | ||
|
|
680c6877e4 | ||
|
|
16fa960a0b | ||
|
|
c0b0ef66ff | ||
|
|
3e845b32b6 | ||
|
|
b0a05b5905 | ||
|
|
afc2017e1e | ||
|
|
5a37f15bc7 | ||
|
|
6e97466a86 | ||
|
|
0d96010865 | ||
|
|
fea42aba3b | ||
|
|
065cdfa8c1 | ||
|
|
16d15242c7 | ||
|
|
4d8b74ee41 | ||
|
|
f5bdaf85fd | ||
|
|
9c1b9b2939 | ||
|
|
d6ea531cea | ||
|
|
d19f08cf86 | ||
|
|
e805fbc7f3 | ||
|
|
0db1754603 | ||
|
|
f70ab87952 | ||
|
|
561216320f | ||
|
|
1ae85611bf | ||
|
|
cab66e844b | ||
|
|
d693cfe58d | ||
|
|
86387ef5b8 | ||
|
|
0707e60c26 | ||
|
|
44457665ef | ||
|
|
19cee069ec | ||
|
|
9416305f61 | ||
|
|
7b115caf61 | ||
|
|
7076acc540 | ||
|
|
8a2e2c074b | ||
|
|
e438617241 | ||
|
|
4098827253 | ||
|
|
945fd709d4 | ||
|
|
30927ac6db | ||
|
|
dd96cac3d0 | ||
|
|
b0f5141e7d | ||
|
|
578b372335 | ||
|
|
a871fb971b | ||
|
|
b41bc3bb7d | ||
|
|
af8071bf0e | ||
|
|
fcf2ffa39c | ||
|
|
2505aad1e8 | ||
|
|
5da4f748a3 | ||
|
|
63138e818c | ||
|
|
b3c31d14ad | ||
|
|
57b6c5daa2 | ||
|
|
8ec7542a60 | ||
|
|
d64f1e033b | ||
|
|
6b9ca15dff | ||
|
|
2ca3d152d1 | ||
|
|
5ab0b32705 | ||
|
|
5f82cf9066 | ||
|
|
46570a4152 | ||
|
|
34c3dff137 | ||
|
|
91e0cb5838 | ||
|
|
90a2c0539c | ||
|
|
7f24902fc7 | ||
|
|
81f0a9515f | ||
|
|
7d7b037741 | ||
|
|
37af11d3e1 | ||
|
|
c35025aedb | ||
|
|
70e25b7792 | ||
|
|
d91093cf01 | ||
|
|
8226f6e1b3 | ||
|
|
d023519cd8 | ||
|
|
2de4067b03 | ||
|
|
44009cfd92 | ||
|
|
ddcd7d7dc1 | ||
|
|
3e6c856ee5 | ||
|
|
29218a5311 | ||
|
|
dcca7d6952 | ||
|
|
deaf9cd0db | ||
|
|
38eeb56741 | ||
|
|
9b61700b79 | ||
|
|
c90961408f | ||
|
|
0fac6150c7 | ||
|
|
719fa62752 | ||
|
|
f19fbb66e7 | ||
|
|
8aac439a5b | ||
|
|
1d7f531f6a | ||
|
|
eefbec5358 | ||
|
|
3aaea594f4 | ||
|
|
40610fd98f | ||
|
|
9a1ca386ca | ||
|
|
3a2d85265d | ||
|
|
01c69b9396 | ||
|
|
d61f94c42e | ||
|
|
70d128edab | ||
|
|
a3966a72ad | ||
|
|
5c4d73f34a | ||
|
|
d2b8689f48 | ||
|
|
c9eb856f19 | ||
|
|
7ba19364b9 | ||
|
|
dce27530e0 | ||
|
|
fd8b8b5c29 | ||
|
|
3b626bd0a6 | ||
|
|
37bf87e23e | ||
|
|
93f1ef1aca | ||
|
|
4536f917f6 | ||
|
|
d7d6767361 | ||
|
|
a707c6c988 | ||
|
|
8750c2da55 | ||
|
|
308aff5392 | ||
|
|
a7c63c748a | ||
|
|
8e104114f7 | ||
|
|
bf8d2de176 | ||
|
|
7734e47742 | ||
|
|
913d126203 | ||
|
|
146b9d0cdf | ||
|
|
af1ee922ad | ||
|
|
06074f0490 | ||
|
|
e82f42e51a | ||
|
|
f91252f678 | ||
|
|
625edfe33a | ||
|
|
780c2f55dc | ||
|
|
897312831e | ||
|
|
8746a2646a | ||
|
|
82d6d37fd7 | ||
|
|
aa36235ab1 | ||
|
|
f9667ff2e4 | ||
|
|
59ece2f8a8 | ||
|
|
9c3102392b | ||
|
|
e618992fb5 | ||
|
|
c7b4c14d66 | ||
|
|
aedd1a2577 | ||
|
|
f93e771bab | ||
|
|
bfd5a8f6fa | ||
|
|
935d134bbd | ||
|
|
f9ae9985a4 | ||
|
|
03299bae16 | ||
|
|
fc82c8bcf6 | ||
|
|
3882c57cfc | ||
|
|
d9b0d6c740 | ||
|
|
8204f6d2da | ||
|
|
476aec1916 | ||
|
|
940937a9b7 | ||
|
|
598ac6cbd3 | ||
|
|
109b5e71e2 | ||
|
|
9872f505b6 | ||
|
|
2483b7b6d0 | ||
|
|
56283a1480 | ||
|
|
8a01f30a8d | ||
|
|
d429f181a0 | ||
|
|
ab72e5eb62 | ||
|
|
d77f51a5e2 | ||
|
|
1697dc7f7b | ||
|
|
012b2419e5 | ||
|
|
ef53eb62ae | ||
|
|
2ca3d017bf | ||
|
|
6faff7f780 | ||
|
|
91ec675cbe | ||
|
|
c71af9f23a | ||
|
|
30ab1c2196 | ||
|
|
3810ab4ae9 | ||
|
|
db486360cc | ||
|
|
879b6b3b7e | ||
|
|
3fb8b77f87 | ||
|
|
2824c1a3f8 | ||
|
|
e994edbf0a | ||
|
|
1c7316408d | ||
|
|
0105f1d669 | ||
|
|
32145980f4 | ||
|
|
c62ac4359a | ||
|
|
d002d308c4 | ||
|
|
01361bfcaa | ||
|
|
25a0f4b65a | ||
|
|
65f3dd896c | ||
|
|
25ab474fba | ||
|
|
e19e9bfdc7 | ||
|
|
523b762cac | ||
|
|
3864d54936 | ||
|
|
58e41d99c9 | ||
|
|
1c24c805df | ||
|
|
784803336c | ||
|
|
72daf9ebd2 | ||
|
|
940a6b0577 | ||
|
|
ed9df940af | ||
|
|
d32c6f70a1 | ||
|
|
bd2936b05e | ||
|
|
52e7acb4ee | ||
|
|
1734d11639 | ||
|
|
318a487e10 | ||
|
|
ddd614acd0 | ||
|
|
b1d0554a07 | ||
|
|
254d8619fe | ||
|
|
44af8bb340 | ||
|
|
260ad798ed | ||
|
|
a024dd2278 | ||
|
|
ce1d3d4807 | ||
|
|
4697af6c27 | ||
|
|
247f763c11 | ||
|
|
c8319ed014 | ||
|
|
75d1f719ae | ||
|
|
98a3c89435 | ||
|
|
9fc6a1eb57 | ||
|
|
e6d5e35f29 | ||
|
|
8870564066 | ||
|
|
f23510da3a | ||
|
|
bf99bd402f | ||
|
|
7eb756cae6 | ||
|
|
51c6037f3f | ||
|
|
d2dc99d7a1 | ||
|
|
1299ff0f05 | ||
|
|
b82f8aed2b | ||
|
|
fcbe629e48 | ||
|
|
a198fab204 | ||
|
|
a50b74d042 | ||
|
|
e4a85985c2 | ||
|
|
d6630e05d4 | ||
|
|
8c068d3be7 | ||
|
|
3a240f107c | ||
|
|
6f8469eb0b | ||
|
|
ef47624b9d | ||
|
|
b919f51ecb | ||
|
|
7fec6cbb75 | ||
|
|
dc23df8b3a | ||
|
|
b185de70ca | ||
|
|
d232fa0d14 | ||
|
|
3e845f45f7 | ||
|
|
8b1ff7af2b | ||
|
|
570492cea9 | ||
|
|
8aa17ed097 | ||
|
|
e4d073471c | ||
|
|
ae4d087ad6 | ||
|
|
b1e5e45b43 | ||
|
|
eb6ab49543 | ||
|
|
834f8f7d7e | ||
|
|
43d5e8ff23 | ||
|
|
1d122abb17 | ||
|
|
83070a9c01 | ||
|
|
b308e2a8a0 | ||
|
|
c447b2699c | ||
|
|
43c22b8ed5 | ||
|
|
8c7ca490c8 | ||
|
|
e8da60cb77 | ||
|
|
110ff995f8 | ||
|
|
c8a20f4f57 | ||
|
|
f3ab328d74 | ||
|
|
82f1e1b486 | ||
|
|
4b2058fbe6 | ||
|
|
6f623ae080 | ||
|
|
715375831a | ||
|
|
3e0cc865b0 | ||
|
|
7c462cbb64 | ||
|
|
c5e7b4c8a2 | ||
|
|
a00409e003 | ||
|
|
acfabaae23 | ||
|
|
4dc660a52d | ||
|
|
ee8c36f70b | ||
|
|
fa5eb6a285 | ||
|
|
281b44a240 | ||
|
|
6c03a6fb7a | ||
|
|
8a6a2bef02 | ||
|
|
29e4bb5932 | ||
|
|
70e98dfe78 | ||
|
|
6ab9f4232b | ||
|
|
e98ccdc580 | ||
|
|
e1c1be3e50 | ||
|
|
a547a04258 | ||
|
|
0b90af9ccc | ||
|
|
615185deb9 | ||
|
|
d82eac6175 | ||
|
|
b87a3dd92c | ||
|
|
cc403f2624 | ||
|
|
61c85128e8 | ||
|
|
ac7e19144d | ||
|
|
8fb87b18e8 | ||
|
|
bf089193d4 | ||
|
|
c8b1231322 | ||
|
|
be3f6ea301 | ||
|
|
4330b814a6 | ||
|
|
1cab186403 | ||
|
|
5ee5982e3c | ||
|
|
03ee116ed4 | ||
|
|
f702054ac4 | ||
|
|
4bf0a2fa5d | ||
|
|
a2b86ff5f6 | ||
|
|
1b827bcf76 | ||
|
|
6338fb65d1 | ||
|
|
70b1c67f90 | ||
|
|
eaa55ab068 | ||
|
|
62c23c248f | ||
|
|
25c2cfc84e | ||
|
|
4ac3649c90 | ||
|
|
10b9044aa8 | ||
|
|
eb5f985712 | ||
|
|
62ea993847 | ||
|
|
73fda1fb25 | ||
|
|
919abd3bd3 | ||
|
|
5d3d8eeedc | ||
|
|
de2f247c5f | ||
|
|
d52c12606f | ||
|
|
678949aff2 | ||
|
|
d5b0d82adc | ||
|
|
637f424a70 | ||
|
|
2274cdd343 | ||
|
|
aa34132047 | ||
|
|
54b9ae8cd4 | ||
|
|
c6b7ed0ef3 | ||
|
|
6645d579a2 | ||
|
|
ec07d54ed9 | ||
|
|
2ad4e76bc3 | ||
|
|
7c51cf7588 | ||
|
|
b32007b1ca | ||
|
|
80b887c874 | ||
|
|
ff521eb559 | ||
|
|
e9b1bfd2a0 | ||
|
|
9eb4458c73 | ||
|
|
e9f19b6834 | ||
|
|
fa796b9609 | ||
|
|
e6d33a9e1a | ||
|
|
6f9a5a6009 | ||
|
|
d178ce40b5 | ||
|
|
ddc8ba7105 | ||
|
|
006fa3fa4a | ||
|
|
fd088fdbc8 | ||
|
|
ef1669dd4c | ||
|
|
58ec0e7abb | ||
|
|
f5d141d59f | ||
|
|
b30f44a0a1 | ||
|
|
02b372fa3d | ||
|
|
aab9704d24 | ||
|
|
9b69074352 | ||
|
|
9702118f3f | ||
|
|
ee04b728c9 | ||
|
|
b80a86a366 | ||
|
|
3e7e2000d5 | ||
|
|
9070b77b30 | ||
|
|
99445cc8d6 | ||
|
|
bfc2a8ae34 | ||
|
|
6badcc2887 | ||
|
|
b10b51f839 | ||
|
|
9496999773 | ||
|
|
c77df7228e | ||
|
|
fdc696691a | ||
|
|
ff2d04351f | ||
|
|
45ca7aa348 | ||
|
|
24cbcd3937 | ||
|
|
489aece7c5 | ||
|
|
39a6254ee1 | ||
|
|
0bbab221a2 | ||
|
|
9094a167eb | ||
|
|
39b8358ef5 | ||
|
|
a571b77117 | ||
|
|
fd3f85f2b7 | ||
|
|
55d67e92d3 | ||
|
|
a85d0df668 | ||
|
|
f623de53d4 | ||
|
|
5aaa174b20 | ||
|
|
c273fc451a | ||
|
|
62fa1d532c | ||
|
|
92835a1e10 | ||
|
|
20fbeda124 | ||
|
|
56bc96314a | ||
|
|
688f797acf | ||
|
|
5f9edb4fcc | ||
|
|
f35df5e418 | ||
|
|
a64e13a021 | ||
|
|
294f7f6fae | ||
|
|
746ddf6457 | ||
|
|
373a5f004b | ||
|
|
ad35f85c3a | ||
|
|
0d31bfe3c3 | ||
|
|
bd9947a705 | ||
|
|
d30f7ba5ba | ||
|
|
3ea3ff288b | ||
|
|
4e602fc5e8 | ||
|
|
7099a33dc4 | ||
|
|
ee15b00533 | ||
|
|
d480840353 | ||
|
|
16dd423016 | ||
|
|
fb05cf6280 | ||
|
|
c2eccd23a8 | ||
|
|
ed23268672 | ||
|
|
975473b2e4 | ||
|
|
c0707e8cb5 | ||
|
|
d58edf0614 | ||
|
|
ce1cb3a15a | ||
|
|
c650887c04 | ||
|
|
991a5e695a | ||
|
|
2f18ecb562 | ||
|
|
370b786ed0 | ||
|
|
a092922145 | ||
|
|
ff6d722e44 | ||
|
|
d325203e8b | ||
|
|
c11567f095 | ||
|
|
bd38c7dc49 | ||
|
|
80994ffafb | ||
|
|
a1374d228e | ||
|
|
c491f18ba5 | ||
|
|
c7f86311aa | ||
|
|
6b95cc6a5c | ||
|
|
3ce702fc0d | ||
|
|
6c72fb9689 | ||
|
|
7cca8f1889 | ||
|
|
c92d4982c6 | ||
|
|
5f5885cb18 | ||
|
|
b113e2b729 | ||
|
|
8d05d786ce | ||
|
|
b6c146f123 | ||
|
|
73229f51a1 | ||
|
|
26b3fe67a3 | ||
|
|
01dab1613d | ||
|
|
6f4e1a45d1 | ||
|
|
297eb71ff7 | ||
|
|
b66a40fa41 | ||
|
|
ac1ae71f11 | ||
|
|
00cea06899 | ||
|
|
07926bad1e | ||
|
|
57eb506bdf | ||
|
|
5cc8407077 | ||
|
|
f1eca63b5a | ||
|
|
ac1e636caa | ||
|
|
567ef561c0 | ||
|
|
035738f4c5 | ||
|
|
61bd2967b0 | ||
|
|
2fa856e790 | ||
|
|
a8aa3be08a | ||
|
|
6ae57158b7 | ||
|
|
6fec1958b8 | ||
|
|
efdfc809bc | ||
|
|
e62b1b4999 | ||
|
|
7f10548ecb | ||
|
|
53ca7aef1a | ||
|
|
6aa2f49321 | ||
|
|
e0d07c80ac | ||
|
|
fb5d89e987 | ||
|
|
f84f42d8bd | ||
|
|
c28433688a | ||
|
|
80fe93c6c4 | ||
|
|
3071c079ba | ||
|
|
8f33f1f0c9 | ||
|
|
f31209162a | ||
|
|
c1d76bbd29 | ||
|
|
3507117cff | ||
|
|
efb8ec66a2 | ||
|
|
97cae93bb5 | ||
|
|
16bf6bfc2c | ||
|
|
116ca3cbfe | ||
|
|
713e048db9 | ||
|
|
1f18b6b0da | ||
|
|
5eb53725fb | ||
|
|
dab462de35 | ||
|
|
d895fc6a09 | ||
|
|
1c7fe3fd3e | ||
|
|
8cbe832a67 | ||
|
|
e61a6a0b7f | ||
|
|
cd3b9e1422 | ||
|
|
76b41b693b | ||
|
|
115ce8148a | ||
|
|
1fbad779af | ||
|
|
fa78f7b9b3 | ||
|
|
b4a52e244d | ||
|
|
46ae326781 | ||
|
|
af03b30352 | ||
|
|
2857378c87 | ||
|
|
1723525077 | ||
|
|
ad1625dbb3 | ||
|
|
b585ba7a8b | ||
|
|
cafa301ea8 | ||
|
|
7f739a4bc1 | ||
|
|
59c7140ce6 | ||
|
|
aeaa41fcbe | ||
|
|
223c91b6b9 | ||
|
|
30f9f381cd | ||
|
|
7aed1d83e3 | ||
|
|
5e069033b3 | ||
|
|
a919f5edbc | ||
|
|
325feb5637 | ||
|
|
61bd3978a4 | ||
|
|
56ec0f8911 | ||
|
|
48baed897c | ||
|
|
aa52f3d2b5 | ||
|
|
2eb8b496cd | ||
|
|
178aae3883 | ||
|
|
17a4e546a5 | ||
|
|
46dc3b2d00 | ||
|
|
6dea7c3fec | ||
|
|
425722ef65 | ||
|
|
36f57be6cc | ||
|
|
fadc6032fb | ||
|
|
85f97b86da | ||
|
|
e8ca20a7e2 | ||
|
|
4368e1a5f7 | ||
|
|
a129cc5f95 | ||
|
|
98488e5798 | ||
|
|
b80f520162 | ||
|
|
1d7ca67053 | ||
|
|
58c30a638f | ||
|
|
077e625512 | ||
|
|
c166c87479 | ||
|
|
19a15bedfa | ||
|
|
cf41bef449 | ||
|
|
a27cf28544 | ||
|
|
d9d7508292 | ||
|
|
5847e7d2c2 | ||
|
|
f855426a9f | ||
|
|
18f0d9109e | ||
|
|
a78421d79a | ||
|
|
a49cb0935d | ||
|
|
27a408c9f1 | ||
|
|
682aaafc85 | ||
|
|
636a0b5c4d | ||
|
|
e24d3ca33f | ||
|
|
76ba9e5e35 | ||
|
|
b9e4fcf1a6 | ||
|
|
f821707cc6 | ||
|
|
c151ef1526 | ||
|
|
170e441744 | ||
|
|
be908cdf0e | ||
|
|
0defb639c2 | ||
|
|
3d455d0fc9 | ||
|
|
561c36bfe0 | ||
|
|
f07a8f6c2b | ||
|
|
1c7b97d8db | ||
|
|
ee46722a3d | ||
|
|
4fb4c7c85d | ||
|
|
8526c24c3e | ||
|
|
db72bffcbd | ||
|
|
b3396fa62e | ||
|
|
17d3208cbd | ||
|
|
f9d2e04609 | ||
|
|
a2355e3225 | ||
|
|
c076dcb2c7 | ||
|
|
ae1e2599a8 | ||
|
|
8858deb42f | ||
|
|
e3c1280278 | ||
|
|
8794146df7 | ||
|
|
4ce5d989c5 | ||
|
|
1dc5776cb8 | ||
|
|
43cf0f5cba | ||
|
|
e4a3ec37c5 | ||
|
|
2cef7980ea | ||
|
|
db5ea158c3 | ||
|
|
daacb3ca98 | ||
|
|
888c8c453a | ||
|
|
c9db3df251 | ||
|
|
faf3e887b9 | ||
|
|
8269228b8a | ||
|
|
d9b6f0482a | ||
|
|
b8d8e877d7 | ||
|
|
db6f71c8cb | ||
|
|
9c6533bceb | ||
|
|
0d43006a14 | ||
|
|
8a62a68d6e | ||
|
|
3dc23d906e | ||
|
|
9d5474f352 | ||
|
|
3ae9740336 | ||
|
|
5531dabae4 | ||
|
|
7c28c37d0b | ||
|
|
b6bdca7b89 | ||
|
|
c952178749 | ||
|
|
395dc379ed | ||
|
|
3873028209 | ||
|
|
4a11554fa5 | ||
|
|
2fb97fbc51 | ||
|
|
c12ddf43a2 | ||
|
|
a481b5c6cd | ||
|
|
797418ce1a | ||
|
|
95baa55472 | ||
|
|
b105c436ba | ||
|
|
0d880dda50 | ||
|
|
3f24e73978 | ||
|
|
7eaa5cfb22 | ||
|
|
a66d29ba2e | ||
|
|
c3de2e906b | ||
|
|
30d03d280c | ||
|
|
3971b18b04 | ||
|
|
46c3ed0b0d | ||
|
|
bef704d445 | ||
|
|
6e027811ee | ||
|
|
7f9dbaec2a | ||
|
|
3bbccf6c89 | ||
|
|
528855c5d5 | ||
|
|
78dd449baf | ||
|
|
ada4e786ec | ||
|
|
57094d6cce | ||
|
|
de37ae245d | ||
|
|
e1c4be005f | ||
|
|
df728f6856 | ||
|
|
1445760cc0 | ||
|
|
3c0640ce11 | ||
|
|
9268ef5d2f | ||
|
|
adad98f3e2 | ||
|
|
ea87c48586 | ||
|
|
9c06c9802d | ||
|
|
2686f80502 | ||
|
|
5e50ef4045 | ||
|
|
c4fe71c59f | ||
|
|
860371e698 | ||
|
|
1d0abc4cb9 | ||
|
|
d0f052177e | ||
|
|
429e8d2704 | ||
|
|
464865c091 | ||
|
|
7f5a9763e7 | ||
|
|
da707dda4b | ||
|
|
2205ad1eb8 | ||
|
|
db722badaf | ||
|
|
e6387e124f | ||
|
|
c72e2cb852 | ||
|
|
e35b76d0f4 | ||
|
|
f00c21be91 | ||
|
|
62fa5515e1 | ||
|
|
e6287631aa | ||
|
|
377f5000a5 | ||
|
|
82c041329b | ||
|
|
f1c410deaa | ||
|
|
429553a6df | ||
|
|
2a1b2bf7ac | ||
|
|
bdca32be49 | ||
|
|
95e8ef9fc4 | ||
|
|
a2433956fe | ||
|
|
1fe8bad37b | ||
|
|
78eec3f6c8 | ||
|
|
6d0886423c | ||
|
|
e4f4396a6b | ||
|
|
4336266b7f | ||
|
|
e5617d53ee | ||
|
|
a037fb5487 | ||
|
|
80c1b5722f | ||
|
|
95401fb8c4 | ||
|
|
42dd293aa8 | ||
|
|
f4566ce812 | ||
|
|
cb20acfa52 | ||
|
|
565e957387 | ||
|
|
9fcdb2bffa | ||
|
|
1c72196943 | ||
|
|
845fd7ee03 | ||
|
|
5dc4c32eba | ||
|
|
d58e5f9fc2 | ||
|
|
027e5dd61b | ||
|
|
a60c66ad33 | ||
|
|
51179c083c | ||
|
|
ee8779cc00 | ||
|
|
913e7cdb02 | ||
|
|
46e06f2c9d | ||
|
|
ad06f989b7 | ||
|
|
f5a4922aa3 | ||
|
|
856e3542e8 | ||
|
|
8011bd997c | ||
|
|
f345db49cd | ||
|
|
2896da7f9d | ||
|
|
c314c4cba3 | ||
|
|
686503c83c | ||
|
|
fe3a448231 | ||
|
|
1b2f6f4e3d | ||
|
|
262d6d551b | ||
|
|
4d2d01195a | ||
|
|
a68977d9dd | ||
|
|
952bde0e96 | ||
|
|
93bb1c0c96 | ||
|
|
544f37a75b | ||
|
|
141c613eb9 | ||
|
|
6faa7b4cb6 | ||
|
|
c4f666fdd6 | ||
|
|
e90aa13890 | ||
|
|
cb6850f287 | ||
|
|
58b93a9fa9 | ||
|
|
809fd2f7ff | ||
|
|
5813c5d9d8 | ||
|
|
62139047b3 | ||
|
|
6974c53194 | ||
|
|
3832299463 | ||
|
|
8f51de45d0 | ||
|
|
ea5b9518ad | ||
|
|
d1029af180 | ||
|
|
88c5e28577 | ||
|
|
ce8aee9192 | ||
|
|
08643df831 | ||
|
|
42a115e93b | ||
|
|
9c9391c95b | ||
|
|
82ae513d17 | ||
|
|
c3101e92b4 | ||
|
|
14dca2f984 | ||
|
|
d531a47c70 | ||
|
|
47c2024f67 | ||
|
|
5eb323c662 | ||
|
|
2181c74b3e | ||
|
|
1665c4de22 | ||
|
|
62cb1fb3f1 | ||
|
|
a2bd50ee22 | ||
|
|
61365ff3c2 | ||
|
|
86d9e25f17 | ||
|
|
e0b146039f | ||
|
|
54a299f23c | ||
|
|
24a5ae8122 | ||
|
|
27063c9e45 | ||
|
|
78e40807b1 | ||
|
|
7fef6bde78 | ||
|
|
7f78cf23d7 | ||
|
|
34424dfbc3 | ||
|
|
d51f25ea88 | ||
|
|
c8ea1bcd8c | ||
|
|
036301e34f | ||
|
|
e3022f42ab | ||
|
|
548a597843 | ||
|
|
f6479826ca | ||
|
|
e4551713dc | ||
|
|
18254fd385 | ||
|
|
0dbc9f7a8a | ||
|
|
7e36ffc2b4 | ||
|
|
16dc339b2d | ||
|
|
ae7c35c75d | ||
|
|
5845c079cb | ||
|
|
b6f8fb3354 | ||
|
|
48db3d4aa2 | ||
|
|
702504e1d5 | ||
|
|
619ab8a6ed | ||
|
|
594bcffd7c | ||
|
|
46dfd0bfa0 | ||
|
|
bd523f307b | ||
|
|
b220d979bd | ||
|
|
85a904e173 | ||
|
|
16a467b7d1 | ||
|
|
f4b6deb06a | ||
|
|
4b6ab58048 | ||
|
|
b0a1ac3ee4 | ||
|
|
8e1d22bbb9 | ||
|
|
dc01abf48f | ||
|
|
fa7c6907be | ||
|
|
cbc46ed2e0 | ||
|
|
f0e39e92b7 | ||
|
|
db3117b92e | ||
|
|
3f6339887b | ||
|
|
8057e9d0af | ||
|
|
b08aa2ae1f | ||
|
|
a7a22f39d2 | ||
|
|
6f416ab33f | ||
|
|
17ef0a5ca7 | ||
|
|
643a2be9a2 | ||
|
|
29a47b87cd | ||
|
|
36b5430861 | ||
|
|
4f03015486 | ||
|
|
7b48d41d29 | ||
|
|
2fe1bcbdff | ||
|
|
489483e22a | ||
|
|
89c7e734d4 | ||
|
|
b93f75aade | ||
|
|
157c233ab1 | ||
|
|
72e7445f87 | ||
|
|
ccf7ff82a1 | ||
|
|
8342259054 | ||
|
|
abd5b865e1 | ||
|
|
973afe08c5 | ||
|
|
3201655870 | ||
|
|
c051d28c95 | ||
|
|
ab151cc409 | ||
|
|
3c94068910 | ||
|
|
544ae39a95 | ||
|
|
26c79e6740 | ||
|
|
cb48f73fd9 | ||
|
|
728fa279dd | ||
|
|
3d5ca3313b | ||
|
|
ca080b2dcc | ||
|
|
476eabd0fe | ||
|
|
39ad8347c7 | ||
|
|
294410bfd2 | ||
|
|
92b3d08ea0 | ||
|
|
91b049e3a7 | ||
|
|
5af3a490d4 | ||
|
|
855b03fb34 | ||
|
|
bb55246197 | ||
|
|
395e43a3e6 | ||
|
|
656377875b | ||
|
|
32c5f56be1 | ||
|
|
ea155cd68c | ||
|
|
7016afa6f8 | ||
|
|
cc99d4f93f | ||
|
|
424d805def | ||
|
|
cb9c43c2e7 | ||
|
|
ca5648ed03 | ||
|
|
e0f6121dc9 | ||
|
|
c79f2896da | ||
|
|
a02896333a | ||
|
|
409e658f80 | ||
|
|
0c8bd49908 | ||
|
|
5e150ee16a | ||
|
|
09bbe05f08 | ||
|
|
60965b767c | ||
|
|
64c29d495d | ||
|
|
52238e5d27 | ||
|
|
1a9e11a258 | ||
|
|
14d1726a7f | ||
|
|
5ebaf24c04 | ||
|
|
8bba5ea2b6 | ||
|
|
9e259f02d8 | ||
|
|
ce2a9b0bde | ||
|
|
bfe56579aa | ||
|
|
10bd90ab18 | ||
|
|
cd3a4e0e63 | ||
|
|
8b1057b97d | ||
|
|
d915e97ad2 | ||
|
|
64855cba77 | ||
|
|
9e4c71a3f0 | ||
|
|
4c2b67a5aa | ||
|
|
bde1258c87 | ||
|
|
78edc79fc2 | ||
|
|
9ad121f7d7 | ||
|
|
407e474896 | ||
|
|
c2a74df26d | ||
|
|
4018155899 | ||
|
|
a10f416f15 | ||
|
|
4ac78c5b30 | ||
|
|
838f9f592c | ||
|
|
f13dceaa34 | ||
|
|
ab15d05ffd | ||
|
|
715e6dc8be | ||
|
|
4a98610b67 | ||
|
|
f4a53bee61 | ||
|
|
cb1774a678 | ||
|
|
0543ac33d2 | ||
|
|
e3b5f0cc77 | ||
|
|
74a15cd0a1 | ||
|
|
7c4b6d5b20 | ||
|
|
627a0d6f9e | ||
|
|
464e8b4899 | ||
|
|
b2a89a46ca | ||
|
|
8fcf3f1baa | ||
|
|
405ab20ab2 | ||
|
|
e5bbe3a553 | ||
|
|
5e1c681a8d | ||
|
|
faa4309ece | ||
|
|
79ca1523ef | ||
|
|
8be39a6871 | ||
|
|
cc4bc0c3b0 | ||
|
|
2ef56c0cbb | ||
|
|
8d74b8f133 | ||
|
|
8b446e2de0 | ||
|
|
8882cd9558 | ||
|
|
174b8923dc | ||
|
|
26cd91ceb2 | ||
|
|
3a50ab3b91 | ||
|
|
0a83695c45 | ||
|
|
571fa8c885 | ||
|
|
1901a8535a | ||
|
|
a23ffdcd1f | ||
|
|
970652d81f | ||
|
|
6124cdd806 | ||
|
|
97a87c718a | ||
|
|
f368114b88 | ||
|
|
c4b0bf0ee0 | ||
|
|
fb9acf8da4 | ||
|
|
2dfc8f930e | ||
|
|
68c132b003 | ||
|
|
c2292fdadc | ||
|
|
0e7f00cddd | ||
|
|
0cca9f8152 | ||
|
|
d8a6bf08cb | ||
|
|
6c8216d360 | ||
|
|
f365a43f7b | ||
|
|
af75f4c3a3 | ||
|
|
84e05e9490 | ||
|
|
4cc2a11cc5 | ||
|
|
2565d8155d | ||
|
|
e147efd358 | ||
|
|
7c0f9585e7 | ||
|
|
8bb88f397e | ||
|
|
11f4ff4594 | ||
|
|
29298ef978 | ||
|
|
437fa5c128 | ||
|
|
f104d5c891 | ||
|
|
5803fe18ed | ||
|
|
6cccf09bc1 | ||
|
|
bb22845b54 | ||
|
|
f3233528d6 | ||
|
|
319a971080 | ||
|
|
0a978f07d9 | ||
|
|
4919347095 | ||
|
|
9f943fcaa5 | ||
|
|
7c8d11ca20 | ||
|
|
1d9684e11e | ||
|
|
7e46fb8720 | ||
|
|
3bceafef80 | ||
|
|
5dfd04ad70 | ||
|
|
bd51fe383b | ||
|
|
58d2362390 | ||
|
|
254cf3d9cf | ||
|
|
09bbe650e3 | ||
|
|
552459310c | ||
|
|
137dba1bb4 | ||
|
|
f7f5a3cf50 | ||
|
|
2aeaf330a8 | ||
|
|
bd42eebdcb | ||
|
|
78d55d6c78 | ||
|
|
41ea5e4cc7 | ||
|
|
33a33867b5 | ||
|
|
7b71f32d18 | ||
|
|
c77fae435f | ||
|
|
369a6f1977 | ||
|
|
d5c78f58c0 | ||
|
|
a2d038eb46 | ||
|
|
09ca9b5351 | ||
|
|
c0169e7bcb | ||
|
|
418f9ba4c9 | ||
|
|
c627849382 | ||
|
|
0c57a145b8 | ||
|
|
852e78c5a6 | ||
|
|
dad4af5c34 | ||
|
|
ff683fa361 | ||
|
|
29a4607bdc | ||
|
|
3b12128953 | ||
|
|
06750ad9ae | ||
|
|
8968c899e0 | ||
|
|
4624d2698d | ||
|
|
c6126f3e9c | ||
|
|
d8ebc1003a | ||
|
|
4da3c2f6fc | ||
|
|
de2ee00957 | ||
|
|
5b8c42b418 | ||
|
|
187fff9311 | ||
|
|
4ebcfb9b09 | ||
|
|
01b622decc | ||
|
|
b438090092 | ||
|
|
e1714934b4 | ||
|
|
f27da5c95d | ||
|
|
70803664f9 | ||
|
|
93ac3f8c41 | ||
|
|
18756a840d | ||
|
|
7386e12e6d | ||
|
|
04dcbd6075 | ||
|
|
1e69aa227b | ||
|
|
d9984f39ba | ||
|
|
8ad466e091 | ||
|
|
4a4ce57864 | ||
|
|
11c33bb1b0 | ||
|
|
047665afac | ||
|
|
dc54cef126 | ||
|
|
e2483037ef | ||
|
|
586a3972af | ||
|
|
61eb7276a6 | ||
|
|
327428d253 | ||
|
|
e76976b2f3 | ||
|
|
1730404436 | ||
|
|
f273471a0b | ||
|
|
cff5cb1570 | ||
|
|
1d09151a2d | ||
|
|
662d53812b | ||
|
|
1d74f1d78f | ||
|
|
f89fb41ebb | ||
|
|
52b5a81c26 | ||
|
|
02501f0804 | ||
|
|
4b27d1180a | ||
|
|
2334a0e6a8 | ||
|
|
c443eefa20 | ||
|
|
c1b7cba559 | ||
|
|
ee13e451d7 | ||
|
|
1f5e3ba646 | ||
|
|
421dd0d05c | ||
|
|
3d7692ded3 | ||
|
|
aca2692602 | ||
|
|
ddcfa242fc | ||
|
|
390358cc21 | ||
|
|
ce9335f0b0 | ||
|
|
318ab35eab | ||
|
|
7deef26f80 | ||
|
|
35729e5726 | ||
|
|
ae92b030c5 | ||
|
|
abc1891694 | ||
|
|
066179a2ff | ||
|
|
0e03420bd2 | ||
|
|
928a4f129b | ||
|
|
0deb4d5c11 | ||
|
|
4fd1241589 | ||
|
|
c1db11beaf | ||
|
|
1e552a94bf | ||
|
|
555e2a1330 | ||
|
|
fd2d9d1226 | ||
|
|
38d9e09c0e | ||
|
|
ac19cee575 | ||
|
|
8ab7f7474c | ||
|
|
9310c13c01 | ||
|
|
ec509410e5 | ||
|
|
5af39cdd09 | ||
|
|
4dfc21f4b5 | ||
|
|
ede66c2fa7 | ||
|
|
cfe04dea19 | ||
|
|
91554f99ff | ||
|
|
9e4d75f7f4 | ||
|
|
248ae23997 | ||
|
|
ccd2ce915e | ||
|
|
c187643eb1 | ||
|
|
1ca4440921 | ||
|
|
3c2aea7f36 | ||
|
|
9b121a026d | ||
|
|
f1cfa57f42 | ||
|
|
839efbdb7c | ||
|
|
023f29cc59 | ||
|
|
c7bb59d991 | ||
|
|
d4d95b7835 | ||
|
|
546db7355b | ||
|
|
256a3ed77a | ||
|
|
08575dd4d1 | ||
|
|
ed851b76ba | ||
|
|
ed009c9e39 | ||
|
|
50178d40de | ||
|
|
35944bb3a1 | ||
|
|
7e694af88f | ||
|
|
44ef7aa142 | ||
|
|
8b56d6137d | ||
|
|
0ee446ffce | ||
|
|
d623b597b5 | ||
|
|
794233c25a | ||
|
|
1c28b688c9 | ||
|
|
20ac33a22e | ||
|
|
926413992b | ||
|
|
5ee9178416 | ||
|
|
6765505ff0 | ||
|
|
7335360c26 | ||
|
|
04bfce0406 | ||
|
|
c4eb94a9bf | ||
|
|
226095bec7 | ||
|
|
197ceb9e69 | ||
|
|
b03ef2d42f | ||
|
|
e5b0005931 | ||
|
|
414f5451f5 | ||
|
|
db384d9680 | ||
|
|
624ebe1607 | ||
|
|
72b92408a1 | ||
|
|
32c8a098c5 | ||
|
|
53e9d92ea4 | ||
|
|
747f5288b8 | ||
|
|
7519c69de1 | ||
|
|
463ade0ade | ||
|
|
32f4307674 | ||
|
|
4e396e990d | ||
|
|
a19264a2bd | ||
|
|
f6938eba52 | ||
|
|
9294fa9b8e | ||
|
|
49992690a0 | ||
|
|
3d5fdb66d0 | ||
|
|
b0cd5524dd | ||
|
|
8df1d2218d | ||
|
|
ba1708beaa | ||
|
|
29e8542803 | ||
|
|
83dc8a7b13 | ||
|
|
855f8c11c1 | ||
|
|
fc7a17d5e2 | ||
|
|
09f5f4f254 | ||
|
|
e841a8fbc8 | ||
|
|
6dba85d41b | ||
|
|
e907dbaaca | ||
|
|
949d2ca917 | ||
|
|
41f6139137 | ||
|
|
8050194502 | ||
|
|
b6f2418a71 | ||
|
|
666a4f1ad0 | ||
|
|
76334e03e6 | ||
|
|
f748424943 | ||
|
|
8249f2c3a6 | ||
|
|
8397debb00 | ||
|
|
d14a62730a | ||
|
|
1f9a55699c | ||
|
|
918b0d678d | ||
|
|
684f913254 | ||
|
|
27c2b288f6 | ||
|
|
bfcdf19869 | ||
|
|
f31b3daeac | ||
|
|
4e613d0d1e | ||
|
|
8cf2e96abb | ||
|
|
35151ad83f | ||
|
|
c3be132dff | ||
|
|
17d9237a7a | ||
|
|
60af85a380 | ||
|
|
04e7533687 | ||
|
|
05d76898dc | ||
|
|
01a10fb806 | ||
|
|
48b185ae0c | ||
|
|
7a541ff797 | ||
|
|
f895d2ac19 | ||
|
|
7e4cf381be | ||
|
|
4392da062b | ||
|
|
1c46d9ae3c | ||
|
|
d5e4e71a68 | ||
|
|
348ab4604c | ||
|
|
d80094f2e6 | ||
|
|
834f141320 | ||
|
|
b4c4bc117d | ||
|
|
95afa84cbd | ||
|
|
ff5d973cdb | ||
|
|
c9270c5b81 | ||
|
|
d220a8e6e0 | ||
|
|
eb81c498b9 | ||
|
|
ae19526fe0 | ||
|
|
613b7bdb17 | ||
|
|
d3a333d9bd | ||
|
|
f6cf858089 | ||
|
|
63462c35f2 | ||
|
|
73e4ec358a | ||
|
|
5eac7212a5 | ||
|
|
6d08487154 | ||
|
|
4d90ee0f15 | ||
|
|
951a4aeb50 | ||
|
|
981ee0ac8e | ||
|
|
6bb6daadba | ||
|
|
f551398652 | ||
|
|
ea32b92465 | ||
|
|
77993d74aa | ||
|
|
48a68ab5c2 | ||
|
|
e8f94a489f | ||
|
|
a2535cef56 | ||
|
|
48fb7dfa92 | ||
|
|
0733ced356 | ||
|
|
e725eb4e7b | ||
|
|
d54a707816 | ||
|
|
91601cdd8c | ||
|
|
22c7783c18 | ||
|
|
7607a5cd57 | ||
|
|
c313e06f8f | ||
|
|
33b4c09ffd | ||
|
|
b80f2fc9a2 | ||
|
|
2e9ed6af4a | ||
|
|
e60cc20a23 | ||
|
|
122581fd68 | ||
|
|
f3a6480278 | ||
|
|
ae54179976 | ||
|
|
a8d8690d92 | ||
|
|
98f6206e75 | ||
|
|
222d60cfdd | ||
|
|
0f67ca79dd | ||
|
|
10e29db302 | ||
|
|
c2a649c50e | ||
|
|
8cd42efa8e | ||
|
|
a11e66f404 | ||
|
|
e395cff106 | ||
|
|
22d4a8baf1 | ||
|
|
b1b426389d | ||
|
|
182d80a630 | ||
|
|
29ee69fc7b | ||
|
|
d313b31e12 | ||
|
|
09d5868820 | ||
|
|
31e30e6214 | ||
|
|
8338ea8814 | ||
|
|
a629c3264d | ||
|
|
1767ae559b | ||
|
|
df4d136305 | ||
|
|
ea190fdfb7 | ||
|
|
455165f00b | ||
|
|
b2a7ae7629 | ||
|
|
2fd0b87467 | ||
|
|
7136a991b3 | ||
|
|
b467569308 | ||
|
|
4264ecc933 | ||
|
|
bca39042d7 | ||
|
|
1af9954606 | ||
|
|
36047921b8 | ||
|
|
360f30df4b | ||
|
|
048b1c91a3 | ||
|
|
b0e25f9a67 | ||
|
|
a8bdca0c26 | ||
|
|
feec8b9758 | ||
|
|
09bde054d0 | ||
|
|
74fd59a541 | ||
|
|
0e4c07ef8a | ||
|
|
0169ab6af8 | ||
|
|
7bba871907 | ||
|
|
6ba12d3a7d | ||
|
|
bdb72d4c2f | ||
|
|
8c6975a859 | ||
|
|
c39f350bdd | ||
|
|
0df5ff6c30 | ||
|
|
6859a321cc | ||
|
|
76455c1491 | ||
|
|
3aedf9be45 | ||
|
|
4c6155b305 | ||
|
|
5a14a2d1eb | ||
|
|
9054420536 | ||
|
|
9a086ef205 | ||
|
|
9726ed04ea | ||
|
|
812ed6ec68 | ||
|
|
dc65d27603 | ||
|
|
536bfd0020 | ||
|
|
9f5951280a | ||
|
|
785be0b1fd | ||
|
|
8bcc9a2087 | ||
|
|
4c6ead64f4 | ||
|
|
5c657261dd | ||
|
|
2033e8d6af | ||
|
|
864b42b98c | ||
|
|
06b65d616e | ||
|
|
00761f9106 | ||
|
|
bf72baf112 | ||
|
|
3f1715a401 | ||
|
|
e348576f3d | ||
|
|
2f40a5c74d | ||
|
|
083c1afa11 | ||
|
|
890d217ab7 | ||
|
|
375f43cfa5 | ||
|
|
61ecc91a2d | ||
|
|
28883abe80 | ||
|
|
5e5937382d | ||
|
|
312f8c51c2 | ||
|
|
e6fb014d6b | ||
|
|
0317f59f0b | ||
|
|
5380ef0348 | ||
|
|
12731d2a35 | ||
|
|
af3ef13063 | ||
|
|
1865a2a749 | ||
|
|
5c992b1aa8 | ||
|
|
ad280076c0 | ||
|
|
6279daa8de | ||
|
|
fbe6526eb1 | ||
|
|
d132b9b0a3 | ||
|
|
66edb57cd0 | ||
|
|
522c77493e | ||
|
|
73e5f06333 | ||
|
|
4fda4cfc6b | ||
|
|
889c42a4c6 | ||
|
|
35102a8af8 | ||
|
|
af8464d89e | ||
|
|
fa6cb4515a | ||
|
|
d38aaeec9c | ||
|
|
a68a816cff | ||
|
|
4d1adf6227 | ||
|
|
7c3722a90a | ||
|
|
9cf50d3c06 | ||
|
|
0e3c82b519 | ||
|
|
b463025a0e | ||
|
|
4ff06f8494 | ||
|
|
b418aa2732 | ||
|
|
832a75726f | ||
|
|
382805ddeb | ||
|
|
31f3cb3391 | ||
|
|
327f296ac0 | ||
|
|
b3d0897897 | ||
|
|
98d7a6b5d4 | ||
|
|
edefe2dc07 | ||
|
|
97cba944c0 | ||
|
|
31826d5b43 | ||
|
|
cc2b97fba4 | ||
|
|
0e6a2017f3 | ||
|
|
c307dbc1de | ||
|
|
01c8735caf | ||
|
|
8a09115623 | ||
|
|
84fbeae5a6 | ||
|
|
221c893f45 | ||
|
|
40c68f68f0 | ||
|
|
5dcd9faba5 | ||
|
|
0ecf331959 | ||
|
|
67081ee577 | ||
|
|
68f0d4102c | ||
|
|
e784ca7b77 | ||
|
|
fdaaf0943c | ||
|
|
85580121a7 | ||
|
|
d839aef96e | ||
|
|
ccd7bcea62 | ||
|
|
de32e634a0 | ||
|
|
9e7062ed8d | ||
|
|
11776db13c | ||
|
|
32d111e613 | ||
|
|
dbe0655f0f | ||
|
|
584880e4df | ||
|
|
6db5b54a86 | ||
|
|
3fc596c448 | ||
|
|
6cebb8cb92 | ||
|
|
2ca76ef44f | ||
|
|
662f01eee4 | ||
|
|
af1d12fa2f | ||
|
|
5a6531a75f | ||
|
|
8f0f71d82b | ||
|
|
6d2ae8007a | ||
|
|
163e2adfde | ||
|
|
59eb436423 | ||
|
|
a1d1bc035e | ||
|
|
8f47248195 | ||
|
|
54a65f713a | ||
|
|
b511867e62 | ||
|
|
b067f83784 | ||
|
|
959f87b425 | ||
|
|
9fbd4fbc40 | ||
|
|
226abd9e19 | ||
|
|
92957bf1d2 | ||
|
|
d8fb9a22b9 | ||
|
|
4d0c2398a0 | ||
|
|
edb55fdf6b | ||
|
|
80c0e6df36 | ||
|
|
08547ea663 | ||
|
|
cf09f82523 | ||
|
|
66c2a74c8e | ||
|
|
774b37c73c | ||
|
|
8e893b60a1 | ||
|
|
f3178a0854 | ||
|
|
fbed7f4f74 | ||
|
|
59bcb63dee | ||
|
|
ee40995f34 | ||
|
|
11f36dcb63 | ||
|
|
483443fd60 | ||
|
|
0895db1a62 | ||
|
|
bb74f14736 | ||
|
|
47a26144c1 | ||
|
|
f0e8e6cd06 | ||
|
|
791e27f479 | ||
|
|
cf3b68cc1b | ||
|
|
8b86f91c1f | ||
|
|
66edc747be | ||
|
|
0520765bdc | ||
|
|
1e637f0f7c | ||
|
|
6df00e7ee8 | ||
|
|
007f823676 | ||
|
|
5bb5bd16ae | ||
|
|
68fb96e3a1 | ||
|
|
91a23cb286 | ||
|
|
f3ad232c1f | ||
|
|
b9c178cc29 | ||
|
|
d9dce51823 | ||
|
|
ce3d333a98 | ||
|
|
a6b3b33587 | ||
|
|
24c653d039 | ||
|
|
8721a68db5 | ||
|
|
e01ffc0211 | ||
|
|
59a9290832 | ||
|
|
dd1ec48cbd | ||
|
|
42846e6b45 | ||
|
|
f0fe5cc418 | ||
|
|
ea9d6c48bc | ||
|
|
072db50727 | ||
|
|
eb63d63276 | ||
|
|
f0cb889215 | ||
|
|
3c830760c2 | ||
|
|
a230f603c6 | ||
|
|
db8b6f2dfb |
1803 changed files with 111035 additions and 71897 deletions
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -39,7 +39,7 @@ If you are using a SDK that isn't the latest release, please update first as it'
|
|||
|
||||
5. **SDK logs** (mandatory)
|
||||
|
||||
Enable debug logs in advanced section of the settings, restart the app, reproduce the issue and then go back to advanced settings, click on "Send logs" and copy/paste the link here.
|
||||
Click on "Share logs" in Help -> Troubleshooting view and copy/paste the link here.
|
||||
|
||||
It's also explained [in the README](https://github.com/BelledonneCommunications/linphone-android#behavior-issue).
|
||||
|
||||
|
|
|
|||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: SDK issues
|
||||
url: https://github.com/BelledonneCommunications/linphone-sdk/issues
|
||||
about: Please post issues about the SDK here.
|
||||
- name: Desktop issues
|
||||
url: https://github.com/BelledonneCommunications/linphone-desktop/issues
|
||||
about: Please post issues about the Desktop (Linux, MacOSX, Windows) application here.
|
||||
- name: iOS issues
|
||||
url: https://github.com/BelledonneCommunications/linphone-iphone/issues
|
||||
about: Please post issues about the iPhone application here.
|
||||
- name: Contacts
|
||||
url: https://www.linphone.org/contact
|
||||
about: For any contacts like commercial, licensing, mailing-lists
|
||||
39
.gitignore
vendored
39
.gitignore
vendored
|
|
@ -1,29 +1,14 @@
|
|||
*.orig
|
||||
*.rej
|
||||
.DS_Store
|
||||
.gradle
|
||||
.idea
|
||||
.settings
|
||||
adb.pid
|
||||
bc-android.keystore
|
||||
build
|
||||
*.iml
|
||||
lint.xml
|
||||
.gradle
|
||||
/local.properties
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
res/.DS_Store
|
||||
res/raw/lpconfig.xsd
|
||||
.d
|
||||
.*clang*
|
||||
**/*.iml
|
||||
**/.classpath
|
||||
**/.project
|
||||
**/*.kdev4
|
||||
**/.vscode
|
||||
res/value-hi_IN
|
||||
linphone-sdk-android/*.aar
|
||||
app/debug
|
||||
app/release
|
||||
app/releaseAppBundle
|
||||
app/releaseWithCrashlytics
|
||||
keystore.properties
|
||||
app/src/main/res/xml/contacts.xml
|
||||
app/debug/
|
||||
app/release/
|
||||
.idea/
|
||||
app/bc-android.keystore
|
||||
.kotlin/
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ job-android:
|
|||
|
||||
stage: build
|
||||
tags: [ "docker-android" ]
|
||||
image: gitlab.linphone.org:4567/bc/public/linphone-android/bc-dev-android:20220609_android_33
|
||||
image: gitlab.linphone.org:4567/bc/public/linphone-android/bc-dev-android:20230414_bullseye_jdk_17_cleaned
|
||||
|
||||
before_script:
|
||||
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then eval $(ssh-agent -s); fi
|
||||
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then echo "$SCP_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null; fi
|
||||
- echo "$ANDROID_SETTINGS_GRADLE" > settings.gradle
|
||||
- if ! [ -z ${ANDROID_SETTINGS_GRADLE+x} ]; then echo "$ANDROID_SETTINGS_GRADLE" > settings.gradle.kts; fi
|
||||
- git config --global --add safe.directory /builds/BC/public/linphone-android
|
||||
|
||||
script:
|
||||
- sdkmanager
|
||||
- scp -oStrictHostKeyChecking=no $DEPLOY_SERVER:$ANDROID_KEYSTORE_PATH app/
|
||||
- scp -oStrictHostKeyChecking=no $DEPLOY_SERVER:$ANDROID_GOOGLE_SERVICES_PATH app/
|
||||
- echo storePassword=$ANDROID_KEYSTORE_PASSWORD > keystore.properties
|
||||
|
|
@ -18,15 +18,15 @@ job-android:
|
|||
- echo keyAlias=$ANDROID_KEYSTORE_KEY_ALIAS >> keystore.properties
|
||||
- echo storeFile=$ANDROID_KEYSTORE_FILE >> keystore.properties
|
||||
- ./gradlew app:dependencies | grep org.linphone
|
||||
- ./gradlew clean
|
||||
- ./gradlew assembleDebug
|
||||
- ./gradlew assembleRelease
|
||||
|
||||
artifacts:
|
||||
paths:
|
||||
- ./app/build/outputs/apk/debug/linphone-android-debug-*.apk
|
||||
- ./app/build/outputs/apk/release/linphone-android-release-*.apk
|
||||
when: always
|
||||
expire_in: 1 week
|
||||
expire_in: 1 day
|
||||
|
||||
|
||||
.scheduled-job-android:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
job-android-upload:
|
||||
|
||||
stage: deploy
|
||||
tags: [ "deploy" ]
|
||||
tags: [ "docker-deploy" ]
|
||||
|
||||
only:
|
||||
- schedules
|
||||
dependencies:
|
||||
- job-android
|
||||
|
||||
before_script:
|
||||
- if ! [ -z ${SCP_PRIVATE_KEY+x} ] && ! [ -z ${DEPLOY_SERVER_HOST_KEYS+x} ]; then eval $(ssh-agent -s); fi
|
||||
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then echo "$SCP_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null; fi
|
||||
- if ! [ -z ${DEPLOY_SERVER_HOST_KEYS+x} ]; then mkdir -p ~/.ssh && chmod 700 ~/.ssh; fi
|
||||
- if ! [ -z ${DEPLOY_SERVER_HOST_KEYS+x} ]; then echo "$DEPLOY_SERVER_HOST_KEYS" >> ~/.ssh/known_hosts; fi
|
||||
|
||||
script:
|
||||
- cd app/build/outputs/apk/ && rsync ./debug/*.apk $DEPLOY_SERVER:$ANDROID_DEPLOY_DIRECTORY
|
||||
# Launches rsync in partial mode, which means that we are using a temp_dir in case of a transfer issue
|
||||
# Upon a job relaunch, the files in temp_dir would then be re-used, and deleted if the transfer succeeds
|
||||
- cd app/build/outputs/apk/ && rsync --partial --partial-dir=$CI_PIPELINE_ID_$CI_JOB_NAME ./release/*.apk $DEPLOY_SERVER:$ANDROID_DEPLOY_DIRECTORY
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
Linphone
|
||||
123
.idea/codeStyles/Project.xml
generated
Normal file
123
.idea/codeStyles/Project.xml
generated
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
6
.idea/compiler.xml
generated
Normal file
6
.idea/compiler.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/misc.xml
generated
Normal file
9
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="temurin-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
670
CHANGELOG.md
670
CHANGELOG.md
|
|
@ -10,7 +10,671 @@ Group changes to describe their impact on the project, as follows:
|
|||
Fixed for any bug fixes.
|
||||
Security to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
## [4.7.0] - Unreleased
|
||||
## [6.1.0] - Unreleased
|
||||
|
||||
### Added
|
||||
- Added the ability to edit/delete chat messages sent less than 24 hours ago.
|
||||
- Added keyboard shortcuts on IncomingCallFragment: Ctrl + Shift + A to answer the call, Ctrl + Shift + D to decline it
|
||||
- Added seeking feature to recordings & media player within app
|
||||
- Added PDF preview in conversation (message bubble & documents list)
|
||||
- Added hover effect when using a mouse (useful for tablets or devices with desktop mode)
|
||||
- Support right click on some items to open bottom sheet/menu
|
||||
- Added toggle speaker action in active call notification
|
||||
- Increased text size for chat messages that only contains emoji(s)
|
||||
- Use user-input to filter participants list after typing "@" in conversation send area
|
||||
- Handle read-only CardDAV address books, disable edit/delete menus for contacts in read-only FriendList
|
||||
- Added swipe/pull to refresh on contacts list of a CardDAV addressbook has been configured to force the synchronization
|
||||
- Show information to user when filtering contacts doesn't show them all and user may have to refine it's search
|
||||
- Show Android notification when an account goes to failed registration state (only when background mode is enabled)
|
||||
- New settings:
|
||||
- one for user to choose whether to sort contacts by first name or last name
|
||||
- one to hide contacts that have neither a SIP address nor a phone number
|
||||
- one to let app auto-answer call with video sending already enabled
|
||||
- one to let edit native contacts Linphone copy in-app instead of opening native addressbook third party app
|
||||
- Added a vu meter for recording & playback volumes (must be enabled in developer settings)
|
||||
- Added support for HDMI audio devices
|
||||
|
||||
### Changed
|
||||
- No longer follow TelecomManager audio endpoint during calls, using our own routing policy
|
||||
- Join a conference using default layout instead of audio only when clicking on a meeting SIP URI
|
||||
- Removing an account will also remove all related data in the local database (auth info, call logs, conversations, meetings, etc...)
|
||||
- Hide SIP address/phone number picker dialog if contact has exactly one SIP address matching both the app default domain & the currently selected account domain
|
||||
- Hide SIP address associated to phone number through presence mecanism in contact details & editor views.
|
||||
- Improved UI on tablets with screen sw600dp and higher, will look more like our desktop app
|
||||
- Improved navigation within app when using a keyboard
|
||||
- Now loading media/documents contents in conversation by chunks (instead of all of them at once)
|
||||
- Simplified audio device name in settings
|
||||
- Reworked some settings (moved calls related ones from advanced settings to advanced calls settings)
|
||||
- Increased shared media preview size in chat
|
||||
- Un-encrypted conversation warning will be more visible for accounts that support end-to-end encrypted conversations
|
||||
- Made numpad buttons larger by changing their shape
|
||||
- All LDAP fields are mandatory now
|
||||
- Improved how Android shortcuts are created
|
||||
- Permission fragment will only show missing ones
|
||||
- Added more info into StartupListener logs
|
||||
- Updated password forgotten procedure, will use online account manager platform
|
||||
|
||||
### Fixed
|
||||
- Copy raw message content instead of modified one when it contains a participant mention ("@username")
|
||||
|
||||
## [6.0.21] - 2025-12-16
|
||||
|
||||
### Added
|
||||
- Allow linphone-config: scheme URIs in in-app QR code scanner
|
||||
|
||||
### Changed
|
||||
- Workaround for audio focus & audio manager mode on devices that do not support TelecomManager APIs
|
||||
- Set front camera as default after using back camera when scanning a QR code
|
||||
- Added back largeHeap flag in AndroidManifest.xml
|
||||
|
||||
### Fixed
|
||||
- Fixed call recording indicator not showing local record in progress in case UPDATE isn't answered
|
||||
- Fixed native addressbook reload when a contact is updated in the OS default app
|
||||
- Fixed issue with linphone-config scheme URIs if scheme is followed by "//"
|
||||
- Fixed Job & Company contact field not updated if field content was removed
|
||||
- Fixed local avatar not displayed when calling ourselves
|
||||
- Prevent crashes due to some ActivityNotFound exceptions
|
||||
- Prevent crash due to empty clipboard on some devices
|
||||
|
||||
## [6.0.20] - 2025-11-21
|
||||
|
||||
### Changed
|
||||
- Added shrink resources to release config in gradle
|
||||
|
||||
### Fixed
|
||||
- Remove AuthInfo when configuring a CardDAV friend list if synchronization fails
|
||||
- Added missing toast when starting a group call or meeting if there's an issue
|
||||
- Fixed crash in RecordingPlayerFragment due to used lateinit property before it's initialized
|
||||
|
||||
## [6.0.19] - 2025-10-16
|
||||
|
||||
### Added
|
||||
- Spanish and Slovakian translations thanks to Weblate contributors
|
||||
|
||||
### Changed
|
||||
- SIP addresses domain hidden in Suggestions if it matches the currently selected account SIP identity domain
|
||||
- Start proximity sensor when an incoming call is answered from the notification (disabling screen when device is near)
|
||||
|
||||
### Fixed
|
||||
- Black screen when trying to scan a QR Code right after granting CAMERA permission (only happened on some devices)
|
||||
- Possible crash due to ConcurrentModificationException
|
||||
- Camera preview in conference that was black sometimes after switching layout
|
||||
- Possibly wrong screen sharing participant name in conference
|
||||
- Presence SUBSCRIBE that was only sent for sip.linphone.org accounts
|
||||
- Keyboard suggestions in participant picker textfield
|
||||
- Account labelled as Disabled instead of Disconnected when network isn't reachable
|
||||
- Suggestions generated avatar if username starts with '+'
|
||||
- Two LDAP fields label where swapped
|
||||
|
||||
## [6.0.18] - 2025-09-15
|
||||
|
||||
### Added
|
||||
- Added menu icon next to currently selected account avatar to make the drawer menu easier to understand
|
||||
- Added missing dialpad floating action button in the call transfer fragment
|
||||
|
||||
### Changed
|
||||
- Improved bodyless friendlist presence process when it's received
|
||||
|
||||
### Fixed
|
||||
- Fixed "End-to-end encrypted call" label while in conference, the call may be end-to-end encrypted but only to the conference server, not to all participants
|
||||
- Fixed missing meeting subject when calling the conference SIP URI if the conference info doesn't exist yet
|
||||
- Finish CallActivity if no call is found when trying to answer/decline a call from the IncomingCallFragment
|
||||
- Prevent empty screen when rotating the device and clicking on the empty part next to the list while in landscape and then rotating the device back to portrait
|
||||
|
||||
## [6.0.17] - 2025-09-02
|
||||
|
||||
### Changed
|
||||
- Portuguese translation updated from Weblate (still not complete)
|
||||
|
||||
### Fixed
|
||||
- Vibrator not stopped when call is terminated sometimes (SDK fix)
|
||||
- Chat conversation not visible sometimes (SDK fix)
|
||||
|
||||
## [6.0.16] - 2025-08-25
|
||||
|
||||
## Added
|
||||
- Access to Help/Troubleshooting pages from Assistant
|
||||
|
||||
## Fixed
|
||||
- Some Core methods being called from UI thread causing either a crash or a deadlock sometimes
|
||||
- Scrolling issue when doing a search in a conversation with only one result
|
||||
- Contacts not updated after body less presence notify was received
|
||||
- VFS issue due to encrypted.pref file being backed up by Android OS
|
||||
|
||||
## [6.0.15] - 2025-08-11
|
||||
|
||||
### Fixed
|
||||
- Crash due to changes in SDK triggering fatal error if linphone_core_stop() is called from linphone_core_iterate() loop (which was done when scanning QR code)
|
||||
|
||||
### Changed
|
||||
- Prevent leaving assistant after doing a remote provisioning if there is still no account after it (if there was no account before and no account was provided in downloaded config)
|
||||
|
||||
## [6.0.14] - 2025-08-06
|
||||
|
||||
### Fixed
|
||||
- Fixed ANR due to deadlock caused by method being called from wrong thread
|
||||
- Fixed microphone not always recording audio while app in background or if screen is turned off
|
||||
- Fixed missing favorites in start call / create conversation views
|
||||
- Fixed outgoing call view in full screen
|
||||
- Fixed generated avatar for SIP URIs without username
|
||||
|
||||
## [6.0.13] - 2025-07-31
|
||||
|
||||
### Fixed
|
||||
- Missing favourites if contacts list size exceeds magic search max results setting
|
||||
- Muted call on some devices due to Telecom Manager quickly muting/unmuting call
|
||||
- Full screen without video during outgoing early media call if video has been declined by remote end
|
||||
- Removed duplicated week label if "no meeting today" is the first entry for current week
|
||||
- Prevent crash during file export if no app on the device can handle it
|
||||
- Prevent crash that could happen with chat message notification if sender name (or group chat room subject) is empty
|
||||
|
||||
### Changed
|
||||
- Back gesture / navigation button will close the numpad bottom sheet if it's open instead of leaving the page directly
|
||||
- Updated bell and bell_slash icons
|
||||
|
||||
## [6.0.12] - 2025-07-18
|
||||
|
||||
### Fixed
|
||||
- Reactions list in bottom sheet update while opened
|
||||
- Crashes due to late init properties being used before initialized
|
||||
|
||||
## [6.0.11] - 2025-07-11
|
||||
|
||||
### Added
|
||||
- Added toggle in LDAP configuration to allow to quickly enable/disable it
|
||||
|
||||
### Changed
|
||||
- Reduced maximum number of contacts displayed in contacts list, new call/conversation, meeting participant selection etc...
|
||||
- Updated translations
|
||||
|
||||
### Fixed
|
||||
- Calls top bar wrong notification label when going from two calls to one.
|
||||
|
||||
## [6.0.10] - 2025-06-27
|
||||
|
||||
### Added
|
||||
- Added a new top bar alert area for pending file/text sharing.
|
||||
|
||||
### Changed
|
||||
- Reworked in-app top bar alerts, now can show both an account alert and an active call alert.
|
||||
- Hide SIP address/phone number picker dialog if contact has exactly one SIP address matching the default domain and currently default account domain.
|
||||
|
||||
### Fixed
|
||||
- Bluetooth not being used automatically when device is connected during a call.
|
||||
- Call encryption status label stuck in "Waiting for encryption".
|
||||
- Group chat room creation if LIME server URL isn't set.
|
||||
- Participant mention if more than one in the same chat message.
|
||||
- Force default account in call params when starting one.
|
||||
|
||||
## [6.0.9] - 2025-06-06
|
||||
|
||||
### Added
|
||||
- German translation (88% complete)
|
||||
- Link to user guide in Help section
|
||||
- Missing scroll views for help & debug layouts
|
||||
|
||||
### Changed
|
||||
- Prevent port from being set in the SIP identity address in third party account login + remove port (if any) from SIP identity for existing accounts
|
||||
- Show last message timestamp instead of conversation last updated timestamp in conversations list
|
||||
|
||||
### Fixed
|
||||
- Prevent blinking in conversations list when removing message from chat room
|
||||
- Prevent empty (can even lead to crash) display name in call notification (using all identification fields from vCard)
|
||||
|
||||
## [6.0.8] - 2025-05-23
|
||||
|
||||
### Added
|
||||
- Ukrainian & simplified Chinese translations from Weblate
|
||||
- Sliding answer/decline button in incoming call fragment if device is locked (will help prevent calls from being unintentionally picked up or hung up while the device is being removed from a pocket)
|
||||
|
||||
### Changed
|
||||
- Show files with square design when more than one (as it is for media files)
|
||||
- Outgoing chat bubbles will now display the sent file size (as it is for received messages)
|
||||
|
||||
### Fixed
|
||||
- Fixed issue with bluetooth hearing aids
|
||||
- Fixed audio call being answered on speakerphone
|
||||
- Fixed events related to joined/left conversation being briefly visible sometimes for 1-1 conversations
|
||||
- Fixed files/media grid in chat bubble using more than 3 columns in landscape
|
||||
- Fixed logs upload server URL setting
|
||||
|
||||
## [6.0.7] - 2025-05-16
|
||||
|
||||
### Added
|
||||
- CS, NL and RU translations from Weblate
|
||||
|
||||
### Changed
|
||||
- Improved find contact performances
|
||||
- Make sure speaker audio device is used for playing the ringtone during early media
|
||||
- Reworked bottom navigation bar in portrait and unread count indicators
|
||||
- No longer delete conversations when deleting account (for now); causes user to leave group which is an issue when using multiple devices
|
||||
|
||||
### Fixed
|
||||
- Fixed no default account after remote provisioning
|
||||
- Prevent lists from refreshing too many times when using LDAP or remote CardDAV contact directories
|
||||
- Fixed black miniatures in conference if bundle mode is disabled in account params
|
||||
- Fixed long press on a chat message containing a SIP URI triggering call
|
||||
- Disable IMDN bottom sheet for incoming messages in groups instead of showing it empty
|
||||
- Refresh conversations list after clearing conversation history
|
||||
- Fixed another race condition issue related to foreground call service
|
||||
|
||||
## [6.0.6] - 2025-05-02
|
||||
|
||||
### Added
|
||||
- Added recover phone account when clicking on "Forgotten password" in the assistant
|
||||
- Improved message when contacts list is empty depending on the currently selected filter and added a button to open the filter popup menu for users that didn't notice the icon on the top right corner of the screen when contacts list is empty and "SIP contacts only" filter is set.
|
||||
- Added "Logs collection sharing server URL" setting in developper area
|
||||
- Added "Disable sending logs to Crashlytics" advanced setting.
|
||||
|
||||
### Changed
|
||||
- Improved VFS message in confirmation dialog
|
||||
- Moved "Print logs in logcat" and "File sharing server URL" settings to developper area
|
||||
|
||||
### Fixed
|
||||
- Fixed crash when opening a password protected PDF
|
||||
- Fixed chat room lookup while in 1-1 call, using SDK method for getting chat room from conference
|
||||
- Fixed newly created contact not being visible in contacts list without reloading it
|
||||
- Fixed missing event icon for group conversations
|
||||
- Another attempts at preventing crashes due to In-Call service not being started as foreground before being stopped
|
||||
|
||||
## [6.0.5] - 2025-04-18
|
||||
|
||||
### Changed
|
||||
- When calling a SIP URI that looks like a phone number in the username and an IP in the domain, replace the domain with the one of the currently selected account to workaround issue with PBXs using IPs instead of domains in From header
|
||||
- Improved account creation page UI when push notifications aren't available
|
||||
- Improved called account display on incoming call screen when more than one account configured
|
||||
- Updated telecom package from beta to release candidate
|
||||
|
||||
### Fixed
|
||||
- Fixed transfer call view numpad button starting a new call instead of forwarding the current one
|
||||
- Fixed incoming call not displayed in call history depending on how the From & To headers are formatted (SDK fix)
|
||||
- Fixed crashes related to foreground service not being started
|
||||
- Fixed crash due to lateinit property not being initialized before used
|
||||
|
||||
## [6.0.4] - 2025-04-11
|
||||
|
||||
### Changed
|
||||
- Third party SIP accounts push notifications will be disabled and setting will be hidden unless if list of supported domains (to prevent issues, specifically when used with UDP transport protocol causing bigger packets getting lost)
|
||||
|
||||
### Fixed
|
||||
- Prevent refresh of views due to contacts changes to happen to frequently at startup
|
||||
- Prevent crash in Help view if app is built without Firebase
|
||||
|
||||
## [6.0.3] - 2025-04-04
|
||||
|
||||
### Added
|
||||
- Show alert when default account is disabled
|
||||
- Refesh list details when going back from background after one hour or more (when keep app alive using service is enabled)
|
||||
- Click to copy SIP URI in call history shortcut
|
||||
- Added developper settings, must click 8 times on version (in Help) to make it appear (E2E encryption for meetings & group calls setting was moved there)
|
||||
- Circular indicator while search is in progress in contacts lists
|
||||
|
||||
### Changed
|
||||
- Force some default values on notifications channels
|
||||
- Contacts list filter is now applied to new call / conversation & other contact pickers
|
||||
- Attach file icon stays visible while typing message in conversation instead of emoji picker icon
|
||||
|
||||
### Fixed
|
||||
- No default account being selected if the default one is removed
|
||||
- Navigation bar turning orange when opening search bar
|
||||
- Incoming call showed as video even if video is disabled locally
|
||||
- Concurrent modification crash in Contacts loader
|
||||
- Meetings list not properly sorted when CCMP is used
|
||||
- POST_NOTIFICATIONS permission check on old Android devices
|
||||
|
||||
## [6.0.2] - 2025-03-28
|
||||
|
||||
### Added
|
||||
- Show on top bar if FULL_SCREEN_INTENT permission isn't granted, clicking on it sends to the matching settings so user can fix it easily, without it incoming call screen won't be displayed if screen is off
|
||||
- Ring during incoming early media call setting added back
|
||||
- Added a floating action button to open dialpad during outgoing early media call
|
||||
|
||||
### Changed
|
||||
- Delete all related call history / conversations / meetings when removing an account
|
||||
- Delay / use a separated thread for heavy contacts related tasks to ensure call is correctly handled and foreground service is started quickly enough
|
||||
- Newly created account in app will be kept disabled until SMS code validation is done
|
||||
- Keep app alive foreground service notification no shows a content message to ease clicking on it to open the app & workaround a crash on some devices
|
||||
- Automatically show dialpad setting will now also work on new / transfer call while in call as well
|
||||
|
||||
### Fixed
|
||||
- Improved POST_NOTIFICATIONS permission check on Android 13 and newer, should prevent crashes
|
||||
- Fixed contact lookup if phone number starts by "00" instead of "+"
|
||||
- Fixed "delete all call history" sometimes not removing all call logs
|
||||
- Fixed LDAP / remote CardDAV contacts sometimes not displayed in contacts list when doing a search
|
||||
- Fixed issue where contact filter could be set to only show sip.linphone.org contacts even when third party account was being selected
|
||||
- Fixed sometimes wrong displayed SIP URI in detailed call history
|
||||
- Fixed invisible meeting icon in status bar
|
||||
- Fixed missed call count indicator behavior with some third party providers
|
||||
- Prevent today indicator & meeting icon in bottom nav bar from blinking / briefly appearing
|
||||
- Fixed bottom nav bar sometimes being hidden
|
||||
- Fixed missing share logs server URL when migrating from 5.2 if that value was removed back then
|
||||
- Other crashes fixed
|
||||
|
||||
## [6.0.1] - 2025-03-21
|
||||
|
||||
### Added
|
||||
- Start at boot & auto answer settings added back
|
||||
- Interface setting to have dialpad automatically opened in start call view
|
||||
- Replace "+" by "00" and do not apply prefix for calls & chat account settings
|
||||
- Setting to let user choose whether to record calls using MKV or SMFF format (the later allows to record H265/AV1 video but is a proprietary file format that can't be read outside of Linphone)
|
||||
|
||||
### Changed
|
||||
- Reverted the way of playing incoming call ringone (you may have to configure your own ringtone again), was causing various issues depending on devices/firmwares
|
||||
- Show all call history entries if only one account is configured (workaround for missing history for now until a proper fix will be done in SDK)
|
||||
|
||||
### Fixed
|
||||
- Issue preventing bluetooth Hearing Aids from working properly (and fixed earpiece/hearing aids icon)
|
||||
- Prevent Qr Code scanner to use static picture camera
|
||||
- Prevent user from connecting the same account multiple times
|
||||
- Quit menu visibility not updated when changing Keep Alive setting
|
||||
- Participant selection in group when typing "@"
|
||||
- Recordings order has been reversed to have newest ones at top
|
||||
- Improved message when network is not reachable due to "Wifi only mode" being enabled
|
||||
- Various crash & bug fixes
|
||||
|
||||
## [6.0.0] - 2025-03-11
|
||||
|
||||
6.0.0 release is a complete rework of Linphone Android, with a fully redesigned UI, so it is impossible to list everything here.
|
||||
|
||||
### Changed
|
||||
- Separated threads: Contrary to previous versions, our SDK is now running in it's own thread, meaning it won't freeze the UI anymore in case of heavy work, thus reducing the number of ANR and greatly increasing the fluidity of the app.
|
||||
- Asymmetrical video : you no longer need to send your own camera feed to receive the one from the remote end of the call, and vice versa.
|
||||
- Improved multi account: you'll only see history, conversations, meetings etc... related to currently selected account, and you can switch the default account in two clicks.
|
||||
- Call transfer: Blind & Attended call transfer have been merged into one: during a call, if you initiate a transfer action, either pick another call to do the attended transfer or select a contact from the list (you can input a SIP URI not already in the suggestions list) to start a blind transfer.
|
||||
- User can only send up to 12 files in a single chat message.
|
||||
- IMDNs are now only sent to the message sender, preventing huge traffic in large groups, and thus the delivery status icon for received messages is now hidden in groups (as it was in 1-1 conversations).
|
||||
- Settings: a lot of them are gone, the one that are still there have been reworked to increase user friendliness.
|
||||
- Default screen (between contacts, call history, conversations & meetings list) will change depending on where you were when the app was paused or killed, and you will return to that last visited screen on the next startup.
|
||||
- Gradle files have been migrated from Groovy to Kotlin DSL, and dependencies are now in a separated file (libs.versions.toml).
|
||||
- Account creation no longer allows you to use your phone number as username, but it is still required to provide it to receive activation code by SMS.
|
||||
- Minimum supported Android OS version is now 9 (API level 28).
|
||||
- Telecom Manager support is now based on androidx.core.core-telecom package.
|
||||
- Some settings have changed name and/or section in linphonerc file.
|
||||
|
||||
### Added
|
||||
- Contacts trust: contacts for which all devices have been validated through a ZRTP call with SAS exchange are now highlighted with a blue circle (and with a red one in case of mistrust). That trust is now handled at contact level (instead of conversation level in previous versions).
|
||||
- Media & documents exchanged in a conversation can be easily found through a dedicated screen.
|
||||
- A brand new chat message search feature has been added to conversations.
|
||||
- You can now react to a chat message using any emoji.
|
||||
- If next message is also a voice recording, playback will automatically start after the currently playing one ends.
|
||||
- Chat while in call: a shortcut to a conversation screen with the remote.
|
||||
- Chat while in a conference: if the conference has a text stream enabled, you can chat with the other participants of the conference while it lasts. At the end, you'll find the messages history in the call history (and not in the list of conversations).
|
||||
- Auto export of media to native gallery even when auto download is enabled (but still not if VFS is enabled nor for ephemeral messages).
|
||||
- Save / export document & media from ephemeral messages will be disabled, and secure policy that prevents screenshots will be enforced in file viewer even if the setting is disabled.
|
||||
- Notification showing upload/download of files shared through chat will let user know the progress and keep the app alive during that process.
|
||||
- Screen sharing in conference: only desktop app starting with 6.0 version is able to start it, but on mobiles you'll be able to see it.
|
||||
- You can choose whatever ringtone you'd like for incoming calls (in Android notification channel settings).
|
||||
- Security focus: security & trust is more visible than ever, and unsecure conversations & calls are even more visible than before.
|
||||
- CardDAV: you can configure as many CardDAV servers you want to synchronize you contacts in Linphone (in addition or in replacement of native addressbook import).
|
||||
- OpenID: when used with a SSO compliant SIP server (such as Flexisip), we support single-sign-on login.
|
||||
- MWI support: display and allow to call your voicemail when you have new messages (if supported by your VoIP provider and properly configured in your account params).
|
||||
- CCMP support: if you configure a CCMP server URL in your accounts params, it will be used when scheduling meetings & to fetch list of meetings you've organized/been invited to.
|
||||
- Devices list: check on which device your sip.linphone.org account is connected and the last connection date & time (like on subscribe.linphone.org).
|
||||
- Protobuf dependency to allow logging native crashes stack traces at next app startup.
|
||||
- Android 15 startup listener, allowing us to log type of start (cold, warm, etc...) and some other useful info.
|
||||
- Dialer & in-call numpad show letters under the digit.
|
||||
|
||||
### Removed
|
||||
- Dialer: the previous home screen (dialer) has been removed, you'll find it as an input option in the new start call screen.
|
||||
- Peer-to-peer: a SIP account (sip.linphone.org or other) is now required.
|
||||
- Contacts: we no longer add contacts created in-app in the native addressbook (WRITE_CONTACTS permission was removed), but we still import them if you grant us the READ_CONTACTS permission.
|
||||
|
||||
### Fixed
|
||||
- No longer trying to play vocal messages & call recordings using bluetooth when connected to an Android Auto car, causing playback issues.
|
||||
- AAudio driver no longer causes delay when switching between devices (SDK fix).
|
||||
|
||||
## [5.2.5] - 2024-05-03
|
||||
|
||||
### Changed
|
||||
- Updated translations
|
||||
|
||||
## [5.2.4] - 2024-04-22
|
||||
|
||||
### Fixed
|
||||
- Active speaker video hidden when you are the first one to join a meeting
|
||||
- Show camera icon instead of microphone for incoming video calls
|
||||
- SIP URI parsing from native contact due to international prefix being applied when it shouldn't
|
||||
- Various fixes for broadcast mode
|
||||
|
||||
## [5.2.3] - 2024-01-31
|
||||
|
||||
### Fixed
|
||||
- Crash due to OOM for some images sent/received in chat
|
||||
- Crash while navigating to account settings
|
||||
|
||||
### Changed
|
||||
- Updated translations (Romanian, Polish, Portuguese)
|
||||
|
||||
## [5.2.2] - 2024-01-15
|
||||
|
||||
### Fixed
|
||||
- Local conference created my merging audio streams
|
||||
|
||||
## [5.2.1] - 2023-12-23
|
||||
|
||||
### Fixed
|
||||
- Crash when Service starts before CoreContext
|
||||
|
||||
## [5.2.0] - 2023-12-21
|
||||
|
||||
### Added
|
||||
- Chat messages emoji "reactions"
|
||||
- Hearing aids should be working the same way bluetooth headset does
|
||||
- Hardware video codecs (H264, H265) are now used in priority when possible (SDK)
|
||||
- Broadcast mode for scheduled meetings (hidden)
|
||||
- Android 14 support
|
||||
|
||||
### Changed
|
||||
- BLUETOOTH_CONNECT permission is no longer required
|
||||
|
||||
### Fixed
|
||||
- Correctly switching to either bottom or back microphone depending on wether the earpiece or the speaker is used,
|
||||
and also use the same device for input and output if the one set as output as RECORD capability
|
||||
(fixes echo issue while on speakerphone on some devices such as Samsung's)
|
||||
- Connection status & color when in refreshing state
|
||||
- Sent content type for files attached to a chat message
|
||||
- Toggle mute mic while in conference
|
||||
- Calling right after creating a chat room
|
||||
|
||||
## [5.1.4] - 2023-10-20
|
||||
|
||||
### Fixed
|
||||
- Various fixes in the SDK (5.2.110)
|
||||
|
||||
### Changed
|
||||
- Updated translations from Weblate
|
||||
|
||||
## [5.1.3] - 2023-09-23
|
||||
|
||||
### Fixed
|
||||
- Core not able to open database due to issue in 5.2.107 SDK from last update
|
||||
- Incoming call activity and lock screen interaction
|
||||
- Selected "meeting" filter icon color
|
||||
|
||||
## [5.1.2] - 2023-09-22
|
||||
|
||||
### Added
|
||||
- Italian translation completed
|
||||
|
||||
### Fixed
|
||||
- Multiple authentication requested dialogs stacking above each other sometimes
|
||||
- Downgraded navigation version to try to prevent some crashes reported on the Play Store
|
||||
|
||||
## [5.1.1] - 2023-09-06
|
||||
|
||||
### Fixed
|
||||
- Fixed issue in SDK randomly generated password when creating account from app
|
||||
- Various issues reported on the Play Store
|
||||
|
||||
## [5.1.0] - 2023-08-21
|
||||
|
||||
### Added
|
||||
- Showing short term presence for contacts whom publish it + added setting to disable it (enabled by default for sip.linphone.org accounts)
|
||||
- Confirmation dialog before removing account
|
||||
- Attended transfer instead of blind transfer if there is more than 1 call
|
||||
- Last sent message delivery status (IMDN) icon in chat rooms list
|
||||
- Emoji picker in chat room, and increase size of text if it only contains emojis
|
||||
- Hidden setting to disable video completely
|
||||
- Hidden setting to prevent adding / editing / removing native contacts
|
||||
- Hidden setting to protect settings access using account password
|
||||
- SIP URI in call can be selected using long press
|
||||
- Dialog showing up asking for correct account password in case of failed authentication
|
||||
|
||||
### Changed
|
||||
- Switched Account Creator backend from XMLRPC to FlexiAPI, it now requires to be able to receive a push notification
|
||||
- Email account creation form is now only available if TELEPHONY feature is not available, not related to screen size anymore
|
||||
- Replaced voice recordings file name by localized placeholder text, like for video conferences invitations
|
||||
- Decline incoming calls with Busy reason if there is at least another active call
|
||||
- Open keyboard when replying to a message if no text / file / voice record is pending
|
||||
- Removed jetifier as it is not needed
|
||||
- Switched from gradle 7.5 to 8.0, requires JDK 17 (instead of 11)
|
||||
|
||||
### Fixed
|
||||
- Messages not marked as reply in basic chat room if sending more than 1 content
|
||||
- Chat message video attachment display when failing to get a preview picture
|
||||
|
||||
## [5.0.14] - 2023-06-20
|
||||
|
||||
### Changed
|
||||
- SDK update only
|
||||
|
||||
## [5.0.13] - 2023-06-15
|
||||
|
||||
### Changed
|
||||
- SDK update only
|
||||
|
||||
## [5.0.12] - 2023-05-23
|
||||
|
||||
### Fixed
|
||||
- Crash if notification manager throws an exception
|
||||
- Video preview not moving if call was started in audio only
|
||||
|
||||
## [5.0.11] - 2023-05-09
|
||||
|
||||
### Fixed
|
||||
- Wrong call displayed when hanging up a call while an incoming one is ringing
|
||||
- Crash related to call history
|
||||
- Crash due to wrongly format string
|
||||
- Add/remove missing listener on FriendLists created after Core has been created
|
||||
|
||||
### Changed
|
||||
- Improved GSM call interruption
|
||||
- Updated translations
|
||||
|
||||
## [5.0.11] - 2023-05-09
|
||||
|
||||
### Fixed
|
||||
- Wrong call displayed when hanging up a call while an incoming one is ringing
|
||||
- Crash related to call history
|
||||
- Crash due to wrongly format string
|
||||
- Add/remove missing listener on FriendLists created after Core has been created
|
||||
|
||||
### Changed
|
||||
- Improved GSM call interruption
|
||||
- Updated translations
|
||||
|
||||
## [5.0.10] - 2023-04-04
|
||||
|
||||
### Fixed
|
||||
- Plain copy of encrypted files (when VFS is enabled) not cleaned
|
||||
- Avatar display issue if contact's "initials" contains more than 1 emoji or an emoji + a character
|
||||
|
||||
## [5.0.9] - 2023-03-30
|
||||
|
||||
### Fixed
|
||||
- Admin weren't visible for non admin users in group chat rooms
|
||||
- Crash when clicking on URI in chat if not matching app is found on Android to handle it
|
||||
- LIME update threshold wasn't set, causing a request to be made after each REGISTER
|
||||
|
||||
### Changed
|
||||
- Now SDK automatically handles TextureView's listener, removed it from app
|
||||
- Bumped license year to 2023
|
||||
- Force remove LIME X3DH server URL for third party accounts
|
||||
|
||||
## [5.0.8] - 2023-03-20
|
||||
|
||||
### Fixed
|
||||
- Trying to prevent crash in call history
|
||||
- Color icon in dark mode in chat for files & replies
|
||||
|
||||
### Changed
|
||||
- Updated translations
|
||||
|
||||
## [5.0.7] - 2023-02-27
|
||||
|
||||
### Fixed
|
||||
- Fixed navigating to a contact that doesn't have a native ID, but using it's SIP address instead
|
||||
- Fixed account creator resolved country name & create button not enabled
|
||||
|
||||
### Changed
|
||||
- Updated translations
|
||||
|
||||
## [5.0.6] - 2023-02-17
|
||||
|
||||
### Fixed
|
||||
- Wrong country displayed in assistant after picking it in the list if another country has the same international prefix (such as +1)
|
||||
- SIP URI clickable pattern missing '~'
|
||||
- Crash that happens sometimes when CallActivity is destroyed
|
||||
- Pressing send message button while recording a voice message not sending it
|
||||
- Missing ephemeral icon next to send message icon
|
||||
- Headers colors in IMDN details
|
||||
- Pixel issue in call quality indicator 2 icon
|
||||
|
||||
### Changed
|
||||
- Improved incoming call layout when receiving early-media video
|
||||
- Hidden "Echo Tester" setting unless in debug mode as it can mislead user and isn't useful for end user
|
||||
|
||||
## [5.0.5] - 2023-01-19
|
||||
|
||||
### Fixed
|
||||
- Issue with how replies where added to chat message notification from reply action
|
||||
|
||||
## [5.0.4] - 2023-01-18
|
||||
|
||||
### Added
|
||||
- Show a progress bar while importing files to the chat sending area
|
||||
|
||||
### Changed
|
||||
- Prevent keyboard from auto-replacing some user input such as username, breaking SIP URIs unknowingly
|
||||
|
||||
### Fixed
|
||||
- Prevent copy of files that weren't sent in chat to be kept in app local folder
|
||||
|
||||
## [5.0.3] - 2023-01-13
|
||||
|
||||
### Added
|
||||
- Voice message recording/playback will use bluetooth/headset/headphones/hearing aid device if available
|
||||
- Chat message notifications are now compatible with Android Auto
|
||||
|
||||
### Changed
|
||||
- In video conference, when in active speaker layout, currently speaking participant miniature will be hidden
|
||||
- Attach file, voice recording and send message icons are now a bit bigger
|
||||
- Updated Firebase BoM, gradle & some dependencies
|
||||
|
||||
### Fixed
|
||||
- ANR happening sometimes during voice message playback
|
||||
|
||||
## [5.0.2] - 2023-01-05
|
||||
|
||||
### Changed
|
||||
- Export files to native gallery is now available even if automatically download files setting is enabled
|
||||
|
||||
### Fixed
|
||||
- Makes sure sip.linphone.org accounts have a LIME X3DH server URL for E2E chat messages encryption
|
||||
- Files not being exported to native gallery sometimes
|
||||
- Crashes reported by Google Play Store & Crashlytics
|
||||
|
||||
## [5.0.1] - 2022-12-16
|
||||
|
||||
### Changed
|
||||
- File transfer progress indication & error status improvements
|
||||
|
||||
### Fixed
|
||||
- Wrong LIME status for participant that has multiple devices
|
||||
- No longer sends video when switching from audio only to another conference layout
|
||||
- SIP URI regex pattern to prevent HTTP URLs containing '@' to be handled as SIP URI
|
||||
|
||||
## [5.0.0] - 2022-12-06
|
||||
|
||||
### Added
|
||||
- Post Quantum encryption when using ZRTP
|
||||
|
|
@ -28,6 +692,7 @@ Group changes to describe their impact on the project, as follows:
|
|||
|
||||
### Changed
|
||||
- In-call views have been re-designed
|
||||
- "Media Encryption Mandatory" setting now allows for any media encryption (instead of only the one selected in the above setting previously)
|
||||
- Improved how call logs are handled to improve performances
|
||||
- Improved how contact avatars are generated
|
||||
- 3-dots menu even for basic chat rooms with more options
|
||||
|
|
@ -42,10 +707,13 @@ Group changes to describe their impact on the project, as follows:
|
|||
- Show service notification sooner to prevent crash if Core creation takes too long
|
||||
- Incoming call screen not being showed up to user (& screen staying off) when using app in Samsung secure folder
|
||||
- One to one chat room creation process waiting indefinitely if chat room already exists
|
||||
- Contact edition (SIP addresses & phone numbers) not working due to original value being lost in Friend parsing
|
||||
- Automatically start call recording
|
||||
- "Blinking" in some views when presence is being received
|
||||
- Trying to keep the preferred driver (OpenSLES / AAudio) when switching device
|
||||
- Issues when storing presence in native contacts + potentially duplicated SIP addresses in contact details
|
||||
- Chat room scroll position lost when going into sub-view
|
||||
- Trim user input to remove any space at end of string due to keyboard auto completion
|
||||
- No longer makes requests to our LIME server (end-to-end encryption keys server) for non sip.linphone.org accounts
|
||||
- Fixed incoming call/notification not ringing if Do not Disturb mode is enabled except for favorite contacts
|
||||
|
||||
|
|
|
|||
59
README.md
59
README.md
|
|
@ -1,11 +1,12 @@
|
|||
|
||||
[](https://gitlab.linphone.org/BC/public/linphone-android/commits/master) [](https://weblate.linphone.org/engage/linphone/?utm_source=widget)
|
||||
[](https://gitlab.linphone.org/BC/public/linphone-android/commits/master)
|
||||
[](https://weblate.linphone.org/engage/linphone/)
|
||||
|
||||
Linphone is an open source softphone for voice and video over IP calling and instant messaging.
|
||||
|
||||
It is fully SIP-based, for all calling, presence and IM features.
|
||||
|
||||
General description is available from [linphone web site](https://www.linphone.org/technical-corner/linphone).
|
||||
General description is available from [linphone web site](https://linphone.org).
|
||||
|
||||
### How to get it
|
||||
|
||||
|
|
@ -21,11 +22,11 @@ Linphone is dual licensed, and is available either :
|
|||
|
||||
- under a [GNU/GPLv3 license](https://www.gnu.org/licenses/gpl-3.0.en.html), for free (open source). Please make sure that you understand and agree with the terms of this license before using it (see LICENSE file for details).
|
||||
|
||||
- under a proprietary license, for a fee, to be used in closed source applications. Contact [Belledonne Communications](https://www.linphone.org/contact) for any question about costs and services.
|
||||
- under a proprietary license, for a fee, to be used in closed source applications. Contact [Belledonne Communications](https://linphone.org/contact) for any question about costs and services.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Supported features and RFCs : https://www.linphone.org/technical-corner/linphone/features
|
||||
- Supported features and RFCs : https://www.linphone.org/linphone-softphone/#linphone-fonctionnalites
|
||||
|
||||
- Linphone public wiki : https://wiki.linphone.org/xwiki/wiki/public/view/Linphone/
|
||||
|
||||
|
|
@ -33,16 +34,11 @@ Linphone is dual licensed, and is available either :
|
|||
|
||||
# What's new
|
||||
|
||||
App has been totally rewritten in Kotlin using modern components such as Navigation, Data Binding, View Models, coroutines, etc...
|
||||
Check the [CHANGELOG](./CHANGELOG.md) file for a more detailed list.
|
||||
The first linphone-android release that will be based on this will be 4.5.0, using 5.0.0 SDK.
|
||||
6.0.0 release is a completely new version, designed with UX/UI experts and marks a turning point in design, features, and user experience. The improvements make this version smoother and simpler for both developers and users.
|
||||
|
||||
We're also taking a fresh start regarding translations so less languages will be available for a while.
|
||||
If you want to contribute, you are welcome to do so, check the [Translations](#Translations) section below.
|
||||
You can take a look at the [CHANGELOG.md](CHANGELOG.md) file for a non-exhaustive list of changes of this new version and of the newly added features, the most exciting ones being the improved fluidity, a real multi-accounts support and asymmetrical video in calls.
|
||||
|
||||
org.linphone.legacy flavor (old java wrapper if you didn't migrate your app code to the new one yet) is no longer supported starting 5.0.0 SDK.
|
||||
|
||||
The sample project has been removed, we now recommend you to take a look at our [tutorials](https://gitlab.linphone.org/BC/public/tutorials/-/tree/master/android/kotlin).
|
||||
This release only works on Android OS 9.0 and newer.
|
||||
|
||||
# Building the app
|
||||
|
||||
|
|
@ -97,6 +93,8 @@ LinphoneSdkBuildDir=/home/<username>/linphone-sdk/build/
|
|||
|
||||
## Known issues
|
||||
|
||||
- If you have the following build issue `AAPT: error: resource drawable/linphone_logo_tinted (aka org.linphone:drawable/linphone_logo_tinted) not found`, delete the `app/src/main/res/xml/contacts.xml` file (you can do it simply with `git clean -f` command) and start the build again.
|
||||
|
||||
- If you encounter the `couldn't find "libc++_shared.so"` crash when the app starts, simply clean the project in Android Studio (under Build menu) and build again.
|
||||
Also check you have built the SDK for the right CPU architecture using the `-DLINPHONESDK_ANDROID_ARCHS=armv7,arm64,x86,x86_64` cmake parameter.
|
||||
|
||||
|
|
@ -106,13 +104,9 @@ Also check you have built the SDK for the right CPU architecture using the `-DLI
|
|||
|
||||
### Behavior issue
|
||||
|
||||
When submitting an issue on our [Github repository](https://github.com/BelledonneCommunications/linphone-android), please follow the template and attach the matching library logs:
|
||||
When submitting an issue on our [Github repository](https://github.com/BelledonneCommunications/linphone-android), please follow the template and attach the matching library logs.
|
||||
|
||||
1. To enable them, go to Settings -> Advanced and toggle `Debug Mode`. If they are already enabled, clear them first using the `Reset logs` button on the About page.
|
||||
|
||||
2. Then restart the app, reproduce the issue and upload the logs using the `Send logs` button on the advanced settings page.
|
||||
|
||||
3. Finally paste the link to the uploaded logs (link is already in the clipboard after a successful upload).
|
||||
Starting 6.0.0 release, logs are always enabled and stored locally on the device, you can clear them/upload them securely on our server for sharing by going into the Help → Troubleshooting page.
|
||||
|
||||
### Native crash
|
||||
|
||||
|
|
@ -120,16 +114,13 @@ First of all, to be able to get a symbolized stack trace, you need the debug ver
|
|||
|
||||
If you haven't built the SDK locally (see [building a local SDK](#BuildingalocalSDK)), here's how to get them:
|
||||
|
||||
1. Go to our [maven repository](https://download.linphone.org/maven_repository/org/linphone/linphone-sdk-android-debug/), in the linphone-android-debug directory.
|
||||
1. Go to our [maven repository](https://download.linphone.org/maven_repository/org/linphone/linphone-sdk-android/) and find the directory that matches the version of our SDK that crashed.
|
||||
|
||||
2. Download the AAR file with **the exact same version** as the AAR that was used to generate the crash's stacktrace.
|
||||
2. Download the linphone-sdk-android-<version>-libs-debug.zip archive.
|
||||
|
||||
3. Extract the AAR somewhere on your computer (it's a simple ZIP file even it's doesn't have the extension). Libraries are stored inside the ```jni``` folder (a directory for each architectured built, usually ```arm64-v8a, armeabi-v7a, x86_64 and x86```).
|
||||
|
||||
4. To get consistent with locally built SDK, rename the ```jni``` directory into ```libs-debug```.
|
||||
3. Extract the symbolized libraries somewhere on your computer, it will create a ```libs-debug``` directory.
|
||||
|
||||
Now you need the ```ndk-stack``` tool and possibly ```adb logcat```.
|
||||
|
||||
If your computer isn't used for Android development, you can download those tools from [Google website](https://developer.android.com/studio#downloads), in the ```Command line tools only``` section.
|
||||
|
||||
Once you have the debug libraries and the proper tools installed, you can use the ```ndk-stack``` tool to symbolize your stacktrace. Note that you also need to know the architecture (armv7, arm64, x86, etc...) of the libraries that were used.
|
||||
|
|
@ -144,14 +135,22 @@ adb logcat -d | ndk-stack -sym ./libs-debug/`adb shell getprop ro.product.cpu.ab
|
|||
```
|
||||
Warning: This command won't print anything until you reproduce the crash!
|
||||
|
||||
Starting [NDK r29](https://github.com/android/ndk/wiki/Changelog-r29) you will be able to directly use the ```libs-debug.zip``` file in ```ndk-stack -sym``` argument.
|
||||
|
||||
## Create an APK with a different package name
|
||||
|
||||
Before the 4.1 release, there were a lot of files to edit to change the package name.
|
||||
Now, simply edit the app/build.gradle file and change the value returned by method ```getPackageName()```
|
||||
Simply edit the ```app/build.gradle.kts``` file and change the value of the ```packageName``` variable.
|
||||
The next build will automatically use this value everywhere thanks to ```manifestPlaceholders``` feature of gradle and Android.
|
||||
|
||||
You may have already noticed that the app installed by Android Studio has ```org.linphone.debug``` package name.
|
||||
If you build the app as release, the package name will be ```org.linphone```.
|
||||
We no longer build the debug flavor with a different package name, but if you still want that behavior you only have to change the value of ```useDifferentPackageNameForDebugBuild``` to ```true```. When enabled, app built and installed by Android studio will have ```org.linphone.debug``` package name instead of ```org.linphone```.
|
||||
|
||||
If you encounter
|
||||
```
|
||||
Execution failed for task ':app:processDebugGoogleServices'.
|
||||
> No matching client found for package name 'your package name'
|
||||
```
|
||||
error when building, make sure you have replaced the ```app/google-services.json``` file by yours (containing your package name).
|
||||
If you don't have such file because you don't rely on Firebase Cloud Messaging features nor Crashlytics, delete the file instead.
|
||||
|
||||
## Firebase push notifications
|
||||
|
||||
|
|
@ -169,6 +168,10 @@ We no longer use transifex for the translation process, instead we have deployed
|
|||
|
||||
Due to the full app rewrite we can't re-use previous translations, so we'll be very happy if you want to contribute.
|
||||
|
||||
<a href="https://weblate.linphone.org/engage/linphone/">
|
||||
<img src="https://weblate.linphone.org/widget/linphone/linphone-android-6-0/multi-auto.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
# CONTRIBUTIONS
|
||||
|
||||
In order to submit a patch for inclusion in linphone's source code:
|
||||
|
|
|
|||
277
app/build.gradle
277
app/build.gradle
|
|
@ -1,277 +0,0 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'org.jlleitschuh.gradle.ktlint'
|
||||
}
|
||||
|
||||
def appVersionName = "4.7.0"
|
||||
// def appVersionCode = 40700
|
||||
def appVersionCode = 40700
|
||||
|
||||
static def getPackageName() {
|
||||
return "org.linphone"
|
||||
}
|
||||
|
||||
def firebaseEnabled = new File(projectDir.absolutePath +'/google-services.json').exists()
|
||||
|
||||
def crashlyticsEnabled = new File(projectDir.absolutePath +'/google-services.json').exists() && new File(LinphoneSdkBuildDir + '/libs/').exists() && new File(LinphoneSdkBuildDir + '/libs-debug/').exists()
|
||||
|
||||
|
||||
if (firebaseEnabled) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
|
||||
if (crashlyticsEnabled) {
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
}
|
||||
|
||||
def gitBranch = new ByteArrayOutputStream()
|
||||
task getGitVersion() {
|
||||
def gitVersion = appVersionName
|
||||
def gitVersionStream = new ByteArrayOutputStream()
|
||||
def gitCommitsCount = new ByteArrayOutputStream()
|
||||
def gitCommitHash = new ByteArrayOutputStream()
|
||||
|
||||
try {
|
||||
exec {
|
||||
executable "git" args "describe", "--abbrev=0"
|
||||
standardOutput = gitVersionStream
|
||||
}
|
||||
exec {
|
||||
executable "git" args "rev-list", gitVersionStream.toString().trim() + "..HEAD", "--count"
|
||||
standardOutput = gitCommitsCount
|
||||
}
|
||||
exec {
|
||||
executable "git" args "rev-parse", "--short", "HEAD"
|
||||
standardOutput = gitCommitHash
|
||||
}
|
||||
exec {
|
||||
executable "git" args "name-rev", "--name-only", "HEAD"
|
||||
standardOutput = gitBranch
|
||||
}
|
||||
|
||||
if (gitCommitsCount.toString().toInteger() == 0) {
|
||||
gitVersion = gitVersionStream.toString().trim()
|
||||
} else {
|
||||
gitVersion = gitVersionStream.toString().trim() + "." + gitCommitsCount.toString().trim() + "+" + gitCommitHash.toString().trim()
|
||||
}
|
||||
println("Git version: " + gitVersion + " (" + appVersionCode + ")")
|
||||
} catch (ignored) {
|
||||
println("Git not found, using " + gitVersion + " (" + appVersionCode + ")")
|
||||
}
|
||||
project.version = gitVersion
|
||||
}
|
||||
|
||||
configurations {
|
||||
customImplementation.extendsFrom implementation
|
||||
}
|
||||
|
||||
task linphoneSdkSource() {
|
||||
doLast {
|
||||
configurations.customImplementation.getIncoming().each {
|
||||
it.getResolutionResult().allComponents.each {
|
||||
if (it.id.getDisplayName().contains("linphone-sdk-android")) {
|
||||
println 'Linphone SDK used is ' + it.moduleVersion.version + ' from ' + it.properties["repositoryName"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.tasks['preBuild'].dependsOn 'getGitVersion'
|
||||
project.tasks['preBuild'].dependsOn 'linphoneSdkSource'
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion '33.0.0'
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
versionCode appVersionCode
|
||||
versionName "${project.version}"
|
||||
applicationId getPackageName()
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.all {
|
||||
outputFileName = "linphone-android-${variant.buildType.name}-${project.version}.apk"
|
||||
}
|
||||
|
||||
var enableFirebaseService = "false"
|
||||
if (firebaseEnabled) {
|
||||
enableFirebaseService = "true"
|
||||
}
|
||||
// See https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for why extractNativeLibs is set to true in debug flavor
|
||||
if (variant.buildType.name == "release" || variant.buildType.name == "releaseWithCrashlytics") {
|
||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
||||
linphone_file_provider: getPackageName() + ".fileprovider",
|
||||
appLabel: "@string/app_name",
|
||||
firebaseServiceEnabled: enableFirebaseService,
|
||||
extractNativeLibs: "false"]
|
||||
} else {
|
||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
||||
linphone_file_provider: getPackageName() + ".debug.fileprovider",
|
||||
appLabel: "@string/app_name_debug",
|
||||
firebaseServiceEnabled: enableFirebaseService,
|
||||
extractNativeLibs: "true"]
|
||||
}
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
signingConfig signingConfigs.release
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
resValue "string", "linphone_app_branch", gitBranch.toString().trim()
|
||||
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
||||
resValue "string", "file_provider", getPackageName() + ".fileprovider"
|
||||
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
||||
|
||||
if (!firebaseEnabled) {
|
||||
resValue "string", "gcm_defaultSenderId", "none"
|
||||
}
|
||||
|
||||
resValue "bool", "crashlytics_enabled", "false"
|
||||
}
|
||||
|
||||
releaseWithCrashlytics {
|
||||
initWith release
|
||||
|
||||
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
|
||||
|
||||
if (crashlyticsEnabled) {
|
||||
firebaseCrashlytics {
|
||||
nativeSymbolUploadEnabled true
|
||||
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
debuggable true
|
||||
jniDebuggable true
|
||||
|
||||
resValue "string", "linphone_app_branch", gitBranch.toString().trim()
|
||||
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
||||
resValue "string", "file_provider", getPackageName() + ".debug.fileprovider"
|
||||
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
||||
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
|
||||
|
||||
if (!firebaseEnabled) {
|
||||
resValue "string", "gcm_defaultSenderId", "none"
|
||||
}
|
||||
|
||||
if (crashlyticsEnabled) {
|
||||
firebaseCrashlytics {
|
||||
nativeSymbolUploadEnabled true
|
||||
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
}
|
||||
|
||||
namespace 'org.linphone'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha03"
|
||||
implementation "androidx.window:window:1.0.0"
|
||||
|
||||
def nav_version = "2.5.2"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
|
||||
|
||||
// https://github.com/material-components/material-components-android/blob/master/LICENSE Apache v2.0
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
// https://github.com/google/flexbox-layout/blob/main/LICENSE Apache v2.0
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
|
||||
implementation 'androidx.emoji:emoji:1.1.0'
|
||||
implementation 'androidx.emoji:emoji-bundled:1.1.0'
|
||||
|
||||
// https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0
|
||||
def coil_version = "2.1.0"
|
||||
implementation("io.coil-kt:coil:$coil_version")
|
||||
implementation("io.coil-kt:coil-gif:$coil_version")
|
||||
implementation("io.coil-kt:coil-svg:$coil_version")
|
||||
implementation("io.coil-kt:coil-video:$coil_version")
|
||||
|
||||
// https://github.com/Baseflow/PhotoView/blob/master/LICENSE Apache v2.0
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
|
||||
implementation platform('com.google.firebase:firebase-bom:30.3.2')
|
||||
if (crashlyticsEnabled) {
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ndk'
|
||||
} else {
|
||||
compileOnly 'com.google.firebase:firebase-crashlytics-ndk'
|
||||
}
|
||||
if (firebaseEnabled) {
|
||||
implementation 'com.google.firebase:firebase-messaging'
|
||||
}
|
||||
|
||||
implementation 'org.linphone:linphone-sdk-android:5.2+'
|
||||
|
||||
// Only enable leak canary prior to release
|
||||
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||
}
|
||||
|
||||
task generateContactsXml(type: Copy) {
|
||||
from 'contacts.xml'
|
||||
into "src/main/res/xml/"
|
||||
outputs.upToDateWhen { file('src/main/res/xml/contacts.xml').exists() }
|
||||
filter {
|
||||
line -> line
|
||||
.replaceAll('%%AUTO_GENERATED%%', 'This file has been automatically generated, do not edit or commit !')
|
||||
.replaceAll('%%PACKAGE_NAME%%', getPackageName())
|
||||
|
||||
}
|
||||
}
|
||||
project.tasks['preBuild'].dependsOn 'generateContactsXml'
|
||||
|
||||
ktlint {
|
||||
android = true
|
||||
ignoreFailures = true
|
||||
}
|
||||
|
||||
project.tasks['preBuild'].dependsOn 'ktlintFormat'
|
||||
|
||||
if (crashlyticsEnabled) {
|
||||
afterEvaluate {
|
||||
assembleDebug.finalizedBy(uploadCrashlyticsSymbolFileDebug)
|
||||
packageDebugBundle.finalizedBy(uploadCrashlyticsSymbolFileDebug)
|
||||
|
||||
assembleReleaseWithCrashlytics.finalizedBy(uploadCrashlyticsSymbolFileReleaseWithCrashlytics)
|
||||
packageReleaseWithCrashlytics.finalizedBy(uploadCrashlyticsSymbolFileReleaseWithCrashlytics)
|
||||
}
|
||||
}
|
||||
319
app/build.gradle.kts
Normal file
319
app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
import com.android.build.gradle.internal.tasks.factory.dependsOn
|
||||
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
|
||||
import com.google.gms.googleservices.GoogleServicesPlugin
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.kapt)
|
||||
alias(libs.plugins.ktlint)
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
alias(libs.plugins.navigation)
|
||||
alias(libs.plugins.crashlytics)
|
||||
}
|
||||
|
||||
val packageName = "org.linphone"
|
||||
val useDifferentPackageNameForDebugBuild = false
|
||||
|
||||
val sdkPath = providers.gradleProperty("LinphoneSdkBuildDir").get()
|
||||
val googleServices = File(projectDir.absolutePath + "/google-services.json")
|
||||
val linphoneLibs = File("$sdkPath/libs/")
|
||||
val linphoneDebugLibs = File("$sdkPath/libs-debug/")
|
||||
val firebaseCloudMessagingAvailable = googleServices.exists()
|
||||
val crashlyticsAvailable = googleServices.exists() && linphoneLibs.exists() && linphoneDebugLibs.exists()
|
||||
|
||||
if (firebaseCloudMessagingAvailable) {
|
||||
println("google-services.json found, enabling CloudMessaging feature")
|
||||
apply<GoogleServicesPlugin>()
|
||||
} else {
|
||||
println("google-services.json not found, disabling CloudMessaging feature")
|
||||
}
|
||||
|
||||
var gitVersion = "6.1.0-alpha"
|
||||
var gitBranch = ""
|
||||
try {
|
||||
val gitDescribe = ProcessBuilder()
|
||||
.command("git", "describe", "--abbrev=0")
|
||||
.directory(project.rootDir)
|
||||
.start()
|
||||
.inputStream.bufferedReader().use(BufferedReader::readText)
|
||||
.trim()
|
||||
println("Git describe: $gitDescribe")
|
||||
|
||||
val gitCommitsCount = ProcessBuilder()
|
||||
.command("git", "rev-list", "$gitDescribe..HEAD", "--count")
|
||||
.directory(project.rootDir)
|
||||
.start()
|
||||
.inputStream.bufferedReader().use(BufferedReader::readText)
|
||||
.trim()
|
||||
println("Git commits count: $gitCommitsCount")
|
||||
|
||||
val gitCommitHash = ProcessBuilder()
|
||||
.command("git", "rev-parse", "--short", "HEAD")
|
||||
.directory(project.rootDir)
|
||||
.start()
|
||||
.inputStream.bufferedReader().use(BufferedReader::readText)
|
||||
.trim()
|
||||
println("Git commit hash: $gitCommitHash")
|
||||
|
||||
gitBranch = ProcessBuilder()
|
||||
.command("git", "name-rev", "--name-only", "HEAD")
|
||||
.directory(project.rootDir)
|
||||
.start()
|
||||
.inputStream.bufferedReader().use(BufferedReader::readText)
|
||||
.trim()
|
||||
println("Git branch name: $gitBranch")
|
||||
|
||||
gitVersion =
|
||||
if (gitCommitsCount.toInt() == 0) {
|
||||
gitDescribe
|
||||
} else {
|
||||
"$gitDescribe.$gitCommitsCount+$gitCommitHash"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Git not found [$e], using $gitVersion")
|
||||
}
|
||||
println("Computed git version: $gitVersion")
|
||||
|
||||
configurations {
|
||||
implementation { isCanBeResolved = true }
|
||||
}
|
||||
|
||||
tasks.register("linphoneSdkSource") {
|
||||
doLast {
|
||||
configurations.implementation.get().incoming.resolutionResult.allComponents.forEach {
|
||||
if (it.id.displayName.contains("linphone-sdk-android")) {
|
||||
println("Linphone SDK used is ${it.moduleVersion?.version}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
project.tasks.preBuild.dependsOn("linphoneSdkSource")
|
||||
|
||||
android {
|
||||
namespace = "org.linphone"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = packageName
|
||||
minSdk = 28
|
||||
targetSdk = 36
|
||||
versionCode = 601002 // 6.01.002
|
||||
versionName = "6.1.0-alpha"
|
||||
|
||||
manifestPlaceholders["appAuthRedirectScheme"] = packageName
|
||||
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
val variant = this
|
||||
variant.outputs
|
||||
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
output.outputFileName = "linphone-android-${variant.buildType.name}-${project.version}.apk"
|
||||
}
|
||||
}
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
val keystoreProperties = Properties()
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
val keyStorePath = keystoreProperties["storeFile"] as String
|
||||
val keyStore = project.file(keyStorePath)
|
||||
if (keyStore.exists()) {
|
||||
storeFile = keyStore
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
println("Signing config release is using keystore [$storeFile]")
|
||||
} else {
|
||||
println("Keystore [$storeFile] doesn't exists!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
if (useDifferentPackageNameForDebugBuild) {
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
|
||||
val appVersion = gitVersion
|
||||
val appBranch = gitBranch
|
||||
println("Setting app version [$appVersion] app branch [$appBranch]")
|
||||
resValue("string", "linphone_app_version", appVersion)
|
||||
resValue("string", "linphone_app_branch", appBranch)
|
||||
if (useDifferentPackageNameForDebugBuild) {
|
||||
resValue("string", "file_provider", "$packageName.debug.fileprovider")
|
||||
} else {
|
||||
resValue("string", "file_provider", "$packageName.fileprovider")
|
||||
}
|
||||
resValue("string", "linphone_openid_callback_scheme", packageName)
|
||||
|
||||
if (crashlyticsAvailable) {
|
||||
val path = File("$sdkPath/libs-debug/").toString()
|
||||
configure<CrashlyticsExtension> {
|
||||
nativeSymbolUploadEnabled = true
|
||||
unstrippedNativeLibsDir = path
|
||||
}
|
||||
}
|
||||
buildConfigField("Boolean", "CRASHLYTICS_ENABLED", crashlyticsAvailable.toString())
|
||||
}
|
||||
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
|
||||
val appVersion = gitVersion
|
||||
val appBranch = gitBranch
|
||||
println("Setting app version [$appVersion] app branch [$appBranch]")
|
||||
resValue("string", "linphone_app_version", appVersion)
|
||||
resValue("string", "linphone_app_branch", appBranch)
|
||||
resValue("string", "file_provider", "$packageName.fileprovider")
|
||||
resValue("string", "linphone_openid_callback_scheme", packageName)
|
||||
|
||||
if (crashlyticsAvailable) {
|
||||
val path = File("$sdkPath/libs-debug/").toString()
|
||||
configure<CrashlyticsExtension> {
|
||||
nativeSymbolUploadEnabled = true
|
||||
unstrippedNativeLibsDir = path
|
||||
}
|
||||
}
|
||||
buildConfigField("Boolean", "CRASHLYTICS_ENABLED", crashlyticsAvailable.toString())
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.annotations)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.constraint.layout)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.splashscreen)
|
||||
implementation(libs.androidx.telecom)
|
||||
implementation(libs.androidx.media)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
implementation(libs.androidx.swiperefreshlayout)
|
||||
implementation(libs.androidx.slidingpanelayout)
|
||||
implementation(libs.androidx.window)
|
||||
implementation(libs.androidx.gridlayout)
|
||||
implementation(libs.androidx.security.crypto.ktx)
|
||||
implementation(libs.androidx.navigation.fragment.ktx)
|
||||
implementation(libs.androidx.navigation.ui.ktx)
|
||||
implementation(libs.androidx.emoji2)
|
||||
implementation(libs.androidx.car)
|
||||
|
||||
// https://github.com/google/flexbox-layout/blob/main/LICENSE Apache v2.0
|
||||
implementation(libs.google.flexbox)
|
||||
// https://github.com/material-components/material-components-android/blob/master/LICENSE Apache v2.0
|
||||
implementation(libs.google.material)
|
||||
// To be able to parse native crash tombstone and print them with SDK logs the next time the app will start
|
||||
implementation(libs.google.protobuf)
|
||||
|
||||
implementation(platform(libs.google.firebase.bom))
|
||||
implementation(libs.google.firebase.messaging)
|
||||
implementation(libs.google.firebase.crashlytics)
|
||||
|
||||
// https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0
|
||||
implementation(libs.coil)
|
||||
implementation(libs.coil.gif)
|
||||
implementation(libs.coil.svg)
|
||||
implementation(libs.coil.video)
|
||||
// https://github.com/tommybuonomo/dotsindicator/blob/master/LICENSE Apache v2.0
|
||||
implementation(libs.dots.indicator)
|
||||
// https://github.com/Baseflow/PhotoView/blob/master/LICENSE Apache v2.0
|
||||
implementation(libs.photoview)
|
||||
// https://github.com/openid/AppAuth-Android/blob/master/LICENSE Apache v2.0
|
||||
implementation(libs.openid.appauth)
|
||||
|
||||
implementation(libs.linphone)
|
||||
}
|
||||
|
||||
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
|
||||
android.set(true)
|
||||
ignoreFailures.set(true)
|
||||
additionalEditorconfig.set(
|
||||
mapOf(
|
||||
"max_line_length" to "120",
|
||||
"ktlint_standard_max-line-length" to "disabled",
|
||||
"ktlint_standard_function-signature" to "disabled",
|
||||
"ktlint_standard_no-blank-line-before-rbrace" to "disabled",
|
||||
"ktlint_standard_no-empty-class-body" to "disabled",
|
||||
"ktlint_standard_annotation-spacing" to "disabled",
|
||||
"ktlint_standard_class-signature" to "disabled",
|
||||
"ktlint_standard_function-expression-body" to "disabled",
|
||||
"ktlint_standard_function-type-modifier-spacing" to "disabled",
|
||||
"ktlint_standard_if-else-wrapping" to "disabled",
|
||||
"ktlint_standard_argument-list-wrapping" to "disabled",
|
||||
"ktlint_standard_trailing-comma-on-call-site" to "disabled",
|
||||
"ktlint_standard_trailing-comma-on-declaration-site" to "disabled",
|
||||
"ktlint_standard_no-empty-first-line-in-class-body" to "disabled",
|
||||
"ktlint_standard_no-empty-first-line-in-method-block" to "disabled",
|
||||
"ktlint_standard_no-trailing-spaces" to "disabled",
|
||||
"ktlint_standard_no-blank-line-in-list" to "disabled",
|
||||
"ktlint_standard_no-multi-spaces" to "disabled",
|
||||
"ktlint_standard_try-catch-finally-spacing" to "disabled",
|
||||
"ktlint_standard_block-comment-initial-star-alignment" to "disabled",
|
||||
"ktlint_standard_spacing-between-declarations-with-comments" to "disabled",
|
||||
"ktlint_standard_no-consecutive-comments" to "disabled",
|
||||
"ktlint_standard_multiline-expression-wrapping" to "disabled",
|
||||
"ktlint_standard_parameter-list-wrapping" to "disabled",
|
||||
"ktlint_standard_comment-wrapping" to "disabled",
|
||||
"ktlint_standard_discouraged-comment-location" to "disabled",
|
||||
"ktlint_standard_string-template-indent" to "disabled",
|
||||
"ktlint_standard_parameter-list-spacing" to "disabled",
|
||||
"ktlint_standard_statement-wrapping" to "disabled",
|
||||
"ktlint_standard_import-ordering" to "disabled",
|
||||
"ktlint_standard_paren-spacing" to "disabled",
|
||||
"ktlint_standard_curly-spacing" to "disabled",
|
||||
"ktlint_standard_indent" to "disabled",
|
||||
)
|
||||
)
|
||||
}
|
||||
project.tasks.preBuild.dependsOn("ktlintFormat")
|
||||
|
||||
if (crashlyticsAvailable) {
|
||||
afterEvaluate {
|
||||
tasks.getByName("assembleDebug").finalizedBy(
|
||||
tasks.getByName("uploadCrashlyticsSymbolFileDebug"),
|
||||
)
|
||||
tasks.getByName("packageDebug").finalizedBy(
|
||||
tasks.getByName("uploadCrashlyticsSymbolFileDebug"),
|
||||
)
|
||||
tasks.getByName("assembleRelease").finalizedBy(
|
||||
tasks.getByName("uploadCrashlyticsSymbolFileRelease"),
|
||||
)
|
||||
tasks.getByName("packageRelease").finalizedBy(
|
||||
tasks.getByName("uploadCrashlyticsSymbolFileRelease"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ContactsSource xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- %%AUTO_GENERATED%% -->
|
||||
<ContactsDataKind
|
||||
android:detailColumn="data3"
|
||||
android:detailSocialSummary="true"
|
||||
android:icon="@drawable/linphone_logo_tinted"
|
||||
android:mimeType="vnd.android.cursor.item/vnd.%%PACKAGE_NAME%%.provider.sip_address"
|
||||
android:summaryColumn="data2" />
|
||||
<!-- You can't use @string/linphone_address_mime_type above ! You have to hardcode it... -->
|
||||
</ContactsSource>
|
||||
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
|
|
@ -1,6 +1,6 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
|
@ -19,6 +19,3 @@
|
|||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep public class * extends androidx.fragment.app.Fragment { *; }
|
||||
-dontwarn com.google.errorprone.annotations.Immutable
|
||||
|
|
|
|||
|
|
@ -1,84 +1,87 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<!-- To be able to display contacts list & match calling/called numbers -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<!-- For in-app contact edition -->
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||
|
||||
<!-- Helps filling phone number and country code in assistant -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
<!-- Needed for auto start at boot and to ensure the service won't be killed by OS while in call -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- Starting Android 13 we need to ask notification permission -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- Needed for full screen intent in incoming call notifications -->
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<!-- To vibrate when pressing DTMF keys on numpad & incoming calls -->
|
||||
<!-- To vibrate while receiving an incoming call -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- Needed to attach file(s) in chat room fragment -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"/>
|
||||
<!-- Starting Android 13 you need those 3 permissions instead (https://developer.android.com/about/versions/13/behavior-changes-13) -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<!-- Needed to shared downloaded files if setting is on -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
<!-- Both permissions below are for contacts sync account, needed to store presence in native contact if enabled -->
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
|
||||
<!-- Needed for Telecom Manager -->
|
||||
<!-- Needed for foreground service
|
||||
(https://developer.android.com/guide/components/foreground-services) -->
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<!-- Needed for Android 14
|
||||
https://developer.android.com/about/versions/14/behavior-changes-14#fgs-types -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
||||
<!-- Required for foreground service started when a push is being received,
|
||||
without it app won't be able to access network if data saver is ON (for example) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<!-- Needed to keep a permanent foreground service and keep app alive to be able to receive
|
||||
messages & calls for third party accounts for which push notifications aren't available,
|
||||
and starting Android 15 dataSync is limited to 6 hours per day
|
||||
and can't be used with RECEIVE_BOOT_COMPLETED intent either -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
|
||||
<!-- Needed for overlay -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<!-- Needed to check current Do not disturb policy -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||
|
||||
<!-- Needed starting Android 12 for broadcast receiver
|
||||
to be triggered when BT device is connected / disconnected
|
||||
(https://developer.android.com/guide/topics/connectivity/bluetooth/permissions) -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<!-- Needed for auto start at boot if keep alive service is enabled -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application
|
||||
android:name=".LinphoneApplication"
|
||||
android:allowBackup="false"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="${appLabel}"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:extractNativeLibs="${extractNativeLibs}"
|
||||
android:theme="@style/AppTheme"
|
||||
android:allowNativeHeapPointerTagging="false">
|
||||
android:label="@string/app_name"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:theme="@style/Theme.Linphone"
|
||||
android:appCategory="social"
|
||||
android:largeHeap="true"
|
||||
tools:targetApi="35">
|
||||
|
||||
<activity android:name=".activities.main.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
<!-- Required for chat message & call notifications to be displayed in Android auto -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc"/>
|
||||
|
||||
<!--<meta-data
|
||||
android:name="androidx.car.app.minCarApiLevel"
|
||||
android:value="1"/>-->
|
||||
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:theme="@style/AppSplashScreenTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/AppSplashScreenTheme">
|
||||
<nav-graph android:value="@navigation/main_nav_graph" />
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW_LOCUS" />
|
||||
</intent-filter>
|
||||
|
|
@ -103,63 +106,83 @@
|
|||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="${linphone_address_mime_type}" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.DIAL" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.CALL" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="tel" />
|
||||
<data android:scheme="sip" />
|
||||
<data android:scheme="callto" />
|
||||
<data android:scheme="sips" />
|
||||
<data android:scheme="linphone" />
|
||||
<data android:scheme="sip-linphone" />
|
||||
<data android:scheme="sips-linphone" />
|
||||
<data android:scheme="linphone-sip" />
|
||||
<data android:scheme="linphone-sips" />
|
||||
<data android:scheme="linphone-config" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="sms" />
|
||||
<data android:scheme="smsto" />
|
||||
<data android:scheme="mms" />
|
||||
<data android:scheme="mmsto" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".activities.assistant.AssistantActivity"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
|
||||
<activity android:name=".activities.voip.CallActivity"
|
||||
<activity
|
||||
android:name=".ui.welcome.WelcomeActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.assistant.AssistantActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.fileviewer.MediaViewerActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.fileviewer.FileViewerActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.call.CallActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.LinphoneInCall"
|
||||
android:launchMode="singleInstance"
|
||||
android:turnScreenOn="true"
|
||||
android:showWhenLocked="true"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.chat_bubble.ChatBubbleActivity"
|
||||
android:allowEmbedded="true"
|
||||
android:documentLaunchMode="always"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
<service
|
||||
android:name=".core.CoreService"
|
||||
android:name=".core.CoreInCallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="phoneCall|camera|microphone"
|
||||
android:stopWithTask="false"
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<service
|
||||
android:name=".core.CorePushService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="false"
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<service
|
||||
android:name=".core.CoreFileTransferService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="false"
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<service android:name="org.linphone.core.tools.firebase.FirebaseMessaging"
|
||||
android:enabled="${firebaseServiceEnabled}"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
|
|
@ -167,38 +190,24 @@
|
|||
</service>
|
||||
|
||||
<service
|
||||
android:name=".contact.DummySyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_adapter" />
|
||||
<meta-data
|
||||
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||
android:resource="@xml/contacts" />
|
||||
android:name=".core.CoreKeepAliveThirdPartyAccountsService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:stopWithTask="false"
|
||||
android:label="@string/app_name">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Needed to keep app alive to be able to receive messages and calls from third party SIP servers for which push notifications aren't available." />
|
||||
</service>
|
||||
|
||||
<service android:name=".contact.DummyAuthenticationService"
|
||||
<!--<service
|
||||
android:name=".telecom.auto.AndroidAutoService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
<action android:name="androidx.car.app.CarAppService"/>
|
||||
<category android:name="androidx.car.app.category.CALLING"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/authenticator" />
|
||||
</service>
|
||||
|
||||
<service android:name=".telecom.TelecomConnectionService"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</service>-->
|
||||
|
||||
<!-- Receivers -->
|
||||
|
||||
|
|
@ -226,7 +235,7 @@
|
|||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${linphone_file_provider}"
|
||||
android:authorities="@string/file_provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
<section name="proxy_default_values">
|
||||
<entry name="avpf" overwrite="true">1</entry>
|
||||
<entry name="dial_escape_plus" overwrite="true">0</entry>
|
||||
<entry name="publish" overwrite="true">0</entry>
|
||||
<entry name="publish" overwrite="true">1</entry>
|
||||
<entry name="publish_expires" overwrite="true">120</entry>
|
||||
<entry name="quality_reporting_collector" overwrite="true">sip:voip-metrics@sip.linphone.org;transport=tls</entry>
|
||||
<entry name="quality_reporting_enabled" overwrite="true">1</entry>
|
||||
<entry name="quality_reporting_interval" overwrite="true">180</entry>
|
||||
|
|
@ -20,22 +21,15 @@
|
|||
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">1</entry>
|
||||
<entry name="rtp_bundle" overwrite="true">1</entry>
|
||||
<entry name="lime_server_url" overwrite="true">https://lime.linphone.org/lime-server/lime-server.php</entry>
|
||||
<entry name="lime_algo" overwrite="true">c25519</entry>
|
||||
<entry name="supported" overwrite="true"></entry>
|
||||
</section>
|
||||
<section name="nat_policy_default_values">
|
||||
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>
|
||||
<entry name="protocols" overwrite="true">stun,ice</entry>
|
||||
</section>
|
||||
<section name="net">
|
||||
<entry name="friendlist_subscription_enabled" overwrite="true">1</entry>
|
||||
</section>
|
||||
<section name="assistant">
|
||||
<entry name="domain" overwrite="true">sip.linphone.org</entry>
|
||||
<entry name="algorithm" overwrite="true">SHA-256</entry>
|
||||
<entry name="password_max_length" overwrite="true">-1</entry>
|
||||
<entry name="password_min_length" overwrite="true">1</entry>
|
||||
<entry name="username_length" overwrite="true">-1</entry>
|
||||
<entry name="username_max_length" overwrite="true">64</entry>
|
||||
<entry name="username_min_length" overwrite="true">1</entry>
|
||||
<entry name="username_regex" overwrite="true">^[a-z0-9+_.\-]*$</entry>
|
||||
<section name="sip">
|
||||
<entry name="media_encryption" overwrite="true">srtp</entry>
|
||||
<entry name="media_encryption_mandatory">1</entry>
|
||||
</section>
|
||||
</config>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<entry name="avpf" overwrite="true">0</entry>
|
||||
<entry name="dial_escape_plus" overwrite="true">0</entry>
|
||||
<entry name="publish" overwrite="true">0</entry>
|
||||
<entry name="publish_expires" overwrite="true">-1</entry>
|
||||
<entry name="quality_reporting_collector" overwrite="true"></entry>
|
||||
<entry name="quality_reporting_enabled" overwrite="true">0</entry>
|
||||
<entry name="quality_reporting_interval" overwrite="true">0</entry>
|
||||
|
|
@ -20,22 +21,18 @@
|
|||
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">0</entry>
|
||||
<entry name="rtp_bundle" overwrite="true">0</entry>
|
||||
<entry name="lime_server_url" overwrite="true"></entry>
|
||||
<entry name="lime_algo" overwrite="true"></entry>
|
||||
<entry name="supported" overwrite="true">outbound</entry>
|
||||
</section>
|
||||
<section name="nat_policy_default_values">
|
||||
<entry name="stun_server" overwrite="true"></entry>
|
||||
<entry name="protocols" overwrite="true"></entry>
|
||||
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>
|
||||
<entry name="protocols" overwrite="true">stun,ice</entry>
|
||||
</section>
|
||||
<section name="net">
|
||||
<entry name="friendlist_subscription_enabled" overwrite="true">0</entry>
|
||||
<section name="sip">
|
||||
<entry name="media_encryption">srtp</entry>
|
||||
<entry name="media_encryption_mandatory" overwrite="true">0</entry>
|
||||
</section>
|
||||
<section name="assistant">
|
||||
<entry name="domain" overwrite="true"></entry>
|
||||
<entry name="algorithm" overwrite="true">MD5</entry>
|
||||
<entry name="password_max_length" overwrite="true">-1</entry>
|
||||
<entry name="password_min_length" overwrite="true">0</entry>
|
||||
<entry name="username_length" overwrite="true">-1</entry>
|
||||
<entry name="username_max_length" overwrite="true">128</entry>
|
||||
<entry name="username_min_length" overwrite="true">1</entry>
|
||||
<entry name="username_regex" overwrite="true">^[a-zA-Z0-9+_.\-]*$</entry>
|
||||
<section name="ui">
|
||||
<entry name="automatically_show_dialpad" overwrite="true">1</entry>
|
||||
</section>
|
||||
</config>
|
||||
|
|
@ -10,6 +10,10 @@ sip_port=-1
|
|||
sip_tcp_port=-1
|
||||
sip_tls_port=-1
|
||||
media_encryption=none
|
||||
update_presence_model_timestamp_before_publish_expires_refresh=1
|
||||
use_rfc2833=1
|
||||
use_info=1
|
||||
rls_uri=sips:rls@sip.linphone.org
|
||||
|
||||
[net]
|
||||
#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit"
|
||||
|
|
@ -18,26 +22,38 @@ upload_bw=0
|
|||
|
||||
[video]
|
||||
size=vga
|
||||
automatically_accept=1
|
||||
automatically_initiate=0
|
||||
automatically_accept_direction=2 #receive only
|
||||
|
||||
[app]
|
||||
tunnel=disabled
|
||||
auto_start=1
|
||||
record_aware=1
|
||||
auto_download_incoming_voice_recordings=1
|
||||
auto_download_incoming_icalendars=1
|
||||
|
||||
[tunnel]
|
||||
host=
|
||||
port=443
|
||||
|
||||
[misc]
|
||||
log_collection_upload_server_url=https://www.linphone.org:444/lft.php
|
||||
file_transfer_server_url=https://www.linphone.org:444/lft.php
|
||||
version_check_url_root=https://www.linphone.org/releases
|
||||
log_collection_upload_server_url=https://files.linphone.org/http-file-transfer-server/hft.php
|
||||
file_transfer_server_url=https://files.linphone.org/http-file-transfer-server/hft.php
|
||||
version_check_url_root=https://download.linphone.org/releases
|
||||
max_calls=10
|
||||
history_max_size=100
|
||||
conference_layout=1
|
||||
hide_empty_chat_rooms=1
|
||||
|
||||
[in-app-purchase]
|
||||
server_url=https://subscribe.linphone.org:444/inapp.php
|
||||
purchasable_items_ids=test_account_subscription
|
||||
[fec]
|
||||
fec_enabled=1
|
||||
|
||||
[magic_search]
|
||||
return_empty_friends=1
|
||||
|
||||
[chat]
|
||||
imdn_to_everybody_threshold=1
|
||||
|
||||
[ui]
|
||||
contacts_filter=sip.linphone.org
|
||||
|
||||
## End of default rc
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
mtu=1300
|
||||
force_ice_disablement=0
|
||||
|
||||
[rtp]
|
||||
accept_any_encryption=1
|
||||
|
||||
[sip]
|
||||
guess_hostname=1
|
||||
register_only_when_network_is_up=1
|
||||
|
|
@ -15,37 +18,42 @@ auto_net_state_mon=1
|
|||
auto_answer_replacing_calls=1
|
||||
ping_with_options=0
|
||||
use_cpim=1
|
||||
zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_KYB512
|
||||
zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_MLK512,MS_ZRTP_KEY_AGREEMENT_K255_KYB512
|
||||
chat_messages_aggregation_delay=1000
|
||||
chat_messages_aggregation=1
|
||||
update_presence_model_timestamp_before_publish_expires_refresh=1
|
||||
|
||||
[sound]
|
||||
#remove this property for any application that is not Linphone public version itself
|
||||
ec_calibrator_cool_tones=1
|
||||
disable_ringing=0
|
||||
|
||||
[audio]
|
||||
android_disable_audio_focus_requests=1
|
||||
android_monitor_audio_devices=0
|
||||
|
||||
[video]
|
||||
displaytype=MSAndroidTextureDisplay
|
||||
auto_resize_preview_to_keep_ratio=1
|
||||
max_mosaic_size=vga
|
||||
max_conference_size=vga
|
||||
|
||||
[misc]
|
||||
enable_basic_to_client_group_chat_room_migration=0
|
||||
enable_simple_group_chat_message_state=0
|
||||
aggregate_imdn=1
|
||||
notify_each_friend_individually_when_presence_received=0
|
||||
store_friends=0
|
||||
|
||||
[app]
|
||||
activation_code_length=4
|
||||
prefer_basic_chat_room=1
|
||||
record_aware=1
|
||||
|
||||
[assistant]
|
||||
xmlrpc_url=https://subscribe.linphone.org:444/wizard.php
|
||||
|
||||
[account_creator]
|
||||
backend=0
|
||||
url=https://subscribe.linphone.org/api/
|
||||
|
||||
[lime]
|
||||
lime_update_threshold=-1
|
||||
lime_update_threshold=86400
|
||||
|
||||
[alerts]
|
||||
alerts_enabled=1
|
||||
|
||||
## End of factory rc
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
* Copyright (c) 2010-2023 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
|
|
@ -22,108 +22,142 @@ package org.linphone
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.SvgDecoder
|
||||
import coil.decode.VideoFrameDecoder
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import org.linphone.core.*
|
||||
import android.os.PowerManager
|
||||
import androidx.annotation.MainThread
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.disk.directory
|
||||
import coil3.imageLoader
|
||||
import coil3.memory.MemoryCache
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.crossfade
|
||||
import coil3.svg.SvgDecoder
|
||||
import coil3.video.VideoFrameDecoder
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.CoreContext
|
||||
import org.linphone.core.CorePreferences
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.LogCollectionState
|
||||
import org.linphone.core.LogLevel
|
||||
import org.linphone.core.VFS
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.mediastream.Version
|
||||
|
||||
class LinphoneApplication : Application(), ImageLoaderFactory {
|
||||
@MainThread
|
||||
class LinphoneApplication : Application(), SingletonImageLoader.Factory {
|
||||
companion object {
|
||||
private const val TAG = "[Linphone Application]"
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var corePreferences: CorePreferences
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var coreContext: CoreContext
|
||||
|
||||
private fun createConfig(context: Context) {
|
||||
if (::corePreferences.isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
Factory.instance().setLogCollectionPath(context.filesDir.absolutePath)
|
||||
Factory.instance().enableLogCollection(LogCollectionState.Enabled)
|
||||
|
||||
// For VFS
|
||||
Factory.instance().setCacheDir(context.cacheDir.absolutePath)
|
||||
|
||||
corePreferences = CorePreferences(context)
|
||||
corePreferences.copyAssetsFromPackage()
|
||||
|
||||
if (corePreferences.vfsEnabled) {
|
||||
CoreContext.activateVFS()
|
||||
}
|
||||
|
||||
val config = Factory.instance().createConfigWithFactory(corePreferences.configPath, corePreferences.factoryConfigPath)
|
||||
corePreferences.config = config
|
||||
|
||||
val appName = context.getString(R.string.app_name)
|
||||
Factory.instance().setLoggerDomain(appName)
|
||||
Factory.instance().enableLogcatLogs(corePreferences.logcatLogsOutput)
|
||||
if (corePreferences.debugLogs) {
|
||||
Factory.instance().loggingService.setLogLevel(LogLevel.Message)
|
||||
}
|
||||
|
||||
Log.i("[Application] Core config & preferences created")
|
||||
}
|
||||
|
||||
fun ensureCoreExists(
|
||||
context: Context,
|
||||
pushReceived: Boolean = false,
|
||||
service: CoreService? = null,
|
||||
useAutoStartDescription: Boolean = false
|
||||
): Boolean {
|
||||
if (::coreContext.isInitialized && !coreContext.stopped) {
|
||||
Log.d("[Application] Skipping Core creation (push received? $pushReceived)")
|
||||
return false
|
||||
}
|
||||
|
||||
Log.i("[Application] Core context is being created ${if (pushReceived) "from push" else ""}")
|
||||
coreContext = CoreContext(context, corePreferences.config, service, useAutoStartDescription)
|
||||
coreContext.start()
|
||||
return true
|
||||
}
|
||||
|
||||
fun contextExists(): Boolean {
|
||||
return ::coreContext.isInitialized
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val appName = getString(R.string.app_name)
|
||||
android.util.Log.i("[$appName]", "Application is being created")
|
||||
createConfig(applicationContext)
|
||||
Log.i("[Application] Created")
|
||||
val context = applicationContext
|
||||
|
||||
val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager
|
||||
val wakeLock = powerManager.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"Linphone:AppCreation"
|
||||
)
|
||||
wakeLock.acquire(20000L) // 20 seconds
|
||||
|
||||
Factory.instance().setLogCollectionPath(context.filesDir.absolutePath)
|
||||
Factory.instance().enableLogCollection(LogCollectionState.Enabled)
|
||||
// For VFS
|
||||
Factory.instance().setCacheDir(context.cacheDir.absolutePath)
|
||||
|
||||
corePreferences = CorePreferences(context)
|
||||
corePreferences.copyAssetsFromPackage()
|
||||
|
||||
if (VFS.isEnabled(context)) {
|
||||
VFS.setup(context)
|
||||
}
|
||||
|
||||
val config = Factory.instance().createConfigWithFactory(
|
||||
corePreferences.configPath,
|
||||
corePreferences.factoryConfigPath
|
||||
)
|
||||
corePreferences.config = config
|
||||
|
||||
val appName = context.getString(R.string.app_name)
|
||||
Factory.instance().setLoggerDomain(appName)
|
||||
Factory.instance().loggingService.setLogLevel(LogLevel.Message)
|
||||
Factory.instance().enableLogcatLogs(corePreferences.printLogsInLogcat)
|
||||
|
||||
Log.i("$TAG Report Core preferences initialized")
|
||||
Compatibility.setupAppStartupListener(context)
|
||||
|
||||
coreContext = CoreContext(context)
|
||||
coreContext.start()
|
||||
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
wakeLock.release()
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
override fun onTrimMemory(level: Int) {
|
||||
Log.w("$TAG onTrimMemory called with level [${trimLevelToString(level)}]($level) !")
|
||||
when (level) {
|
||||
TRIM_MEMORY_RUNNING_LOW,
|
||||
TRIM_MEMORY_RUNNING_CRITICAL,
|
||||
TRIM_MEMORY_MODERATE,
|
||||
TRIM_MEMORY_COMPLETE -> {
|
||||
Log.i("$TAG Memory trim required, clearing imageLoader memory cache")
|
||||
imageLoader.memoryCache?.clear()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
super.onTrimMemory(level)
|
||||
}
|
||||
|
||||
override fun newImageLoader(context: Context): ImageLoader {
|
||||
// When VFS is enabled, prevent Coil from keeping plain version of files on disk
|
||||
val diskCachePolicy = if (VFS.isEnabled(applicationContext)) {
|
||||
CachePolicy.DISABLED
|
||||
} else {
|
||||
CachePolicy.ENABLED
|
||||
}
|
||||
|
||||
return ImageLoader.Builder(this)
|
||||
.crossfade(false)
|
||||
.components {
|
||||
add(VideoFrameDecoder.Factory())
|
||||
// add(GifDecoder.Factory) // Do not add it, GIFs are properly rendered without it and adding it breaks resizing...
|
||||
add(SvgDecoder.Factory())
|
||||
if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
.memoryCache {
|
||||
MemoryCache.Builder(this)
|
||||
.maxSizePercent(0.25)
|
||||
MemoryCache.Builder()
|
||||
.maxSizePercent(context, 0.25)
|
||||
.build()
|
||||
}
|
||||
.diskCache {
|
||||
val cache = cacheDir.resolve("image_cache")
|
||||
DiskCache.Builder()
|
||||
.directory(cacheDir.resolve("image_cache"))
|
||||
.directory(cache)
|
||||
.maxSizePercent(0.02)
|
||||
.build()
|
||||
}
|
||||
.networkCachePolicy(CachePolicy.ENABLED)
|
||||
.diskCachePolicy(diskCachePolicy)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun trimLevelToString(level: Int): String {
|
||||
return when (level) {
|
||||
TRIM_MEMORY_UI_HIDDEN -> "Hidden UI"
|
||||
TRIM_MEMORY_RUNNING_MODERATE -> "Moderate (Running)"
|
||||
TRIM_MEMORY_RUNNING_LOW -> "Low"
|
||||
TRIM_MEMORY_RUNNING_CRITICAL -> "Critical"
|
||||
TRIM_MEMORY_BACKGROUND -> "Background"
|
||||
TRIM_MEMORY_MODERATE -> "Moderate"
|
||||
TRIM_MEMORY_COMPLETE -> "Complete"
|
||||
else -> level.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Display
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import androidx.window.layout.FoldingFeature
|
||||
import androidx.window.layout.WindowInfoTracker
|
||||
import androidx.window.layout.WindowLayoutInfo
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.LinphoneApplication.Companion.ensureCoreExists
|
||||
import org.linphone.R
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class GenericActivity : AppCompatActivity() {
|
||||
private var timer: Timer? = null
|
||||
private var _isDestructionPending = false
|
||||
val isDestructionPending: Boolean
|
||||
get() = _isDestructionPending
|
||||
|
||||
open fun onLayoutChanges(foldingFeature: FoldingFeature?) { }
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Log.i("[Generic Activity] Ensuring Core exists")
|
||||
ensureCoreExists(applicationContext)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
WindowInfoTracker
|
||||
.getOrCreate(this@GenericActivity)
|
||||
.windowLayoutInfo(this@GenericActivity)
|
||||
.collect { newLayoutInfo ->
|
||||
updateCurrentLayout(newLayoutInfo)
|
||||
}
|
||||
}
|
||||
|
||||
requestedOrientation = if (corePreferences.forcePortrait) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
|
||||
_isDestructionPending = false
|
||||
val nightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
val darkModeEnabled = corePreferences.darkMode
|
||||
when (nightMode) {
|
||||
Configuration.UI_MODE_NIGHT_NO, Configuration.UI_MODE_NIGHT_UNDEFINED -> {
|
||||
if (darkModeEnabled == 1) {
|
||||
// Force dark mode
|
||||
Log.w("[Generic Activity] Forcing night mode")
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
_isDestructionPending = true
|
||||
}
|
||||
}
|
||||
Configuration.UI_MODE_NIGHT_YES -> {
|
||||
if (darkModeEnabled == 0) {
|
||||
// Force light mode
|
||||
Log.w("[Generic Activity] Forcing day mode")
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
_isDestructionPending = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateScreenSize()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Remove service notification if it has been started by device boot
|
||||
coreContext.notificationsManager.stopForegroundNotificationIfPossible()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
|
||||
}
|
||||
|
||||
fun isTablet(): Boolean {
|
||||
return resources.getBoolean(R.bool.isTablet)
|
||||
}
|
||||
|
||||
private fun updateScreenSize() {
|
||||
val metrics = DisplayMetrics()
|
||||
val display: Display = windowManager.defaultDisplay
|
||||
display.getRealMetrics(metrics)
|
||||
val screenWidth = metrics.widthPixels.toFloat()
|
||||
val screenHeight = metrics.heightPixels.toFloat()
|
||||
coreContext.screenWidth = screenWidth
|
||||
coreContext.screenHeight = screenHeight
|
||||
}
|
||||
|
||||
private fun updateCurrentLayout(newLayoutInfo: WindowLayoutInfo) {
|
||||
if (newLayoutInfo.displayFeatures.isEmpty()) {
|
||||
onLayoutChanges(null)
|
||||
} else {
|
||||
for (feature in newLayoutInfo.displayFeatures) {
|
||||
val foldingFeature = feature as? FoldingFeature
|
||||
if (foldingFeature != null) {
|
||||
onLayoutChanges(foldingFeature)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class GenericFragment<T : ViewDataBinding> : Fragment() {
|
||||
companion object {
|
||||
val emptyFragmentsIds = arrayListOf(
|
||||
R.id.emptyChatFragment,
|
||||
R.id.emptyContactFragment,
|
||||
R.id.emptySettingsFragment,
|
||||
R.id.emptyCallHistoryFragment
|
||||
)
|
||||
}
|
||||
|
||||
private var _binding: T? = null
|
||||
protected val binding get() = _binding!!
|
||||
|
||||
protected var useMaterialSharedAxisXForwardAnimation = true
|
||||
|
||||
protected lateinit var sharedViewModel: SharedMainViewModel
|
||||
|
||||
protected fun isSharedViewModelInitialized(): Boolean {
|
||||
return ::sharedViewModel.isInitialized
|
||||
}
|
||||
|
||||
protected fun isBindingAvailable(): Boolean {
|
||||
return _binding != null
|
||||
}
|
||||
|
||||
private fun getFragmentRealClassName(): String {
|
||||
return this.javaClass.name
|
||||
}
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
try {
|
||||
val navController = findNavController()
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} handleOnBackPressed")
|
||||
if (!navController.popBackStack()) {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} couldn't pop")
|
||||
if (!navController.navigateUp()) {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} couldn't navigate up")
|
||||
// Disable this callback & start a new back press event
|
||||
isEnabled = false
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e("[Generic Fragment] ${getFragmentRealClassName()} Can't go back: $ise")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getLayoutId(): Int
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedMainViewModel::class.java]
|
||||
}
|
||||
|
||||
sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} shared main VM sliding pane has changed")
|
||||
onBackPressedCallback.isEnabled = backPressedCallBackEnabled()
|
||||
}
|
||||
|
||||
_binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
|
||||
return _binding!!.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
if (useMaterialSharedAxisXForwardAnimation && corePreferences.enableAnimations) {
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
|
||||
postponeEnterTransition()
|
||||
binding.root.doOnPreDraw { startPostponedEnterTransition() }
|
||||
}
|
||||
|
||||
setupBackPressCallback()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
|
||||
onBackPressedCallback.remove()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
protected fun goBack() {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
private fun setupBackPressCallback() {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} setupBackPressCallback")
|
||||
|
||||
val backButton = binding.root.findViewById<ImageView>(R.id.back)
|
||||
if (backButton != null) {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} found back button")
|
||||
// If popping navigation back stack entry would bring us to an "empty" fragment
|
||||
// then don't do it if sliding pane layout isn't "flat"
|
||||
onBackPressedCallback.isEnabled = backPressedCallBackEnabled()
|
||||
backButton.setOnClickListener { goBack() }
|
||||
} else {
|
||||
onBackPressedCallback.isEnabled = false
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackPressedCallback)
|
||||
}
|
||||
|
||||
private fun backPressedCallBackEnabled(): Boolean {
|
||||
// This allow to navigate a SlidingPane child nav graph.
|
||||
// This only concerns fragments for which the nav graph is inside a SlidingPane layout.
|
||||
// In our case it's all graphs except the main one.
|
||||
if (findNavController().graph.id == R.id.main_nav_graph_xml) return false
|
||||
|
||||
val isSlidingPaneFlat = sharedViewModel.isSlidingPaneSlideable.value == false
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} isSlidingPaneFlat ? $isSlidingPaneFlat")
|
||||
val isPreviousFragmentEmpty = findNavController().previousBackStackEntry?.destination?.id in emptyFragmentsIds
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} isPreviousFragmentEmpty ? $isPreviousFragmentEmpty")
|
||||
val popBackStack = isSlidingPaneFlat || !isPreviousFragmentEmpty
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} popBackStack ? $popBackStack")
|
||||
return popBackStack
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,89 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.os.PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class ProximitySensorActivity : GenericActivity() {
|
||||
private lateinit var proximityWakeLock: PowerManager.WakeLock
|
||||
private var proximitySensorEnabled = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
if (!powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
|
||||
Log.w("[Proximity Sensor Activity] PROXIMITY_SCREEN_OFF_WAKE_LOCK isn't supported on this device!")
|
||||
}
|
||||
|
||||
proximityWakeLock = powerManager.newWakeLock(
|
||||
PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
|
||||
"$packageName;proximity_sensor"
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (coreContext.core.callsNb > 0) {
|
||||
val videoEnabled = coreContext.core.currentCall?.currentParams?.isVideoEnabled ?: false
|
||||
enableProximitySensor(!videoEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
enableProximitySensor(false)
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
enableProximitySensor(false)
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
protected fun enableProximitySensor(enable: Boolean) {
|
||||
if (enable) {
|
||||
if (!proximitySensorEnabled) {
|
||||
Log.i("[Proximity Sensor Activity] Enabling proximity sensor turning off screen")
|
||||
if (!proximityWakeLock.isHeld) {
|
||||
Log.i("[Proximity Sensor Activity] Acquiring PROXIMITY_SCREEN_OFF_WAKE_LOCK")
|
||||
proximityWakeLock.acquire()
|
||||
}
|
||||
proximitySensorEnabled = true
|
||||
}
|
||||
} else {
|
||||
if (proximitySensorEnabled) {
|
||||
Log.i("[Proximity Sensor Activity] Disabling proximity sensor turning off screen")
|
||||
if (proximityWakeLock.isHeld) {
|
||||
Log.i("[Proximity Sensor Activity] Releasing PROXIMITY_SCREEN_OFF_WAKE_LOCK")
|
||||
proximityWakeLock.release(RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY)
|
||||
}
|
||||
proximitySensorEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.SnackBarActivity
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
|
||||
class AssistantActivity : GenericActivity(), SnackBarActivity {
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
private lateinit var coordinator: CoordinatorLayout
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.assistant_activity)
|
||||
|
||||
sharedViewModel = ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
|
||||
coordinator = findViewById(R.id.coordinator)
|
||||
|
||||
corePreferences.firstStart = false
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int) {
|
||||
Snackbar.make(coordinator, resourceId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int, action: Int, listener: () -> Unit) {
|
||||
Snackbar
|
||||
.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG)
|
||||
.setAction(action) {
|
||||
listener()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(message: String) {
|
||||
Snackbar.make(coordinator, message, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import android.widget.TextView
|
||||
import kotlin.collections.ArrayList
|
||||
import org.linphone.R
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.core.Factory
|
||||
|
||||
class CountryPickerAdapter : BaseAdapter(), Filterable {
|
||||
private var countries: ArrayList<DialPlan>
|
||||
|
||||
init {
|
||||
val dialPlans = Factory.instance().dialPlans
|
||||
countries = arrayListOf()
|
||||
countries.addAll(dialPlans)
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view: View = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.assistant_country_picker_cell, parent, false)
|
||||
val dialPlan: DialPlan = countries[position]
|
||||
|
||||
val name = view.findViewById<TextView>(R.id.country_name)
|
||||
name.text = dialPlan.country
|
||||
|
||||
val dialCode = view.findViewById<TextView>(R.id.country_prefix)
|
||||
dialCode.text = String.format("(%s)", dialPlan.countryCallingCode)
|
||||
|
||||
view.tag = dialPlan
|
||||
return view
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): DialPlan {
|
||||
return countries[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return countries.size
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
override fun performFiltering(constraint: CharSequence): FilterResults {
|
||||
val filteredCountries = arrayListOf<DialPlan>()
|
||||
for (dialPlan in Factory.instance().dialPlans) {
|
||||
if (dialPlan.country.contains(constraint, ignoreCase = true) ||
|
||||
dialPlan.countryCallingCode.contains(constraint)
|
||||
) {
|
||||
filteredCountries.add(dialPlan)
|
||||
}
|
||||
}
|
||||
val filterResults = FilterResults()
|
||||
filterResults.values = filteredCountries
|
||||
return filterResults
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun publishResults(
|
||||
constraint: CharSequence,
|
||||
results: FilterResults
|
||||
) {
|
||||
countries = results.values as ArrayList<DialPlan>
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.viewmodels.AbstractPhoneViewModel
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.PermissionHelper
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
abstract class AbstractPhoneFragment<T : ViewDataBinding> : GenericFragment<T>() {
|
||||
companion object {
|
||||
const val READ_PHONE_STATE_PERMISSION_REQUEST_CODE = 0
|
||||
}
|
||||
|
||||
abstract val viewModel: AbstractPhoneViewModel
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == READ_PHONE_STATE_PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i("[Assistant] READ_PHONE_STATE/READ_PHONE_NUMBERS permission granted")
|
||||
updateFromDeviceInfo()
|
||||
} else {
|
||||
Log.w("[Assistant] READ_PHONE_STATE/READ_PHONE_NUMBERS permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun checkPermissions() {
|
||||
if (!resources.getBoolean(R.bool.isTablet)) {
|
||||
if (!PermissionHelper.get().hasReadPhoneStateOrPhoneNumbersPermission()) {
|
||||
Log.i("[Assistant] Asking for READ_PHONE_STATE/READ_PHONE_NUMBERS permission")
|
||||
Compatibility.requestReadPhoneStateOrNumbersPermission(this, READ_PHONE_STATE_PERMISSION_REQUEST_CODE)
|
||||
} else {
|
||||
updateFromDeviceInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFromDeviceInfo() {
|
||||
val phoneNumber = PhoneNumberUtils.getDevicePhoneNumber(requireContext())
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(requireContext())
|
||||
viewModel.updateFromPhoneNumberAndOrDialPlan(phoneNumber, dialPlan)
|
||||
}
|
||||
|
||||
protected fun showPhoneNumberInfoDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.assistant_phone_number_info_title))
|
||||
.setMessage(
|
||||
getString(R.string.assistant_phone_number_link_info_content) + "\n" +
|
||||
getString(
|
||||
R.string.assistant_phone_number_link_info_content_already_account
|
||||
)
|
||||
)
|
||||
.setNegativeButton(getString(R.string.dialog_ok), null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.AccountLoginViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.AccountLoginViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||
import org.linphone.databinding.AssistantAccountLoginFragmentBinding
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragmentBinding>() {
|
||||
override lateinit var viewModel: AccountLoginViewModel
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_account_login_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
AccountLoginViewModelFactory(sharedAssistantViewModel.getAccountCreator())
|
||||
)[AccountLoginViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
if (resources.getBoolean(R.bool.isTablet)) {
|
||||
viewModel.loginWithUsernamePassword.value = true
|
||||
}
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
binding.setForgotPasswordClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.data = Uri.parse(getString(R.string.assistant_forgotten_password_link))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsLogin", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
navigateToPhoneAccountValidation(args)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
coreContext.newAccountConfigured(true)
|
||||
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.invalidCredentialsEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val dialogViewModel =
|
||||
DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
viewModel.removeInvalidProxyConfig()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialogViewModel.showDeleteButton(
|
||||
{
|
||||
viewModel.continueEvenIfInvalidCredentials()
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.assistant_continue_even_if_credentials_invalid)
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.*
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.adapters.CountryPickerAdapter
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.databinding.AssistantCountryPickerFragmentBinding
|
||||
|
||||
class CountryPickerFragment(private val listener: CountryPickedListener) : DialogFragment() {
|
||||
private var _binding: AssistantCountryPickerFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var adapter: CountryPickerAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_TITLE, R.style.assistant_country_dialog_style)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = AssistantCountryPickerFragmentBinding.inflate(inflater, container, false)
|
||||
|
||||
adapter = CountryPickerAdapter()
|
||||
binding.countryList.adapter = adapter
|
||||
|
||||
binding.countryList.setOnItemClickListener { _, _, position, _ ->
|
||||
if (position >= 0 && position < adapter.count) {
|
||||
val dialPlan = adapter.getItem(position)
|
||||
listener.onCountryClicked(dialPlan)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.searchCountry.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
adapter.filter.filter(s)
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
|
||||
})
|
||||
|
||||
binding.setCancelClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
interface CountryPickedListener {
|
||||
fun onCountryClicked(dialPlan: DialPlan)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.viewmodels.EchoCancellerCalibrationViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantEchoCancellerCalibrationFragmentBinding
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerCalibrationFragmentBinding>() {
|
||||
companion object {
|
||||
const val RECORD_AUDIO_PERMISSION_REQUEST_CODE = 0
|
||||
}
|
||||
|
||||
private lateinit var viewModel: EchoCancellerCalibrationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_echo_canceller_calibration_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel = ViewModelProvider(this)[EchoCancellerCalibrationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.echoCalibrationTerminated.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
|
||||
if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) {
|
||||
Log.i("[Echo Canceller Calibration] Asking for RECORD_AUDIO permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), RECORD_AUDIO_PERMISSION_REQUEST_CODE)
|
||||
} else {
|
||||
viewModel.startEchoCancellerCalibration()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
|
||||
val granted =
|
||||
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted")
|
||||
viewModel.startEchoCancellerCalibration()
|
||||
} else {
|
||||
Log.w("[Echo Canceller Calibration] RECORD_AUDIO permission denied")
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToEmailAccountValidation
|
||||
import org.linphone.databinding.AssistantEmailAccountCreationFragmentBinding
|
||||
|
||||
class EmailAccountCreationFragment : GenericFragment<AssistantEmailAccountCreationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: EmailAccountCreationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_email_account_creation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, EmailAccountCreationViewModelFactory(sharedAssistantViewModel.getAccountCreator()))[EmailAccountCreationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.goToEmailValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
navigateToEmailAccountValidation()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.*
|
||||
import org.linphone.activities.navigateToAccountLinking
|
||||
import org.linphone.databinding.AssistantEmailAccountValidationFragmentBinding
|
||||
|
||||
class EmailAccountValidationFragment : GenericFragment<AssistantEmailAccountValidationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: EmailAccountValidationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_email_account_validation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, EmailAccountValidationViewModelFactory(sharedAssistantViewModel.getAccountCreator()))[EmailAccountValidationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
coreContext.newAccountConfigured(true)
|
||||
|
||||
val args = Bundle()
|
||||
args.putBoolean("AllowSkip", true)
|
||||
args.putString("Username", viewModel.accountCreator.username)
|
||||
args.putString("Password", viewModel.accountCreator.password)
|
||||
navigateToAccountLinking(args)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.GenericLoginViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.GenericLoginViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.databinding.AssistantGenericAccountLoginFragmentBinding
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
class GenericAccountLoginFragment : GenericFragment<AssistantGenericAccountLoginFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: GenericLoginViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_generic_account_login_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, GenericLoginViewModelFactory(sharedAssistantViewModel.getAccountCreator(true)))[GenericLoginViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val isLinphoneAccount = viewModel.domain.value.orEmpty() == corePreferences.defaultDomain
|
||||
coreContext.newAccountConfigured(isLinphoneAccount)
|
||||
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.invalidCredentialsEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val dialogViewModel =
|
||||
DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
viewModel.removeInvalidProxyConfig()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialogViewModel.showDeleteButton(
|
||||
{
|
||||
viewModel.continueEvenIfInvalidCredentials()
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.assistant_continue_even_if_credentials_invalid)
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||
import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding
|
||||
|
||||
class PhoneAccountCreationFragment :
|
||||
AbstractPhoneFragment<AssistantPhoneAccountCreationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
override lateinit var viewModel: PhoneAccountCreationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_phone_account_creation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
PhoneAccountCreationViewModelFactory(sharedAssistantViewModel.getAccountCreator())
|
||||
)[PhoneAccountCreationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsCreation", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
navigateToPhoneAccountValidation(args)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.SnackBarActivity
|
||||
import org.linphone.activities.assistant.viewmodels.*
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding
|
||||
|
||||
class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountLinkingFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
override lateinit var viewModel: PhoneAccountLinkingViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_phone_account_linking_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
val accountCreator = sharedAssistantViewModel.getAccountCreator()
|
||||
viewModel = ViewModelProvider(this, PhoneAccountLinkingViewModelFactory(accountCreator))[PhoneAccountLinkingViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
val username = arguments?.getString("Username")
|
||||
Log.i("[Phone Account Linking] username to link is $username")
|
||||
viewModel.username.value = username
|
||||
|
||||
val password = arguments?.getString("Password")
|
||||
accountCreator.password = password
|
||||
|
||||
val ha1 = arguments?.getString("HA1")
|
||||
accountCreator.ha1 = ha1
|
||||
|
||||
val allowSkip = arguments?.getBoolean("AllowSkip", false)
|
||||
viewModel.allowSkip.value = allowSkip
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsLinking", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
navigateToPhoneAccountValidation(args)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as SnackBarActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.SnackBarActivity
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToAccountSettings
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding
|
||||
|
||||
class PhoneAccountValidationFragment : GenericFragment<AssistantPhoneAccountValidationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: PhoneAccountValidationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_phone_account_validation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, PhoneAccountValidationViewModelFactory(sharedAssistantViewModel.getAccountCreator()))[PhoneAccountValidationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.phoneNumber.value = arguments?.getString("PhoneNumber")
|
||||
viewModel.isLogin.value = arguments?.getBoolean("IsLogin", false)
|
||||
viewModel.isCreation.value = arguments?.getBoolean("IsCreation", false)
|
||||
viewModel.isLinking.value = arguments?.getBoolean("IsLinking", false)
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
when {
|
||||
viewModel.isLogin.value == true || viewModel.isCreation.value == true -> {
|
||||
coreContext.newAccountConfigured(true)
|
||||
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
viewModel.isLinking.value == true -> {
|
||||
if (findNavController().graph.id == R.id.settings_nav_graph_xml) {
|
||||
val args = Bundle()
|
||||
args.putString(
|
||||
"Identity",
|
||||
"sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}"
|
||||
)
|
||||
navigateToAccountSettings(args)
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as SnackBarActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
// This won't work starting Android 10 as clipboard access is denied unless app has focus,
|
||||
// which won't be the case when the SMS arrives unless it is added into clipboard from a notification
|
||||
val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.addPrimaryClipChangedListener {
|
||||
val data = clipboard.primaryClip
|
||||
if (data != null && data.itemCount > 0) {
|
||||
val clip = data.getItemAt(0).text.toString()
|
||||
if (clip.length == 4) {
|
||||
Log.i("[Assistant] [Phone Account Validation] Found 4 digits as primary clip in clipboard, using it and clear it")
|
||||
viewModel.code.value = clip
|
||||
clipboard.clearPrimaryClip()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.viewmodels.QrCodeViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantQrCodeFragmentBinding
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
|
||||
companion object {
|
||||
const val CAMERA_PERMISSION_REQUEST_CODE = 0
|
||||
}
|
||||
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: QrCodeViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_qr_code_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this)[QrCodeViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.qrCodeFoundEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { url ->
|
||||
sharedAssistantViewModel.remoteProvisioningUrl.value = url
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
viewModel.setBackCamera()
|
||||
|
||||
if (!PermissionHelper.required(requireContext()).hasCameraPermission()) {
|
||||
Log.i("[QR Code] Asking for CAMERA permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
coreContext.core.nativePreviewWindowId = binding.qrCodeCaptureTexture
|
||||
coreContext.core.isQrcodeVideoPreviewEnabled = true
|
||||
coreContext.core.isVideoPreviewEnabled = true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
coreContext.core.nativePreviewWindowId = null
|
||||
coreContext.core.isQrcodeVideoPreviewEnabled = false
|
||||
coreContext.core.isVideoPreviewEnabled = false
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
|
||||
val granted =
|
||||
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[QR Code] CAMERA permission granted")
|
||||
coreContext.core.reloadVideoDevices()
|
||||
viewModel.setBackCamera()
|
||||
} else {
|
||||
Log.w("[QR Code] CAMERA permission denied")
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.RemoteProvisioningViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.activities.navigateToQrCode
|
||||
import org.linphone.databinding.AssistantRemoteProvisioningFragmentBinding
|
||||
|
||||
class RemoteProvisioningFragment : GenericFragment<AssistantRemoteProvisioningFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: RemoteProvisioningViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_remote_provisioning_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this)[RemoteProvisioningViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setQrCodeClickListener {
|
||||
navigateToQrCode()
|
||||
}
|
||||
|
||||
viewModel.fetchSuccessfulEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { success ->
|
||||
if (success) {
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
} else {
|
||||
val activity = requireActivity() as AssistantActivity
|
||||
activity.showSnackBar(R.string.assistant_remote_provisioning_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.urlToFetch.value = sharedAssistantViewModel.remoteProvisioningUrl.value ?: coreContext.core.provisioningUri
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (::sharedAssistantViewModel.isInitialized) {
|
||||
sharedAssistantViewModel.remoteProvisioningUrl.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import java.util.regex.Pattern
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.*
|
||||
import org.linphone.activities.assistant.viewmodels.WelcomeViewModel
|
||||
import org.linphone.activities.navigateToAccountLogin
|
||||
import org.linphone.activities.navigateToEmailAccountCreation
|
||||
import org.linphone.activities.navigateToRemoteProvisioning
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantWelcomeFragmentBinding
|
||||
|
||||
class WelcomeFragment : GenericFragment<AssistantWelcomeFragmentBinding>() {
|
||||
private lateinit var viewModel: WelcomeViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_welcome_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel = ViewModelProvider(this)[WelcomeViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setCreateAccountClickListener {
|
||||
if (resources.getBoolean(R.bool.isTablet)) {
|
||||
navigateToEmailAccountCreation()
|
||||
} else {
|
||||
navigateToPhoneAccountCreation()
|
||||
}
|
||||
}
|
||||
|
||||
binding.setAccountLoginClickListener {
|
||||
navigateToAccountLogin()
|
||||
}
|
||||
|
||||
binding.setGenericAccountLoginClickListener {
|
||||
navigateToGenericLoginWarning()
|
||||
}
|
||||
|
||||
binding.setRemoteProvisioningClickListener {
|
||||
navigateToRemoteProvisioning()
|
||||
}
|
||||
|
||||
viewModel.termsAndPrivacyAccepted.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
if (it) corePreferences.readAndAgreeTermsAndPrivacy = true
|
||||
}
|
||||
|
||||
setUpTermsAndPrivacyLinks()
|
||||
}
|
||||
|
||||
private fun setUpTermsAndPrivacyLinks() {
|
||||
val terms = getString(R.string.assistant_general_terms)
|
||||
val privacy = getString(R.string.assistant_privacy_policy)
|
||||
|
||||
val label = getString(
|
||||
R.string.assistant_read_and_agree_terms,
|
||||
terms,
|
||||
privacy
|
||||
)
|
||||
val spannable = SpannableString(label)
|
||||
|
||||
val termsMatcher = Pattern.compile(terms).matcher(label)
|
||||
if (termsMatcher.find()) {
|
||||
val clickableSpan: ClickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.assistant_general_terms_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (e: Exception) {
|
||||
Log.e("[Welcome] Can't start activity: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
spannable.setSpan(clickableSpan, termsMatcher.start(0), termsMatcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
|
||||
val policyMatcher = Pattern.compile(privacy).matcher(label)
|
||||
if (policyMatcher.find()) {
|
||||
val clickableSpan: ClickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.assistant_privacy_policy_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (e: Exception) {
|
||||
Log.e("[Welcome] Can't start activity: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
spannable.setSpan(clickableSpan, policyMatcher.start(0), policyMatcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
|
||||
binding.termsAndPrivacy.text = spannable
|
||||
binding.termsAndPrivacy.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.activities.assistant.fragments.CountryPickerFragment
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
abstract class AbstractPhoneViewModel(val accountCreator: AccountCreator) :
|
||||
ViewModel(),
|
||||
CountryPickerFragment.CountryPickedListener {
|
||||
|
||||
val prefix = MutableLiveData<String>()
|
||||
|
||||
val phoneNumber = MutableLiveData<String>()
|
||||
val phoneNumberError = MutableLiveData<String>()
|
||||
|
||||
val countryName: LiveData<String> = Transformations.switchMap(prefix) {
|
||||
getCountryNameFromPrefix(it)
|
||||
}
|
||||
|
||||
init {
|
||||
prefix.value = "+"
|
||||
}
|
||||
|
||||
override fun onCountryClicked(dialPlan: DialPlan) {
|
||||
prefix.value = "+${dialPlan.countryCallingCode}"
|
||||
}
|
||||
|
||||
fun isPhoneNumberOk(): Boolean {
|
||||
return countryName.value.orEmpty().isNotEmpty() && phoneNumber.value.orEmpty().isNotEmpty() && phoneNumberError.value.orEmpty().isEmpty()
|
||||
}
|
||||
|
||||
fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
|
||||
val internationalPrefix = "+${dialPlan?.countryCallingCode}"
|
||||
if (dialPlan != null) {
|
||||
Log.i("[Assistant] Found prefix from dial plan: ${dialPlan.countryCallingCode}")
|
||||
prefix.value = internationalPrefix
|
||||
}
|
||||
|
||||
if (number != null) {
|
||||
Log.i("[Assistant] Found phone number: $number")
|
||||
phoneNumber.value = if (number.startsWith(internationalPrefix)) {
|
||||
number.substring(internationalPrefix.length)
|
||||
} else {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCountryNameFromPrefix(prefix: String?): MutableLiveData<String> {
|
||||
val country = MutableLiveData<String>()
|
||||
country.value = ""
|
||||
|
||||
if (prefix != null && prefix.isNotEmpty()) {
|
||||
val countryCode = if (prefix.first() == '+') prefix.substring(1) else prefix
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(countryCode)
|
||||
Log.i("[Assistant] Found dial plan $dialPlan from country code: $countryCode")
|
||||
country.value = dialPlan?.country
|
||||
}
|
||||
return country
|
||||
}
|
||||
}
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
class AccountLoginViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return AccountLoginViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||
val loginWithUsernamePassword = MutableLiveData<Boolean>()
|
||||
|
||||
val username = MutableLiveData<String>()
|
||||
val usernameError = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
val passwordError = MutableLiveData<String>()
|
||||
|
||||
val loginEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val leaveAssistantEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val invalidCredentialsEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val goToSmsValidationEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onRecoverAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Account Login] Recover account status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.RequestOk) {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var proxyConfigToCheck: ProxyConfig? = null
|
||||
|
||||
private val coreListener = object : CoreListenerStub() {
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRegistrationStateChanged(
|
||||
core: Core,
|
||||
cfg: ProxyConfig,
|
||||
state: RegistrationState,
|
||||
message: String
|
||||
) {
|
||||
if (cfg == proxyConfigToCheck) {
|
||||
Log.i("[Assistant] [Account Login] Registration state is $state: $message")
|
||||
if (state == RegistrationState.Ok) {
|
||||
waitForServerAnswer.value = false
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
} else if (state == RegistrationState.Failed) {
|
||||
waitForServerAnswer.value = false
|
||||
invalidCredentialsEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
loginWithUsernamePassword.value = coreContext.context.resources.getBoolean(R.bool.isTablet)
|
||||
|
||||
loginEnabled.value = false
|
||||
loginEnabled.addSource(prefix) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(phoneNumber) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(username) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(password) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(loginWithUsernamePassword) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(phoneNumberError) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun removeInvalidProxyConfig() {
|
||||
val cfg = proxyConfigToCheck
|
||||
cfg ?: return
|
||||
val authInfo = cfg.findAuthInfo()
|
||||
if (authInfo != null) coreContext.core.removeAuthInfo(authInfo)
|
||||
coreContext.core.removeProxyConfig(cfg)
|
||||
proxyConfigToCheck = null
|
||||
}
|
||||
|
||||
fun continueEvenIfInvalidCredentials() {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
|
||||
fun login() {
|
||||
accountCreator.displayName = displayName.value
|
||||
|
||||
if (loginWithUsernamePassword.value == true) {
|
||||
val result = accountCreator.setUsername(username.value)
|
||||
if (result != AccountCreator.UsernameStatus.Ok) {
|
||||
Log.e("[Assistant] [Account Login] Error [${result.name}] setting the username: ${username.value}")
|
||||
usernameError.value = result.name
|
||||
return
|
||||
}
|
||||
Log.i("[Assistant] [Account Login] Username is ${accountCreator.username}")
|
||||
|
||||
val result2 = accountCreator.setPassword(password.value)
|
||||
if (result2 != AccountCreator.PasswordStatus.Ok) {
|
||||
Log.e("[Assistant] [Account Login] Error [${result2.name}] setting the password")
|
||||
passwordError.value = result2.name
|
||||
return
|
||||
}
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
coreContext.core.addListener(coreListener)
|
||||
if (!createProxyConfig()) {
|
||||
waitForServerAnswer.value = false
|
||||
coreContext.core.removeListener(coreListener)
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
}
|
||||
} else {
|
||||
val result = AccountCreator.PhoneNumberStatus.fromInt(accountCreator.setPhoneNumber(phoneNumber.value, prefix.value))
|
||||
if (result != AccountCreator.PhoneNumberStatus.Ok) {
|
||||
Log.e("[Assistant] [Account Login] Error [$result] setting the phone number: ${phoneNumber.value} with prefix: ${prefix.value}")
|
||||
phoneNumberError.value = result.name
|
||||
return
|
||||
}
|
||||
Log.i("[Assistant] [Account Login] Phone number is ${accountCreator.phoneNumber}")
|
||||
|
||||
val result2 = accountCreator.setUsername(accountCreator.phoneNumber)
|
||||
if (result2 != AccountCreator.UsernameStatus.Ok) {
|
||||
Log.e("[Assistant] [Account Login] Error [${result2.name}] setting the username: ${accountCreator.phoneNumber}")
|
||||
usernameError.value = result2.name
|
||||
return
|
||||
}
|
||||
Log.i("[Assistant] [Account Login] Username is ${accountCreator.username}")
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.recoverAccount()
|
||||
Log.i("[Assistant] [Account Login] Recover account returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLoginButtonEnabled(): Boolean {
|
||||
return if (loginWithUsernamePassword.value == true) {
|
||||
username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
|
||||
} else {
|
||||
isPhoneNumberOk()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProxyConfig(): Boolean {
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
proxyConfigToCheck = proxyConfig
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Account Login] Account creator couldn't create proxy config")
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
return false
|
||||
}
|
||||
|
||||
proxyConfig.isPushNotificationAllowed = true
|
||||
|
||||
if (proxyConfig.dialPrefix.isNullOrEmpty()) {
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(coreContext.context)
|
||||
if (dialPlan != null) {
|
||||
Log.i("[Assistant] [Account Login] Found dial plan country ${dialPlan.country} with international prefix ${dialPlan.countryCallingCode}")
|
||||
proxyConfig.edit()
|
||||
proxyConfig.dialPrefix = dialPlan.countryCallingCode
|
||||
proxyConfig.done()
|
||||
} else {
|
||||
Log.w("[Assistant] [Account Login] Failed to find dial plan")
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("[Assistant] [Account Login] Proxy config created")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.EcCalibratorStatus
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EchoCancellerCalibrationViewModel : ViewModel() {
|
||||
val echoCalibrationTerminated = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onEcCalibrationResult(core: Core, status: EcCalibratorStatus, delayMs: Int) {
|
||||
if (status == EcCalibratorStatus.InProgress) return
|
||||
echoCancellerCalibrationFinished(status, delayMs)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
}
|
||||
|
||||
fun startEchoCancellerCalibration() {
|
||||
coreContext.core.startEchoCancellerCalibration()
|
||||
}
|
||||
|
||||
fun echoCancellerCalibrationFinished(status: EcCalibratorStatus, delay: Int) {
|
||||
coreContext.core.removeListener(listener)
|
||||
when (status) {
|
||||
EcCalibratorStatus.DoneNoEcho -> {
|
||||
Log.i("[Echo Canceller Calibration] Done, no echo")
|
||||
}
|
||||
EcCalibratorStatus.Done -> {
|
||||
Log.i("[Echo Canceller Calibration] Done, delay is ${delay}ms")
|
||||
}
|
||||
EcCalibratorStatus.Failed -> {
|
||||
Log.w("[Echo Canceller Calibration] Failed")
|
||||
}
|
||||
EcCalibratorStatus.InProgress -> {
|
||||
Log.i("[Echo Canceller Calibration] In progress")
|
||||
}
|
||||
}
|
||||
echoCalibrationTerminated.value = Event(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EmailAccountCreationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return EmailAccountCreationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EmailAccountCreationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
val username = MutableLiveData<String>()
|
||||
val usernameError = MutableLiveData<String>()
|
||||
|
||||
val email = MutableLiveData<String>()
|
||||
val emailError = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
val passwordError = MutableLiveData<String>()
|
||||
|
||||
val passwordConfirmation = MutableLiveData<String>()
|
||||
val passwordConfirmationError = MutableLiveData<String>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val createEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val goToEmailValidationEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountExist(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Account Creation] onIsAccountExist status is $status")
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
||||
waitForServerAnswer.value = false
|
||||
usernameError.value = AppUtils.getString(R.string.assistant_error_username_already_exists)
|
||||
}
|
||||
AccountCreator.Status.AccountNotExist -> {
|
||||
val createAccountStatus = creator.createAccount()
|
||||
if (createAccountStatus != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Account Creation] onCreateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountCreated -> {
|
||||
goToEmailValidationEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
createEnabled.value = false
|
||||
createEnabled.addSource(username) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(usernameError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(email) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(emailError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(password) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordConfirmation) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordConfirmationError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun create() {
|
||||
accountCreator.username = username.value
|
||||
accountCreator.password = password.value
|
||||
accountCreator.email = email.value
|
||||
accountCreator.displayName = displayName.value
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.isAccountExist
|
||||
Log.i("[Assistant] [Account Creation] Account exists returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCreateButtonEnabled(): Boolean {
|
||||
return username.value.orEmpty().isNotEmpty() &&
|
||||
email.value.orEmpty().isNotEmpty() &&
|
||||
password.value.orEmpty().isNotEmpty() &&
|
||||
passwordConfirmation.value.orEmpty().isNotEmpty() &&
|
||||
password.value == passwordConfirmation.value &&
|
||||
usernameError.value.orEmpty().isEmpty() &&
|
||||
emailError.value.orEmpty().isEmpty() &&
|
||||
passwordError.value.orEmpty().isEmpty() &&
|
||||
passwordConfirmationError.value.orEmpty().isEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.ProxyConfig
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
class EmailAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return EmailAccountValidationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
val email = MutableLiveData<String>()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountActivated(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Account Validation] onIsAccountActivated status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountActivated -> {
|
||||
if (createProxyConfig()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AccountNotActivated -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
email.value = accountCreator.email
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.isAccountActivated
|
||||
Log.i("[Assistant] [Account Validation] Account exists returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProxyConfig(): Boolean {
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Account Validation] Account creator couldn't create proxy config")
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
return false
|
||||
}
|
||||
|
||||
proxyConfig.isPushNotificationAllowed = true
|
||||
|
||||
if (proxyConfig.dialPrefix.isNullOrEmpty()) {
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(LinphoneApplication.coreContext.context)
|
||||
if (dialPlan != null) {
|
||||
Log.i("[Assistant] [Account Validation] Found dial plan country ${dialPlan.country} with international prefix ${dialPlan.countryCallingCode}")
|
||||
proxyConfig.edit()
|
||||
proxyConfig.dialPrefix = dialPlan.countryCallingCode
|
||||
proxyConfig.done()
|
||||
} else {
|
||||
Log.w("[Assistant] [Account Validation] Failed to find dial plan")
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("[Assistant] [Account Validation] Proxy config created")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class GenericLoginViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return GenericLoginViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewModel() {
|
||||
val username = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
|
||||
val domain = MutableLiveData<String>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val transport = MutableLiveData<TransportType>()
|
||||
|
||||
val loginEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val invalidCredentialsEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private var proxyConfigToCheck: ProxyConfig? = null
|
||||
|
||||
private val coreListener = object : CoreListenerStub() {
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRegistrationStateChanged(
|
||||
core: Core,
|
||||
cfg: ProxyConfig,
|
||||
state: RegistrationState,
|
||||
message: String
|
||||
) {
|
||||
if (cfg == proxyConfigToCheck) {
|
||||
Log.i("[Assistant] [Generic Login] Registration state is $state: $message")
|
||||
if (state == RegistrationState.Ok) {
|
||||
waitForServerAnswer.value = false
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
} else if (state == RegistrationState.Failed) {
|
||||
waitForServerAnswer.value = false
|
||||
invalidCredentialsEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
transport.value = TransportType.Tls
|
||||
|
||||
loginEnabled.value = false
|
||||
loginEnabled.addSource(username) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(password) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(domain) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTransport(transportType: TransportType) {
|
||||
transport.value = transportType
|
||||
}
|
||||
|
||||
fun removeInvalidProxyConfig() {
|
||||
val cfg = proxyConfigToCheck
|
||||
cfg ?: return
|
||||
val authInfo = cfg.findAuthInfo()
|
||||
if (authInfo != null) coreContext.core.removeAuthInfo(authInfo)
|
||||
coreContext.core.removeProxyConfig(cfg)
|
||||
proxyConfigToCheck = null
|
||||
}
|
||||
|
||||
fun continueEvenIfInvalidCredentials() {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
|
||||
fun createProxyConfig() {
|
||||
waitForServerAnswer.value = true
|
||||
coreContext.core.addListener(coreListener)
|
||||
|
||||
accountCreator.username = username.value
|
||||
accountCreator.password = password.value
|
||||
accountCreator.domain = domain.value
|
||||
accountCreator.displayName = displayName.value
|
||||
accountCreator.transport = transport.value
|
||||
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
proxyConfigToCheck = proxyConfig
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Generic Login] Account creator couldn't create proxy config")
|
||||
coreContext.core.removeListener(coreListener)
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
waitForServerAnswer.value = false
|
||||
return
|
||||
}
|
||||
|
||||
Log.i("[Assistant] [Generic Login] Proxy config created")
|
||||
}
|
||||
|
||||
private fun isLoginButtonEnabled(): Boolean {
|
||||
return username.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountCreationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountCreationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||
val username = MutableLiveData<String>()
|
||||
val useUsername = MutableLiveData<Boolean>()
|
||||
val usernameError = MutableLiveData<String>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val createEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val goToSmsValidationEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountExist(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Creation] onIsAccountExist status is $status")
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
||||
waitForServerAnswer.value = false
|
||||
if (useUsername.value == true) {
|
||||
usernameError.value = AppUtils.getString(R.string.assistant_error_username_already_exists)
|
||||
} else {
|
||||
phoneNumberError.value = AppUtils.getString(R.string.assistant_error_phone_number_already_exists)
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AccountNotExist -> {
|
||||
val createAccountStatus = creator.createAccount()
|
||||
Log.i("[Phone Account Creation] createAccount returned $createAccountStatus")
|
||||
if (createAccountStatus != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Creation] onCreateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountCreated -> {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
}
|
||||
AccountCreator.Status.AccountExistWithAlias -> {
|
||||
phoneNumberError.value = AppUtils.getString(R.string.assistant_error_phone_number_already_exists)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
useUsername.value = false
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
createEnabled.value = false
|
||||
createEnabled.addSource(prefix) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(phoneNumber) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(useUsername) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(username) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(usernameError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(phoneNumberError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun create() {
|
||||
accountCreator.displayName = displayName.value
|
||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||
if (useUsername.value == true) {
|
||||
accountCreator.username = username.value
|
||||
} else {
|
||||
accountCreator.username = accountCreator.phoneNumber
|
||||
}
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.isAccountExist
|
||||
Log.i("[Phone Account Creation] isAccountExist returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCreateButtonEnabled(): Boolean {
|
||||
val usernameRegexp = corePreferences.config.getString("assistant", "username_regex", "^[a-z0-9+_.\\-]*\$")
|
||||
return isPhoneNumberOk() && usernameRegexp != null &&
|
||||
(
|
||||
useUsername.value == false ||
|
||||
username.value.orEmpty().matches(Regex(usernameRegexp)) &&
|
||||
username.value.orEmpty().isNotEmpty() &&
|
||||
usernameError.value.orEmpty().isEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountLinkingViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountLinkingViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||
val username = MutableLiveData<String>()
|
||||
|
||||
val allowSkip = MutableLiveData<Boolean>()
|
||||
|
||||
val linkEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val goToSmsValidationEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAliasUsed(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Linking] onIsAliasUsed status is $status")
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AliasNotExist -> {
|
||||
if (creator.linkAccount() != AccountCreator.Status.RequestOk) {
|
||||
Log.e("[Phone Account Linking] linkAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AliasExist, AccountCreator.Status.AliasIsAccount -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLinkAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Linking] onLinkAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.RequestOk -> {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
linkEnabled.value = false
|
||||
linkEnabled.addSource(prefix) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
linkEnabled.addSource(phoneNumber) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
linkEnabled.addSource(phoneNumberError) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun link() {
|
||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||
accountCreator.username = username.value
|
||||
Log.i("[Assistant] [Phone Account Linking] Phone number is ${accountCreator.phoneNumber}")
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status: AccountCreator.Status = accountCreator.isAliasUsed
|
||||
Log.i("[Phone Account Linking] isAliasUsed returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
fun skip() {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
|
||||
private fun isLinkButtonEnabled(): Boolean {
|
||||
return isPhoneNumberOk()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.ProxyConfig
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountValidationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
val phoneNumber = MutableLiveData<String>()
|
||||
|
||||
val code = MutableLiveData<String>()
|
||||
|
||||
val isLogin = MutableLiveData<Boolean>()
|
||||
|
||||
val isCreation = MutableLiveData<Boolean>()
|
||||
|
||||
val isLinking = MutableLiveData<Boolean>()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onLoginLinphoneAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onLoginLinphoneAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.RequestOk) {
|
||||
if (createProxyConfig()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
}
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivateAlias(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onActivateAlias status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountActivated -> {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onActivateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.AccountActivated) {
|
||||
if (createProxyConfig()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
}
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
accountCreator.activationCode = code.value.orEmpty()
|
||||
Log.i("[Assistant] [Phone Account Validation] Phone number is ${accountCreator.phoneNumber} and activation code is ${accountCreator.activationCode}")
|
||||
waitForServerAnswer.value = true
|
||||
|
||||
val status = when {
|
||||
isLogin.value == true -> accountCreator.loginLinphoneAccount()
|
||||
isCreation.value == true -> accountCreator.activateAccount()
|
||||
isLinking.value == true -> accountCreator.activateAlias()
|
||||
else -> AccountCreator.Status.UnexpectedError
|
||||
}
|
||||
Log.i("[Assistant] [Phone Account Validation] Code validation result is $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProxyConfig(): Boolean {
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Phone Account Validation] Account creator couldn't create proxy config")
|
||||
return false
|
||||
}
|
||||
|
||||
proxyConfig.isPushNotificationAllowed = true
|
||||
Log.i("[Assistant] [Phone Account Validation] Proxy config created")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class QrCodeViewModel : ViewModel() {
|
||||
val qrCodeFoundEvent = MutableLiveData<Event<String>>()
|
||||
|
||||
val showSwitchCamera = MutableLiveData<Boolean>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onQrcodeFound(core: Core, result: String?) {
|
||||
Log.i("[QR Code] Found [$result]")
|
||||
if (result != null) qrCodeFoundEvent.postValue(Event(result))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
showSwitchCamera.value = coreContext.showSwitchCameraButton()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun setBackCamera() {
|
||||
showSwitchCamera.value = coreContext.showSwitchCameraButton()
|
||||
|
||||
for (camera in coreContext.core.videoDevicesList) {
|
||||
if (camera.contains("Back")) {
|
||||
Log.i("[QR Code] Found back facing camera: $camera")
|
||||
coreContext.core.videoDevice = camera
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val first = coreContext.core.videoDevicesList.firstOrNull()
|
||||
if (first != null) {
|
||||
Log.i("[QR Code] Using first camera found: $first")
|
||||
coreContext.core.videoDevice = first
|
||||
}
|
||||
}
|
||||
|
||||
fun switchCamera() {
|
||||
coreContext.switchCamera()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.ConfiguringState
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class RemoteProvisioningViewModel : ViewModel() {
|
||||
val urlToFetch = MutableLiveData<String>()
|
||||
val urlError = MutableLiveData<String>()
|
||||
|
||||
val fetchEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
val fetchInProgress = MutableLiveData<Boolean>()
|
||||
val fetchSuccessfulEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onConfiguringStatus(core: Core, status: ConfiguringState, message: String?) {
|
||||
fetchInProgress.value = false
|
||||
when (status) {
|
||||
ConfiguringState.Successful -> {
|
||||
fetchSuccessfulEvent.value = Event(true)
|
||||
}
|
||||
ConfiguringState.Failed -> {
|
||||
fetchSuccessfulEvent.value = Event(false)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
fetchInProgress.value = false
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
fetchEnabled.value = false
|
||||
fetchEnabled.addSource(urlToFetch) {
|
||||
fetchEnabled.value = isFetchEnabled()
|
||||
}
|
||||
fetchEnabled.addSource(urlError) {
|
||||
fetchEnabled.value = isFetchEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun fetchAndApply() {
|
||||
val url = urlToFetch.value.orEmpty()
|
||||
coreContext.core.provisioningUri = url
|
||||
Log.w("[Remote Provisioning] Url set to [$url], restarting Core")
|
||||
fetchInProgress.value = true
|
||||
coreContext.core.stop()
|
||||
coreContext.core.start()
|
||||
}
|
||||
|
||||
private fun isFetchEnabled(): Boolean {
|
||||
return urlToFetch.value.orEmpty().isNotEmpty() && urlError.value.orEmpty().isEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import java.util.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class SharedAssistantViewModel : ViewModel() {
|
||||
val remoteProvisioningUrl = MutableLiveData<String>()
|
||||
|
||||
private var accountCreator: AccountCreator
|
||||
private var useGenericSipAccount: Boolean = false
|
||||
|
||||
init {
|
||||
Log.i("[Assistant] Loading linphone default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
|
||||
accountCreator = coreContext.core.createAccountCreator(corePreferences.xmlRpcServerUrl)
|
||||
accountCreator.language = Locale.getDefault().language
|
||||
}
|
||||
|
||||
fun getAccountCreator(genericAccountCreator: Boolean = false): AccountCreator {
|
||||
if (genericAccountCreator != useGenericSipAccount) {
|
||||
accountCreator.reset()
|
||||
accountCreator.language = Locale.getDefault().language
|
||||
|
||||
if (genericAccountCreator) {
|
||||
Log.i("[Assistant] Loading default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.defaultValuesPath)
|
||||
} else {
|
||||
Log.i("[Assistant] Loading linphone default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
|
||||
}
|
||||
useGenericSipAccount = genericAccountCreator
|
||||
}
|
||||
return accountCreator
|
||||
}
|
||||
}
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.chat_bubble
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.*
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomListenerStub
|
||||
import org.linphone.core.EventLog
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatBubbleActivityBinding
|
||||
import org.linphone.utils.FileUtils
|
||||
|
||||
class ChatBubbleActivity : GenericActivity() {
|
||||
private lateinit var binding: ChatBubbleActivityBinding
|
||||
private lateinit var viewModel: ChatRoomViewModel
|
||||
private lateinit var listViewModel: ChatMessagesListViewModel
|
||||
private lateinit var chatSendingViewModel: ChatMessageSendingViewModel
|
||||
private lateinit var adapter: ChatMessagesListAdapter
|
||||
|
||||
private val observer = object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
if (positionStart == adapter.itemCount - itemCount) {
|
||||
adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array<out EventLog>) {
|
||||
chatRoom.markAsRead()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.chat_bubble_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
val localSipUri = intent.getStringExtra("LocalSipUri")
|
||||
val remoteSipUri = intent.getStringExtra("RemoteSipUri")
|
||||
var chatRoom: ChatRoom? = null
|
||||
|
||||
if (localSipUri != null && remoteSipUri != null) {
|
||||
Log.i("[Chat Bubble] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments")
|
||||
val localAddress = Factory.instance().createAddress(localSipUri)
|
||||
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
|
||||
chatRoom = coreContext.core.searchChatRoom(
|
||||
null, localAddress, remoteSipAddress,
|
||||
arrayOfNulls(
|
||||
0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Chat Bubble] Chat room is null, aborting!")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatRoomViewModelFactory(chatRoom)
|
||||
)[ChatRoomViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
listViewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatMessagesListViewModelFactory(chatRoom)
|
||||
)[ChatMessagesListViewModel::class.java]
|
||||
|
||||
chatSendingViewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatMessageSendingViewModelFactory(chatRoom)
|
||||
)[ChatMessageSendingViewModel::class.java]
|
||||
binding.chatSendingViewModel = chatSendingViewModel
|
||||
|
||||
val listSelectionViewModel = ViewModelProvider(this)[ListTopBarViewModel::class.java]
|
||||
adapter = ChatMessagesListAdapter(listSelectionViewModel, this)
|
||||
// SubmitList is done on a background thread
|
||||
// We need this adapter data observer to know when to scroll
|
||||
binding.chatMessagesList.adapter = adapter
|
||||
adapter.registerAdapterDataObserver(observer)
|
||||
|
||||
// Disable context menu on each message
|
||||
adapter.disableAdvancedContextMenuOptions()
|
||||
|
||||
adapter.openContentEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume { content ->
|
||||
if (content.isFileEncrypted) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
R.string.chat_bubble_cant_open_enrypted_file,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
FileUtils.openFileInThirdPartyApp(this, content.filePath.orEmpty(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val layoutManager = LinearLayoutManager(this)
|
||||
layoutManager.stackFromEnd = true
|
||||
binding.chatMessagesList.layoutManager = layoutManager
|
||||
|
||||
listViewModel.events.observe(
|
||||
this
|
||||
) { events ->
|
||||
adapter.submitList(events)
|
||||
}
|
||||
|
||||
chatSendingViewModel.textToSend.observe(
|
||||
this
|
||||
) {
|
||||
chatSendingViewModel.onTextToSendChanged(it)
|
||||
}
|
||||
|
||||
binding.setOpenAppClickListener {
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, false)
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.putExtra("RemoteSipUri", remoteSipUri)
|
||||
intent.putExtra("LocalSipUri", localSipUri)
|
||||
intent.putExtra("Chat", true)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK and Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.setCloseBubbleClickListener {
|
||||
coreContext.notificationsManager.dismissChatNotification(viewModel.chatRoom)
|
||||
}
|
||||
|
||||
binding.setSendMessageClickListener {
|
||||
chatSendingViewModel.sendMessage()
|
||||
binding.message.text?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
viewModel.chatRoom.addListener(listener)
|
||||
|
||||
// Workaround for the removed notification when a chat room is marked as read
|
||||
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, true)
|
||||
viewModel.chatRoom.markAsRead()
|
||||
|
||||
val peerAddress = viewModel.chatRoom.peerAddress.asStringUriOnly()
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = peerAddress
|
||||
coreContext.notificationsManager.resetChatNotificationCounterForSipUri(peerAddress)
|
||||
|
||||
lifecycleScope.launch {
|
||||
// Without the delay the scroll to bottom doesn't happen...
|
||||
delay(100)
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
viewModel.chatRoom.removeListener(listener)
|
||||
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, false)
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun scrollToBottom() {
|
||||
if (adapter.itemCount > 0) {
|
||||
binding.chatMessagesList.scrollToPosition(adapter.itemCount - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,529 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main
|
||||
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.window.layout.FoldingFeature
|
||||
import coil.imageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLDecoder
|
||||
import kotlin.math.abs
|
||||
import kotlinx.coroutines.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.*
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.main.viewmodels.CallOverlayViewModel
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.activities.navigateToDialer
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.CorePreferences
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.MainActivityBinding
|
||||
import org.linphone.utils.*
|
||||
|
||||
class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestinationChangedListener {
|
||||
private lateinit var binding: MainActivityBinding
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
private lateinit var callOverlayViewModel: CallOverlayViewModel
|
||||
|
||||
private val listener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
Log.i("[Main Activity] Contact(s) updated, update shortcuts")
|
||||
if (corePreferences.contactsShortcuts) {
|
||||
ShortcutsHelper.createShortcutsToContacts(this@MainActivity)
|
||||
} else if (corePreferences.chatRoomShortcuts) {
|
||||
ShortcutsHelper.createShortcutsToChatRooms(this@MainActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var tabsFragment: FragmentContainerView
|
||||
private lateinit var statusFragment: FragmentContainerView
|
||||
|
||||
private var overlayX = 0f
|
||||
private var overlayY = 0f
|
||||
private var initPosX = 0f
|
||||
private var initPosY = 0f
|
||||
private var overlay: View? = null
|
||||
|
||||
private val componentCallbacks = object : ComponentCallbacks2 {
|
||||
override fun onConfigurationChanged(newConfig: Configuration) { }
|
||||
|
||||
override fun onLowMemory() {
|
||||
Log.w("[Main Activity] onLowMemory !")
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
Log.w("[Main Activity] onTrimMemory called with level $level !")
|
||||
applicationContext.imageLoader.memoryCache?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
|
||||
sharedViewModel.layoutChangedEvent.value = Event(true)
|
||||
}
|
||||
|
||||
private var shouldTabsBeVisibleDependingOnDestination = true
|
||||
private var shouldTabsBeVisibleDueToOrientationAndKeyboard = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Must be done before the setContentView
|
||||
installSplashScreen()
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = ViewModelProvider(this)[SharedMainViewModel::class.java]
|
||||
binding.viewModel = sharedViewModel
|
||||
|
||||
callOverlayViewModel = ViewModelProvider(this)[CallOverlayViewModel::class.java]
|
||||
binding.callOverlayViewModel = callOverlayViewModel
|
||||
|
||||
sharedViewModel.toggleDrawerEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume {
|
||||
if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) {
|
||||
binding.sideMenu.closeDrawer(binding.sideMenuContent, true)
|
||||
} else {
|
||||
binding.sideMenu.openDrawer(binding.sideMenuContent, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coreContext.callErrorMessageResourceId.observe(
|
||||
this
|
||||
) {
|
||||
it.consume { message ->
|
||||
showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
if (coreContext.core.accountList.isEmpty()) {
|
||||
if (corePreferences.firstStart) {
|
||||
startActivity(Intent(this, AssistantActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
tabsFragment = findViewById(R.id.tabs_fragment)
|
||||
statusFragment = findViewById(R.id.status_fragment)
|
||||
|
||||
binding.root.doOnAttach {
|
||||
Log.i("[Main Activity] Report UI has been fully drawn (TTFD)")
|
||||
try {
|
||||
reportFullyDrawn()
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("[Main Activity] Security exception when doing reportFullyDrawn(): $se")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
if (intent != null) handleIntentParams(intent)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
coreContext.contactsManager.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
coreContext.contactsManager.removeListener(listener)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int) {
|
||||
Snackbar.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int, action: Int, listener: () -> Unit) {
|
||||
Snackbar
|
||||
.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG)
|
||||
.setAction(action) {
|
||||
Log.i("[Snack Bar] Action listener triggered")
|
||||
listener()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(message: String) {
|
||||
Snackbar.make(findViewById(R.id.coordinator), message, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
|
||||
registerComponentCallbacks(componentCallbacks)
|
||||
findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener(this)
|
||||
|
||||
binding.rootCoordinatorLayout.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
||||
val keyboardVisible = ViewCompat.getRootWindowInsets(binding.rootCoordinatorLayout)
|
||||
?.isVisible(WindowInsetsCompat.Type.ime()) == true
|
||||
Log.d("[Tabs Fragment] Keyboard is ${if (keyboardVisible) "visible" else "invisible"}")
|
||||
shouldTabsBeVisibleDueToOrientationAndKeyboard = !portraitOrientation || !keyboardVisible
|
||||
updateTabsFragmentVisibility()
|
||||
}
|
||||
|
||||
initOverlay()
|
||||
|
||||
if (intent != null) handleIntentParams(intent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
findNavController(R.id.nav_host_fragment).removeOnDestinationChangedListener(this)
|
||||
unregisterComponentCallbacks(componentCallbacks)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onDestinationChanged(
|
||||
controller: NavController,
|
||||
destination: NavDestination,
|
||||
arguments: Bundle?
|
||||
) {
|
||||
hideKeyboard()
|
||||
if (statusFragment.visibility == View.GONE) {
|
||||
statusFragment.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
shouldTabsBeVisibleDependingOnDestination = when (destination.id) {
|
||||
R.id.masterCallLogsFragment, R.id.masterContactsFragment, R.id.dialerFragment, R.id.masterChatRoomsFragment ->
|
||||
true
|
||||
else -> false
|
||||
}
|
||||
updateTabsFragmentVisibility()
|
||||
}
|
||||
|
||||
fun hideKeyboard() {
|
||||
currentFocus?.hideKeyboard()
|
||||
}
|
||||
|
||||
fun hideStatusFragment(hide: Boolean) {
|
||||
statusFragment.visibility = if (hide) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
private fun updateTabsFragmentVisibility() {
|
||||
tabsFragment.visibility = if (shouldTabsBeVisibleDependingOnDestination && shouldTabsBeVisibleDueToOrientationAndKeyboard) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun handleIntentParams(intent: Intent) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_MAIN -> {
|
||||
val core = coreContext.core
|
||||
val call = core.currentCall ?: core.calls.firstOrNull()
|
||||
if (call != null) {
|
||||
Log.i("[Main Activity] Launcher clicked while there is at least one active call, go to CallActivity")
|
||||
val callIntent = Intent(this, org.linphone.activities.voip.CallActivity::class.java)
|
||||
callIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
startActivity(callIntent)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SEND, Intent.ACTION_SENDTO -> {
|
||||
if (intent.type == "text/plain") {
|
||||
handleSendText(intent)
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
handleSendFile(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SEND_MULTIPLE -> {
|
||||
lifecycleScope.launch {
|
||||
handleSendMultipleFiles(intent)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
val uri = intent.data
|
||||
if (intent.type == AppUtils.getString(R.string.linphone_address_mime_type)) {
|
||||
if (uri != null) {
|
||||
val contactId = coreContext.contactsManager.getAndroidContactIdFromUri(uri)
|
||||
if (contactId != null) {
|
||||
Log.i("[Main Activity] Found contact URI parameter in intent: $uri")
|
||||
navigateToContact(contactId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (uri != null) {
|
||||
handleTelOrSipUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_DIAL, Intent.ACTION_CALL -> {
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
handleTelOrSipUri(uri)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_VIEW_LOCUS -> {
|
||||
if (corePreferences.disableChat) return
|
||||
val locus = Compatibility.extractLocusIdFromIntent(intent)
|
||||
if (locus != null) {
|
||||
Log.i("[Main Activity] Found chat room locus intent extra: $locus")
|
||||
handleLocusOrShortcut(locus)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
when {
|
||||
intent.hasExtra("ContactId") -> {
|
||||
val id = intent.getStringExtra("ContactId")
|
||||
Log.i("[Main Activity] Found contact ID in extras: $id")
|
||||
navigateToContact(id)
|
||||
}
|
||||
intent.hasExtra("Chat") -> {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
if (intent.hasExtra("RemoteSipUri") && intent.hasExtra("LocalSipUri")) {
|
||||
val peerAddress = intent.getStringExtra("RemoteSipUri")
|
||||
val localAddress = intent.getStringExtra("LocalSipUri")
|
||||
Log.i("[Main Activity] Found chat room intent extra: local SIP URI=[$localAddress], peer SIP URI=[$peerAddress]")
|
||||
navigateToChatRoom(localAddress, peerAddress)
|
||||
} else {
|
||||
Log.i("[Main Activity] Found chat intent extra, go to chat rooms list")
|
||||
navigateToChatRooms()
|
||||
}
|
||||
}
|
||||
intent.hasExtra("Dialer") -> {
|
||||
Log.i("[Main Activity] Found dialer intent extra, go to dialer")
|
||||
val args = Bundle()
|
||||
args.putBoolean("Transfer", intent.getBooleanExtra("Transfer", false))
|
||||
navigateToDialer(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent this intent to be processed again
|
||||
intent.action = null
|
||||
intent.data = null
|
||||
intent.extras?.clear()
|
||||
}
|
||||
|
||||
private fun handleTelOrSipUri(uri: Uri) {
|
||||
Log.i("[Main Activity] Found uri: $uri to call")
|
||||
val stringUri = uri.toString()
|
||||
var addressToCall: String = stringUri
|
||||
|
||||
when {
|
||||
addressToCall.startsWith("tel:") -> {
|
||||
Log.i("[Main Activity] Removing tel: prefix")
|
||||
addressToCall = addressToCall.substring("tel:".length)
|
||||
}
|
||||
addressToCall.startsWith("linphone:") -> {
|
||||
Log.i("[Main Activity] Removing linphone: prefix")
|
||||
addressToCall = addressToCall.substring("linphone:".length)
|
||||
}
|
||||
addressToCall.startsWith("sip-linphone:") -> {
|
||||
Log.i("[Main Activity] Removing linphone: sip-linphone")
|
||||
addressToCall = addressToCall.substring("sip-linphone:".length)
|
||||
}
|
||||
}
|
||||
|
||||
val address = coreContext.core.interpretUrl(addressToCall, LinphoneUtils.applyInternationalPrefix())
|
||||
if (address != null) {
|
||||
addressToCall = address.asStringUriOnly()
|
||||
}
|
||||
|
||||
Log.i("[Main Activity] Starting dialer with pre-filled URI $addressToCall")
|
||||
val args = Bundle()
|
||||
args.putString("URI", addressToCall)
|
||||
navigateToDialer(args)
|
||||
}
|
||||
|
||||
private fun handleSendText(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
sharedViewModel.textToShare.value = it
|
||||
}
|
||||
|
||||
handleSendChatRoom(intent)
|
||||
}
|
||||
|
||||
private suspend fun handleSendFile(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
Log.i("[Main Activity] Found single file to share with type ${intent.type}")
|
||||
|
||||
(intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||
val list = arrayListOf<String>()
|
||||
coroutineScope {
|
||||
val deferred = async {
|
||||
FileUtils.getFilePath(this@MainActivity, it)
|
||||
}
|
||||
val path = deferred.await()
|
||||
if (path != null) {
|
||||
list.add(path)
|
||||
Log.i("[Main Activity] Found single file to share: $path")
|
||||
}
|
||||
}
|
||||
sharedViewModel.filesToShare.value = list
|
||||
}
|
||||
|
||||
// Check that the current fragment hasn't already handled the event on filesToShare
|
||||
// If it has, don't go further.
|
||||
// For example this may happen when picking a GIF from the keyboard while inside a chat room
|
||||
if (!sharedViewModel.filesToShare.value.isNullOrEmpty()) {
|
||||
handleSendChatRoom(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleSendMultipleFiles(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM)?.let {
|
||||
val list = arrayListOf<String>()
|
||||
coroutineScope {
|
||||
val deferred = arrayListOf<Deferred<String?>>()
|
||||
for (parcelable in it) {
|
||||
val uri = parcelable as Uri
|
||||
deferred.add(async { FileUtils.getFilePath(this@MainActivity, uri) })
|
||||
}
|
||||
val paths = deferred.awaitAll()
|
||||
for (path in paths) {
|
||||
Log.i("[Main Activity] Found file to share: $path")
|
||||
if (path != null) list.add(path)
|
||||
}
|
||||
}
|
||||
sharedViewModel.filesToShare.value = list
|
||||
}
|
||||
|
||||
handleSendChatRoom(intent)
|
||||
}
|
||||
|
||||
private fun handleSendChatRoom(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
Log.i("[Main Activity] Found uri: $uri to send a message to")
|
||||
val stringUri = uri.toString()
|
||||
var addressToIM: String = stringUri
|
||||
try {
|
||||
addressToIM = URLDecoder.decode(stringUri, "UTF-8")
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
Log.e("[Main Activity] UnsupportedEncodingException: $e")
|
||||
}
|
||||
|
||||
when {
|
||||
addressToIM.startsWith("sms:") ->
|
||||
addressToIM = addressToIM.substring("sms:".length)
|
||||
addressToIM.startsWith("smsto:") ->
|
||||
addressToIM = addressToIM.substring("smsto:".length)
|
||||
addressToIM.startsWith("mms:") ->
|
||||
addressToIM = addressToIM.substring("mms:".length)
|
||||
addressToIM.startsWith("mmsto:") ->
|
||||
addressToIM = addressToIM.substring("mmsto:".length)
|
||||
}
|
||||
|
||||
val localAddress =
|
||||
coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly()
|
||||
val peerAddress = coreContext.core.interpretUrl(addressToIM, LinphoneUtils.applyInternationalPrefix())?.asStringUriOnly()
|
||||
Log.i("[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses")
|
||||
navigateToChatRoom(localAddress, peerAddress)
|
||||
} else {
|
||||
val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID
|
||||
if (shortcutId != null) {
|
||||
Log.i("[Main Activity] Found shortcut ID: $shortcutId")
|
||||
handleLocusOrShortcut(shortcutId)
|
||||
} else {
|
||||
Log.i("[Main Activity] Going into chat rooms list")
|
||||
navigateToChatRooms()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLocusOrShortcut(id: String) {
|
||||
val split = id.split("~")
|
||||
if (split.size == 2) {
|
||||
val localAddress = split[0]
|
||||
val peerAddress = split[1]
|
||||
Log.i("[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses, computed from shortcut/locus id")
|
||||
navigateToChatRoom(localAddress, peerAddress)
|
||||
} else {
|
||||
Log.e("[Main Activity] Failed to parse shortcut/locus id: $id, going to chat rooms list")
|
||||
navigateToChatRooms()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initOverlay() {
|
||||
overlay = binding.root.findViewById(R.id.call_overlay)
|
||||
val callOverlay = overlay
|
||||
callOverlay ?: return
|
||||
|
||||
callOverlay.setOnTouchListener { view, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
overlayX = view.x - event.rawX
|
||||
overlayY = view.y - event.rawY
|
||||
initPosX = view.x
|
||||
initPosY = view.y
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
view.animate()
|
||||
.x(event.rawX + overlayX)
|
||||
.y(event.rawY + overlayY)
|
||||
.setDuration(0)
|
||||
.start()
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (abs(initPosX - view.x) < CorePreferences.OVERLAY_CLICK_SENSITIVITY &&
|
||||
abs(initPosY - view.y) < CorePreferences.OVERLAY_CLICK_SENSITIVITY
|
||||
) {
|
||||
view.performClick()
|
||||
}
|
||||
}
|
||||
else -> return@setOnTouchListener false
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
callOverlay.setOnClickListener {
|
||||
coreContext.onCallOverlayClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.about
|
||||
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AboutFragmentBinding
|
||||
|
||||
class AboutFragment : SecureFragment<AboutFragmentBinding>() {
|
||||
private lateinit var viewModel: AboutViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.about_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel = ViewModelProvider(this)[AboutViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setPrivacyPolicyClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_privacy_policy_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("[About] Failed to start browser intent, $se")
|
||||
}
|
||||
}
|
||||
|
||||
binding.setLicenseClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_license_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("[About] Failed to start browser intent, $se")
|
||||
}
|
||||
}
|
||||
|
||||
binding.setWeblateClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_weblate_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("[About] Failed to start browser intent, $se")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.about
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
|
||||
class AboutViewModel : ViewModel() {
|
||||
val appVersion: String = coreContext.appVersion
|
||||
|
||||
val sdkVersion: String = coreContext.sdkVersion
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.adapters
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
|
||||
abstract class SelectionListAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
diff: DiffUtil.ItemCallback<T>
|
||||
) :
|
||||
ListAdapter<T, VH>(diff) {
|
||||
|
||||
private var _selectionViewModel: ListTopBarViewModel? = selectionVM
|
||||
protected val selectionViewModel get() = _selectionViewModel!!
|
||||
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView)
|
||||
_selectionViewModel = null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,519 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupWindow
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.adapters.SelectionListAdapter
|
||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||
import org.linphone.activities.main.chat.data.EventData
|
||||
import org.linphone.activities.main.chat.data.EventLogData
|
||||
import org.linphone.activities.main.chat.data.OnContentClickedListener
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatEventListCellBinding
|
||||
import org.linphone.databinding.ChatMessageListCellBinding
|
||||
import org.linphone.databinding.ChatMessageLongPressMenuBindingImpl
|
||||
import org.linphone.databinding.ChatUnreadMessagesListHeaderBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.HeaderAdapter
|
||||
|
||||
class ChatMessagesListAdapter(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : SelectionListAdapter<EventLogData, RecyclerView.ViewHolder>(selectionVM, ChatMessageDiffCallback()),
|
||||
HeaderAdapter {
|
||||
companion object {
|
||||
const val MAX_TIME_TO_GROUP_MESSAGES = 60 // 1 minute
|
||||
}
|
||||
|
||||
val resendMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val deleteMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val forwardMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val replyMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val showImdnForMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val addSipUriToContactEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val openContentEvent: MutableLiveData<Event<Content>> by lazy {
|
||||
MutableLiveData<Event<Content>>()
|
||||
}
|
||||
|
||||
val urlClickEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val sipUriClickedEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val callConferenceEvent: MutableLiveData<Event<Pair<String, String?>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String?>>>()
|
||||
}
|
||||
|
||||
val scrollToChatMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val errorEvent: MutableLiveData<Event<Int>> by lazy {
|
||||
MutableLiveData<Event<Int>>()
|
||||
}
|
||||
|
||||
private val contentClickedListener = object : OnContentClickedListener {
|
||||
override fun onContentClicked(content: Content) {
|
||||
openContentEvent.value = Event(content)
|
||||
}
|
||||
|
||||
override fun onWebUrlClicked(url: String) {
|
||||
if (popup?.isShowing == true) {
|
||||
Log.w("[Chat Message Data] Long press that displayed context menu detected, aborting click on URL [$url]")
|
||||
return
|
||||
}
|
||||
urlClickEvent.value = Event(url)
|
||||
}
|
||||
|
||||
override fun onSipAddressClicked(sipUri: String) {
|
||||
if (popup?.isShowing == true) {
|
||||
Log.w("[Chat Message Data] Long press that displayed context menu detected, aborting click on SIP URI [$sipUri]")
|
||||
return
|
||||
}
|
||||
sipUriClickedEvent.value = Event(sipUri)
|
||||
}
|
||||
|
||||
override fun onCallConference(address: String, subject: String?) {
|
||||
callConferenceEvent.value = Event(Pair(address, subject))
|
||||
}
|
||||
|
||||
override fun onError(messageId: Int) {
|
||||
errorEvent.value = Event(messageId)
|
||||
}
|
||||
}
|
||||
|
||||
private var advancedContextMenuOptionsDisabled: Boolean = false
|
||||
private var popup: PopupWindow? = null
|
||||
|
||||
private var unreadMessagesCount: Int = 0
|
||||
private var firstUnreadMessagePosition: Int = -1
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
EventLog.Type.ConferenceChatMessage.toInt() -> createChatMessageViewHolder(parent)
|
||||
else -> createEventViewHolder(parent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChatMessageViewHolder(parent: ViewGroup): ChatMessageViewHolder {
|
||||
val binding: ChatMessageListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_message_list_cell, parent, false
|
||||
)
|
||||
return ChatMessageViewHolder(binding)
|
||||
}
|
||||
|
||||
private fun createEventViewHolder(parent: ViewGroup): EventViewHolder {
|
||||
val binding: ChatEventListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_event_list_cell, parent, false
|
||||
)
|
||||
return EventViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val eventLog = getItem(position)
|
||||
when (holder) {
|
||||
is ChatMessageViewHolder -> holder.bind(eventLog)
|
||||
is EventViewHolder -> holder.bind(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val eventLog = getItem(position)
|
||||
return eventLog.eventLog.type.toInt()
|
||||
}
|
||||
|
||||
override fun onCurrentListChanged(
|
||||
previousList: MutableList<EventLogData>,
|
||||
currentList: MutableList<EventLogData>
|
||||
) {
|
||||
// Need to wait for messages to be added before computing new first unread message position
|
||||
firstUnreadMessagePosition = -1
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
if (unreadMessagesCount > 0 && firstUnreadMessagePosition == -1) {
|
||||
computeFirstUnreadMessagePosition()
|
||||
}
|
||||
return position == firstUnreadMessagePosition
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
val binding: ChatUnreadMessagesListHeaderBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.chat_unread_messages_list_header, null, false
|
||||
)
|
||||
binding.title = AppUtils.getStringWithPlural(R.plurals.chat_room_unread_messages_event, unreadMessagesCount)
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun disableAdvancedContextMenuOptions() {
|
||||
advancedContextMenuOptionsDisabled = true
|
||||
}
|
||||
|
||||
fun setUnreadMessageCount(count: Int, forceUpdate: Boolean) {
|
||||
// Once list has been filled once, don't show the unread message header
|
||||
// when new messages are added to the history whilst it is visible
|
||||
unreadMessagesCount = if (itemCount == 0 || forceUpdate) count else 0
|
||||
firstUnreadMessagePosition = -1
|
||||
}
|
||||
|
||||
fun getFirstUnreadMessagePosition(): Int {
|
||||
return firstUnreadMessagePosition
|
||||
}
|
||||
|
||||
private fun computeFirstUnreadMessagePosition() {
|
||||
if (unreadMessagesCount > 0) {
|
||||
var messageCount = 0
|
||||
for (position in itemCount - 1 downTo 0) {
|
||||
val eventLog = getItem(position)
|
||||
val data = eventLog.data
|
||||
if (data is ChatMessageData) {
|
||||
messageCount += 1
|
||||
if (messageCount == unreadMessagesCount) {
|
||||
firstUnreadMessagePosition = position
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ChatMessageViewHolder(
|
||||
val binding: ChatMessageListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(eventLog: EventLogData) {
|
||||
with(binding) {
|
||||
if (eventLog.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessageViewModel = eventLog.data as ChatMessageData
|
||||
chatMessageViewModel.setContentClickListener(contentClickedListener)
|
||||
|
||||
val chatMessage = chatMessageViewModel.chatMessage
|
||||
data = chatMessageViewModel
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
setReplyClickListener {
|
||||
val reply = chatMessageViewModel.replyData.value?.chatMessage
|
||||
if (reply != null) {
|
||||
scrollToChatMessageEvent.value = Event(reply)
|
||||
}
|
||||
}
|
||||
|
||||
// Grouping
|
||||
var hasPrevious = false
|
||||
var hasNext = false
|
||||
|
||||
if (bindingAdapterPosition > 0) {
|
||||
val previousItem = getItem(bindingAdapterPosition - 1)
|
||||
if (previousItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val previousMessage = previousItem.eventLog.chatMessage
|
||||
if (previousMessage != null && previousMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
|
||||
if (chatMessage.time - previousMessage.time < MAX_TIME_TO_GROUP_MESSAGES) {
|
||||
hasPrevious = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bindingAdapterPosition >= 0 && bindingAdapterPosition < itemCount - 1) {
|
||||
val nextItem = getItem(bindingAdapterPosition + 1)
|
||||
if (nextItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val nextMessage = nextItem.eventLog.chatMessage
|
||||
if (nextMessage != null && nextMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
|
||||
if (nextMessage.time - chatMessage.time < MAX_TIME_TO_GROUP_MESSAGES) {
|
||||
hasNext = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chatMessageViewModel.updateBubbleBackground(hasPrevious, hasNext)
|
||||
|
||||
executePendingBindings()
|
||||
|
||||
setContextMenuClickListener {
|
||||
val popupView: ChatMessageLongPressMenuBindingImpl = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(root.context),
|
||||
R.layout.chat_message_long_press_menu, null, false
|
||||
)
|
||||
|
||||
val itemSize = AppUtils.getDimension(R.dimen.chat_message_popup_item_height).toInt()
|
||||
var totalSize = itemSize * 7
|
||||
if (chatMessage.chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
|
||||
// No message id
|
||||
popupView.imdnHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.state != ChatMessage.State.NotDelivered) {
|
||||
popupView.resendHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.contents.find { content -> content.isText } == null) {
|
||||
popupView.copyTextHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.isOutgoing ||
|
||||
chatMessageViewModel.contact.value != null ||
|
||||
advancedContextMenuOptionsDisabled
|
||||
) {
|
||||
popupView.addToContactsHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.chatRoom.isReadOnly) {
|
||||
popupView.replyHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (advancedContextMenuOptionsDisabled) {
|
||||
popupView.forwardHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
|
||||
// When using WRAP_CONTENT instead of real size, fails to place the
|
||||
// popup window above if not enough space is available below
|
||||
val popupWindow = PopupWindow(
|
||||
popupView.root,
|
||||
AppUtils.getDimension(R.dimen.chat_message_popup_width).toInt(),
|
||||
totalSize,
|
||||
true
|
||||
)
|
||||
popup = popupWindow
|
||||
|
||||
// Elevation is for showing a shadow around the popup
|
||||
popupWindow.elevation = 20f
|
||||
|
||||
popupView.setResendClickListener {
|
||||
resendMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setCopyTextClickListener {
|
||||
copyTextToClipboard()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setForwardClickListener {
|
||||
forwardMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setReplyClickListener {
|
||||
replyMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setImdnClickListener {
|
||||
showImdnDeliveryFragment()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setAddToContactsClickListener {
|
||||
addSenderToContacts()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setDeleteClickListener {
|
||||
deleteMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
|
||||
val gravity = if (chatMessage.isOutgoing) Gravity.END else Gravity.START
|
||||
popupWindow.showAsDropDown(background, 0, 0, gravity or Gravity.TOP)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resendMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
chatMessage.userData = bindingAdapterPosition
|
||||
resendMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyTextToClipboard() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
val content = chatMessage.contents.find { content -> content.isText }
|
||||
if (content != null) {
|
||||
val clipboard: ClipboardManager =
|
||||
coreContext.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Message", content.utf8Text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun forwardMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
forwardMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun replyMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
replyMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showImdnDeliveryFragment() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
showImdnForMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
chatMessage.userData = bindingAdapterPosition
|
||||
deleteMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSenderToContacts() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
val copy = chatMessage.fromAddress.clone()
|
||||
copy.clean() // To remove gruu if any
|
||||
addSipUriToContactEvent.value = Event(copy.asStringUriOnly())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class EventViewHolder(
|
||||
private val binding: ChatEventListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(eventLog: EventLogData) {
|
||||
with(binding) {
|
||||
val eventViewModel = eventLog.data as EventData
|
||||
data = eventViewModel
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
binding.setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ChatMessageDiffCallback : DiffUtil.ItemCallback<EventLogData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: EventLogData,
|
||||
newItem: EventLogData
|
||||
): Boolean {
|
||||
return if (oldItem.type == EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.type == EventLog.Type.ConferenceChatMessage
|
||||
) {
|
||||
val oldData = (oldItem.data as ChatMessageData)
|
||||
val newData = (newItem.data as ChatMessageData)
|
||||
|
||||
oldData.time.value == newData.time.value &&
|
||||
oldData.isOutgoing == newData.isOutgoing
|
||||
} else oldItem.notifyId == newItem.notifyId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: EventLogData,
|
||||
newItem: EventLogData
|
||||
): Boolean {
|
||||
return if (oldItem.type == EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.type == EventLog.Type.ConferenceChatMessage
|
||||
) {
|
||||
val oldData = (oldItem.data as ChatMessageData)
|
||||
val newData = (newItem.data as ChatMessageData)
|
||||
|
||||
val previous = oldData.hasPreviousMessage == newData.hasPreviousMessage
|
||||
val next = oldData.hasNextMessage == newData.hasNextMessage
|
||||
val isDisplayed = newData.isDisplayed.value == true
|
||||
isDisplayed && previous && next
|
||||
} else {
|
||||
oldItem.type != EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.type != EventLog.Type.ConferenceChatMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.adapters.SelectionListAdapter
|
||||
import org.linphone.activities.main.chat.data.ChatRoomData
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.databinding.ChatRoomListCellBinding
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class ChatRoomsListAdapter(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : SelectionListAdapter<ChatRoom, RecyclerView.ViewHolder>(selectionVM, ChatRoomDiffCallback()) {
|
||||
val selectedChatRoomEvent: MutableLiveData<Event<ChatRoom>> by lazy {
|
||||
MutableLiveData<Event<ChatRoom>>()
|
||||
}
|
||||
|
||||
private var isForwardPending = false
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_list_cell, parent, false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
fun forwardPending(pending: Boolean) {
|
||||
isForwardPending = pending
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ChatRoomListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(chatRoom: ChatRoom) {
|
||||
with(binding) {
|
||||
data = ChatRoomData(chatRoom)
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
forwardPending = isForwardPending
|
||||
|
||||
setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
} else {
|
||||
selectedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
setLongClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == false) {
|
||||
selectionViewModel.isEditionEnabled.value = true
|
||||
// Selection will be handled by click listener
|
||||
true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ChatRoomDiffCallback : DiffUtil.ItemCallback<ChatRoom>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ChatRoom,
|
||||
newItem: ChatRoom
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ChatRoom,
|
||||
newItem: ChatRoom
|
||||
): Boolean {
|
||||
return false // To force redraw
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.activities.main.chat.data.GroupInfoParticipantData
|
||||
import org.linphone.databinding.ChatRoomGroupInfoParticipantCellBinding
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class GroupInfoParticipantsAdapter(
|
||||
private val viewLifecycleOwner: LifecycleOwner,
|
||||
private val isEncryptionEnabled: Boolean
|
||||
) : ListAdapter<GroupInfoParticipantData, RecyclerView.ViewHolder>(ParticipantDiffCallback()) {
|
||||
private var showAdmin: Boolean = false
|
||||
|
||||
val participantRemovedEvent: MutableLiveData<Event<GroupChatRoomMember>> by lazy {
|
||||
MutableLiveData<Event<GroupChatRoomMember>>()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomGroupInfoParticipantCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_group_info_participant_cell, parent, false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
fun showAdminControls(show: Boolean) {
|
||||
showAdmin = show
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: ChatRoomGroupInfoParticipantCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(participantViewModel: GroupInfoParticipantData) {
|
||||
with(binding) {
|
||||
participantViewModel.showAdminControls.value = showAdmin
|
||||
data = participantViewModel
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
setRemoveClickListener {
|
||||
participantRemovedEvent.value = Event(participantViewModel.participant)
|
||||
}
|
||||
isEncrypted = isEncryptionEnabled
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ParticipantDiffCallback : DiffUtil.ItemCallback<GroupInfoParticipantData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: GroupInfoParticipantData,
|
||||
newItem: GroupInfoParticipantData
|
||||
): Boolean {
|
||||
return oldItem.sipUri == newItem.sipUri
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: GroupInfoParticipantData,
|
||||
newItem: GroupInfoParticipantData
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.data.ImdnParticipantData
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.databinding.ChatRoomImdnParticipantCellBinding
|
||||
import org.linphone.databinding.ImdnListHeaderBinding
|
||||
import org.linphone.utils.HeaderAdapter
|
||||
|
||||
class ImdnAdapter(
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : ListAdapter<ImdnParticipantData, RecyclerView.ViewHolder>(ParticipantImdnStateDiffCallback()), HeaderAdapter {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomImdnParticipantCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_imdn_participant_cell, parent, false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: ChatRoomImdnParticipantCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(participantImdnData: ImdnParticipantData) {
|
||||
with(binding) {
|
||||
data = participantImdnData
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
if (position >= itemCount) return false
|
||||
val participantImdnState = getItem(position)
|
||||
val previousPosition = position - 1
|
||||
return if (previousPosition >= 0) {
|
||||
getItem(previousPosition).imdnState.state != participantImdnState.imdnState.state
|
||||
} else true
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
val participantImdnState = getItem(position).imdnState
|
||||
val binding: ImdnListHeaderBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.imdn_list_header, null, false
|
||||
)
|
||||
when (participantImdnState.state) {
|
||||
ChatMessage.State.Displayed -> {
|
||||
binding.title = R.string.chat_message_imdn_displayed
|
||||
binding.textColor = R.color.imdn_read_color
|
||||
binding.icon = R.drawable.chat_read
|
||||
}
|
||||
ChatMessage.State.DeliveredToUser -> {
|
||||
binding.title = R.string.chat_message_imdn_delivered
|
||||
binding.textColor = R.color.grey_color
|
||||
binding.icon = R.drawable.chat_delivered
|
||||
}
|
||||
ChatMessage.State.Delivered -> {
|
||||
binding.title = R.string.chat_message_imdn_sent
|
||||
binding.textColor = R.color.grey_color
|
||||
binding.icon = R.drawable.chat_delivered
|
||||
}
|
||||
ChatMessage.State.NotDelivered -> {
|
||||
binding.title = R.string.chat_message_imdn_undelivered
|
||||
binding.textColor = R.color.red_color
|
||||
binding.icon = R.drawable.chat_error
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
|
||||
private class ParticipantImdnStateDiffCallback : DiffUtil.ItemCallback<ImdnParticipantData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ImdnParticipantData,
|
||||
newItem: ImdnParticipantData
|
||||
): Boolean {
|
||||
return oldItem.sipUri == newItem.sipUri
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ImdnParticipantData,
|
||||
newItem: ImdnParticipantData
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.data
|
||||
|
||||
import org.linphone.utils.FileUtils
|
||||
|
||||
class ChatMessageAttachmentData(
|
||||
val path: String,
|
||||
private val deleteCallback: (attachment: ChatMessageAttachmentData) -> Unit
|
||||
) {
|
||||
val fileName: String = FileUtils.getNameFromFilePath(path)
|
||||
val isImage: Boolean = FileUtils.isExtensionImage(path)
|
||||
val isVideo: Boolean = FileUtils.isExtensionVideo(path)
|
||||
val isAudio: Boolean = FileUtils.isExtensionAudio(path)
|
||||
val isPdf: Boolean = FileUtils.isExtensionPdf(path)
|
||||
|
||||
fun delete() {
|
||||
deleteCallback(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,481 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.data
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.UnderlineSpan
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileReader
|
||||
import java.lang.StringBuilder
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatMessageContentData(
|
||||
private val chatMessage: ChatMessage,
|
||||
private val contentIndex: Int,
|
||||
) {
|
||||
var listener: OnContentClickedListener? = null
|
||||
|
||||
val isOutgoing = chatMessage.isOutgoing
|
||||
|
||||
val isImage = MutableLiveData<Boolean>()
|
||||
val isVideo = MutableLiveData<Boolean>()
|
||||
val isAudio = MutableLiveData<Boolean>()
|
||||
val isPdf = MutableLiveData<Boolean>()
|
||||
val isGenericFile = MutableLiveData<Boolean>()
|
||||
val isVoiceRecording = MutableLiveData<Boolean>()
|
||||
val isConferenceSchedule = MutableLiveData<Boolean>()
|
||||
val isConferenceUpdated = MutableLiveData<Boolean>()
|
||||
val isConferenceCancelled = MutableLiveData<Boolean>()
|
||||
|
||||
val fileName = MutableLiveData<String>()
|
||||
val filePath = MutableLiveData<String>()
|
||||
|
||||
val downloadable = MutableLiveData<Boolean>()
|
||||
val downloadEnabled = MutableLiveData<Boolean>()
|
||||
val downloadProgressInt = MutableLiveData<Int>()
|
||||
val downloadProgressString = MutableLiveData<String>()
|
||||
val downloadLabel = MutableLiveData<Spannable>()
|
||||
|
||||
val voiceRecordDuration = MutableLiveData<Int>()
|
||||
val formattedDuration = MutableLiveData<String>()
|
||||
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
||||
val isVoiceRecordPlaying = MutableLiveData<Boolean>()
|
||||
|
||||
val conferenceSubject = MutableLiveData<String>()
|
||||
val conferenceDescription = MutableLiveData<String>()
|
||||
val conferenceParticipantCount = MutableLiveData<String>()
|
||||
val conferenceDate = MutableLiveData<String>()
|
||||
val conferenceTime = MutableLiveData<String>()
|
||||
val conferenceDuration = MutableLiveData<String>()
|
||||
var conferenceAddress = MutableLiveData<String>()
|
||||
val showDuration = MutableLiveData<Boolean>()
|
||||
|
||||
val isAlone: Boolean
|
||||
get() {
|
||||
var count = 0
|
||||
for (content in chatMessage.contents) {
|
||||
if (content.isFileTransfer || content.isFile) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
return count == 1
|
||||
}
|
||||
|
||||
private var isFileEncrypted: Boolean = false
|
||||
|
||||
private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
|
||||
|
||||
private lateinit var voiceRecordingPlayer: Player
|
||||
private val playerListener = PlayerListener {
|
||||
Log.i("[Voice Recording] End of file reached")
|
||||
stopVoiceRecording()
|
||||
}
|
||||
|
||||
private fun getContent(): Content {
|
||||
return chatMessage.contents[contentIndex]
|
||||
}
|
||||
|
||||
private val chatMessageListener: ChatMessageListenerStub = object : ChatMessageListenerStub() {
|
||||
override fun onFileTransferProgressIndication(
|
||||
message: ChatMessage,
|
||||
c: Content,
|
||||
offset: Int,
|
||||
total: Int
|
||||
) {
|
||||
if (c.filePath == getContent().filePath) {
|
||||
val percent = offset * 100 / total
|
||||
Log.d("[Content] Download progress is: $offset / $total ($percent%)")
|
||||
|
||||
downloadProgressInt.value = percent
|
||||
downloadProgressString.value = "$percent%"
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
|
||||
downloadEnabled.value = state != ChatMessage.State.FileTransferInProgress
|
||||
|
||||
if (state == ChatMessage.State.FileTransferDone || state == ChatMessage.State.FileTransferError) {
|
||||
updateContent()
|
||||
|
||||
if (state == ChatMessage.State.FileTransferDone) {
|
||||
Log.i("[Chat Message] File transfer done")
|
||||
if (!message.isOutgoing && !message.isEphemeral) {
|
||||
Log.i("[Chat Message] Adding content to media store")
|
||||
coreContext.addContentToMediaStore(getContent())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
isVoiceRecordPlaying.value = false
|
||||
voiceRecordDuration.value = 0
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
|
||||
updateContent()
|
||||
chatMessage.addListener(chatMessageListener)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
scope.cancel()
|
||||
|
||||
deletePlainFilePath()
|
||||
chatMessage.removeListener(chatMessageListener)
|
||||
|
||||
if (this::voiceRecordingPlayer.isInitialized) {
|
||||
Log.i("[Voice Recording] Destroying voice record")
|
||||
stopVoiceRecording()
|
||||
voiceRecordingPlayer.removeListener(playerListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun download() {
|
||||
if (chatMessage.isFileTransferInProgress) {
|
||||
Log.w("[Content] Another FileTransfer content for this message is currently being downloaded, can't start another one for now")
|
||||
listener?.onError(R.string.chat_message_download_already_in_progress)
|
||||
return
|
||||
}
|
||||
|
||||
val content = getContent()
|
||||
val filePath = content.filePath
|
||||
if (content.isFileTransfer) {
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
val contentName = content.name
|
||||
if (contentName != null) {
|
||||
val file = FileUtils.getFileStoragePath(contentName)
|
||||
content.filePath = file.path
|
||||
Log.i("[Content] Started downloading $contentName into ${content.filePath}")
|
||||
} else {
|
||||
Log.e("[Content] Content name is null, can't download it!")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Log.w("[Content] File path already set [$filePath] using it (auto download that failed probably)")
|
||||
}
|
||||
|
||||
downloadEnabled.value = false
|
||||
if (!chatMessage.downloadContent(content)) {
|
||||
Log.e("[Content] Failed to start content download!")
|
||||
}
|
||||
} else {
|
||||
Log.e("[Content] Content is not a FileTransfer, can't download it!")
|
||||
}
|
||||
}
|
||||
|
||||
fun openFile() {
|
||||
listener?.onContentClicked(getContent())
|
||||
}
|
||||
|
||||
private fun deletePlainFilePath() {
|
||||
val path = filePath.value.orEmpty()
|
||||
if (path.isNotEmpty() && isFileEncrypted) {
|
||||
Log.i("[Content] Deleting file used for preview: $path")
|
||||
FileUtils.deleteFile(path)
|
||||
filePath.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateContent() {
|
||||
Log.i("[Content] Updating content")
|
||||
deletePlainFilePath()
|
||||
|
||||
val content = getContent()
|
||||
isFileEncrypted = content.isFileEncrypted
|
||||
Log.i("[Content] Is ${if (content.isFile) "file" else "file transfer"} content encrypted ? $isFileEncrypted")
|
||||
|
||||
filePath.value = ""
|
||||
fileName.value = if (content.name.isNullOrEmpty() && !content.filePath.isNullOrEmpty()) {
|
||||
FileUtils.getNameFromFilePath(content.filePath!!)
|
||||
} else {
|
||||
content.name
|
||||
}
|
||||
|
||||
// Display download size and underline text
|
||||
val fileSize = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
|
||||
val spannable = SpannableString("${AppUtils.getString(R.string.chat_message_download_file)} ($fileSize)")
|
||||
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
downloadLabel.value = spannable
|
||||
|
||||
isImage.value = false
|
||||
isVideo.value = false
|
||||
isAudio.value = false
|
||||
isPdf.value = false
|
||||
isVoiceRecording.value = false
|
||||
isConferenceSchedule.value = false
|
||||
isConferenceUpdated.value = false
|
||||
isConferenceCancelled.value = false
|
||||
|
||||
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
|
||||
val path = if (isFileEncrypted) {
|
||||
Log.i("[Content] Content is encrypted, requesting plain file path")
|
||||
content.exportPlainFile()
|
||||
} else {
|
||||
content.filePath ?: ""
|
||||
}
|
||||
downloadable.value = content.filePath.orEmpty().isEmpty()
|
||||
|
||||
val isVoiceRecord = content.isVoiceRecording
|
||||
isVoiceRecording.value = isVoiceRecord
|
||||
|
||||
val isConferenceIcs = content.isIcalendar
|
||||
isConferenceSchedule.value = isConferenceIcs
|
||||
|
||||
if (path.isNotEmpty()) {
|
||||
Log.i("[Content] Found displayable content: $path")
|
||||
filePath.value = path
|
||||
isImage.value = FileUtils.isExtensionImage(path)
|
||||
isVideo.value = FileUtils.isExtensionVideo(path) && !isVoiceRecord
|
||||
isAudio.value = FileUtils.isExtensionAudio(path) && !isVoiceRecord
|
||||
isPdf.value = FileUtils.isExtensionPdf(path)
|
||||
|
||||
if (isVoiceRecord) {
|
||||
val duration = content.fileDuration // duration is in ms
|
||||
voiceRecordDuration.value = duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration)
|
||||
Log.i("[Content] Voice recording duration is ${voiceRecordDuration.value} ($duration)")
|
||||
} else if (isConferenceIcs) {
|
||||
parseConferenceInvite(content)
|
||||
}
|
||||
} else if (isConferenceIcs) {
|
||||
Log.i("[Content] Found content with icalendar file")
|
||||
parseConferenceInvite(content)
|
||||
} else {
|
||||
Log.w("[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path...")
|
||||
isImage.value = false
|
||||
isVideo.value = false
|
||||
isAudio.value = false
|
||||
isPdf.value = false
|
||||
isVoiceRecording.value = false
|
||||
isConferenceSchedule.value = false
|
||||
}
|
||||
} else if (content.isFileTransfer) {
|
||||
downloadable.value = true
|
||||
isImage.value = FileUtils.isExtensionImage(fileName.value!!)
|
||||
isVideo.value = FileUtils.isExtensionVideo(fileName.value!!)
|
||||
isAudio.value = FileUtils.isExtensionAudio(fileName.value!!)
|
||||
isPdf.value = FileUtils.isExtensionPdf(fileName.value!!)
|
||||
isVoiceRecording.value = false
|
||||
isConferenceSchedule.value = false
|
||||
} else if (content.isIcalendar) {
|
||||
Log.i("[Content] Found content with icalendar body")
|
||||
isConferenceSchedule.value = true
|
||||
parseConferenceInvite(content)
|
||||
} else {
|
||||
Log.w("[Content] Found content that's neither a file or a file transfer")
|
||||
}
|
||||
|
||||
isGenericFile.value = !isPdf.value!! && !isAudio.value!! && !isVideo.value!! && !isImage.value!! && !isVoiceRecording.value!! && !isConferenceSchedule.value!!
|
||||
downloadEnabled.value = !chatMessage.isFileTransferInProgress
|
||||
downloadProgressInt.value = 0
|
||||
downloadProgressString.value = "0%"
|
||||
}
|
||||
|
||||
private fun parseConferenceInvite(content: Content) {
|
||||
val conferenceInfo = Factory.instance().createConferenceInfoFromIcalendarContent(content)
|
||||
val conferenceUri = conferenceInfo?.uri?.asStringUriOnly()
|
||||
if (conferenceInfo != null && conferenceUri != null) {
|
||||
conferenceAddress.value = conferenceUri!!
|
||||
Log.i("[Content] Created conference info from ICS with address ${conferenceAddress.value}")
|
||||
conferenceSubject.value = conferenceInfo.subject
|
||||
conferenceDescription.value = conferenceInfo.description
|
||||
|
||||
val state = conferenceInfo.state
|
||||
isConferenceUpdated.value = state == ConferenceInfo.State.Updated
|
||||
isConferenceCancelled.value = state == ConferenceInfo.State.Cancelled
|
||||
|
||||
conferenceDate.value = TimestampUtils.dateToString(conferenceInfo.dateTime)
|
||||
conferenceTime.value = TimestampUtils.timeToString(conferenceInfo.dateTime)
|
||||
|
||||
val minutes = conferenceInfo.duration
|
||||
val hours = TimeUnit.MINUTES.toHours(minutes.toLong())
|
||||
val remainMinutes = minutes - TimeUnit.HOURS.toMinutes(hours).toInt()
|
||||
conferenceDuration.value = TimestampUtils.durationToString(hours.toInt(), remainMinutes)
|
||||
showDuration.value = minutes > 0
|
||||
|
||||
conferenceParticipantCount.value = String.format(AppUtils.getString(R.string.conference_invite_participants_count), conferenceInfo.participants.size + 1) // +1 for organizer
|
||||
} else if (conferenceInfo == null) {
|
||||
if (content.filePath != null) {
|
||||
try {
|
||||
val br = BufferedReader(FileReader(content.filePath))
|
||||
var line: String?
|
||||
val textBuilder = StringBuilder()
|
||||
while (br.readLine().also { line = it } != null) {
|
||||
textBuilder.append(line)
|
||||
textBuilder.append('\n')
|
||||
}
|
||||
br.close()
|
||||
Log.e("[Content] Failed to create conference info from ICS file [${content.filePath}]: $textBuilder")
|
||||
} catch (e: Exception) {
|
||||
Log.e("[Content] Failed to read content of ICS file [${content.filePath}]: $e")
|
||||
}
|
||||
} else {
|
||||
Log.e("[Content] Failed to create conference info from ICS: ${content.utf8Text}")
|
||||
}
|
||||
} else if (conferenceInfo.uri == null) {
|
||||
Log.e("[Content] Failed to find the conference URI in conference info [$conferenceInfo]")
|
||||
}
|
||||
}
|
||||
|
||||
fun callConferenceAddress() {
|
||||
val address = conferenceAddress.value
|
||||
if (address == null) {
|
||||
Log.e("[Content] Can't call null conference address!")
|
||||
return
|
||||
}
|
||||
listener?.onCallConference(address, conferenceSubject.value)
|
||||
}
|
||||
|
||||
/** Voice recording specifics */
|
||||
|
||||
fun playVoiceRecording() {
|
||||
Log.i("[Voice Recording] Playing voice record")
|
||||
if (isPlayerClosed()) {
|
||||
Log.w("[Voice Recording] Player closed, let's open it first")
|
||||
initVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
if (AppUtils.isMediaVolumeLow(coreContext.context)) {
|
||||
Toast.makeText(coreContext.context, R.string.chat_message_voice_recording_playback_low_volume, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AppUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
voiceRecordingPlayer.start()
|
||||
isVoiceRecordPlaying.value = true
|
||||
tickerFlow().onEach {
|
||||
withContext(Dispatchers.Main) {
|
||||
voiceRecordPlayingPosition.value = voiceRecordingPlayer.currentPosition
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun pauseVoiceRecording() {
|
||||
Log.i("[Voice Recording] Pausing voice record")
|
||||
if (!isPlayerClosed()) {
|
||||
voiceRecordingPlayer.pause()
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isVoiceRecordPlaying.value = false
|
||||
}
|
||||
|
||||
private fun tickerFlow() = flow {
|
||||
while (isVoiceRecordPlaying.value == true) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initVoiceRecordPlayer() {
|
||||
Log.i("[Voice Recording] Creating player for voice record")
|
||||
// In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
|
||||
// If none are available, default one will be used
|
||||
var headphonesCard: String? = null
|
||||
var speakerCard: String? = null
|
||||
var earpieceCard: String? = null
|
||||
for (device in coreContext.core.audioDevices) {
|
||||
if (device.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
|
||||
when (device.type) {
|
||||
AudioDevice.Type.Speaker -> {
|
||||
speakerCard = device.id
|
||||
}
|
||||
AudioDevice.Type.Earpiece -> {
|
||||
earpieceCard = device.id
|
||||
}
|
||||
AudioDevice.Type.Headphones, AudioDevice.Type.Headset -> {
|
||||
headphonesCard = device.id
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i("[Voice Recording] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
|
||||
|
||||
val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
|
||||
if (localPlayer != null) {
|
||||
voiceRecordingPlayer = localPlayer
|
||||
} else {
|
||||
Log.e("[Voice Recording] Couldn't create local player!")
|
||||
return
|
||||
}
|
||||
voiceRecordingPlayer.addListener(playerListener)
|
||||
|
||||
val path = filePath.value
|
||||
voiceRecordingPlayer.open(path.orEmpty())
|
||||
voiceRecordDuration.value = voiceRecordingPlayer.duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(voiceRecordingPlayer.duration) // is already in milliseconds
|
||||
Log.i("[Voice Recording] Duration is ${voiceRecordDuration.value} (${voiceRecordingPlayer.duration})")
|
||||
}
|
||||
|
||||
private fun stopVoiceRecording() {
|
||||
if (!isPlayerClosed()) {
|
||||
Log.i("[Voice Recording] Stopping voice record")
|
||||
pauseVoiceRecording()
|
||||
voiceRecordingPlayer.seek(0)
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
voiceRecordingPlayer.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPlayerClosed(): Boolean {
|
||||
return !this::voiceRecordingPlayer.isInitialized || voiceRecordingPlayer.state == Player.State.Closed
|
||||
}
|
||||
}
|
||||
|
||||
interface OnContentClickedListener {
|
||||
fun onContentClicked(content: Content)
|
||||
|
||||
fun onSipAddressClicked(sipUri: String)
|
||||
|
||||
fun onWebUrlClicked(url: String)
|
||||
|
||||
fun onCallConference(address: String, subject: String?)
|
||||
|
||||
fun onError(messageId: Int)
|
||||
}
|
||||
|
|
@ -1,258 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.data
|
||||
|
||||
import android.os.CountDownTimer
|
||||
import android.text.Spannable
|
||||
import android.util.Patterns
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.util.regex.Pattern
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatMessageListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.PatternClickableSpan
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) {
|
||||
private var contentListener: OnContentClickedListener? = null
|
||||
|
||||
val sendInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val transferInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val showImdn = MutableLiveData<Boolean>()
|
||||
|
||||
val imdnIcon = MutableLiveData<Int>()
|
||||
|
||||
val backgroundRes = MutableLiveData<Int>()
|
||||
|
||||
val hideAvatar = MutableLiveData<Boolean>()
|
||||
|
||||
val hideTime = MutableLiveData<Boolean>()
|
||||
|
||||
val contents = MutableLiveData<ArrayList<ChatMessageContentData>>()
|
||||
|
||||
val time = MutableLiveData<String>()
|
||||
|
||||
val ephemeralLifetime = MutableLiveData<String>()
|
||||
|
||||
val text = MutableLiveData<Spannable>()
|
||||
|
||||
val replyData = MutableLiveData<ChatMessageData>()
|
||||
|
||||
val isDisplayed = MutableLiveData<Boolean>()
|
||||
|
||||
val isOutgoing = chatMessage.isOutgoing
|
||||
|
||||
var hasPreviousMessage = false
|
||||
var hasNextMessage = false
|
||||
|
||||
private var countDownTimer: CountDownTimer? = null
|
||||
|
||||
private val listener = object : ChatMessageListenerStub() {
|
||||
override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
|
||||
time.value = TimestampUtils.toString(chatMessage.time)
|
||||
updateChatMessageState(state)
|
||||
}
|
||||
|
||||
override fun onEphemeralMessageTimerStarted(message: ChatMessage) {
|
||||
updateEphemeralTimer()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatMessage.addListener(listener)
|
||||
|
||||
backgroundRes.value = if (chatMessage.isOutgoing) R.drawable.chat_bubble_outgoing_full else R.drawable.chat_bubble_incoming_full
|
||||
hideAvatar.value = false
|
||||
|
||||
if (chatMessage.isReply) {
|
||||
val reply = chatMessage.replyMessage
|
||||
if (reply != null) {
|
||||
Log.i("[Chat Message Data] Message is a reply of message id [${chatMessage.replyMessageId}] sent by [${chatMessage.replyMessageSenderAddress?.asStringUriOnly()}]")
|
||||
replyData.value = ChatMessageData(reply)
|
||||
}
|
||||
}
|
||||
|
||||
time.value = TimestampUtils.toString(chatMessage.time)
|
||||
updateEphemeralTimer()
|
||||
|
||||
updateChatMessageState(chatMessage.state)
|
||||
updateContentsList()
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
super.destroy()
|
||||
|
||||
if (chatMessage.isReply) {
|
||||
replyData.value?.destroy()
|
||||
}
|
||||
|
||||
contents.value.orEmpty().forEach(ChatMessageContentData::destroy)
|
||||
chatMessage.removeListener(listener)
|
||||
contentListener = null
|
||||
}
|
||||
|
||||
fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) {
|
||||
hasPreviousMessage = hasPrevious
|
||||
hasNextMessage = hasNext
|
||||
hideTime.value = false
|
||||
hideAvatar.value = false
|
||||
|
||||
if (hasPrevious) {
|
||||
hideTime.value = true
|
||||
}
|
||||
|
||||
if (chatMessage.isOutgoing) {
|
||||
if (hasNext && hasPrevious) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_2
|
||||
} else if (hasNext) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_1
|
||||
} else if (hasPrevious) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_3
|
||||
} else {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_full
|
||||
}
|
||||
} else {
|
||||
if (hasNext && hasPrevious) {
|
||||
hideAvatar.value = true
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_2
|
||||
} else if (hasNext) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_1
|
||||
} else if (hasPrevious) {
|
||||
hideAvatar.value = true
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_3
|
||||
} else {
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_full
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setContentClickListener(listener: OnContentClickedListener) {
|
||||
contentListener = listener
|
||||
|
||||
for (data in contents.value.orEmpty()) {
|
||||
data.listener = listener
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChatMessageState(state: ChatMessage.State) {
|
||||
transferInProgress.value = state == ChatMessage.State.FileTransferInProgress
|
||||
|
||||
sendInProgress.value = state == ChatMessage.State.InProgress || state == ChatMessage.State.FileTransferInProgress
|
||||
|
||||
showImdn.value = when (state) {
|
||||
ChatMessage.State.DeliveredToUser, ChatMessage.State.Displayed, ChatMessage.State.NotDelivered -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
imdnIcon.value = when (state) {
|
||||
ChatMessage.State.DeliveredToUser -> R.drawable.chat_delivered
|
||||
ChatMessage.State.Displayed -> R.drawable.chat_read
|
||||
else -> R.drawable.chat_error
|
||||
}
|
||||
|
||||
isDisplayed.value = state == ChatMessage.State.Displayed
|
||||
}
|
||||
|
||||
private fun updateContentsList() {
|
||||
contents.value.orEmpty().forEach(ChatMessageContentData::destroy)
|
||||
val list = arrayListOf<ChatMessageContentData>()
|
||||
|
||||
val contentsList = chatMessage.contents
|
||||
for (index in contentsList.indices) {
|
||||
val content = contentsList[index]
|
||||
if (content.isFileTransfer || content.isFile || content.isIcalendar) {
|
||||
val data = ChatMessageContentData(chatMessage, index)
|
||||
data.listener = contentListener
|
||||
list.add(data)
|
||||
} else if (content.isText) {
|
||||
val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text?.trim())
|
||||
text.value = PatternClickableSpan()
|
||||
.add(
|
||||
Pattern.compile("(?:<?sips?:)?[^@\\s]+(?:@([^\\s]+))+"),
|
||||
object : PatternClickableSpan.SpannableClickedListener {
|
||||
override fun onSpanClicked(text: String) {
|
||||
Log.i("[Chat Message Data] Clicked on SIP URI: $text")
|
||||
contentListener?.onSipAddressClicked(text)
|
||||
}
|
||||
}
|
||||
)
|
||||
.add(
|
||||
Patterns.WEB_URL,
|
||||
object : PatternClickableSpan.SpannableClickedListener {
|
||||
override fun onSpanClicked(text: String) {
|
||||
Log.i("[Chat Message Data] Clicked on web URL: $text")
|
||||
contentListener?.onWebUrlClicked(text)
|
||||
}
|
||||
}
|
||||
)
|
||||
.add(
|
||||
Patterns.PHONE,
|
||||
object : PatternClickableSpan.SpannableClickedListener {
|
||||
override fun onSpanClicked(text: String) {
|
||||
Log.i("[Chat Message Data] Clicked on phone number: $text")
|
||||
contentListener?.onSipAddressClicked(text)
|
||||
}
|
||||
}
|
||||
).build(spannable)
|
||||
} else {
|
||||
Log.e("[Chat Message Data] Unexpected content with type: ${content.type}/${content.subtype}")
|
||||
}
|
||||
}
|
||||
|
||||
contents.value = list
|
||||
}
|
||||
|
||||
private fun updateEphemeralTimer() {
|
||||
if (chatMessage.isEphemeral) {
|
||||
if (chatMessage.ephemeralExpireTime == 0L) {
|
||||
// This means the message hasn't been read by all participants yet, so the countdown hasn't started
|
||||
// In this case we simply display the configured value for lifetime
|
||||
ephemeralLifetime.value = formatLifetime(chatMessage.ephemeralLifetime)
|
||||
} else {
|
||||
// Countdown has started, display remaining time
|
||||
val remaining = chatMessage.ephemeralExpireTime - (System.currentTimeMillis() / 1000)
|
||||
ephemeralLifetime.value = formatLifetime(remaining)
|
||||
if (countDownTimer == null) {
|
||||
countDownTimer = object : CountDownTimer(remaining * 1000, 1000) {
|
||||
override fun onFinish() {}
|
||||
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
ephemeralLifetime.postValue(formatLifetime(millisUntilFinished / 1000))
|
||||
}
|
||||
}
|
||||
countDownTimer?.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLifetime(seconds: Long): String {
|
||||
val days = seconds / 86400
|
||||
return when {
|
||||
days >= 1L -> AppUtils.getStringWithPlural(R.plurals.days, days.toInt())
|
||||
else -> String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, (seconds % 60))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2022 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.data
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.ContactDataInterface
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatRoomData(private val chatRoom: ChatRoom) : ContactDataInterface {
|
||||
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
|
||||
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
|
||||
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
|
||||
override val showGroupChatAvatar: Boolean
|
||||
get() = conferenceChatRoom && !oneToOneChatRoom
|
||||
override val coroutineScope: CoroutineScope = coreContext.coroutineScope
|
||||
|
||||
val unreadMessagesCount = MutableLiveData<Int>()
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
||||
val securityLevelIcon = MutableLiveData<Int>()
|
||||
|
||||
val securityLevelContentDescription = MutableLiveData<Int>()
|
||||
|
||||
val ephemeralEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val lastUpdate = MutableLiveData<String>()
|
||||
|
||||
val lastMessageText = MutableLiveData<SpannableStringBuilder>()
|
||||
|
||||
val notificationsMuted = MutableLiveData<Boolean>()
|
||||
|
||||
private val basicChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
|
||||
}
|
||||
|
||||
val oneToOneChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
|
||||
}
|
||||
|
||||
private val conferenceChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt())
|
||||
}
|
||||
|
||||
val encryptedChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())
|
||||
}
|
||||
|
||||
init {
|
||||
unreadMessagesCount.value = chatRoom.unreadMessagesCount
|
||||
|
||||
subject.value = chatRoom.subject
|
||||
updateSecurityIcon()
|
||||
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
|
||||
|
||||
contactLookup()
|
||||
formatLastMessage(chatRoom.lastMessageInHistory)
|
||||
|
||||
notificationsMuted.value = areNotificationsMuted()
|
||||
}
|
||||
|
||||
private fun updateSecurityIcon() {
|
||||
val level = chatRoom.securityLevel
|
||||
securityLevel.value = level
|
||||
|
||||
securityLevelIcon.value = when (level) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
securityLevelContentDescription.value = when (level) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
private fun contactLookup() {
|
||||
displayName.value = when {
|
||||
basicChatRoom -> LinphoneUtils.getDisplayName(
|
||||
chatRoom.peerAddress
|
||||
)
|
||||
oneToOneChatRoom -> LinphoneUtils.getDisplayName(
|
||||
chatRoom.participants.firstOrNull()?.address ?: chatRoom.peerAddress
|
||||
)
|
||||
conferenceChatRoom -> chatRoom.subject.orEmpty()
|
||||
else -> chatRoom.peerAddress.asStringUriOnly()
|
||||
}
|
||||
|
||||
if (oneToOneChatRoom) {
|
||||
searchMatchingContact()
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMatchingContact() {
|
||||
val remoteAddress = if (basicChatRoom) {
|
||||
chatRoom.peerAddress
|
||||
} else {
|
||||
if (chatRoom.participants.isNotEmpty()) {
|
||||
chatRoom.participants[0].address
|
||||
} else {
|
||||
Log.e("[Chat Room] ${chatRoom.peerAddress} doesn't have any participant (state ${chatRoom.state})!")
|
||||
null
|
||||
}
|
||||
}
|
||||
if (remoteAddress != null) {
|
||||
contact.value = coreContext.contactsManager.findContactByAddress(remoteAddress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLastMessage(msg: ChatMessage?) {
|
||||
val lastUpdateTime = chatRoom.lastUpdateTime
|
||||
lastUpdate.value = "00:00"
|
||||
coroutineScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
lastUpdate.postValue(TimestampUtils.toString(lastUpdateTime, true))
|
||||
}
|
||||
}
|
||||
|
||||
val builder = SpannableStringBuilder()
|
||||
if (msg == null) {
|
||||
lastMessageText.value = builder
|
||||
return
|
||||
}
|
||||
|
||||
val sender: String =
|
||||
coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.name
|
||||
?: LinphoneUtils.getDisplayName(msg.fromAddress)
|
||||
builder.append(sender)
|
||||
builder.append(": ")
|
||||
|
||||
for (content in msg.contents) {
|
||||
if (content.isIcalendar) {
|
||||
val body = AppUtils.getString(R.string.conference_invitation)
|
||||
builder.append(body)
|
||||
builder.setSpan(StyleSpan(Typeface.ITALIC), builder.length - body.length, builder.length, 0)
|
||||
} else if (content.isFile || content.isFileTransfer) {
|
||||
builder.append(content.name + " ")
|
||||
} else if (content.isText) {
|
||||
builder.append(content.utf8Text + " ")
|
||||
}
|
||||
}
|
||||
|
||||
builder.trim()
|
||||
lastMessageText.value = builder
|
||||
}
|
||||
|
||||
private fun areNotificationsMuted(): Boolean {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress)
|
||||
return corePreferences.chatRoomMuted(id)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.data
|
||||
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.ChatRoomSecurityLevel
|
||||
import org.linphone.core.ParticipantDevice
|
||||
|
||||
class DevicesListChildData(private val device: ParticipantDevice) {
|
||||
val deviceName: String = device.name.orEmpty()
|
||||
|
||||
val securityLevelIcon: Int by lazy {
|
||||
when (device.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
}
|
||||
|
||||
val securityContentDescription: Int by lazy {
|
||||
when (device.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
fun onClick() {
|
||||
coreContext.startCall(device.address, forceZRTP = true)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ChatRoomSecurityLevel
|
||||
import org.linphone.core.Participant
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class DevicesListGroupData(private val participant: Participant) : GenericContactData(participant.address) {
|
||||
private val device = if (participant.devices.isEmpty()) null else participant.devices.first()
|
||||
|
||||
val securityLevelIcon: Int by lazy {
|
||||
when (device?.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
}
|
||||
|
||||
val securityLevelContentDescription: Int by lazy {
|
||||
when (device?.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
val sipUri: String get() = LinphoneUtils.getDisplayableAddress(participant.address)
|
||||
|
||||
val isExpanded = MutableLiveData<Boolean>()
|
||||
|
||||
val devices = MutableLiveData<ArrayList<DevicesListChildData>>()
|
||||
|
||||
init {
|
||||
securityLevel.value = participant.securityLevel
|
||||
isExpanded.value = false
|
||||
|
||||
val list = arrayListOf<DevicesListChildData>()
|
||||
for (device in participant.devices) {
|
||||
list.add(DevicesListChildData((device)))
|
||||
}
|
||||
devices.value = list
|
||||
}
|
||||
|
||||
fun toggleExpanded() {
|
||||
isExpanded.value = isExpanded.value != true
|
||||
}
|
||||
|
||||
fun onClick() {
|
||||
if (device?.address != null) coreContext.startCall(device.address, forceZRTP = true)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.EventLog
|
||||
|
||||
class EventData(private val eventLog: EventLog) : GenericContactData(
|
||||
if (eventLog.type == EventLog.Type.ConferenceSecurityEvent) {
|
||||
eventLog.securityEventFaultyDeviceAddress!!
|
||||
} else {
|
||||
if (eventLog.participantAddress == null) {
|
||||
eventLog.peerAddress!!
|
||||
} else {
|
||||
eventLog.participantAddress!!
|
||||
}
|
||||
}
|
||||
) {
|
||||
val text = MutableLiveData<String>()
|
||||
|
||||
val isSecurity: Boolean by lazy {
|
||||
when (eventLog.type) {
|
||||
EventLog.Type.ConferenceSecurityEvent -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val isGroupLeft: Boolean by lazy {
|
||||
when (eventLog.type) {
|
||||
EventLog.Type.ConferenceTerminated -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
updateEventText()
|
||||
}
|
||||
|
||||
private fun getName(): String {
|
||||
return contact.value?.name ?: displayName.value ?: ""
|
||||
}
|
||||
|
||||
private fun updateEventText() {
|
||||
val context: Context = coreContext.context
|
||||
|
||||
text.value = when (eventLog.type) {
|
||||
EventLog.Type.ConferenceCreated -> context.getString(R.string.chat_event_conference_created)
|
||||
EventLog.Type.ConferenceTerminated -> context.getString(R.string.chat_event_conference_destroyed)
|
||||
EventLog.Type.ConferenceParticipantAdded -> context.getString(R.string.chat_event_participant_added).format(getName())
|
||||
EventLog.Type.ConferenceParticipantRemoved -> context.getString(R.string.chat_event_participant_removed).format(getName())
|
||||
EventLog.Type.ConferenceSubjectChanged -> context.getString(R.string.chat_event_subject_changed).format(eventLog.subject)
|
||||
EventLog.Type.ConferenceParticipantSetAdmin -> context.getString(R.string.chat_event_admin_set).format(getName())
|
||||
EventLog.Type.ConferenceParticipantUnsetAdmin -> context.getString(R.string.chat_event_admin_unset).format(getName())
|
||||
EventLog.Type.ConferenceParticipantDeviceAdded -> context.getString(R.string.chat_event_device_added).format(getName())
|
||||
EventLog.Type.ConferenceParticipantDeviceRemoved -> context.getString(R.string.chat_event_device_removed).format(getName())
|
||||
EventLog.Type.ConferenceSecurityEvent -> {
|
||||
val name = getName()
|
||||
when (eventLog.securityEventType) {
|
||||
EventLog.SecurityEventType.EncryptionIdentityKeyChanged -> context.getString(R.string.chat_security_event_lime_identity_key_changed).format(name)
|
||||
EventLog.SecurityEventType.ManInTheMiddleDetected -> context.getString(R.string.chat_security_event_man_in_the_middle_detected).format(name)
|
||||
EventLog.SecurityEventType.SecurityLevelDowngraded -> context.getString(R.string.chat_security_event_security_level_downgraded).format(name)
|
||||
EventLog.SecurityEventType.ParticipantMaxDeviceCountExceeded -> context.getString(R.string.chat_security_event_participant_max_count_exceeded).format(name)
|
||||
else -> "Unexpected security event for $name: ${eventLog.securityEventType}"
|
||||
}
|
||||
}
|
||||
EventLog.Type.ConferenceEphemeralMessageDisabled -> context.getString(R.string.chat_event_ephemeral_disabled)
|
||||
EventLog.Type.ConferenceEphemeralMessageEnabled -> context.getString(R.string.chat_event_ephemeral_enabled).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime))
|
||||
EventLog.Type.ConferenceEphemeralMessageLifetimeChanged -> context.getString(R.string.chat_event_ephemeral_lifetime_changed).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime))
|
||||
else -> "Unexpected event: ${eventLog.type}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatEphemeralExpiration(context: Context, duration: Long): String {
|
||||
return when (duration) {
|
||||
0L -> context.getString(R.string.chat_room_ephemeral_message_disabled)
|
||||
60L -> context.getString(R.string.chat_room_ephemeral_message_one_minute)
|
||||
3600L -> context.getString(R.string.chat_room_ephemeral_message_one_hour)
|
||||
86400L -> context.getString(R.string.chat_room_ephemeral_message_one_day)
|
||||
259200L -> context.getString(R.string.chat_room_ephemeral_message_three_days)
|
||||
604800L -> context.getString(R.string.chat_room_ephemeral_message_one_week)
|
||||
else -> "Unexpected duration"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ChatRoomSecurityLevel
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class GroupInfoParticipantData(val participant: GroupChatRoomMember) : GenericContactData(participant.address) {
|
||||
val sipUri: String get() = LinphoneUtils.getDisplayableAddress(participant.address)
|
||||
|
||||
val isAdmin = MutableLiveData<Boolean>()
|
||||
|
||||
val showAdminControls = MutableLiveData<Boolean>()
|
||||
|
||||
// A participant not yet added to a group can't be set admin at the same time it's added
|
||||
val canBeSetAdmin = MutableLiveData<Boolean>()
|
||||
|
||||
val securityLevelIcon: Int by lazy {
|
||||
when (participant.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
}
|
||||
|
||||
val securityLevelContentDescription: Int by lazy {
|
||||
when (participant.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
securityLevel.value = participant.securityLevel
|
||||
isAdmin.value = participant.isAdmin
|
||||
showAdminControls.value = false
|
||||
canBeSetAdmin.value = participant.canBeSetAdmin
|
||||
}
|
||||
|
||||
fun setAdmin() {
|
||||
isAdmin.value = true
|
||||
participant.isAdmin = true
|
||||
}
|
||||
|
||||
fun unSetAdmin() {
|
||||
isAdmin.value = false
|
||||
participant.isAdmin = false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.data
|
||||
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ParticipantImdnState
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ImdnParticipantData(val imdnState: ParticipantImdnState) : GenericContactData(imdnState.participant.address) {
|
||||
val sipUri: String = imdnState.participant.address.asStringUriOnly()
|
||||
|
||||
val time: String = TimestampUtils.toString(imdnState.stateChangeTime)
|
||||
}
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.viewmodels.ChatRoomCreationViewModel
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.activities.navigateToChatRoom
|
||||
import org.linphone.activities.navigateToGroupInfo
|
||||
import org.linphone.contact.ContactsSelectionAdapter
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomCreationFragmentBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>() {
|
||||
private lateinit var viewModel: ChatRoomCreationViewModel
|
||||
private lateinit var adapter: ContactsSelectionAdapter
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_creation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
|
||||
|
||||
val createGroup = arguments?.getBoolean("createGroup") ?: false
|
||||
|
||||
viewModel = ViewModelProvider(this)[ChatRoomCreationViewModel::class.java]
|
||||
viewModel.createGroupChat.value = createGroup
|
||||
|
||||
viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom
|
||||
|
||||
binding.viewModel = viewModel
|
||||
|
||||
adapter = ContactsSelectionAdapter(viewLifecycleOwner)
|
||||
adapter.setGroupChatCapabilityRequired(viewModel.createGroupChat.value == true)
|
||||
adapter.setLimeCapabilityRequired(viewModel.isEncrypted.value == true)
|
||||
binding.contactsList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.contactsList.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
binding.contactsList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||
|
||||
binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
binding.setAllContactsToggleClickListener {
|
||||
viewModel.sipContactsSelected.value = false
|
||||
}
|
||||
|
||||
binding.setSipContactsToggleClickListener {
|
||||
viewModel.sipContactsSelected.value = true
|
||||
}
|
||||
|
||||
viewModel.contactsList.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
|
||||
viewModel.isEncrypted.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.setLimeCapabilityRequired(it)
|
||||
}
|
||||
|
||||
viewModel.sipContactsSelected.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
viewModel.applyFilter()
|
||||
}
|
||||
|
||||
viewModel.selectedAddresses.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.updateSelectedAddresses(it)
|
||||
}
|
||||
|
||||
viewModel.chatRoomCreatedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel))
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.filter.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
viewModel.applyFilter()
|
||||
}
|
||||
|
||||
adapter.selectedContact.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { searchResult ->
|
||||
if (createGroup) {
|
||||
viewModel.toggleSelectionForSearchResult(searchResult)
|
||||
} else {
|
||||
viewModel.createOneToOneChat(searchResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addParticipantsFromSharedViewModel()
|
||||
|
||||
// Next button is only used to go to group chat info fragment
|
||||
binding.setNextClickListener {
|
||||
sharedViewModel.createEncryptedChatRoom = viewModel.isEncrypted.value == true
|
||||
sharedViewModel.chatRoomParticipants.value = viewModel.selectedAddresses.value
|
||||
navigateToGroupInfo()
|
||||
}
|
||||
|
||||
viewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
}
|
||||
|
||||
if (corePreferences.enableNativeAddressBookIntegration) {
|
||||
if (!PermissionHelper.get().hasReadContactsPermission()) {
|
||||
Log.i("[Chat Room Creation] Asking for READ_CONTACTS permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == 0) {
|
||||
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[Chat Room Creation] READ_CONTACTS permission granted")
|
||||
coreContext.fetchContacts()
|
||||
} else {
|
||||
Log.w("[Chat Room Creation] READ_CONTACTS permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
viewModel.secureChatAvailable.value = LinphoneUtils.isEndToEndEncryptedChatAvailable()
|
||||
}
|
||||
|
||||
private fun addParticipantsFromSharedViewModel() {
|
||||
val participants = sharedViewModel.chatRoomParticipants.value
|
||||
if (participants != null && participants.size > 0) {
|
||||
viewModel.selectedAddresses.value = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.viewmodels.DevicesListViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.DevicesListViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomDevicesFragmentBinding
|
||||
|
||||
class DevicesFragment : SecureFragment<ChatRoomDevicesFragmentBinding>() {
|
||||
private lateinit var listViewModel: DevicesListViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_devices_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Devices] Chat room is null, aborting!")
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
||||
|
||||
listViewModel = ViewModelProvider(
|
||||
this,
|
||||
DevicesListViewModelFactory(chatRoom)
|
||||
)[DevicesListViewModel::class.java]
|
||||
binding.viewModel = listViewModel
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
listViewModel.updateParticipants()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.viewmodels.EphemeralViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.EphemeralViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomEphemeralFragmentBinding
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EphemeralFragment : SecureFragment<ChatRoomEphemeralFragmentBinding>() {
|
||||
private lateinit var viewModel: EphemeralViewModel
|
||||
|
||||
override fun getLayoutId(): Int {
|
||||
return R.layout.chat_room_ephemeral_fragment
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
isSecure = true
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Ephemeral] Chat room is null, aborting!")
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
EphemeralViewModelFactory(chatRoom)
|
||||
)[EphemeralViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setValidClickListener {
|
||||
viewModel.updateChatRoomEphemeralDuration()
|
||||
sharedViewModel.refreshChatRoomInListEvent.value = Event(true)
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.activities.main.chat.adapters.GroupInfoParticipantsAdapter
|
||||
import org.linphone.activities.main.chat.data.GroupInfoParticipantData
|
||||
import org.linphone.activities.main.chat.viewmodels.GroupInfoViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.GroupInfoViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToChatRoom
|
||||
import org.linphone.activities.navigateToChatRoomCreation
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomCapabilities
|
||||
import org.linphone.databinding.ChatRoomGroupInfoFragmentBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
|
||||
private lateinit var viewModel: GroupInfoViewModel
|
||||
private lateinit var adapter: GroupInfoParticipantsAdapter
|
||||
private var meAdminStatusChangedDialog: Dialog? = null
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_group_info_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom: ChatRoom? = sharedViewModel.selectedGroupChatRoom.value
|
||||
isSecure = chatRoom?.currentParams?.isEncryptionEnabled ?: false
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
GroupInfoViewModelFactory(chatRoom)
|
||||
)[GroupInfoViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom
|
||||
|
||||
adapter = GroupInfoParticipantsAdapter(
|
||||
viewLifecycleOwner,
|
||||
chatRoom?.hasCapability(ChatRoomCapabilities.Encrypted.toInt()) ?: (viewModel.isEncrypted.value == true)
|
||||
)
|
||||
binding.participants.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.participants.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
binding.participants.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||
|
||||
viewModel.participants.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
|
||||
viewModel.isMeAdmin.observe(
|
||||
viewLifecycleOwner
|
||||
) { isMeAdmin ->
|
||||
adapter.showAdminControls(isMeAdmin && chatRoom != null)
|
||||
}
|
||||
|
||||
viewModel.meAdminChangedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { isMeAdmin ->
|
||||
showMeAdminStateChanged(isMeAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.participantRemovedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { participant ->
|
||||
viewModel.removeParticipant(participant)
|
||||
}
|
||||
}
|
||||
|
||||
addParticipantsFromSharedViewModel()
|
||||
|
||||
viewModel.createdChatRoomEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
goToChatRoom(chatRoom, true)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.updatedChatRoomEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
goToChatRoom(chatRoom, false)
|
||||
}
|
||||
}
|
||||
|
||||
binding.setNextClickListener {
|
||||
if (viewModel.chatRoom != null) {
|
||||
viewModel.updateRoom()
|
||||
} else {
|
||||
viewModel.createChatRoom()
|
||||
}
|
||||
}
|
||||
|
||||
binding.setParticipantsClickListener {
|
||||
sharedViewModel.createEncryptedChatRoom = corePreferences.forceEndToEndEncryptedChat || viewModel.isEncrypted.value == true
|
||||
|
||||
val list = arrayListOf<Address>()
|
||||
for (participant in viewModel.participants.value.orEmpty()) {
|
||||
list.add(participant.participant.address)
|
||||
}
|
||||
sharedViewModel.chatRoomParticipants.value = list
|
||||
sharedViewModel.chatRoomSubject = viewModel.subject.value.orEmpty()
|
||||
|
||||
val args = Bundle()
|
||||
args.putBoolean("createGroup", true)
|
||||
navigateToChatRoomCreation(args)
|
||||
}
|
||||
|
||||
binding.setLeaveClickListener {
|
||||
val dialogViewModel = DialogViewModel(getString(R.string.chat_room_group_info_leave_dialog_message))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showDeleteButton(
|
||||
{
|
||||
viewModel.leaveGroup()
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.chat_room_group_info_leave_dialog_button)
|
||||
)
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
viewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addParticipantsFromSharedViewModel() {
|
||||
val participants = sharedViewModel.chatRoomParticipants.value
|
||||
if (participants != null && participants.size > 0) {
|
||||
val list = arrayListOf<GroupInfoParticipantData>()
|
||||
|
||||
for (address in participants) {
|
||||
val exists = viewModel.participants.value?.find {
|
||||
it.participant.address.weakEqual(address)
|
||||
}
|
||||
|
||||
if (exists != null) {
|
||||
list.add(exists)
|
||||
} else {
|
||||
list.add(
|
||||
GroupInfoParticipantData(
|
||||
GroupChatRoomMember(address, false, hasLimeX3DHCapability = viewModel.isEncrypted.value == true)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.participants.value = list
|
||||
}
|
||||
|
||||
if (sharedViewModel.chatRoomSubject.isNotEmpty()) {
|
||||
viewModel.subject.value = sharedViewModel.chatRoomSubject
|
||||
sharedViewModel.chatRoomSubject = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMeAdminStateChanged(isMeAdmin: Boolean) {
|
||||
meAdminStatusChangedDialog?.dismiss()
|
||||
|
||||
val message = if (isMeAdmin) {
|
||||
getString(R.string.chat_room_group_info_you_are_now_admin)
|
||||
} else {
|
||||
getString(R.string.chat_room_group_info_you_are_no_longer_admin)
|
||||
}
|
||||
val dialogViewModel = DialogViewModel(message)
|
||||
val dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showOkButton({
|
||||
dialog.dismiss()
|
||||
})
|
||||
|
||||
dialog.show()
|
||||
meAdminStatusChangedDialog = dialog
|
||||
}
|
||||
|
||||
private fun goToChatRoom(chatRoom: ChatRoom, created: Boolean) {
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel), created)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.adapters.ImdnAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.ImdnViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.ImdnViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomImdnFragmentBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.RecyclerViewHeaderDecoration
|
||||
|
||||
class ImdnFragment : SecureFragment<ChatRoomImdnFragmentBinding>() {
|
||||
private lateinit var viewModel: ImdnViewModel
|
||||
private lateinit var adapter: ImdnAdapter
|
||||
|
||||
override fun getLayoutId(): Int {
|
||||
return R.layout.chat_room_imdn_fragment
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom == null) {
|
||||
Log.e("[IMDN] Chat room is null, aborting!")
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
||||
|
||||
if (arguments != null) {
|
||||
val messageId = arguments?.getString("MessageId")
|
||||
val message = if (messageId != null) chatRoom.findMessage(messageId) else null
|
||||
if (message != null) {
|
||||
Log.i("[IMDN] Found message $message with id $messageId")
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
ImdnViewModelFactory(message)
|
||||
)[ImdnViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
} else {
|
||||
Log.e("[IMDN] Couldn't find message with id $messageId in chat room $chatRoom")
|
||||
findNavController().popBackStack()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Log.e("[IMDN] Couldn't find message id in intent arguments")
|
||||
findNavController().popBackStack()
|
||||
return
|
||||
}
|
||||
|
||||
adapter = ImdnAdapter(viewLifecycleOwner)
|
||||
binding.participantsList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.participantsList.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
binding.participantsList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||
|
||||
// Displays state header
|
||||
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
|
||||
binding.participantsList.addItemDecoration(headerItemDecoration)
|
||||
|
||||
viewModel.participants.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,399 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.clearDisplayedChatRoom
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.adapters.ChatRoomsListAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.ChatRoomsListViewModel
|
||||
import org.linphone.activities.main.fragments.MasterFragment
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToChatRoom
|
||||
import org.linphone.activities.navigateToChatRoomCreation
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomMasterFragmentBinding
|
||||
import org.linphone.utils.*
|
||||
|
||||
class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, ChatRoomsListAdapter>() {
|
||||
override val dialogConfirmationMessageBeforeRemoval = R.plurals.chat_room_delete_dialog
|
||||
private lateinit var listViewModel: ChatRoomsListViewModel
|
||||
|
||||
private val observer = object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onChanged() {
|
||||
scrollToTop()
|
||||
}
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
if (positionStart == 0 && itemCount == 1) {
|
||||
scrollToTop()
|
||||
}
|
||||
}
|
||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||
scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_master_fragment
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding.chatList.adapter = null
|
||||
adapter.unregisterAdapterDataObserver(observer)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
useMaterialSharedAxisXForwardAnimation = false
|
||||
if (corePreferences.enableAnimations) {
|
||||
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
||||
val axis = if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
|
||||
enterTransition = MaterialSharedAxis(axis, true)
|
||||
reenterTransition = MaterialSharedAxis(axis, true)
|
||||
returnTransition = MaterialSharedAxis(axis, false)
|
||||
exitTransition = MaterialSharedAxis(axis, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
isSecure = true
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
listViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[ChatRoomsListViewModel::class.java]
|
||||
}
|
||||
binding.viewModel = listViewModel
|
||||
|
||||
/* Shared view model & sliding pane related */
|
||||
|
||||
setUpSlidingPane(binding.slidingPane)
|
||||
|
||||
binding.slidingPane.addPanelSlideListener(object : SlidingPaneLayout.PanelSlideListener {
|
||||
override fun onPanelSlide(panel: View, slideOffset: Float) { }
|
||||
|
||||
override fun onPanelOpened(panel: View) { }
|
||||
|
||||
override fun onPanelClosed(panel: View) {
|
||||
// Conversation isn't visible anymore, any new message received in it will trigger a notification
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||
}
|
||||
})
|
||||
|
||||
// Chat room loading can take some time, so wait until it is ready before opening the pane
|
||||
sharedViewModel.chatRoomFragmentOpenedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
binding.slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.layoutChangedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
|
||||
if (binding.slidingPane.isSlideable) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
|
||||
if (navHostFragment.navController.currentDestination?.id == R.id.emptyChatFragment) {
|
||||
Log.i("[Chat] Foldable device has been folded, closing side pane with empty fragment")
|
||||
binding.slidingPane.closePane()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.refreshChatRoomInListEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom != null) {
|
||||
listViewModel.notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* End of shared view model & sliding pane related */
|
||||
|
||||
_adapter = ChatRoomsListAdapter(listSelectionViewModel, viewLifecycleOwner)
|
||||
// SubmitList is done on a background thread
|
||||
// We need this adapter data observer to know when to scroll
|
||||
adapter.registerAdapterDataObserver(observer)
|
||||
binding.chatList.setHasFixedSize(true)
|
||||
binding.chatList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.chatList.layoutManager = layoutManager
|
||||
|
||||
// Swipe action
|
||||
val swipeConfiguration = RecyclerViewSwipeConfiguration()
|
||||
val white = ContextCompat.getColor(requireContext(), R.color.white_color)
|
||||
|
||||
swipeConfiguration.rightToLeftAction = RecyclerViewSwipeConfiguration.Action(
|
||||
requireContext().getString(R.string.dialog_delete),
|
||||
white,
|
||||
ContextCompat.getColor(requireContext(), R.color.red_color)
|
||||
)
|
||||
swipeConfiguration.leftToRightAction = RecyclerViewSwipeConfiguration.Action(
|
||||
requireContext().getString(R.string.received_chat_notification_mark_as_read_label),
|
||||
white,
|
||||
ContextCompat.getColor(requireContext(), R.color.imdn_read_color)
|
||||
)
|
||||
val swipeListener = object : RecyclerViewSwipeListener {
|
||||
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||
val index = viewHolder.bindingAdapterPosition
|
||||
if (index < 0 || index >= adapter.currentList.size) {
|
||||
Log.e("[Chat] Index is out of bound, can't mark chat room as read")
|
||||
} else {
|
||||
val chatRoom = adapter.currentList[viewHolder.bindingAdapterPosition]
|
||||
chatRoom.markAsRead()
|
||||
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||
val viewModel = DialogViewModel(getString(R.string.chat_room_delete_one_dialog))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||
|
||||
val index = viewHolder.bindingAdapterPosition
|
||||
if (index < 0 || index >= adapter.currentList.size) {
|
||||
Log.e("[Chat] Index is out of bound, can't delete chat room")
|
||||
} else {
|
||||
viewModel.showCancelButton {
|
||||
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
viewModel.showDeleteButton(
|
||||
{
|
||||
val deletedChatRoom =
|
||||
adapter.currentList[index]
|
||||
listViewModel.deleteChatRoom(deletedChatRoom)
|
||||
if (!binding.slidingPane.isSlideable &&
|
||||
deletedChatRoom == sharedViewModel.selectedChatRoom.value
|
||||
) {
|
||||
Log.i("[Chat] Currently displayed chat room has been deleted, removing detail fragment")
|
||||
clearDisplayedChatRoom()
|
||||
}
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.dialog_delete)
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
RecyclerViewSwipeUtils(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, swipeConfiguration, swipeListener)
|
||||
.attachToRecyclerView(binding.chatList)
|
||||
|
||||
// Divider between items
|
||||
binding.chatList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||
|
||||
listViewModel.chatRooms.observe(
|
||||
viewLifecycleOwner
|
||||
) { chatRooms ->
|
||||
adapter.submitList(chatRooms)
|
||||
}
|
||||
|
||||
listViewModel.chatRoomIndexUpdatedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { index ->
|
||||
adapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.selectedChatRoomEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
if ((requireActivity() as GenericActivity).isDestructionPending) {
|
||||
Log.w("[Chat] Activity is pending destruction, don't start navigating now!")
|
||||
sharedViewModel.destructionPendingChatRoom = chatRoom
|
||||
} else {
|
||||
if (chatRoom.peerAddress.asStringUriOnly() == coreContext.notificationsManager.currentlyDisplayedChatRoomAddress) {
|
||||
if (!binding.slidingPane.isOpen) {
|
||||
Log.w("[Chat] Chat room is displayed but sliding pane is closed...")
|
||||
if (!binding.slidingPane.openPane()) {
|
||||
Log.e("[Chat] Tried to open pane to workaround already displayed chat room issue, failed!")
|
||||
}
|
||||
} else {
|
||||
Log.w("[Chat] This chat room is already displayed!")
|
||||
}
|
||||
} else {
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
navigateToChatRoom(
|
||||
AppUtils.createBundleWithSharedTextAndFiles(
|
||||
sharedViewModel
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.setEditClickListener {
|
||||
listSelectionViewModel.isEditionEnabled.value = true
|
||||
}
|
||||
|
||||
binding.setCancelForwardClickListener {
|
||||
sharedViewModel.messageToForwardEvent.value?.consume {
|
||||
Log.i("[Chat] Cancelling message forward")
|
||||
}
|
||||
sharedViewModel.isPendingMessageForward.value = false
|
||||
}
|
||||
|
||||
binding.setCancelSharingClickListener {
|
||||
Log.i("[Chat] Cancelling text/files sharing")
|
||||
sharedViewModel.textToShare.value = ""
|
||||
sharedViewModel.filesToShare.value = arrayListOf()
|
||||
listViewModel.fileSharingPending.value = false
|
||||
listViewModel.textSharingPending.value = false
|
||||
}
|
||||
|
||||
binding.setNewOneToOneChatRoomClickListener {
|
||||
sharedViewModel.chatRoomParticipants.value = arrayListOf()
|
||||
navigateToChatRoomCreation(false, binding.slidingPane)
|
||||
}
|
||||
|
||||
binding.setNewGroupChatRoomClickListener {
|
||||
sharedViewModel.selectedGroupChatRoom.value = null
|
||||
sharedViewModel.chatRoomParticipants.value = arrayListOf()
|
||||
navigateToChatRoomCreation(true, binding.slidingPane)
|
||||
}
|
||||
|
||||
val pendingDestructionChatRoom = sharedViewModel.destructionPendingChatRoom
|
||||
if (pendingDestructionChatRoom != null) {
|
||||
Log.w("[Chat] Found pending chat room from before activity was recreated")
|
||||
sharedViewModel.destructionPendingChatRoom = null
|
||||
sharedViewModel.selectedChatRoom.value = pendingDestructionChatRoom
|
||||
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel))
|
||||
}
|
||||
|
||||
val localSipUri = arguments?.getString("LocalSipUri")
|
||||
val remoteSipUri = arguments?.getString("RemoteSipUri")
|
||||
if (localSipUri != null && remoteSipUri != null) {
|
||||
Log.i("[Chat] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments")
|
||||
arguments?.clear()
|
||||
val localAddress = Factory.instance().createAddress(localSipUri)
|
||||
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
|
||||
val chatRoom = coreContext.core.searchChatRoom(null, localAddress, remoteSipAddress, arrayOfNulls(0))
|
||||
if (chatRoom != null) {
|
||||
Log.i("[Chat] Found matching chat room $chatRoom")
|
||||
adapter.selectedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
} else {
|
||||
sharedViewModel.textToShare.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
if (it.isNotEmpty()) {
|
||||
Log.i("[Chat] Found text to share")
|
||||
listViewModel.textSharingPending.value = true
|
||||
clearDisplayedChatRoom()
|
||||
} else {
|
||||
if (sharedViewModel.filesToShare.value.isNullOrEmpty()) {
|
||||
listViewModel.textSharingPending.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
sharedViewModel.filesToShare.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
if (it.isNotEmpty()) {
|
||||
Log.i("[Chat] Found ${it.size} files to share")
|
||||
listViewModel.fileSharingPending.value = true
|
||||
clearDisplayedChatRoom()
|
||||
} else {
|
||||
if (sharedViewModel.textToShare.value.isNullOrEmpty()) {
|
||||
listViewModel.fileSharingPending.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
sharedViewModel.isPendingMessageForward.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
listViewModel.forwardPending.value = it
|
||||
adapter.forwardPending(it)
|
||||
if (it) {
|
||||
Log.i("[Chat] Found chat message to transfer")
|
||||
}
|
||||
}
|
||||
|
||||
listViewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
listViewModel.groupChatAvailable.value = LinphoneUtils.isGroupChatAvailable()
|
||||
}
|
||||
|
||||
override fun deleteItems(indexesOfItemToDelete: ArrayList<Int>) {
|
||||
val list = ArrayList<ChatRoom>()
|
||||
var closeSlidingPane = false
|
||||
for (index in indexesOfItemToDelete) {
|
||||
val chatRoom = adapter.currentList[index]
|
||||
list.add(chatRoom)
|
||||
|
||||
if (chatRoom == sharedViewModel.selectedChatRoom.value) {
|
||||
closeSlidingPane = true
|
||||
}
|
||||
}
|
||||
listViewModel.deleteChatRooms(list)
|
||||
|
||||
if (!binding.slidingPane.isSlideable && closeSlidingPane) {
|
||||
Log.i("[Chat] Currently displayed chat room has been deleted, removing detail fragment")
|
||||
clearDisplayedChatRoom()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToTop() {
|
||||
binding.chatList.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,524 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.data.ChatMessageAttachmentData
|
||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ChatMessageSendingViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ChatMessageSendingViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
var temporaryFileUploadPath: File? = null
|
||||
|
||||
val attachments = MutableLiveData<ArrayList<ChatMessageAttachmentData>>()
|
||||
|
||||
val attachFileEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val sendMessageEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isReadOnly = MutableLiveData<Boolean>()
|
||||
|
||||
var textToSend = MutableLiveData<String>()
|
||||
|
||||
val isPendingAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
var pendingChatMessageToReplyTo = MutableLiveData<ChatMessageData>()
|
||||
|
||||
val requestRecordAudioPermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val messageSentEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val voiceRecordingProgressBarMax = 10000
|
||||
|
||||
val isPendingVoiceRecord = MutableLiveData<Boolean>()
|
||||
|
||||
val isVoiceRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val voiceRecordingDuration = MutableLiveData<Int>()
|
||||
|
||||
val formattedDuration = MutableLiveData<String>()
|
||||
|
||||
val isPlayingVoiceRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
||||
|
||||
val imeFlags: Int = if (chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) {
|
||||
// IME_FLAG_NO_PERSONALIZED_LEARNING is only available on Android 8 and newer
|
||||
Compatibility.getImeFlagsForSecureChatRoom()
|
||||
} else {
|
||||
EditorInfo.IME_FLAG_NO_EXTRACT_UI
|
||||
}
|
||||
|
||||
private val recorder: Recorder
|
||||
|
||||
private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
|
||||
|
||||
private lateinit var voiceRecordingPlayer: Player
|
||||
private val playerListener = PlayerListener {
|
||||
Log.i("[Chat Message Sending] End of file reached")
|
||||
stopVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
updateChatRoomReadOnlyState()
|
||||
}
|
||||
|
||||
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateChatRoomReadOnlyState()
|
||||
}
|
||||
|
||||
override fun onConferenceLeft(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateChatRoomReadOnlyState()
|
||||
}
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
|
||||
attachments.value = arrayListOf()
|
||||
|
||||
attachFileEnabled.value = true
|
||||
sendMessageEnabled.value = false
|
||||
updateChatRoomReadOnlyState()
|
||||
|
||||
val recorderParams = coreContext.core.createRecorderParams()
|
||||
if (corePreferences.voiceMessagesFormatMkv) {
|
||||
recorderParams.fileFormat = RecorderFileFormat.Mkv
|
||||
} else {
|
||||
recorderParams.fileFormat = RecorderFileFormat.Wav
|
||||
}
|
||||
recorder = coreContext.core.createRecorder(recorderParams)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
pendingChatMessageToReplyTo.value?.destroy()
|
||||
|
||||
if (recorder.state != RecorderState.Closed) {
|
||||
recorder.close()
|
||||
}
|
||||
|
||||
if (this::voiceRecordingPlayer.isInitialized) {
|
||||
stopVoiceRecordPlayer()
|
||||
voiceRecordingPlayer.removeListener(playerListener)
|
||||
}
|
||||
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
scope.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun onTextToSendChanged(value: String) {
|
||||
sendMessageEnabled.value = value.trim().isNotEmpty() || attachments.value?.isNotEmpty() == true || isPendingVoiceRecord.value == true
|
||||
if (value.isNotEmpty()) {
|
||||
if (attachFileEnabled.value == true && !corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = false
|
||||
}
|
||||
chatRoom.compose()
|
||||
} else {
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = attachments.value?.isEmpty() ?: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAttachment(path: String) {
|
||||
val list = arrayListOf<ChatMessageAttachmentData>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
list.add(
|
||||
ChatMessageAttachmentData(path) {
|
||||
removeAttachment(it)
|
||||
}
|
||||
)
|
||||
attachments.value = list
|
||||
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().trim().isNotEmpty() || list.isNotEmpty() || isPendingVoiceRecord.value == true
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAttachment(attachment: ChatMessageAttachmentData) {
|
||||
val list = arrayListOf<ChatMessageAttachmentData>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
list.remove(attachment)
|
||||
attachments.value = list
|
||||
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().trim().isNotEmpty() || list.isNotEmpty() || isPendingVoiceRecord.value == true
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = list.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage() {
|
||||
if (!isPlayerClosed()) {
|
||||
stopVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
val pendingMessageToReplyTo = pendingChatMessageToReplyTo.value
|
||||
val message: ChatMessage = if (isPendingAnswer.value == true && pendingMessageToReplyTo != null)
|
||||
chatRoom.createReplyMessage(pendingMessageToReplyTo.chatMessage)
|
||||
else
|
||||
chatRoom.createEmptyMessage()
|
||||
val isBasicChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
|
||||
|
||||
var voiceRecord = false
|
||||
if (isPendingVoiceRecord.value == true && recorder.file != null) {
|
||||
val content = recorder.createContent()
|
||||
if (content != null) {
|
||||
Log.i("[Chat Message Sending] Voice recording content created, file name is ${content.name} and duration is ${content.fileDuration}")
|
||||
message.addContent(content)
|
||||
voiceRecord = true
|
||||
} else {
|
||||
Log.e("[Chat Message Sending] Voice recording content couldn't be created!")
|
||||
}
|
||||
|
||||
isPendingVoiceRecord.value = false
|
||||
isVoiceRecording.value = false
|
||||
}
|
||||
|
||||
val toSend = textToSend.value.orEmpty().trim()
|
||||
if (toSend.isNotEmpty()) {
|
||||
if (voiceRecord && isBasicChatRoom) {
|
||||
val textMessage: ChatMessage = chatRoom.createMessageFromUtf8(toSend)
|
||||
textMessage.send()
|
||||
} else {
|
||||
message.addUtf8TextContent(toSend)
|
||||
}
|
||||
}
|
||||
|
||||
var fileContent = false
|
||||
for (attachment in attachments.value.orEmpty()) {
|
||||
val content = Factory.instance().createContent()
|
||||
|
||||
if (attachment.isImage) {
|
||||
content.type = "image"
|
||||
} else {
|
||||
content.type = "file"
|
||||
}
|
||||
content.subtype = FileUtils.getExtensionFromFileName(attachment.fileName)
|
||||
content.name = attachment.fileName
|
||||
content.filePath = attachment.path // Let the file body handler take care of the upload
|
||||
|
||||
// Do not send file in the same message as the text in a BasicChatRoom
|
||||
// and don't send multiple files in the same message if setting says so
|
||||
if (isBasicChatRoom or (corePreferences.preventMoreThanOneFilePerMessage and (fileContent or voiceRecord))) {
|
||||
val fileMessage: ChatMessage = chatRoom.createFileTransferMessage(content)
|
||||
fileMessage.send()
|
||||
} else {
|
||||
message.addFileContent(content)
|
||||
fileContent = true
|
||||
}
|
||||
}
|
||||
|
||||
if (message.contents.isNotEmpty()) {
|
||||
message.send()
|
||||
}
|
||||
|
||||
cancelReply()
|
||||
attachments.value = arrayListOf()
|
||||
textToSend.value = ""
|
||||
|
||||
messageSentEvent.value = Event(true)
|
||||
}
|
||||
|
||||
fun transferMessage(chatMessage: ChatMessage) {
|
||||
val message = chatRoom.createForwardMessage(chatMessage)
|
||||
message.send()
|
||||
}
|
||||
|
||||
fun cancelReply() {
|
||||
pendingChatMessageToReplyTo.value?.destroy()
|
||||
isPendingAnswer.value = false
|
||||
}
|
||||
|
||||
private fun tickerFlowRecording() = flow {
|
||||
while (recorder.state == RecorderState.Running) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tickerFlowPlaying() = flow {
|
||||
while (voiceRecordingPlayer.state == Player.State.Playing) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleVoiceRecording() {
|
||||
if (corePreferences.holdToRecordVoiceMessage) {
|
||||
// Disables click listener just in case, touch listener will be used instead
|
||||
return
|
||||
}
|
||||
|
||||
if (isVoiceRecording.value == true) {
|
||||
stopVoiceRecording()
|
||||
} else {
|
||||
startVoiceRecording()
|
||||
}
|
||||
}
|
||||
|
||||
fun startVoiceRecording() {
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
requestRecordAudioPermissionEvent.value = Event(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AppUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
|
||||
when (recorder.state) {
|
||||
RecorderState.Running -> Log.w("[Chat Message Sending] Recorder is already recording")
|
||||
RecorderState.Paused -> {
|
||||
Log.w("[Chat Message Sending] Recorder isn't closed, resuming recording")
|
||||
recorder.start()
|
||||
}
|
||||
RecorderState.Closed -> {
|
||||
val extension = when (recorder.params.fileFormat) {
|
||||
RecorderFileFormat.Mkv -> "mkv"
|
||||
else -> "wav"
|
||||
}
|
||||
val tempFileName = "voice-recording-${System.currentTimeMillis()}.$extension"
|
||||
val file = FileUtils.getFileStoragePath(tempFileName)
|
||||
Log.w("[Chat Message Sending] Recorder is closed, starting recording in ${file.absoluteFile}")
|
||||
recorder.open(file.absolutePath)
|
||||
recorder.start()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
val duration = recorder.duration
|
||||
voiceRecordingDuration.value = duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration) // duration is in ms
|
||||
|
||||
isPendingVoiceRecord.value = true
|
||||
isVoiceRecording.value = true
|
||||
sendMessageEnabled.value = true
|
||||
|
||||
tickerFlowRecording().onEach {
|
||||
val duration = recorder.duration
|
||||
voiceRecordingDuration.postValue(recorder.duration % voiceRecordingProgressBarMax)
|
||||
formattedDuration.postValue(SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration)) // duration is in ms
|
||||
|
||||
if (duration >= corePreferences.voiceRecordingMaxDuration) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Log.w("[Chat Message Sending] Max duration for voice recording exceeded (${corePreferences.voiceRecordingMaxDuration}ms), stopping.")
|
||||
stopVoiceRecording()
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun cancelVoiceRecording() {
|
||||
if (recorder.state != RecorderState.Closed) {
|
||||
Log.i("[Chat Message Sending] Closing voice recorder")
|
||||
recorder.close()
|
||||
|
||||
val path = recorder.file
|
||||
if (path != null) {
|
||||
Log.i("[Chat Message Sending] Deleting voice recording file: $path")
|
||||
FileUtils.deleteFile(path)
|
||||
}
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isPendingVoiceRecord.value = false
|
||||
isVoiceRecording.value = false
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().trim().isNotEmpty() == true || attachments.value?.isNotEmpty() == true
|
||||
|
||||
if (!isPlayerClosed()) {
|
||||
stopVoiceRecordPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
fun stopVoiceRecording() {
|
||||
if (recorder.state == RecorderState.Running) {
|
||||
Log.i("[Chat Message Sending] Pausing / closing voice recorder")
|
||||
recorder.pause()
|
||||
recorder.close()
|
||||
voiceRecordingDuration.value = recorder.duration
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isVoiceRecording.value = false
|
||||
if (corePreferences.sendVoiceRecordingRightAway) {
|
||||
Log.i("[Chat Message Sending] Sending voice recording right away")
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
fun playRecordedMessage() {
|
||||
if (isPlayerClosed()) {
|
||||
Log.w("[Chat Message Sending] Player closed, let's open it first")
|
||||
initVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
if (AppUtils.isMediaVolumeLow(coreContext.context)) {
|
||||
Toast.makeText(coreContext.context, R.string.chat_message_voice_recording_playback_low_volume, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AppUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
|
||||
voiceRecordingPlayer.start()
|
||||
isPlayingVoiceRecording.value = true
|
||||
|
||||
tickerFlowPlaying().onEach {
|
||||
voiceRecordPlayingPosition.postValue(voiceRecordingPlayer.currentPosition)
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun pauseRecordedMessage() {
|
||||
Log.i("[Chat Message Sending] Pausing voice record")
|
||||
if (!isPlayerClosed()) {
|
||||
voiceRecordingPlayer.pause()
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isPlayingVoiceRecording.value = false
|
||||
}
|
||||
|
||||
private fun initVoiceRecordPlayer() {
|
||||
Log.i("[Chat Message Sending] Creating player for voice record")
|
||||
// In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
|
||||
// If none are available, default one will be used
|
||||
var headphonesCard: String? = null
|
||||
var speakerCard: String? = null
|
||||
var earpieceCard: String? = null
|
||||
for (device in coreContext.core.audioDevices) {
|
||||
if (device.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
|
||||
when (device.type) {
|
||||
AudioDevice.Type.Speaker -> {
|
||||
speakerCard = device.id
|
||||
}
|
||||
AudioDevice.Type.Earpiece -> {
|
||||
earpieceCard = device.id
|
||||
}
|
||||
AudioDevice.Type.Headphones, AudioDevice.Type.Headset -> {
|
||||
headphonesCard = device.id
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i("[Chat Message Sending] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
|
||||
|
||||
val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
|
||||
if (localPlayer != null) {
|
||||
voiceRecordingPlayer = localPlayer
|
||||
} else {
|
||||
Log.e("[Chat Message Sending] Couldn't create local player!")
|
||||
return
|
||||
}
|
||||
voiceRecordingPlayer.addListener(playerListener)
|
||||
|
||||
val path = recorder.file
|
||||
if (path != null) {
|
||||
voiceRecordingPlayer.open(path)
|
||||
// Update recording duration using player value to ensure proper progress bar animation
|
||||
voiceRecordingDuration.value = voiceRecordingPlayer.duration
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopVoiceRecordPlayer() {
|
||||
if (!isPlayerClosed()) {
|
||||
Log.i("[Chat Message Sending] Stopping voice record")
|
||||
voiceRecordingPlayer.pause()
|
||||
voiceRecordingPlayer.seek(0)
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
voiceRecordingPlayer.close()
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isPlayingVoiceRecording.value = false
|
||||
}
|
||||
|
||||
private fun isPlayerClosed(): Boolean {
|
||||
return !this::voiceRecordingPlayer.isInitialized || voiceRecordingPlayer.state == Player.State.Closed
|
||||
}
|
||||
|
||||
private fun updateChatRoomReadOnlyState() {
|
||||
isReadOnly.value = chatRoom.isReadOnly || (chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt()) && chatRoom.participants.isEmpty())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import org.linphone.activities.main.chat.data.EventLogData
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ChatMessagesListViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ChatMessagesListViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
companion object {
|
||||
private const val MESSAGES_PER_PAGE = 20
|
||||
}
|
||||
|
||||
val events = MutableLiveData<ArrayList<EventLogData>>()
|
||||
|
||||
val messageUpdatedEvent: MutableLiveData<Event<Int>> by lazy {
|
||||
MutableLiveData<Event<Int>>()
|
||||
}
|
||||
|
||||
val requestWriteExternalStoragePermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||
override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array<out EventLog>) {
|
||||
for (eventLog in eventLogs) {
|
||||
addChatMessageEventLog(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChatMessageSending(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
val position = events.value.orEmpty().size
|
||||
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
chatMessage ?: return
|
||||
chatMessage.userData = position
|
||||
}
|
||||
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onSecurityEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
if (!chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConferenceLeft(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
if (!chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
Log.i("[Chat Messages] An ephemeral chat message has expired, removing it from event list")
|
||||
deleteEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun resendMessage(chatMessage: ChatMessage) {
|
||||
val position: Int = chatMessage.userData as Int
|
||||
chatMessage.send()
|
||||
messageUpdatedEvent.value = Event(position)
|
||||
}
|
||||
|
||||
fun deleteMessage(chatMessage: ChatMessage) {
|
||||
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
|
||||
chatRoom.deleteMessage(chatMessage)
|
||||
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
fun deleteEventLogs(listToDelete: ArrayList<EventLogData>) {
|
||||
for (eventLog in listToDelete) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog.eventLog)
|
||||
eventLog.eventLog.deleteFromDatabase()
|
||||
}
|
||||
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
fun loadMoreData(totalItemsCount: Int) {
|
||||
Log.i("[Chat Messages] Load more data, current total is $totalItemsCount")
|
||||
val maxSize: Int = chatRoom.historyEventsSize
|
||||
|
||||
if (totalItemsCount < maxSize) {
|
||||
var upperBound: Int = totalItemsCount + MESSAGES_PER_PAGE
|
||||
if (upperBound > maxSize) {
|
||||
upperBound = maxSize
|
||||
}
|
||||
|
||||
val history: Array<EventLog> = chatRoom.getHistoryRangeEvents(totalItemsCount, upperBound)
|
||||
val list = arrayListOf<EventLogData>()
|
||||
for (eventLog in history) {
|
||||
list.add(EventLogData(eventLog))
|
||||
}
|
||||
list.addAll(events.value.orEmpty())
|
||||
events.value = list
|
||||
}
|
||||
}
|
||||
|
||||
private fun addEvent(eventLog: EventLog) {
|
||||
val list = arrayListOf<EventLogData>()
|
||||
list.addAll(events.value.orEmpty())
|
||||
val found = list.find { data -> data.eventLog == eventLog }
|
||||
if (found == null) {
|
||||
list.add(EventLogData(eventLog))
|
||||
}
|
||||
events.value = list
|
||||
}
|
||||
|
||||
private fun getEvents(): ArrayList<EventLogData> {
|
||||
val list = arrayListOf<EventLogData>()
|
||||
val unreadCount = chatRoom.unreadMessagesCount
|
||||
var loadCount = max(MESSAGES_PER_PAGE, unreadCount)
|
||||
Log.i("[Chat Messages] $unreadCount unread messages in this chat room, loading $loadCount from history")
|
||||
|
||||
val history = chatRoom.getHistoryEvents(loadCount)
|
||||
var messageCount = 0
|
||||
for (eventLog in history) {
|
||||
list.add(EventLogData(eventLog))
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
messageCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Load enough events to have at least all unread messages
|
||||
while (unreadCount > 0 && messageCount < unreadCount) {
|
||||
Log.w("[Chat Messages] There is only $messageCount messages in the last $loadCount events, loading $MESSAGES_PER_PAGE more")
|
||||
val moreHistory = chatRoom.getHistoryRangeEvents(loadCount, loadCount + MESSAGES_PER_PAGE)
|
||||
loadCount += MESSAGES_PER_PAGE
|
||||
for (eventLog in moreHistory) {
|
||||
list.add(EventLogData(eventLog))
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
messageCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
private fun deleteEvent(eventLog: EventLog) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
if (chatMessage != null) {
|
||||
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
|
||||
chatRoom.deleteMessage(chatMessage)
|
||||
}
|
||||
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
private fun addChatMessageEventLog(eventLog: EventLog) {
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
chatMessage ?: return
|
||||
chatMessage.userData = events.value.orEmpty().size
|
||||
|
||||
val existingEvent = events.value.orEmpty().find { data ->
|
||||
data.eventLog == eventLog
|
||||
}
|
||||
if (existingEvent != null) {
|
||||
Log.w("[Chat Messages] Found already present chat message, don't add it it's probably the result of an auto download")
|
||||
return
|
||||
}
|
||||
|
||||
if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
||||
for (content in chatMessage.contents) {
|
||||
if (content.isFileTransfer) {
|
||||
Log.i("[Chat Messages] Android < 10 detected and WRITE_EXTERNAL_STORAGE permission isn't granted yet")
|
||||
requestWriteExternalStoragePermissionEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.ContactsSelectionViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomCreationViewModel : ContactsSelectionViewModel() {
|
||||
val chatRoomCreatedEvent: MutableLiveData<Event<ChatRoom>> by lazy {
|
||||
MutableLiveData<Event<ChatRoom>>()
|
||||
}
|
||||
|
||||
val createGroupChat = MutableLiveData<Boolean>()
|
||||
|
||||
val isEncrypted = MutableLiveData<Boolean>()
|
||||
|
||||
val waitForChatRoomCreation = MutableLiveData<Boolean>()
|
||||
|
||||
val secureChatAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val secureChatMandatory: Boolean = corePreferences.forceEndToEndEncryptedChat
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(room: ChatRoom, state: ChatRoom.State) {
|
||||
if (state == ChatRoom.State.Created) {
|
||||
waitForChatRoomCreation.value = false
|
||||
Log.i("[Chat Room Creation] Chat room created")
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
} else if (state == ChatRoom.State.CreationFailed) {
|
||||
Log.e("[Chat Room Creation] Group chat room creation has failed !")
|
||||
waitForChatRoomCreation.value = false
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
createGroupChat.value = false
|
||||
isEncrypted.value = secureChatMandatory
|
||||
waitForChatRoomCreation.value = false
|
||||
secureChatAvailable.value = LinphoneUtils.isEndToEndEncryptedChatAvailable()
|
||||
}
|
||||
|
||||
fun updateEncryption(encrypted: Boolean) {
|
||||
if (!encrypted && secureChatMandatory) {
|
||||
Log.w("[Chat Room Creation] Something tries to force plain text chat room even if secureChatMandatory is enabled!")
|
||||
return
|
||||
}
|
||||
isEncrypted.value = encrypted
|
||||
}
|
||||
|
||||
fun createOneToOneChat(searchResult: SearchResult) {
|
||||
waitForChatRoomCreation.value = true
|
||||
val defaultAccount = coreContext.core.defaultAccount
|
||||
var room: ChatRoom?
|
||||
|
||||
val address = searchResult.address ?: coreContext.core.interpretUrl(searchResult.phoneNumber ?: "", LinphoneUtils.applyInternationalPrefix())
|
||||
if (address == null) {
|
||||
Log.e("[Chat Room Creation] Can't get a valid address from search result $searchResult")
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
waitForChatRoomCreation.value = false
|
||||
return
|
||||
}
|
||||
|
||||
val encrypted = secureChatMandatory || isEncrypted.value == true
|
||||
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
|
||||
params.backend = ChatRoomBackend.Basic
|
||||
params.isGroupEnabled = false
|
||||
if (encrypted) {
|
||||
params.isEncryptionEnabled = true
|
||||
params.backend = ChatRoomBackend.FlexisipChat
|
||||
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
|
||||
ChatRoomEphemeralMode.DeviceManaged
|
||||
else
|
||||
ChatRoomEphemeralMode.AdminManaged
|
||||
params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
|
||||
Log.i("[Chat Room Creation] Ephemeral mode is ${params.ephemeralMode}, lifetime is ${params.ephemeralLifetime}")
|
||||
params.subject = AppUtils.getString(R.string.chat_room_dummy_subject)
|
||||
}
|
||||
|
||||
val participants = arrayOf(address)
|
||||
val localAddress: Address? = defaultAccount?.params?.identityAddress
|
||||
|
||||
room = coreContext.core.searchChatRoom(params, localAddress, null, participants)
|
||||
if (room == null) {
|
||||
Log.w("[Chat Room Creation] Couldn't find existing 1-1 chat room with remote ${address.asStringUriOnly()}, encryption=$encrypted and local identity ${localAddress?.asStringUriOnly()}")
|
||||
room = coreContext.core.createChatRoom(params, localAddress, participants)
|
||||
|
||||
if (room != null) {
|
||||
if (encrypted) {
|
||||
val state = room.state
|
||||
if (state == ChatRoom.State.Created) {
|
||||
Log.i("[Chat Room Creation] Found already created chat room, using it")
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
waitForChatRoomCreation.value = false
|
||||
} else {
|
||||
Log.i("[Chat Room Creation] Chat room creation is pending [$state], waiting for Created state")
|
||||
room.addListener(listener)
|
||||
}
|
||||
} else {
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
} else {
|
||||
Log.e("[Chat Room Creation] Couldn't create chat room with remote ${address.asStringUriOnly()} and local identity ${localAddress?.asStringUriOnly()}")
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
} else {
|
||||
Log.i("[Chat Room Creation] Found existing 1-1 chat room with remote ${address.asStringUriOnly()}, encryption=$encrypted and local identity ${localAddress?.asStringUriOnly()}")
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,382 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.animation.LinearInterpolator
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.ContactDataInterface
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ChatRoomViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterface {
|
||||
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
|
||||
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
|
||||
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
|
||||
override val showGroupChatAvatar: Boolean
|
||||
get() = conferenceChatRoom && !oneToOneChatRoom
|
||||
override val coroutineScope: CoroutineScope = viewModelScope
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
||||
val participants = MutableLiveData<String>()
|
||||
|
||||
val unreadMessagesCount = MutableLiveData<Int>()
|
||||
|
||||
val remoteIsComposing = MutableLiveData<Boolean>()
|
||||
|
||||
val composingList = MutableLiveData<String>()
|
||||
|
||||
val securityLevelIcon = MutableLiveData<Int>()
|
||||
|
||||
val securityLevelContentDescription = MutableLiveData<Int>()
|
||||
|
||||
val peerSipUri = MutableLiveData<String>()
|
||||
|
||||
val ephemeralEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val basicChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
|
||||
}
|
||||
|
||||
val oneToOneChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
|
||||
}
|
||||
|
||||
private val conferenceChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt())
|
||||
}
|
||||
|
||||
val encryptedChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())
|
||||
}
|
||||
|
||||
val ephemeralChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Ephemeral.toInt())
|
||||
}
|
||||
|
||||
val meAdmin: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>()
|
||||
}
|
||||
|
||||
val isUserScrollingUp = MutableLiveData<Boolean>()
|
||||
|
||||
var oneParticipantOneDevice: Boolean = false
|
||||
|
||||
var onlyParticipantOnlyDeviceAddress: Address? = null
|
||||
|
||||
val chatUnreadCountTranslateY = MutableLiveData<Float>()
|
||||
|
||||
val groupCallAvailable: Boolean
|
||||
get() = LinphoneUtils.isRemoteConferencingAvailable()
|
||||
|
||||
private var addressToCall: Address? = null
|
||||
|
||||
private val bounceAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.tabs_fragment_unread_count_bounce_offset), 0f).apply {
|
||||
addUpdateListener {
|
||||
val value = it.animatedValue as Float
|
||||
chatUnreadCountTranslateY.value = value
|
||||
}
|
||||
interpolator = LinearInterpolator()
|
||||
duration = 250
|
||||
repeatMode = ValueAnimator.REVERSE
|
||||
repeatCount = ValueAnimator.INFINITE
|
||||
}
|
||||
}
|
||||
|
||||
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
Log.d("[Chat Room] Contacts have changed")
|
||||
contactLookup()
|
||||
}
|
||||
}
|
||||
|
||||
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onChatRoomRead(core: Core, room: ChatRoom) {
|
||||
if (room == chatRoom) {
|
||||
updateUnreadMessageCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
Log.i("[Chat Room] $chatRoom state changed: $state")
|
||||
if (state == ChatRoom.State.Created) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
|
||||
override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array<out EventLog>) {
|
||||
updateUnreadMessageCount()
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onIsComposingReceived(
|
||||
chatRoom: ChatRoom,
|
||||
remoteAddr: Address,
|
||||
isComposing: Boolean
|
||||
) {
|
||||
updateRemotesComposing()
|
||||
}
|
||||
|
||||
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
|
||||
override fun onSecurityEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
|
||||
}
|
||||
|
||||
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
meAdmin.value = chatRoom.me?.isAdmin ?: false
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.core.addListener(coreListener)
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
coreContext.contactsManager.addListener(contactsUpdatedListener)
|
||||
|
||||
updateUnreadMessageCount()
|
||||
|
||||
subject.value = chatRoom.subject
|
||||
updateSecurityIcon()
|
||||
meAdmin.value = chatRoom.me?.isAdmin ?: false
|
||||
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
|
||||
|
||||
contactLookup()
|
||||
updateParticipants()
|
||||
|
||||
updateRemotesComposing()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.contactsManager.removeListener(contactsUpdatedListener)
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
chatRoom.core.removeListener(coreListener)
|
||||
if (corePreferences.enableAnimations) bounceAnimator.end()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun contactLookup() {
|
||||
displayName.value = when {
|
||||
basicChatRoom -> LinphoneUtils.getDisplayName(
|
||||
chatRoom.peerAddress
|
||||
)
|
||||
oneToOneChatRoom -> LinphoneUtils.getDisplayName(
|
||||
chatRoom.participants.firstOrNull()?.address ?: chatRoom.peerAddress
|
||||
)
|
||||
conferenceChatRoom -> chatRoom.subject.orEmpty()
|
||||
else -> chatRoom.peerAddress.asStringUriOnly()
|
||||
}
|
||||
|
||||
if (oneToOneChatRoom) {
|
||||
searchMatchingContact()
|
||||
} else {
|
||||
getParticipantsNames()
|
||||
}
|
||||
}
|
||||
|
||||
fun startCall() {
|
||||
val address = addressToCall
|
||||
if (address != null) {
|
||||
coreContext.startCall(address)
|
||||
}
|
||||
}
|
||||
|
||||
fun startGroupCall() {
|
||||
val conferenceScheduler = coreContext.core.createConferenceScheduler()
|
||||
val conferenceInfo = Factory.instance().createConferenceInfo()
|
||||
|
||||
val localAddress = chatRoom.localAddress.clone()
|
||||
localAddress.clean() // Remove GRUU
|
||||
val addresses = Array(chatRoom.participants.size) {
|
||||
index ->
|
||||
chatRoom.participants[index].address
|
||||
}
|
||||
val localAccount = coreContext.core.accountList.find {
|
||||
account ->
|
||||
account.params.identityAddress?.weakEqual(localAddress) ?: false
|
||||
}
|
||||
|
||||
conferenceInfo.organizer = localAddress
|
||||
conferenceInfo.subject = subject.value
|
||||
conferenceInfo.setParticipants(addresses)
|
||||
conferenceScheduler.account = localAccount
|
||||
// Will trigger the conference creation/update automatically
|
||||
conferenceScheduler.info = conferenceInfo
|
||||
}
|
||||
|
||||
fun areNotificationsMuted(): Boolean {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress)
|
||||
return corePreferences.chatRoomMuted(id)
|
||||
}
|
||||
|
||||
fun muteNotifications(mute: Boolean) {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress)
|
||||
corePreferences.muteChatRoom(id, mute)
|
||||
}
|
||||
|
||||
fun getRemoteAddress(): Address? {
|
||||
return if (basicChatRoom) {
|
||||
chatRoom.peerAddress
|
||||
} else {
|
||||
if (chatRoom.participants.isNotEmpty()) {
|
||||
chatRoom.participants[0].address
|
||||
} else {
|
||||
Log.e("[Chat Room] ${chatRoom.peerAddress} doesn't have any participant (state ${chatRoom.state})!")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMatchingContact() {
|
||||
val remoteAddress = getRemoteAddress()
|
||||
if (remoteAddress != null) {
|
||||
contact.value = coreContext.contactsManager.findContactByAddress(remoteAddress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getParticipantsNames() {
|
||||
if (oneToOneChatRoom) return
|
||||
|
||||
var participantsList = ""
|
||||
var index = 0
|
||||
for (participant in chatRoom.participants) {
|
||||
val contact = coreContext.contactsManager.findContactByAddress(participant.address)
|
||||
participantsList += contact?.name ?: LinphoneUtils.getDisplayName(participant.address)
|
||||
index++
|
||||
if (index != chatRoom.nbParticipants) participantsList += ", "
|
||||
}
|
||||
participants.value = participantsList
|
||||
}
|
||||
|
||||
private fun updateSecurityIcon() {
|
||||
val level = chatRoom.securityLevel
|
||||
securityLevel.value = level
|
||||
|
||||
securityLevelIcon.value = when (level) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
securityLevelContentDescription.value = when (level) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRemotesComposing() {
|
||||
val isComposing = chatRoom.isRemoteComposing
|
||||
remoteIsComposing.value = isComposing
|
||||
if (!isComposing) return
|
||||
|
||||
var composing = ""
|
||||
for (address in chatRoom.composingAddresses) {
|
||||
val contact = coreContext.contactsManager.findContactByAddress(address)
|
||||
composing += if (composing.isNotEmpty()) ", " else ""
|
||||
composing += contact?.name ?: LinphoneUtils.getDisplayName(address)
|
||||
}
|
||||
composingList.value = AppUtils.getStringWithPlural(R.plurals.chat_room_remote_composing, chatRoom.composingAddresses.size, composing)
|
||||
}
|
||||
|
||||
private fun updateParticipants() {
|
||||
val participants = chatRoom.participants
|
||||
peerSipUri.value = if (oneToOneChatRoom && !basicChatRoom) {
|
||||
participants.firstOrNull()?.address?.asStringUriOnly()
|
||||
?: chatRoom.peerAddress.asStringUriOnly()
|
||||
} else {
|
||||
chatRoom.peerAddress.asStringUriOnly()
|
||||
}
|
||||
|
||||
oneParticipantOneDevice = oneToOneChatRoom &&
|
||||
chatRoom.me?.devices?.size == 1 &&
|
||||
participants.firstOrNull()?.devices?.size == 1
|
||||
|
||||
addressToCall = if (basicChatRoom)
|
||||
chatRoom.peerAddress
|
||||
else
|
||||
participants.firstOrNull()?.address
|
||||
|
||||
onlyParticipantOnlyDeviceAddress = participants.firstOrNull()?.devices?.firstOrNull()?.address
|
||||
}
|
||||
|
||||
private fun updateUnreadMessageCount() {
|
||||
val count = chatRoom.unreadMessagesCount
|
||||
unreadMessagesCount.value = count
|
||||
if (count > 0 && corePreferences.enableAnimations) bounceAnimator.start()
|
||||
else if (count == 0 && bounceAnimator.isStarted) bounceAnimator.end()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomsListViewModel : MessageNotifierViewModel() {
|
||||
val chatRooms = MutableLiveData<ArrayList<ChatRoom>>()
|
||||
|
||||
val fileSharingPending = MutableLiveData<Boolean>()
|
||||
|
||||
val textSharingPending = MutableLiveData<Boolean>()
|
||||
|
||||
val forwardPending = MutableLiveData<Boolean>()
|
||||
|
||||
val groupChatAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val chatRoomIndexUpdatedEvent: MutableLiveData<Event<Int>> by lazy {
|
||||
MutableLiveData<Event<Int>>()
|
||||
}
|
||||
|
||||
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onChatRoomStateChanged(core: Core, chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
if (state == ChatRoom.State.Created) {
|
||||
updateChatRooms()
|
||||
} else if (state == ChatRoom.State.TerminationFailed) {
|
||||
Log.e("[Chat Rooms] Group chat room removal for address ${chatRoom.peerAddress.asStringUriOnly()} has failed !")
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_removal_failed_snack)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageSent(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
|
||||
onChatRoomMessageEvent(chatRoom)
|
||||
}
|
||||
|
||||
override fun onMessagesReceived(
|
||||
core: Core,
|
||||
chatRoom: ChatRoom,
|
||||
messages: Array<out ChatMessage>
|
||||
) {
|
||||
onChatRoomMessageEvent(chatRoom)
|
||||
}
|
||||
|
||||
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
|
||||
notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
|
||||
override fun onChatRoomEphemeralMessageDeleted(core: Core, chatRoom: ChatRoom) {
|
||||
notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
|
||||
override fun onChatRoomSubjectChanged(core: Core, chatRoom: ChatRoom) {
|
||||
notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
private val chatRoomListener = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, newState: ChatRoom.State) {
|
||||
if (newState == ChatRoom.State.Deleted) {
|
||||
updateChatRooms()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val contactsListener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
updateChatRooms()
|
||||
}
|
||||
}
|
||||
|
||||
private var chatRoomsToDeleteCount = 0
|
||||
|
||||
init {
|
||||
groupChatAvailable.value = LinphoneUtils.isGroupChatAvailable()
|
||||
updateChatRooms()
|
||||
coreContext.core.addListener(listener)
|
||||
coreContext.contactsManager.addListener(contactsListener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.contactsManager.removeListener(contactsListener)
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun deleteChatRoom(chatRoom: ChatRoom?) {
|
||||
for (eventLog in chatRoom?.getHistoryMessageEvents(0).orEmpty()) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
|
||||
}
|
||||
|
||||
chatRoomsToDeleteCount = 1
|
||||
if (chatRoom != null) {
|
||||
coreContext.notificationsManager.dismissChatNotification(chatRoom)
|
||||
Compatibility.removeChatRoomShortcut(coreContext.context, chatRoom)
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
coreContext.core.deleteChatRoom(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteChatRooms(chatRooms: ArrayList<ChatRoom>) {
|
||||
chatRoomsToDeleteCount = chatRooms.size
|
||||
for (chatRoom in chatRooms) {
|
||||
for (eventLog in chatRoom.getHistoryMessageEvents(0)) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
|
||||
}
|
||||
|
||||
coreContext.notificationsManager.dismissChatNotification(chatRoom)
|
||||
Compatibility.removeChatRoomShortcut(coreContext.context, chatRoom)
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
chatRoom.core.deleteChatRoom(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChatRooms() {
|
||||
val list = arrayListOf<ChatRoom>()
|
||||
list.addAll(coreContext.core.chatRooms)
|
||||
chatRooms.value = list
|
||||
}
|
||||
|
||||
fun notifyChatRoomUpdate(chatRoom: ChatRoom) {
|
||||
val index = findChatRoomIndex(chatRoom)
|
||||
if (index == -1) updateChatRooms()
|
||||
else chatRoomIndexUpdatedEvent.value = Event(index)
|
||||
}
|
||||
|
||||
private fun reorderChatRooms() {
|
||||
val list = arrayListOf<ChatRoom>()
|
||||
list.addAll(chatRooms.value.orEmpty())
|
||||
list.sortByDescending { chatRoom -> chatRoom.lastUpdateTime }
|
||||
chatRooms.value = list
|
||||
}
|
||||
|
||||
private fun findChatRoomIndex(chatRoom: ChatRoom): Int {
|
||||
for ((index, cr) in chatRooms.value.orEmpty().withIndex()) {
|
||||
if (LinphoneUtils.areChatRoomsTheSame(cr, chatRoom)) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun onChatRoomMessageEvent(chatRoom: ChatRoom) {
|
||||
when (findChatRoomIndex(chatRoom)) {
|
||||
-1 -> updateChatRooms()
|
||||
0 -> chatRoomIndexUpdatedEvent.value = Event(0)
|
||||
else -> reorderChatRooms()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.activities.main.chat.data.DevicesListGroupData
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomListenerStub
|
||||
import org.linphone.core.EventLog
|
||||
|
||||
class DevicesListViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return DevicesListViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class DevicesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
val participants = MutableLiveData<ArrayList<DevicesListGroupData>>()
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onParticipantDeviceAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
|
||||
chatRoom.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun updateParticipants() {
|
||||
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
|
||||
|
||||
val list = arrayListOf<DevicesListGroupData>()
|
||||
val me = chatRoom.me
|
||||
if (me != null) list.add(DevicesListGroupData(me))
|
||||
for (participant in chatRoom.participants) {
|
||||
list.add(DevicesListGroupData(participant))
|
||||
}
|
||||
|
||||
participants.value = list
|
||||
}
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.data.DurationItemClicked
|
||||
import org.linphone.activities.main.chat.data.EphemeralDurationData
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class EphemeralViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return EphemeralViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
val durationsList = MutableLiveData<ArrayList<EphemeralDurationData>>()
|
||||
|
||||
var currentSelectedDuration: Long = 0
|
||||
|
||||
private val listener = object : DurationItemClicked {
|
||||
override fun onDurationValueChanged(duration: Long) {
|
||||
currentSelectedDuration = duration
|
||||
computeEphemeralDurationValues()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Log.i("[Ephemeral Messages] Current lifetime is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.isEphemeralEnabled}")
|
||||
currentSelectedDuration = if (chatRoom.isEphemeralEnabled) chatRoom.ephemeralLifetime else 0
|
||||
computeEphemeralDurationValues()
|
||||
}
|
||||
|
||||
fun updateChatRoomEphemeralDuration() {
|
||||
Log.i("[Ephemeral Messages] Selected value is $currentSelectedDuration")
|
||||
if (currentSelectedDuration > 0) {
|
||||
if (chatRoom.ephemeralLifetime != currentSelectedDuration) {
|
||||
Log.i("[Ephemeral Messages] Setting new lifetime for ephemeral messages to $currentSelectedDuration")
|
||||
chatRoom.ephemeralLifetime = currentSelectedDuration
|
||||
} else {
|
||||
Log.i("[Ephemeral Messages] Configured lifetime for ephemeral messages was already $currentSelectedDuration")
|
||||
}
|
||||
|
||||
if (!chatRoom.isEphemeralEnabled) {
|
||||
Log.i("[Ephemeral Messages] Ephemeral messages were disabled, enable them")
|
||||
chatRoom.isEphemeralEnabled = true
|
||||
}
|
||||
} else if (chatRoom.isEphemeralEnabled) {
|
||||
Log.i("[Ephemeral Messages] Ephemeral messages were enabled, disable them")
|
||||
chatRoom.isEphemeralEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeEphemeralDurationValues() {
|
||||
val list = arrayListOf<EphemeralDurationData>()
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_disabled, currentSelectedDuration, 0, listener))
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_one_minute, currentSelectedDuration, 60, listener))
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_one_hour, currentSelectedDuration, 3600, listener))
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_one_day, currentSelectedDuration, 86400, listener))
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_three_days, currentSelectedDuration, 259200, listener))
|
||||
list.add(EphemeralDurationData(R.string.chat_room_ephemeral_message_one_week, currentSelectedDuration, 604800, listener))
|
||||
durationsList.value = list
|
||||
}
|
||||
}
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.activities.main.chat.data.GroupInfoParticipantData
|
||||
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class GroupInfoViewModelFactory(private val chatRoom: ChatRoom?) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return GroupInfoViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class GroupInfoViewModel(val chatRoom: ChatRoom?) : MessageNotifierViewModel() {
|
||||
val createdChatRoomEvent = MutableLiveData<Event<ChatRoom>>()
|
||||
val updatedChatRoomEvent = MutableLiveData<Event<ChatRoom>>()
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
||||
val participants = MutableLiveData<ArrayList<GroupInfoParticipantData>>()
|
||||
|
||||
val isEncrypted = MutableLiveData<Boolean>()
|
||||
|
||||
val isMeAdmin = MutableLiveData<Boolean>()
|
||||
|
||||
val canLeaveGroup = MutableLiveData<Boolean>()
|
||||
|
||||
val waitForChatRoomCreation = MutableLiveData<Boolean>()
|
||||
|
||||
val meAdminChangedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
if (state == ChatRoom.State.Created) {
|
||||
waitForChatRoomCreation.value = false
|
||||
createdChatRoomEvent.value = Event(chatRoom) // To trigger going to the chat room
|
||||
} else if (state == ChatRoom.State.CreationFailed) {
|
||||
Log.e("[Chat Room Group Info] Group chat room creation has failed !")
|
||||
waitForChatRoomCreation.value = false
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
val admin = chatRoom.me?.isAdmin ?: false
|
||||
if (admin != isMeAdmin.value) {
|
||||
isMeAdmin.value = admin
|
||||
meAdminChangedEvent.value = Event(admin)
|
||||
}
|
||||
updateParticipants()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
subject.value = chatRoom?.subject
|
||||
isMeAdmin.value = chatRoom == null || (chatRoom.me?.isAdmin == true && !chatRoom.isReadOnly)
|
||||
canLeaveGroup.value = chatRoom != null && !chatRoom.isReadOnly
|
||||
isEncrypted.value = corePreferences.forceEndToEndEncryptedChat || chatRoom?.hasCapability(ChatRoomCapabilities.Encrypted.toInt()) == true
|
||||
|
||||
if (chatRoom != null) updateParticipants()
|
||||
|
||||
chatRoom?.addListener(listener)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
participants.value.orEmpty().forEach(GroupInfoParticipantData::destroy)
|
||||
chatRoom?.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun createChatRoom() {
|
||||
waitForChatRoomCreation.value = true
|
||||
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
|
||||
params.isEncryptionEnabled = corePreferences.forceEndToEndEncryptedChat || isEncrypted.value == true
|
||||
params.isGroupEnabled = true
|
||||
if (params.isEncryptionEnabled) {
|
||||
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
|
||||
ChatRoomEphemeralMode.DeviceManaged
|
||||
else
|
||||
ChatRoomEphemeralMode.AdminManaged
|
||||
}
|
||||
params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
|
||||
Log.i("[Chat Room Group Info] Ephemeral mode is ${params.ephemeralMode}, lifetime is ${params.ephemeralLifetime}")
|
||||
params.subject = subject.value
|
||||
|
||||
val addresses = arrayOfNulls<Address>(participants.value.orEmpty().size)
|
||||
var index = 0
|
||||
for (participant in participants.value.orEmpty()) {
|
||||
addresses[index] = participant.participant.address
|
||||
Log.i("[Chat Room Group Info] Participant ${participant.sipUri} will be added to group")
|
||||
index += 1
|
||||
}
|
||||
|
||||
val chatRoom: ChatRoom? = coreContext.core.createChatRoom(params, coreContext.core.defaultAccount?.params?.identityAddress, addresses)
|
||||
chatRoom?.addListener(listener)
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Chat Room Group Info] Couldn't create chat room!")
|
||||
waitForChatRoomCreation.value = false
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateRoom() {
|
||||
if (chatRoom != null) {
|
||||
// Subject
|
||||
val newSubject = subject.value.orEmpty()
|
||||
if (newSubject.isNotEmpty() && newSubject != chatRoom.subject) {
|
||||
Log.i("[Chat Room Group Info] Subject changed to $newSubject")
|
||||
chatRoom.subject = newSubject
|
||||
}
|
||||
|
||||
// Removed participants
|
||||
val participantsToRemove = arrayListOf<Participant>()
|
||||
for (participant in chatRoom.participants) {
|
||||
val member = participants.value.orEmpty().find { member ->
|
||||
participant.address.weakEqual(member.participant.address)
|
||||
}
|
||||
if (member == null) {
|
||||
Log.w("[Chat Room Group Info] Participant ${participant.address.asStringUriOnly()} will be removed from group")
|
||||
participantsToRemove.add(participant)
|
||||
}
|
||||
}
|
||||
val toRemove = arrayOfNulls<Participant>(participantsToRemove.size)
|
||||
participantsToRemove.toArray(toRemove)
|
||||
chatRoom.removeParticipants(toRemove)
|
||||
|
||||
// Added participants & new admins
|
||||
val participantsToAdd = arrayListOf<Address>()
|
||||
for (member in participants.value.orEmpty()) {
|
||||
val participant = chatRoom.participants.find { participant ->
|
||||
participant.address.weakEqual(member.participant.address)
|
||||
}
|
||||
if (participant != null) {
|
||||
// Participant found, check if admin status needs to be updated
|
||||
if (member.participant.isAdmin != participant.isAdmin) {
|
||||
if (chatRoom.me?.isAdmin == true) {
|
||||
Log.i("[Chat Room Group Info] Participant ${member.sipUri} will be admin? ${member.isAdmin}")
|
||||
chatRoom.setParticipantAdminStatus(participant, member.participant.isAdmin)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.i("[Chat Room Group Info] Participant ${member.sipUri} will be added to group")
|
||||
participantsToAdd.add(member.participant.address)
|
||||
}
|
||||
}
|
||||
val toAdd = arrayOfNulls<Address>(participantsToAdd.size)
|
||||
participantsToAdd.toArray(toAdd)
|
||||
chatRoom.addParticipants(toAdd)
|
||||
|
||||
// Go back to chat room
|
||||
updatedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun leaveGroup() {
|
||||
if (chatRoom != null) {
|
||||
Log.w("[Chat Room Group Info] Leaving group")
|
||||
chatRoom.leave()
|
||||
updatedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeParticipant(participant: GroupChatRoomMember) {
|
||||
val list = arrayListOf<GroupInfoParticipantData>()
|
||||
for (data in participants.value.orEmpty()) {
|
||||
if (!data.participant.address.weakEqual(participant.address)) {
|
||||
list.add(data)
|
||||
}
|
||||
}
|
||||
participants.value = list
|
||||
}
|
||||
|
||||
private fun updateParticipants() {
|
||||
val list = arrayListOf<GroupInfoParticipantData>()
|
||||
|
||||
if (chatRoom != null) {
|
||||
for (participant in chatRoom.participants) {
|
||||
list.add(
|
||||
GroupInfoParticipantData(
|
||||
GroupChatRoomMember(participant.address, participant.isAdmin, participant.securityLevel, canBeSetAdmin = true)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
participants.value = list
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||
import org.linphone.activities.main.chat.data.ImdnParticipantData
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatMessageListenerStub
|
||||
import org.linphone.core.ParticipantImdnState
|
||||
|
||||
class ImdnViewModelFactory(private val chatMessage: ChatMessage) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ImdnViewModel(chatMessage) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ImdnViewModel(private val chatMessage: ChatMessage) : ViewModel() {
|
||||
val participants = MutableLiveData<ArrayList<ImdnParticipantData>>()
|
||||
|
||||
val chatMessageViewModel = ChatMessageData(chatMessage)
|
||||
|
||||
private val listener = object : ChatMessageListenerStub() {
|
||||
override fun onParticipantImdnStateChanged(
|
||||
message: ChatMessage,
|
||||
state: ParticipantImdnState
|
||||
) {
|
||||
updateParticipantsLists()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatMessage.addListener(listener)
|
||||
updateParticipantsLists()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
participants.value.orEmpty().forEach(ImdnParticipantData::destroy)
|
||||
chatMessage.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun updateParticipantsLists() {
|
||||
val list = arrayListOf<ImdnParticipantData>()
|
||||
|
||||
for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.Displayed)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.DeliveredToUser)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.Delivered)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.NotDelivered)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
|
||||
participants.value = list
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.chat.views
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Layout
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* The purpose of this class is to have a TextView declared with wrap_content as width that won't
|
||||
* fill it's parent if it is multi line.
|
||||
*/
|
||||
class MultiLineWrapContentWidthTextView : AppCompatTextView {
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun setText(text: CharSequence?, type: BufferType?) {
|
||||
super.setText(text, type)
|
||||
// Required for PatternClickableSpan
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||
super.onMeasure(widthSpec, heightSpec)
|
||||
|
||||
if (layout != null && layout.lineCount >= 2) {
|
||||
val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
|
||||
val uselessPaddingWidth = layout.width - maxLineWidth
|
||||
val width = measuredWidth - uselessPaddingWidth
|
||||
val height = measuredHeight
|
||||
setMeasuredDimension(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaxLineWidth(layout: Layout): Float {
|
||||
var maxWidth = 0.0f
|
||||
val lines = layout.lineCount
|
||||
for (i in 0 until lines) {
|
||||
if (layout.getLineWidth(i) > maxWidth) {
|
||||
maxWidth = layout.getLineWidth(i)
|
||||
}
|
||||
}
|
||||
return maxWidth
|
||||
}
|
||||
}
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.conference.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.adapters.SelectionListAdapter
|
||||
import org.linphone.activities.main.conference.data.ScheduledConferenceData
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.databinding.ConferenceScheduleCellBinding
|
||||
import org.linphone.databinding.ConferenceScheduleListHeaderBinding
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.HeaderAdapter
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ScheduledConferencesAdapter(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : SelectionListAdapter<ScheduledConferenceData, RecyclerView.ViewHolder>(selectionVM, ConferenceInfoDiffCallback()),
|
||||
HeaderAdapter {
|
||||
val copyAddressToClipboardEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val joinConferenceEvent: MutableLiveData<Event<Pair<String, String?>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String?>>>()
|
||||
}
|
||||
|
||||
val editConferenceEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val deleteConferenceInfoEvent: MutableLiveData<Event<ScheduledConferenceData>> by lazy {
|
||||
MutableLiveData<Event<ScheduledConferenceData>>()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScheduledConferencesAdapter.ViewHolder {
|
||||
val binding: ConferenceScheduleCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.conference_schedule_cell, parent, false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ScheduledConferencesAdapter.ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
if (position >= itemCount) return false
|
||||
val conferenceInfo = getItem(position)
|
||||
val previousPosition = position - 1
|
||||
return if (previousPosition >= 0) {
|
||||
val previousItem = getItem(previousPosition)
|
||||
!TimestampUtils.isSameDay(previousItem.conferenceInfo.dateTime, conferenceInfo.conferenceInfo.dateTime)
|
||||
} else true
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
val data = getItem(position)
|
||||
val binding: ConferenceScheduleListHeaderBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.conference_schedule_list_header, null, false
|
||||
)
|
||||
binding.title = formatDate(context, data.conferenceInfo.dateTime)
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun formatDate(context: Context, date: Long): String {
|
||||
if (TimestampUtils.isToday(date)) {
|
||||
return context.getString(R.string.today)
|
||||
}
|
||||
return TimestampUtils.toString(date, onlyDate = true, shortDate = false, hideYear = false)
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: ConferenceScheduleCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(conferenceData: ScheduledConferenceData) {
|
||||
with(binding) {
|
||||
data = conferenceData
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
} else {
|
||||
conferenceData.toggleExpand()
|
||||
}
|
||||
}
|
||||
|
||||
setLongClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == false) {
|
||||
selectionViewModel.isEditionEnabled.value = true
|
||||
// Selection will be handled by click listener
|
||||
true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
setCopyAddressClickListener {
|
||||
val address = conferenceData.getAddressAsString()
|
||||
if (address.isNotEmpty()) {
|
||||
copyAddressToClipboardEvent.value = Event(address)
|
||||
}
|
||||
}
|
||||
|
||||
setJoinConferenceClickListener {
|
||||
val address = conferenceData.conferenceInfo.uri
|
||||
if (address != null) {
|
||||
joinConferenceEvent.value = Event(Pair(address.asStringUriOnly(), conferenceData.conferenceInfo.subject))
|
||||
}
|
||||
}
|
||||
|
||||
setEditConferenceClickListener {
|
||||
val address = conferenceData.conferenceInfo.uri
|
||||
if (address != null) {
|
||||
editConferenceEvent.value = Event(address.asStringUriOnly())
|
||||
}
|
||||
}
|
||||
|
||||
setDeleteConferenceClickListener {
|
||||
deleteConferenceInfoEvent.value = Event(conferenceData)
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConferenceInfoDiffCallback : DiffUtil.ItemCallback<ScheduledConferenceData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ScheduledConferenceData,
|
||||
newItem: ScheduledConferenceData
|
||||
): Boolean {
|
||||
return oldItem.conferenceInfo == newItem.conferenceInfo
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ScheduledConferenceData,
|
||||
newItem: ScheduledConferenceData
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.conference.data
|
||||
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ConferenceSchedulingParticipantData(
|
||||
private val sipAddress: Address,
|
||||
val showLimeBadge: Boolean = false,
|
||||
val showDivider: Boolean = true
|
||||
) :
|
||||
GenericContactData(sipAddress) {
|
||||
val sipUri: String get() = LinphoneUtils.getDisplayableAddress(sipAddress)
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.conference.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.ConferenceInfo
|
||||
import org.linphone.core.ConferenceInfo.State
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ScheduledConferenceData(val conferenceInfo: ConferenceInfo, private val isFinished: Boolean) {
|
||||
val expanded = MutableLiveData<Boolean>()
|
||||
val backgroundResId = MutableLiveData<Int>()
|
||||
|
||||
val address = MutableLiveData<String>()
|
||||
val subject = MutableLiveData<String>()
|
||||
val description = MutableLiveData<String>()
|
||||
val time = MutableLiveData<String>()
|
||||
val date = MutableLiveData<String>()
|
||||
val duration = MutableLiveData<String>()
|
||||
val organizer = MutableLiveData<String>()
|
||||
val canEdit = MutableLiveData<Boolean>()
|
||||
val participantsShort = MutableLiveData<String>()
|
||||
val participantsExpanded = MutableLiveData<String>()
|
||||
val showDuration = MutableLiveData<Boolean>()
|
||||
val isConferenceCancelled = MutableLiveData<Boolean>()
|
||||
|
||||
init {
|
||||
expanded.value = false
|
||||
|
||||
address.value = conferenceInfo.uri?.asStringUriOnly()
|
||||
subject.value = conferenceInfo.subject
|
||||
description.value = conferenceInfo.description
|
||||
|
||||
time.value = TimestampUtils.timeToString(conferenceInfo.dateTime)
|
||||
date.value = TimestampUtils.toString(conferenceInfo.dateTime, onlyDate = true, shortDate = false, hideYear = false)
|
||||
isConferenceCancelled.value = conferenceInfo.state == State.Cancelled
|
||||
|
||||
val minutes = conferenceInfo.duration
|
||||
val hours = TimeUnit.MINUTES.toHours(minutes.toLong())
|
||||
val remainMinutes = minutes - TimeUnit.HOURS.toMinutes(hours).toInt()
|
||||
duration.value = TimestampUtils.durationToString(hours.toInt(), remainMinutes)
|
||||
showDuration.value = minutes > 0
|
||||
|
||||
val organizerAddress = conferenceInfo.organizer
|
||||
if (organizerAddress != null) {
|
||||
val localAccount = coreContext.core.accountList.find { account ->
|
||||
val address = account.params.identityAddress
|
||||
address != null && organizerAddress.weakEqual(address)
|
||||
}
|
||||
canEdit.value = localAccount != null
|
||||
|
||||
val contact = coreContext.contactsManager.findContactByAddress(organizerAddress)
|
||||
organizer.value = if (contact != null)
|
||||
contact.name
|
||||
else
|
||||
LinphoneUtils.getDisplayName(conferenceInfo.organizer)
|
||||
} else {
|
||||
canEdit.value = false
|
||||
Log.e("[Scheduled Conference] No organizer SIP URI found for: ${conferenceInfo.uri?.asStringUriOnly()}")
|
||||
}
|
||||
|
||||
computeBackgroundResId()
|
||||
computeParticipantsLists()
|
||||
}
|
||||
|
||||
fun destroy() {}
|
||||
|
||||
fun delete() {
|
||||
Log.w("[Scheduled Conference] Deleting conference info with URI: ${conferenceInfo.uri?.asStringUriOnly()}")
|
||||
coreContext.core.deleteConferenceInformation(conferenceInfo)
|
||||
}
|
||||
|
||||
fun toggleExpand() {
|
||||
expanded.value = expanded.value == false
|
||||
computeBackgroundResId()
|
||||
}
|
||||
|
||||
fun getAddressAsString(): String {
|
||||
val address = conferenceInfo.uri?.clone()
|
||||
if (address != null) {
|
||||
address.displayName = conferenceInfo.subject
|
||||
return address.asString()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun computeBackgroundResId() {
|
||||
backgroundResId.value = if (conferenceInfo.state == State.Cancelled) {
|
||||
if (expanded.value == true) {
|
||||
R.drawable.shape_round_red_background_with_orange_border
|
||||
} else {
|
||||
R.drawable.shape_round_red_background
|
||||
}
|
||||
} else if (isFinished) {
|
||||
if (expanded.value == true) {
|
||||
R.drawable.shape_round_dark_gray_background_with_orange_border
|
||||
} else {
|
||||
R.drawable.shape_round_dark_gray_background
|
||||
}
|
||||
} else {
|
||||
if (expanded.value == true) {
|
||||
R.drawable.shape_round_gray_background_with_orange_border
|
||||
} else {
|
||||
R.drawable.shape_round_gray_background
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeParticipantsLists() {
|
||||
var participantsListShort = ""
|
||||
var participantsListExpanded = ""
|
||||
|
||||
for (participant in conferenceInfo.participants) {
|
||||
val contact = coreContext.contactsManager.findContactByAddress(participant)
|
||||
val name = if (contact != null) contact.name else LinphoneUtils.getDisplayName(participant)
|
||||
val address = participant.asStringUriOnly()
|
||||
participantsListShort += "$name, "
|
||||
participantsListExpanded += "$name ($address)\n"
|
||||
}
|
||||
participantsListShort = participantsListShort.dropLast(2)
|
||||
participantsListExpanded = participantsListExpanded.dropLast(1)
|
||||
|
||||
participantsShort.value = participantsListShort
|
||||
participantsExpanded.value = participantsListExpanded
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.conference.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat.is24HourFormat
|
||||
import android.view.View
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import com.google.android.material.datepicker.CalendarConstraints
|
||||
import com.google.android.material.datepicker.DateValidatorPointForward
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
import com.google.android.material.timepicker.TimeFormat
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel
|
||||
import org.linphone.activities.navigateToParticipantsList
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ConferenceSchedulingFragmentBinding
|
||||
|
||||
class ConferenceSchedulingFragment : GenericFragment<ConferenceSchedulingFragmentBinding>() {
|
||||
private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels(R.id.conference_scheduling_nav_graph)
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.conference_scheduling_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.viewModel = viewModel
|
||||
|
||||
sharedViewModel.participantsListForNextScheduledMeeting.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { participants ->
|
||||
Log.i("[Conference Scheduling] Found participants (${participants.size}) to pre-populate for meeting schedule")
|
||||
viewModel.prePopulateParticipantsList(participants, true)
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.addressOfConferenceInfoToEdit.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { address ->
|
||||
val conferenceAddress = Factory.instance().createAddress(address)
|
||||
if (conferenceAddress != null) {
|
||||
Log.i("[Conference Scheduling] Trying to edit conference info using address: $address")
|
||||
val conferenceInfo = coreContext.core.findConferenceInformationFromUri(conferenceAddress)
|
||||
if (conferenceInfo != null) {
|
||||
viewModel.populateFromConferenceInfo(conferenceInfo)
|
||||
} else {
|
||||
Log.e("[Conference Scheduling] Failed to find ConferenceInfo matching address: $address")
|
||||
}
|
||||
} else {
|
||||
Log.e("[Conference Scheduling] Failed to parse conference address: $address")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.setNextClickListener {
|
||||
navigateToParticipantsList()
|
||||
}
|
||||
|
||||
binding.setDatePickerClickListener {
|
||||
val constraintsBuilder =
|
||||
CalendarConstraints.Builder()
|
||||
.setValidator(DateValidatorPointForward.now())
|
||||
val picker =
|
||||
MaterialDatePicker.Builder.datePicker()
|
||||
.setCalendarConstraints(constraintsBuilder.build())
|
||||
.setTitleText(R.string.conference_schedule_date)
|
||||
.setSelection(viewModel.dateTimestamp)
|
||||
.build()
|
||||
picker.addOnPositiveButtonClickListener {
|
||||
val selection = picker.selection
|
||||
if (selection != null) {
|
||||
viewModel.setDate(selection)
|
||||
}
|
||||
}
|
||||
picker.show(requireFragmentManager(), "Date picker")
|
||||
}
|
||||
|
||||
binding.setTimePickerClickListener {
|
||||
val isSystem24Hour = is24HourFormat(requireContext())
|
||||
val clockFormat = if (isSystem24Hour) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H
|
||||
val picker =
|
||||
MaterialTimePicker.Builder()
|
||||
.setTimeFormat(clockFormat)
|
||||
.setTitleText(R.string.conference_schedule_time)
|
||||
.setHour(viewModel.hour)
|
||||
.setMinute(viewModel.minutes)
|
||||
.build()
|
||||
picker.addOnPositiveButtonClickListener {
|
||||
viewModel.setTime(picker.hour, picker.minute)
|
||||
}
|
||||
picker.show(requireFragmentManager(), "Time picker")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.conference.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel
|
||||
import org.linphone.activities.navigateToSummary
|
||||
import org.linphone.contact.ContactsSelectionAdapter
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ConferenceSchedulingParticipantsListFragmentBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ConferenceSchedulingParticipantsListFragment : GenericFragment<ConferenceSchedulingParticipantsListFragmentBinding>() {
|
||||
private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels(R.id.conference_scheduling_nav_graph)
|
||||
private lateinit var adapter: ContactsSelectionAdapter
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.conference_scheduling_participants_list_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.viewModel = viewModel
|
||||
|
||||
adapter = ContactsSelectionAdapter(viewLifecycleOwner)
|
||||
adapter.setLimeCapabilityRequired(viewModel.isEncrypted.value == true)
|
||||
binding.contactsList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.contactsList.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
binding.contactsList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||
|
||||
binding.setNextClickListener {
|
||||
navigateToSummary()
|
||||
}
|
||||
|
||||
viewModel.contactsList.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
viewModel.sipContactsSelected.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
viewModel.applyFilter()
|
||||
}
|
||||
|
||||
viewModel.selectedAddresses.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.updateSelectedAddresses(it)
|
||||
}
|
||||
viewModel.filter.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
viewModel.applyFilter()
|
||||
}
|
||||
|
||||
adapter.selectedContact.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { searchResult ->
|
||||
viewModel.toggleSelectionForSearchResult(searchResult)
|
||||
}
|
||||
}
|
||||
|
||||
if (corePreferences.enableNativeAddressBookIntegration) {
|
||||
if (!PermissionHelper.get().hasReadContactsPermission()) {
|
||||
Log.i("[Conference Creation] Asking for READ_CONTACTS permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == 0) {
|
||||
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[Conference Creation] READ_CONTACTS permission granted")
|
||||
coreContext.fetchContacts()
|
||||
} else {
|
||||
Log.w("[Conference Creation] READ_CONTACTS permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.conference.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel
|
||||
import org.linphone.activities.navigateToDialer
|
||||
import org.linphone.activities.navigateToScheduledConferences
|
||||
import org.linphone.databinding.ConferenceSchedulingSummaryFragmentBinding
|
||||
|
||||
class ConferenceSchedulingSummaryFragment : GenericFragment<ConferenceSchedulingSummaryFragmentBinding>() {
|
||||
private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels(R.id.conference_scheduling_nav_graph)
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.conference_scheduling_summary_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.conferenceCreationCompletedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
if (viewModel.scheduleForLater.value == true) {
|
||||
(requireActivity() as MainActivity).showSnackBar(R.string.conference_schedule_info_created)
|
||||
navigateToScheduledConferences()
|
||||
} else {
|
||||
navigateToDialer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageId ->
|
||||
(activity as MainActivity).showSnackBar(messageId)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.computeParticipantsData()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.conference.fragments
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.conference.viewmodels.ConferenceWaitingRoomViewModel
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ConferenceWaitingRoomFragmentBinding
|
||||
import org.linphone.mediastream.Version
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ConferenceWaitingRoomFragment : GenericFragment<ConferenceWaitingRoomFragmentBinding>() {
|
||||
private lateinit var viewModel: ConferenceWaitingRoomViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.conference_waiting_room_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this
|
||||
)[ConferenceWaitingRoomViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
val conferenceSubject = arguments?.getString("Subject")
|
||||
viewModel.subject.value = conferenceSubject
|
||||
|
||||
viewModel.cancelConferenceJoiningEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
if (viewModel.joinInProgress.value == true) {
|
||||
val conferenceUri = arguments?.getString("Address")
|
||||
val callToCancel = coreContext.core.calls.find { call ->
|
||||
call.remoteAddress.asStringUriOnly() == conferenceUri
|
||||
}
|
||||
if (callToCancel != null) {
|
||||
Log.i("[Conference Waiting Room] Call to conference server with URI [$conferenceUri] was started, terminate it")
|
||||
callToCancel.terminate()
|
||||
} else {
|
||||
Log.w("[Conference Waiting Room] Call to conference server with URI [$conferenceUri] wasn't found!")
|
||||
}
|
||||
}
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.joinConferenceEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { callParams ->
|
||||
val conferenceUri = arguments?.getString("Address")
|
||||
if (conferenceUri != null) {
|
||||
val conferenceAddress = coreContext.core.interpretUrl(conferenceUri, false)
|
||||
if (conferenceAddress != null) {
|
||||
Log.i("[Conference Waiting Room] Calling conference SIP URI: ${conferenceAddress.asStringUriOnly()}")
|
||||
coreContext.startCall(conferenceAddress, callParams)
|
||||
} else {
|
||||
Log.e("[Conference Waiting Room] Failed to parse conference SIP URI: $conferenceUri")
|
||||
}
|
||||
} else {
|
||||
Log.e("[Conference Waiting Room] Failed to find conference SIP URI in arguments")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.askPermissionEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { permission ->
|
||||
Log.i("[Conference Waiting Room] Asking for $permission permission")
|
||||
requestPermissions(arrayOf(permission), 0)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.leaveWaitingRoomEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(activity as MainActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.networkNotReachableEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
(activity as MainActivity).showSnackBar(R.string.call_error_network_unreachable)
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
|
||||
coreContext.core.isVideoPreviewEnabled = viewModel.isVideoEnabled.value == true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
coreContext.core.nativePreviewWindowId = null
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun checkPermissions() {
|
||||
val permissionsRequiredList = arrayListOf<String>()
|
||||
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
Log.i("[Conference Waiting Room] Asking for RECORD_AUDIO permission")
|
||||
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
|
||||
if (!PermissionHelper.get().hasCameraPermission()) {
|
||||
Log.i("[Conference Waiting Room] Asking for CAMERA permission")
|
||||
permissionsRequiredList.add(Manifest.permission.CAMERA)
|
||||
}
|
||||
|
||||
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12) && !PermissionHelper.get().hasBluetoothConnectPermission()) {
|
||||
Log.i("[Conference Waiting Room] Asking for BLUETOOTH_CONNECT permission")
|
||||
permissionsRequiredList.add(Compatibility.BLUETOOTH_CONNECT)
|
||||
}
|
||||
|
||||
if (permissionsRequiredList.isNotEmpty()) {
|
||||
val permissionsRequired = arrayOfNulls<String>(permissionsRequiredList.size)
|
||||
permissionsRequiredList.toArray(permissionsRequired)
|
||||
requestPermissions(permissionsRequired, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == 0) {
|
||||
for (i in permissions.indices) {
|
||||
when (permissions[i]) {
|
||||
Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i("[Conference Waiting Room] RECORD_AUDIO permission has been granted")
|
||||
viewModel.enableMic()
|
||||
}
|
||||
Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i("[Conference Waiting Room] CAMERA permission has been granted")
|
||||
coreContext.core.reloadVideoDevices()
|
||||
viewModel.enableVideo()
|
||||
}
|
||||
Compatibility.BLUETOOTH_CONNECT -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i("[Conference Waiting Room] BLUETOOTH_CONNECT permission has been granted")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.conference.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.conference.adapters.ScheduledConferencesAdapter
|
||||
import org.linphone.activities.main.conference.data.ScheduledConferenceData
|
||||
import org.linphone.activities.main.conference.viewmodels.ScheduledConferencesViewModel
|
||||
import org.linphone.activities.main.fragments.MasterFragment
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToConferenceScheduling
|
||||
import org.linphone.activities.navigateToConferenceWaitingRoom
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ConferencesScheduledFragmentBinding
|
||||
import org.linphone.utils.*
|
||||
|
||||
class ScheduledConferencesFragment : MasterFragment<ConferencesScheduledFragmentBinding, ScheduledConferencesAdapter>() {
|
||||
override val dialogConfirmationMessageBeforeRemoval = R.plurals.conference_scheduled_delete_dialog
|
||||
private lateinit var listViewModel: ScheduledConferencesViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.conferences_scheduled_fragment
|
||||
|
||||
private var deleteConferenceInfoDialog: Dialog? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
listViewModel = ViewModelProvider(
|
||||
this
|
||||
)[ScheduledConferencesViewModel::class.java]
|
||||
binding.viewModel = listViewModel
|
||||
|
||||
_adapter = ScheduledConferencesAdapter(listSelectionViewModel, viewLifecycleOwner)
|
||||
binding.conferenceInfoList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.conferenceInfoList.layoutManager = layoutManager
|
||||
|
||||
// Swipe action
|
||||
val swipeConfiguration = RecyclerViewSwipeConfiguration()
|
||||
val white = ContextCompat.getColor(requireContext(), R.color.white_color)
|
||||
|
||||
swipeConfiguration.rightToLeftAction = RecyclerViewSwipeConfiguration.Action(
|
||||
requireContext().getString(R.string.dialog_delete),
|
||||
white,
|
||||
ContextCompat.getColor(requireContext(), R.color.red_color)
|
||||
)
|
||||
val swipeListener = object : RecyclerViewSwipeListener {
|
||||
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {}
|
||||
|
||||
override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||
val index = viewHolder.bindingAdapterPosition
|
||||
if (index < 0 || index >= adapter.currentList.size) {
|
||||
Log.e("[Scheduled Conferences] Index is out of bound, can't delete conference info")
|
||||
} else {
|
||||
val deletedConfInfo = adapter.currentList[index]
|
||||
showConfInfoDeleteConfirmationDialog(deletedConfInfo, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
RecyclerViewSwipeUtils(ItemTouchHelper.LEFT, swipeConfiguration, swipeListener)
|
||||
.attachToRecyclerView(binding.conferenceInfoList)
|
||||
|
||||
// Displays date header
|
||||
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
|
||||
binding.conferenceInfoList.addItemDecoration(headerItemDecoration)
|
||||
|
||||
listViewModel.conferences.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
|
||||
adapter.copyAddressToClipboardEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { address ->
|
||||
val clipboard =
|
||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Conference address", address)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
|
||||
(activity as MainActivity).showSnackBar(R.string.conference_schedule_address_copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.joinConferenceEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { pair ->
|
||||
navigateToConferenceWaitingRoom(pair.first, pair.second)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.editConferenceEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { address ->
|
||||
sharedViewModel.addressOfConferenceInfoToEdit.value = Event(address)
|
||||
navigateToConferenceScheduling()
|
||||
}
|
||||
}
|
||||
|
||||
adapter.deleteConferenceInfoEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { data ->
|
||||
showConfInfoDeleteConfirmationDialog(data, -1)
|
||||
}
|
||||
}
|
||||
|
||||
binding.setNewConferenceClickListener {
|
||||
navigateToConferenceScheduling()
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteItems(indexesOfItemToDelete: ArrayList<Int>) {
|
||||
val list = ArrayList<ScheduledConferenceData>()
|
||||
for (index in indexesOfItemToDelete) {
|
||||
val conferenceData = adapter.currentList[index]
|
||||
list.add(conferenceData)
|
||||
}
|
||||
listViewModel.deleteConferencesInfo(list)
|
||||
}
|
||||
|
||||
private fun showConfInfoDeleteConfirmationDialog(data: ScheduledConferenceData, index: Int) {
|
||||
val dialogViewModel =
|
||||
DialogViewModel(AppUtils.getString(R.string.conference_scheduled_delete_one_dialog))
|
||||
deleteConferenceInfoDialog =
|
||||
DialogUtils.getVoipDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showCancelButton(
|
||||
{
|
||||
if (index != -1) {
|
||||
adapter.notifyItemChanged(index)
|
||||
}
|
||||
deleteConferenceInfoDialog?.dismiss()
|
||||
},
|
||||
getString(R.string.dialog_cancel)
|
||||
)
|
||||
|
||||
dialogViewModel.showDeleteButton(
|
||||
{
|
||||
listViewModel.deleteConferenceInfo(data)
|
||||
deleteConferenceInfoDialog?.dismiss()
|
||||
(requireActivity() as MainActivity).showSnackBar(R.string.conference_info_removed)
|
||||
},
|
||||
getString(R.string.dialog_delete)
|
||||
)
|
||||
|
||||
deleteConferenceInfoDialog?.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.conference.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.util.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.conference.data.ConferenceSchedulingParticipantData
|
||||
import org.linphone.activities.main.conference.data.Duration
|
||||
import org.linphone.activities.main.conference.data.TimeZoneData
|
||||
import org.linphone.contact.ContactsSelectionViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
|
||||
val subject = MutableLiveData<String>()
|
||||
val description = MutableLiveData<String>()
|
||||
|
||||
val scheduleForLater = MutableLiveData<Boolean>()
|
||||
val isUpdate = MutableLiveData<Boolean>()
|
||||
|
||||
val formattedDate = MutableLiveData<String>()
|
||||
val formattedTime = MutableLiveData<String>()
|
||||
|
||||
val isEncrypted = MutableLiveData<Boolean>()
|
||||
|
||||
val sendInviteViaChat = MutableLiveData<Boolean>()
|
||||
val sendInviteViaEmail = MutableLiveData<Boolean>()
|
||||
|
||||
val participantsData = MutableLiveData<List<ConferenceSchedulingParticipantData>>()
|
||||
|
||||
val address = MutableLiveData<Address>()
|
||||
|
||||
val conferenceCreationInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val conferenceCreationCompletedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val continueEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
var timeZone = MutableLiveData<TimeZoneData>()
|
||||
val timeZones: List<TimeZoneData> = computeTimeZonesList()
|
||||
|
||||
var duration = MutableLiveData<Duration>()
|
||||
val durationList: List<Duration> = computeDurationList()
|
||||
|
||||
var dateTimestamp: Long = System.currentTimeMillis()
|
||||
var hour: Int = 0
|
||||
var minutes: Int = 0
|
||||
|
||||
private var confInfo: ConferenceInfo? = null
|
||||
private val conferenceScheduler = coreContext.core.createConferenceScheduler()
|
||||
|
||||
private val listener = object : ConferenceSchedulerListenerStub() {
|
||||
override fun onStateChanged(
|
||||
conferenceScheduler: ConferenceScheduler,
|
||||
state: ConferenceScheduler.State
|
||||
) {
|
||||
Log.i("[Conference Creation] Conference scheduler state is $state")
|
||||
if (state == ConferenceScheduler.State.Ready) {
|
||||
val conferenceAddress = conferenceScheduler.info?.uri
|
||||
Log.i("[Conference Creation] Conference info created, address will be ${conferenceAddress?.asStringUriOnly()}")
|
||||
conferenceAddress ?: return
|
||||
|
||||
address.value = conferenceAddress!!
|
||||
|
||||
if (scheduleForLater.value == true && sendInviteViaChat.value == true) {
|
||||
// Send conference info even when conf is not scheduled for later
|
||||
// as the conference server doesn't invite participants automatically
|
||||
val chatRoomParams = LinphoneUtils.getConferenceInvitationsChatRoomParams()
|
||||
conferenceScheduler.sendInvitations(chatRoomParams)
|
||||
} else {
|
||||
// Will be done in coreListener
|
||||
}
|
||||
} else if (state == ConferenceScheduler.State.Error) {
|
||||
Log.e("[Conference Creation] Failed to create conference!")
|
||||
conferenceCreationInProgress.value = false
|
||||
onMessageToNotifyEvent.value = Event(R.string.conference_creation_failed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInvitationsSent(
|
||||
conferenceScheduler: ConferenceScheduler,
|
||||
failedInvitations: Array<out Address>?
|
||||
) {
|
||||
conferenceCreationInProgress.value = false
|
||||
|
||||
if (failedInvitations?.isNotEmpty() == true) {
|
||||
for (address in failedInvitations) {
|
||||
Log.e("[Conference Creation] Conference information wasn't sent to participant ${address.asStringUriOnly()}")
|
||||
}
|
||||
onMessageToNotifyEvent.value = Event(R.string.conference_schedule_info_not_sent_to_participant)
|
||||
} else {
|
||||
Log.i("[Conference Creation] Conference information successfully sent to all participants")
|
||||
}
|
||||
|
||||
val conferenceAddress = conferenceScheduler.info?.uri
|
||||
if (conferenceAddress == null) {
|
||||
Log.e("[Conference Creation] Conference address is null!")
|
||||
} else {
|
||||
conferenceCreationCompletedEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State?,
|
||||
message: String
|
||||
) {
|
||||
when (state) {
|
||||
Call.State.OutgoingProgress -> {
|
||||
conferenceCreationInProgress.value = false
|
||||
}
|
||||
Call.State.End -> {
|
||||
Log.i("[Conference Creation] Call has ended, leaving waiting room fragment")
|
||||
conferenceCreationCompletedEvent.value = Event(true)
|
||||
}
|
||||
Call.State.Error -> {
|
||||
Log.w("[Conference Creation] Call has failed, leaving waiting room fragment")
|
||||
conferenceCreationCompletedEvent.value = Event(true)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
sipContactsSelected.value = true
|
||||
|
||||
subject.value = ""
|
||||
scheduleForLater.value = false
|
||||
isUpdate.value = false
|
||||
|
||||
isEncrypted.value = false
|
||||
sendInviteViaChat.value = true
|
||||
sendInviteViaEmail.value = false
|
||||
|
||||
timeZone.value = timeZones.find {
|
||||
it.id == TimeZone.getDefault().id
|
||||
}
|
||||
duration.value = durationList.find {
|
||||
it.value == 3600
|
||||
}
|
||||
|
||||
continueEnabled.value = false
|
||||
continueEnabled.addSource(subject) {
|
||||
continueEnabled.value = allMandatoryFieldsFilled()
|
||||
}
|
||||
continueEnabled.addSource(scheduleForLater) {
|
||||
continueEnabled.value = allMandatoryFieldsFilled()
|
||||
}
|
||||
continueEnabled.addSource(formattedDate) {
|
||||
continueEnabled.value = allMandatoryFieldsFilled()
|
||||
}
|
||||
continueEnabled.addSource(formattedTime) {
|
||||
continueEnabled.value = allMandatoryFieldsFilled()
|
||||
}
|
||||
|
||||
conferenceScheduler.addListener(listener)
|
||||
coreContext.core.addListener(coreListener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(coreListener)
|
||||
conferenceScheduler.removeListener(listener)
|
||||
participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun prePopulateParticipantsList(participants: ArrayList<Address>, isSchedule: Boolean) {
|
||||
selectedAddresses.value = participants
|
||||
scheduleForLater.value = isSchedule
|
||||
}
|
||||
|
||||
fun populateFromConferenceInfo(conferenceInfo: ConferenceInfo) {
|
||||
confInfo = conferenceInfo
|
||||
|
||||
address.value = conferenceInfo.uri
|
||||
subject.value = conferenceInfo.subject
|
||||
description.value = conferenceInfo.description
|
||||
isUpdate.value = true
|
||||
|
||||
val dateTime = conferenceInfo.dateTime
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.timeInMillis = dateTime * 1000
|
||||
setDate(calendar.timeInMillis)
|
||||
setTime(calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE))
|
||||
|
||||
val conferenceDuration = conferenceInfo.duration
|
||||
duration.value = durationList.find { it.value == conferenceDuration }
|
||||
scheduleForLater.value = conferenceDuration > 0
|
||||
|
||||
val participantsList = arrayListOf<Address>()
|
||||
for (participant in conferenceInfo.participants) {
|
||||
participantsList.add(participant)
|
||||
}
|
||||
selectedAddresses.value = participantsList
|
||||
computeParticipantsData()
|
||||
}
|
||||
|
||||
fun toggleSchedule() {
|
||||
scheduleForLater.value = scheduleForLater.value == false
|
||||
}
|
||||
|
||||
fun setDate(d: Long) {
|
||||
dateTimestamp = d
|
||||
formattedDate.value = TimestampUtils.dateToString(dateTimestamp, false)
|
||||
}
|
||||
|
||||
fun setTime(h: Int, m: Int) {
|
||||
hour = h
|
||||
minutes = m
|
||||
formattedTime.value = TimestampUtils.timeToString(hour, minutes)
|
||||
}
|
||||
|
||||
fun updateEncryption(enable: Boolean) {
|
||||
isEncrypted.value = enable
|
||||
}
|
||||
|
||||
fun computeParticipantsData() {
|
||||
participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)
|
||||
val list = arrayListOf<ConferenceSchedulingParticipantData>()
|
||||
|
||||
for (address in selectedAddresses.value.orEmpty()) {
|
||||
val data = ConferenceSchedulingParticipantData(address, isEncrypted.value == true)
|
||||
list.add(data)
|
||||
}
|
||||
|
||||
participantsData.value = list
|
||||
}
|
||||
|
||||
fun createConference() {
|
||||
val participantsCount = selectedAddresses.value.orEmpty().size
|
||||
if (participantsCount == 0) {
|
||||
Log.e("[Conference Creation] Couldn't create conference without any participant!")
|
||||
return
|
||||
}
|
||||
|
||||
conferenceCreationInProgress.value = true
|
||||
val core = coreContext.core
|
||||
val participants = arrayOfNulls<Address>(selectedAddresses.value.orEmpty().size)
|
||||
selectedAddresses.value?.toArray(participants)
|
||||
val localAccount = core.defaultAccount
|
||||
val localAddress = localAccount?.params?.identityAddress
|
||||
|
||||
val conferenceInfo = if (isUpdate.value == true) {
|
||||
confInfo?.clone() ?: Factory.instance().createConferenceInfo()
|
||||
} else {
|
||||
Factory.instance().createConferenceInfo()
|
||||
}
|
||||
conferenceInfo.organizer = localAddress
|
||||
conferenceInfo.subject = subject.value
|
||||
conferenceInfo.description = description.value
|
||||
conferenceInfo.setParticipants(participants)
|
||||
if (scheduleForLater.value == true) {
|
||||
val startTime = getConferenceStartTimestamp()
|
||||
conferenceInfo.dateTime = startTime
|
||||
val duration = duration.value?.value ?: 0
|
||||
conferenceInfo.duration = duration
|
||||
}
|
||||
|
||||
confInfo = conferenceInfo
|
||||
conferenceScheduler.account = localAccount
|
||||
// Will trigger the conference creation/update automatically
|
||||
conferenceScheduler.info = conferenceInfo
|
||||
}
|
||||
|
||||
private fun computeTimeZonesList(): List<TimeZoneData> {
|
||||
return TimeZone.getAvailableIDs().map { id -> TimeZoneData(TimeZone.getTimeZone(id)) }.toList().sorted()
|
||||
}
|
||||
|
||||
private fun computeDurationList(): List<Duration> {
|
||||
// Duration value is in minutes as according to conferenceInfo.setDuration() doc
|
||||
return arrayListOf(Duration(30, "30min"), Duration(60, "1h"), Duration(120, "2h"))
|
||||
}
|
||||
|
||||
private fun allMandatoryFieldsFilled(): Boolean {
|
||||
return !subject.value.isNullOrEmpty() &&
|
||||
(
|
||||
scheduleForLater.value == false ||
|
||||
(
|
||||
!formattedDate.value.isNullOrEmpty() &&
|
||||
!formattedTime.value.isNullOrEmpty()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getConferenceStartTimestamp(): Long {
|
||||
val calendar = Calendar.getInstance(TimeZone.getTimeZone(timeZone.value?.id ?: TimeZone.getDefault().id))
|
||||
calendar.timeInMillis = dateTimestamp
|
||||
calendar.set(Calendar.HOUR_OF_DAY, hour)
|
||||
calendar.set(Calendar.MINUTE, minutes)
|
||||
return calendar.timeInMillis / 1000 // Linphone expects a time_t (so in seconds)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,407 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2021 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* 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.linphone.activities.main.conference.viewmodels
|
||||
|
||||
import android.Manifest
|
||||
import android.animation.ValueAnimator
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
|
||||
import org.linphone.activities.voip.ConferenceDisplayMode
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.*
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class ConferenceWaitingRoomViewModel : MessageNotifierViewModel() {
|
||||
val subject = MutableLiveData<String>()
|
||||
|
||||
val isMicrophoneMuted = MutableLiveData<Boolean>()
|
||||
|
||||
val audioRoutesEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val audioRoutesSelected = MutableLiveData<Boolean>()
|
||||
|
||||
val isSpeakerSelected = MutableLiveData<Boolean>()
|
||||
|
||||
val isBluetoothHeadsetSelected = MutableLiveData<Boolean>()
|
||||
|
||||
val layoutMenuSelected = MutableLiveData<Boolean>()
|
||||
|
||||
val selectedLayout = MutableLiveData<ConferenceDisplayMode>()
|
||||
|
||||
val isVideoAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val isVideoEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isSwitchCameraAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val isLowBandwidth = MutableLiveData<Boolean>()
|
||||
|
||||
val joinInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val networkReachable = MutableLiveData<Boolean>()
|
||||
|
||||
val askPermissionEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val cancelConferenceJoiningEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val joinConferenceEvent: MutableLiveData<Event<CallParams>> by lazy {
|
||||
MutableLiveData<Event<CallParams>>()
|
||||
}
|
||||
|
||||
val leaveWaitingRoomEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val networkNotReachableEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val audioRoutesMenuTranslateY = MutableLiveData<Float>()
|
||||
private val audioRoutesMenuAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.voip_audio_routes_menu_translate_y), 0f).apply {
|
||||
addUpdateListener {
|
||||
val value = it.animatedValue as Float
|
||||
audioRoutesMenuTranslateY.value = value
|
||||
}
|
||||
duration = if (corePreferences.enableAnimations) 500 else 0
|
||||
}
|
||||
}
|
||||
|
||||
val conferenceLayoutMenuTranslateY = MutableLiveData<Float>()
|
||||
private val conferenceLayoutMenuAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.voip_audio_routes_menu_translate_y), 0f).apply {
|
||||
addUpdateListener {
|
||||
val value = it.animatedValue as Float
|
||||
conferenceLayoutMenuTranslateY.value = value
|
||||
}
|
||||
duration = if (corePreferences.enableAnimations) 500 else 0
|
||||
}
|
||||
}
|
||||
|
||||
private val callParams: CallParams = coreContext.core.createCallParams(null)!!
|
||||
|
||||
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onAudioDevicesListUpdated(core: Core) {
|
||||
Log.i("[Conference Waiting Room] Audio devices list updated")
|
||||
onAudioDevicesListUpdated()
|
||||
}
|
||||
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State?,
|
||||
message: String
|
||||
) {
|
||||
when (state) {
|
||||
Call.State.End -> {
|
||||
Log.i("[Conference Waiting Room] Call has ended, leaving waiting room fragment")
|
||||
leaveWaitingRoomEvent.value = Event(true)
|
||||
}
|
||||
Call.State.Error -> {
|
||||
Log.w("[Conference Waiting Room] Call has failed, leaving waiting room fragment")
|
||||
leaveWaitingRoomEvent.value = Event(true)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConferenceStateChanged(
|
||||
core: Core,
|
||||
conference: Conference,
|
||||
state: Conference.State?
|
||||
) {
|
||||
if (state == Conference.State.Created) {
|
||||
Log.i("[Conference Waiting Room] Conference has been created, leaving waiting room fragment")
|
||||
leaveWaitingRoomEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNetworkReachable(core: Core, reachable: Boolean) {
|
||||
Log.i("[Conference Waiting Room] Network reachability changed: [$reachable]")
|
||||
networkReachable.value = reachable
|
||||
if (!reachable) {
|
||||
networkNotReachableEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val core = coreContext.core
|
||||
core.addListener(listener)
|
||||
|
||||
audioRoutesMenuTranslateY.value = AppUtils.getDimension(R.dimen.voip_audio_routes_menu_translate_y)
|
||||
conferenceLayoutMenuTranslateY.value = AppUtils.getDimension(R.dimen.voip_audio_routes_menu_translate_y)
|
||||
|
||||
val reachable = core.isNetworkReachable
|
||||
networkReachable.value = reachable
|
||||
if (!reachable) {
|
||||
networkNotReachableEvent.value = Event(true)
|
||||
}
|
||||
|
||||
callParams.isMicEnabled = PermissionHelper.get().hasRecordAudioPermission() && coreContext.core.isMicEnabled
|
||||
Log.i("[Conference Waiting Room] Microphone will be ${if (callParams.isMicEnabled) "enabled" else "muted"}")
|
||||
updateMicState()
|
||||
|
||||
callParams.isVideoEnabled = isVideoAvailableInCore()
|
||||
callParams.videoDirection = if (core.videoActivationPolicy.automaticallyInitiate) MediaDirection.SendRecv else MediaDirection.RecvOnly
|
||||
updateVideoState()
|
||||
|
||||
isLowBandwidth.value = false
|
||||
if (LinphoneUtils.checkIfNetworkHasLowBandwidth(coreContext.context)) {
|
||||
Log.w("[Conference Waiting Room] Enabling low bandwidth mode, forcing audio only layout!")
|
||||
callParams.isLowBandwidthEnabled = true
|
||||
callParams.isVideoEnabled = false
|
||||
callParams.videoDirection = MediaDirection.Inactive
|
||||
isLowBandwidth.value = true
|
||||
|
||||
updateVideoState()
|
||||
onMessageToNotifyEvent.value = Event(R.string.conference_low_bandwidth)
|
||||
}
|
||||
|
||||
layoutMenuSelected.value = false
|
||||
updateLayout()
|
||||
|
||||
if (AudioRouteUtils.isBluetoothAudioRouteAvailable()) {
|
||||
setBluetoothAudioRoute()
|
||||
} else if (isVideoAvailableInCore() && isVideoEnabled.value == true) {
|
||||
setSpeakerAudioRoute()
|
||||
} else {
|
||||
setEarpieceAudioRoute()
|
||||
}
|
||||
onAudioDevicesListUpdated()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
cancelConferenceJoiningEvent.value = Event(true)
|
||||
}
|
||||
|
||||
fun start() {
|
||||
// Hide menus
|
||||
audioRoutesSelected.value = false
|
||||
layoutMenuSelected.value = false
|
||||
|
||||
joinInProgress.value = true
|
||||
joinConferenceEvent.value = Event(callParams)
|
||||
}
|
||||
|
||||
fun toggleMuteMicrophone() {
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
askPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO)
|
||||
return
|
||||
}
|
||||
|
||||
callParams.isMicEnabled = !callParams.isMicEnabled
|
||||
Log.i("[Conference Waiting Room] Microphone will be ${if (callParams.isMicEnabled) "enabled" else "muted"}")
|
||||
updateMicState()
|
||||
}
|
||||
|
||||
fun enableMic() {
|
||||
Log.i("[Conference Waiting Room] Microphone will be enabled")
|
||||
callParams.isMicEnabled = true
|
||||
updateMicState()
|
||||
}
|
||||
|
||||
fun toggleSpeaker() {
|
||||
if (isSpeakerSelected.value == true) {
|
||||
setEarpieceAudioRoute()
|
||||
} else {
|
||||
setSpeakerAudioRoute()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleAudioRoutesMenu() {
|
||||
audioRoutesSelected.value = audioRoutesSelected.value != true
|
||||
if (audioRoutesSelected.value == true) {
|
||||
audioRoutesMenuAnimator.start()
|
||||
} else {
|
||||
audioRoutesMenuAnimator.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
fun setBluetoothAudioRoute() {
|
||||
Log.i("[Conference Waiting Room] Set default output audio device to Bluetooth")
|
||||
callParams.outputAudioDevice = coreContext.core.audioDevices.find {
|
||||
it.type == AudioDevice.Type.Bluetooth && it.hasCapability(AudioDevice.Capabilities.CapabilityPlay)
|
||||
}
|
||||
callParams.inputAudioDevice = coreContext.core.audioDevices.find {
|
||||
it.type == AudioDevice.Type.Bluetooth && it.hasCapability(AudioDevice.Capabilities.CapabilityRecord)
|
||||
}
|
||||
updateAudioRouteState()
|
||||
}
|
||||
|
||||
fun setSpeakerAudioRoute() {
|
||||
Log.i("[Conference Waiting Room] Set default output audio device to Speaker")
|
||||
callParams.outputAudioDevice = coreContext.core.audioDevices.find {
|
||||
it.type == AudioDevice.Type.Speaker && it.hasCapability(AudioDevice.Capabilities.CapabilityPlay)
|
||||
}
|
||||
callParams.inputAudioDevice = coreContext.core.audioDevices.find {
|
||||
it.type == AudioDevice.Type.Microphone && it.hasCapability(AudioDevice.Capabilities.CapabilityRecord)
|
||||
}
|
||||
updateAudioRouteState()
|
||||
}
|
||||
|
||||
fun setEarpieceAudioRoute() {
|
||||
Log.i("[Conference Waiting Room] Set default output audio device to Earpiece")
|
||||
callParams.outputAudioDevice = coreContext.core.audioDevices.find {
|
||||
it.type == AudioDevice.Type.Earpiece && it.hasCapability(AudioDevice.Capabilities.CapabilityPlay)
|
||||
}
|
||||
callParams.inputAudioDevice = coreContext.core.audioDevices.find {
|
||||
it.type == AudioDevice.Type.Microphone && it.hasCapability(AudioDevice.Capabilities.CapabilityRecord)
|
||||
}
|
||||
updateAudioRouteState()
|
||||
}
|
||||
|
||||
fun toggleLayoutMenu() {
|
||||
layoutMenuSelected.value = layoutMenuSelected.value != true
|
||||
if (layoutMenuSelected.value == true) {
|
||||
conferenceLayoutMenuAnimator.start()
|
||||
} else {
|
||||
conferenceLayoutMenuAnimator.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
fun setMosaicLayout() {
|
||||
Log.i("[Conference Waiting Room] Set default layout to Mosaic")
|
||||
|
||||
callParams.conferenceVideoLayout = ConferenceLayout.Grid
|
||||
callParams.isVideoEnabled = isVideoAvailableInCore()
|
||||
|
||||
updateLayout()
|
||||
updateVideoState()
|
||||
layoutMenuSelected.value = false
|
||||
}
|
||||
|
||||
fun setActiveSpeakerLayout() {
|
||||
Log.i("[Conference Waiting Room] Set default layout to ActiveSpeaker")
|
||||
|
||||
callParams.conferenceVideoLayout = ConferenceLayout.ActiveSpeaker
|
||||
callParams.isVideoEnabled = isVideoAvailableInCore()
|
||||
|
||||
updateLayout()
|
||||
updateVideoState()
|
||||
layoutMenuSelected.value = false
|
||||
}
|
||||
|
||||
fun setAudioOnlyLayout() {
|
||||
Log.i("[Conference Waiting Room] Set default layout to AudioOnly, disabling video in call")
|
||||
callParams.isVideoEnabled = false
|
||||
updateLayout()
|
||||
updateVideoState()
|
||||
layoutMenuSelected.value = false
|
||||
}
|
||||
|
||||
fun toggleVideo() {
|
||||
if (!PermissionHelper.get().hasCameraPermission()) {
|
||||
askPermissionEvent.value = Event(Manifest.permission.CAMERA)
|
||||
return
|
||||
}
|
||||
callParams.isVideoEnabled = isVideoAvailableInCore()
|
||||
callParams.videoDirection = if (callParams.videoDirection == MediaDirection.SendRecv) MediaDirection.RecvOnly else MediaDirection.SendRecv
|
||||
updateVideoState()
|
||||
}
|
||||
|
||||
fun enableVideo() {
|
||||
callParams.isVideoEnabled = isVideoAvailableInCore()
|
||||
callParams.videoDirection = MediaDirection.SendRecv
|
||||
updateVideoState()
|
||||
}
|
||||
|
||||
fun switchCamera() {
|
||||
Log.i("[Conference Waiting Room] Switching camera")
|
||||
coreContext.switchCamera()
|
||||
}
|
||||
|
||||
private fun updateMicState() {
|
||||
isMicrophoneMuted.value = !callParams.isMicEnabled
|
||||
}
|
||||
|
||||
private fun onAudioDevicesListUpdated() {
|
||||
val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable()
|
||||
audioRoutesEnabled.value = bluetoothDeviceAvailable
|
||||
|
||||
if (!bluetoothDeviceAvailable) {
|
||||
audioRoutesSelected.value = false
|
||||
Log.w("[Conference Waiting Room] Bluetooth device no longer available, switching back to default microphone & earpiece/speaker")
|
||||
if (isBluetoothHeadsetSelected.value == true) {
|
||||
for (audioDevice in coreContext.core.audioDevices) {
|
||||
if (isVideoEnabled.value == true) {
|
||||
if (audioDevice.type == AudioDevice.Type.Speaker) {
|
||||
callParams.outputAudioDevice = audioDevice
|
||||
}
|
||||
} else {
|
||||
if (audioDevice.type == AudioDevice.Type.Earpiece) {
|
||||
callParams.outputAudioDevice = audioDevice
|
||||
}
|
||||
}
|
||||
if (audioDevice.type == AudioDevice.Type.Microphone) {
|
||||
callParams.inputAudioDevice = audioDevice
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateAudioRouteState()
|
||||
}
|
||||
|
||||
private fun updateAudioRouteState() {
|
||||
val outputDeviceType = callParams.outputAudioDevice?.type
|
||||
isSpeakerSelected.value = outputDeviceType == AudioDevice.Type.Speaker
|
||||
isBluetoothHeadsetSelected.value = outputDeviceType == AudioDevice.Type.Bluetooth
|
||||
}
|
||||
|
||||
private fun updateLayout() {
|
||||
if (!callParams.isVideoEnabled) {
|
||||
selectedLayout.value = ConferenceDisplayMode.AUDIO_ONLY
|
||||
} else {
|
||||
selectedLayout.value = when (callParams.conferenceVideoLayout) {
|
||||
ConferenceLayout.Grid -> ConferenceDisplayMode.GRID
|
||||
else -> ConferenceDisplayMode.ACTIVE_SPEAKER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateVideoState() {
|
||||
isVideoAvailable.value = callParams.isVideoEnabled
|
||||
isVideoEnabled.value = callParams.isVideoEnabled && callParams.videoDirection == MediaDirection.SendRecv
|
||||
Log.i("[Conference Waiting Room] Video will be ${if (callParams.isVideoEnabled) "enabled" else "disabled"} with direction ${callParams.videoDirection}")
|
||||
|
||||
isSwitchCameraAvailable.value = callParams.isVideoEnabled && coreContext.showSwitchCameraButton()
|
||||
coreContext.core.isVideoPreviewEnabled = isVideoEnabled.value == true
|
||||
}
|
||||
|
||||
private fun isVideoAvailableInCore(): Boolean {
|
||||
val core = coreContext.core
|
||||
return core.isVideoCaptureEnabled || core.isVideoPreviewEnabled
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue