mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-22 14:18:15 +00:00
Compare commits
2942 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29343f8887 | ||
|
|
14f8273588 | ||
|
|
8cb883a978 | ||
|
|
539439a055 | ||
|
|
597581faa5 | ||
|
|
5391c12473 | ||
|
|
cea47a767b | ||
|
|
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 | ||
|
|
04ae18c1ae | ||
|
|
37f5721668 | ||
|
|
02304cdd18 | ||
|
|
05718aa749 | ||
|
|
b426bd567f | ||
|
|
bdffe20fd0 | ||
|
|
88757bd1b8 | ||
|
|
5295ae3b9d | ||
|
|
323dd05731 | ||
|
|
d5564af931 | ||
|
|
fb4019f5b9 | ||
|
|
e4c64a2b5b | ||
|
|
003a73577a | ||
|
|
691bb49c62 | ||
|
|
cb34e761c6 | ||
|
|
4fd7246a74 | ||
|
|
ebaa6067b5 | ||
|
|
2050ed93eb | ||
|
|
795ee0d40c | ||
|
|
a56edd5f4f | ||
|
|
ee2fd51103 | ||
|
|
b4d11291f1 | ||
|
|
3497708da5 | ||
|
|
d37c9549da | ||
|
|
138635230a | ||
|
|
b47ae48529 | ||
|
|
6bc4352313 | ||
|
|
ab6c3004de | ||
|
|
98aae5af37 | ||
|
|
36286b0259 | ||
|
|
e468573551 | ||
|
|
45d6dd0086 | ||
|
|
0f485ed66d | ||
|
|
9d9c58e7f7 | ||
|
|
60067c259b | ||
|
|
303768e2bf | ||
|
|
77331e9b94 | ||
|
|
31ef42c829 | ||
|
|
68d34735a6 | ||
|
|
64a736eeeb | ||
|
|
8373eaeb7d | ||
|
|
05ddd072a3 | ||
|
|
57acb1b7b7 | ||
|
|
1b07cd358f | ||
|
|
0a0221c9b0 | ||
|
|
5a0a210100 | ||
|
|
bb288f03cc | ||
|
|
261dc9791a | ||
|
|
6aa17309d8 | ||
|
|
d26bebae37 | ||
|
|
dabbf10b5f | ||
|
|
5642d4c8c3 | ||
|
|
7c267dad25 | ||
|
|
04e8c3566b | ||
|
|
47aa8e3a12 | ||
|
|
bcb737a456 | ||
|
|
db01f7a006 | ||
|
|
d1b615beec | ||
|
|
4cd0c372b2 | ||
|
|
0eadd0c6ad | ||
|
|
21c476bee3 | ||
|
|
b4917423de | ||
|
|
39973d5605 | ||
|
|
9badc7f585 | ||
|
|
d2da954564 | ||
|
|
1761843c1b | ||
|
|
740a935525 | ||
|
|
f125b54b40 | ||
|
|
4ec5e2452a | ||
|
|
a08e447d31 | ||
|
|
dcf91ddef3 | ||
|
|
37a6c3968e | ||
|
|
b8e65b3ac7 | ||
|
|
a3f55b3baf | ||
|
|
90a5d0d6b4 | ||
|
|
4636f6c4ea | ||
|
|
b588b10811 | ||
|
|
0aa44f8c39 | ||
|
|
6eed295c5c | ||
|
|
2933a014c1 | ||
|
|
b4fd0ec5f4 | ||
|
|
f24f3f2c85 | ||
|
|
59a7771ea1 | ||
|
|
991c1c6e7a | ||
|
|
a19800bb52 | ||
|
|
0f9b4d706d | ||
|
|
95de49be30 | ||
|
|
1dc48002f3 | ||
|
|
1c7886ffb7 | ||
|
|
e0bc94d62d | ||
|
|
286ce06160 | ||
|
|
820d0f1e5a | ||
|
|
30d6e556c1 | ||
|
|
1bb294acf1 | ||
|
|
3ba46dc9ab | ||
|
|
c5313b5425 | ||
|
|
144dca289a | ||
|
|
19f8f8a8a3 | ||
|
|
fcd0779507 | ||
|
|
2ed862426e | ||
|
|
92c89eb56b | ||
|
|
02264a4f93 | ||
|
|
3768fd5b46 | ||
|
|
863fee2a1a | ||
|
|
a04c1f7d77 | ||
|
|
44a25c733f | ||
|
|
56ddf046b5 | ||
|
|
823c0fb645 | ||
|
|
421677b2c0 | ||
|
|
f1628060c6 | ||
|
|
1fbac6e4d5 | ||
|
|
f9ece220eb | ||
|
|
7c9692ea36 | ||
|
|
358ab96d17 | ||
|
|
69ce331a05 | ||
|
|
1c9a4924f6 | ||
|
|
6e5885dec9 | ||
|
|
1204275b9a | ||
|
|
367700e33f | ||
|
|
50bcd84df9 | ||
|
|
cb8f25a468 | ||
|
|
3b1099d202 | ||
|
|
3c53965403 | ||
|
|
024b51e1c7 | ||
|
|
5c79ccfe3a | ||
|
|
0f840e0aa3 | ||
|
|
1c8705933f | ||
|
|
6d50317330 | ||
|
|
318d41a9fc | ||
|
|
f304055671 | ||
|
|
4c3c79e8a2 | ||
|
|
1de4999a65 | ||
|
|
0327928bb1 | ||
|
|
b266f98593 | ||
|
|
3c75ecdefa | ||
|
|
4ab54a50c1 | ||
|
|
6406eb9d7b | ||
|
|
21e500cad3 | ||
|
|
8eda2fac1b | ||
|
|
868daa2078 | ||
|
|
6acc6b6bb0 | ||
|
|
76c9b2482a | ||
|
|
c4c6c6865e | ||
|
|
6a8e66766d | ||
|
|
3deef0c1ed | ||
|
|
2f321a9dee | ||
|
|
8a29c4be61 | ||
|
|
619a13eb97 | ||
|
|
a45549d360 | ||
|
|
9d082e2630 | ||
|
|
f1cd004201 | ||
|
|
4f26895cb4 | ||
|
|
009c6b3333 | ||
|
|
71ce40a8ea | ||
|
|
9f04cc3995 | ||
|
|
f3419951f8 | ||
|
|
219cfed4ce | ||
|
|
244885e0aa | ||
|
|
40de347237 | ||
|
|
eb634c74e7 | ||
|
|
e2a70c33e8 | ||
|
|
c3a4ff443b | ||
|
|
2d233d5264 | ||
|
|
5b984de21a | ||
|
|
a72ee81ec6 | ||
|
|
61454f5923 | ||
|
|
d630f0ceec | ||
|
|
a2d08e7268 | ||
|
|
8f3b17bb0f | ||
|
|
0dd79e5ff8 | ||
|
|
0286abc070 | ||
|
|
2e5603cb41 | ||
|
|
2e1b6baa6c | ||
|
|
4a74a7585d | ||
|
|
bd0ba98a21 | ||
|
|
14685c550c | ||
|
|
c6c146d213 | ||
|
|
210f4a46b2 | ||
|
|
b611f6c0ed | ||
|
|
86cba063d1 | ||
|
|
435e4b84f6 | ||
|
|
6f11fbc586 | ||
|
|
93cdaf781b | ||
|
|
93d00931ac | ||
|
|
ae59117596 | ||
|
|
df396f5abd | ||
|
|
a437c1f5ef | ||
|
|
023e953a2b | ||
|
|
f118ccf049 | ||
|
|
312ae2eaf0 | ||
|
|
c5a34363a3 | ||
|
|
0e0fd6106c | ||
|
|
ee4083389b | ||
|
|
f428a170c5 | ||
|
|
dc155e7302 | ||
|
|
9a6dc56b42 | ||
|
|
f94b65ed0a | ||
|
|
e6c842cd35 | ||
|
|
5fac97b55d | ||
|
|
cbacb97f9d | ||
|
|
635c8e69c7 | ||
|
|
ee5af2b462 | ||
|
|
ed03a721a5 | ||
|
|
6222758126 | ||
|
|
5502324845 | ||
|
|
3172b27b98 | ||
|
|
0ce885a260 | ||
|
|
0b06e22258 | ||
|
|
ec6152c8b8 | ||
|
|
fe8e2b4b49 | ||
|
|
9e20134b13 | ||
|
|
046676e40b | ||
|
|
9a671c36cc | ||
|
|
e6152acf0e | ||
|
|
5af5c8a1a6 | ||
|
|
4723c5d9d0 | ||
|
|
7e391ca518 | ||
|
|
89d072de68 | ||
|
|
fc99465fb9 | ||
|
|
6537e5fc23 | ||
|
|
5dfc18a78d | ||
|
|
4e0d38ca35 | ||
|
|
4883bc8293 | ||
|
|
61134352e9 | ||
|
|
3d7733c86a | ||
|
|
9e2b52b4d2 | ||
|
|
cc36a18d2c | ||
|
|
b4fb169e19 | ||
|
|
47c0dbf842 | ||
|
|
8515e2fcaf | ||
|
|
61bfb1fcff | ||
|
|
8d5d344c2c | ||
|
|
90c7528540 | ||
|
|
8485dea251 | ||
|
|
5b208844c7 | ||
|
|
d0870c1dfa | ||
|
|
24b561ae0a | ||
|
|
8f8fc58e55 | ||
|
|
c3ce265c78 | ||
|
|
ecc94161ee | ||
|
|
3c0dc61828 | ||
|
|
dd26a6059b | ||
|
|
3104ee6471 | ||
|
|
7f122c7ab8 | ||
|
|
bf1f2c3175 | ||
|
|
4a0eb4d93e | ||
|
|
975905428f | ||
|
|
4c809ff7d2 | ||
|
|
3c2640d4bf | ||
|
|
2838d207ca | ||
|
|
cb7732b704 | ||
|
|
429cc17525 | ||
|
|
52247c38d5 | ||
|
|
3a56969158 | ||
|
|
91dae25936 | ||
|
|
363322485e | ||
|
|
4218c5b2a9 | ||
|
|
0611109c04 | ||
|
|
adf1675a06 | ||
|
|
38ba2c758d | ||
|
|
3e774f6605 | ||
|
|
05ee5794f2 | ||
|
|
8f8949cb3e | ||
|
|
4372fd2d9e | ||
|
|
ff3762007e | ||
|
|
70ecd32065 | ||
|
|
27bb172014 | ||
|
|
209f577890 | ||
|
|
d6f83f0057 | ||
|
|
18f36b85bb | ||
|
|
93ecb9c72b | ||
|
|
c1f542cbf5 | ||
|
|
a31381632f | ||
|
|
3eb8382423 | ||
|
|
8d762d09e0 | ||
|
|
562185057b | ||
|
|
4d7ce73422 | ||
|
|
b8685c2dda | ||
|
|
dcbf233774 | ||
|
|
f2b85f6473 | ||
|
|
d51affda59 | ||
|
|
b70b2e4127 | ||
|
|
2e8d061e22 | ||
|
|
5891babc5c | ||
|
|
2be69da8a4 | ||
|
|
9059abbc06 | ||
|
|
60720707d1 | ||
|
|
72eb711d98 | ||
|
|
cec6d307d0 | ||
|
|
8866b19b7f | ||
|
|
85b0d8e6ae | ||
|
|
d1d4ce558a | ||
|
|
a1f9b95432 | ||
|
|
5ce69e63cd | ||
|
|
191ec0b830 | ||
|
|
4aeef970bb | ||
|
|
0c085ed0b5 | ||
|
|
7f63de9e1b | ||
|
|
7b10bdf103 | ||
|
|
e8ad1e039b | ||
|
|
595510028d | ||
|
|
905049f7c7 | ||
|
|
8f291145d2 | ||
|
|
0d363a6868 | ||
|
|
a5df96048b | ||
|
|
3599fd21da | ||
|
|
4d6f614df8 | ||
|
|
ae04a06e42 | ||
|
|
b90ce37ff0 | ||
|
|
4c83e823fc | ||
|
|
2ab24f893a | ||
|
|
5fd69bcd4c | ||
|
|
a04c09b748 | ||
|
|
3660e0bf1e | ||
|
|
d699ba423f | ||
|
|
de20faaaf2 | ||
|
|
6e70ea852e | ||
|
|
8b224f174a | ||
|
|
5f0de830e4 | ||
|
|
898d3d0aa4 | ||
|
|
f71811f65a | ||
|
|
2a1a392cf4 | ||
|
|
3793646a7c | ||
|
|
20dde8f115 | ||
|
|
027cce9756 | ||
|
|
d366dbf2cc | ||
|
|
f33eec8617 | ||
|
|
9128f2eadd | ||
|
|
1f6fecb28b | ||
|
|
341c112d54 | ||
|
|
f83eb5e6b1 | ||
|
|
1b9d15c087 | ||
|
|
a81f7bb711 | ||
|
|
291ae367a6 | ||
|
|
7b933ad76b | ||
|
|
5bae5bc9de | ||
|
|
bf066680af | ||
|
|
3de2936afc | ||
|
|
09047f2af8 | ||
|
|
fd2eea94a6 | ||
|
|
8c2d0ebe0b | ||
|
|
e165e73b0e | ||
|
|
2e327ac30d | ||
|
|
8b60d5eda1 | ||
|
|
ad2f2a98df | ||
|
|
bf96016c32 | ||
|
|
3334983707 | ||
|
|
0aca41b927 | ||
|
|
f4c89fbb2c | ||
|
|
aff768548a | ||
|
|
0fa68e2379 | ||
|
|
c9a9059d68 | ||
|
|
7c4c348f8c | ||
|
|
ef4c7169e6 | ||
|
|
df8721c5fd | ||
|
|
487cae1625 | ||
|
|
d6cd6af645 | ||
|
|
5c3b79abee | ||
|
|
2835f80ee5 | ||
|
|
a6cb6fb975 | ||
|
|
c6b49080af | ||
|
|
70f7f8c100 | ||
|
|
bb31a0014b | ||
|
|
20dcb23b8a | ||
|
|
f9a3703a18 | ||
|
|
154659c083 | ||
|
|
f07bc19d5f | ||
|
|
ed9f590f4f | ||
|
|
2e9e3459e4 | ||
|
|
48c10e863e | ||
|
|
13878ee48d | ||
|
|
71a2140dbd | ||
|
|
2df68f8cf2 | ||
|
|
c40b2e753e | ||
|
|
e16cb01b31 | ||
|
|
0dd411330c | ||
|
|
6559cc24ec | ||
|
|
6044ed07f8 | ||
|
|
edeb854ab3 | ||
|
|
e61ee2981e | ||
|
|
465e41ae79 | ||
|
|
4c616cc395 | ||
|
|
c93ea7a851 | ||
|
|
9fc2009b76 | ||
|
|
3cd8110db3 | ||
|
|
fc901e8131 | ||
|
|
7f035cccbc | ||
|
|
862c5e47c8 | ||
|
|
f86c9dc99c | ||
|
|
cc632d89f3 | ||
|
|
2c846f6727 | ||
|
|
4a05302722 | ||
|
|
8d0b571f38 | ||
|
|
9b69d552aa | ||
|
|
62e2fb580f | ||
|
|
e34965d524 | ||
|
|
00d7f93094 | ||
|
|
fc3d752497 | ||
|
|
34e000e3bb | ||
|
|
9c881ca00e | ||
|
|
0620ef82d0 | ||
|
|
cf97a83651 | ||
|
|
7123e0ff23 | ||
|
|
ab0cc34b24 | ||
|
|
26f06436f4 | ||
|
|
dc90fea30a | ||
|
|
a423038c0a | ||
|
|
97574157ed | ||
|
|
8bcb0278ee | ||
|
|
492a23b61e | ||
|
|
a35b5d4c02 | ||
|
|
aba0af378a | ||
|
|
2748a34c25 | ||
|
|
84f0aceb61 | ||
|
|
d9e805d8d2 | ||
|
|
f20cf11cc9 | ||
|
|
f62d90285f | ||
|
|
7528d4981e | ||
|
|
646ed09f2b | ||
|
|
0f3dba4d38 | ||
|
|
bd222124ef | ||
|
|
562077606d | ||
|
|
24d94b3b84 | ||
|
|
34ca235d7b | ||
|
|
a5658bf5ea | ||
|
|
2794147fd7 | ||
|
|
359645dc4e | ||
|
|
c18230faf5 | ||
|
|
668fd1d78a | ||
|
|
197b45536c | ||
|
|
6ef3dc288e | ||
|
|
51cf7a6711 | ||
|
|
a0d0bb0d48 | ||
|
|
4226dd6d5c | ||
|
|
69d7bff9a5 | ||
|
|
b5c141f081 | ||
|
|
35a110841c | ||
|
|
ed911414d8 |
1639 changed files with 112072 additions and 56366 deletions
60
.github/ISSUE_TEMPLATE/bug_report.md
vendored
60
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -7,35 +7,59 @@ assignees: ''
|
|||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
First of all, please say "Hi" or "Hello", it doesn't cost much.
|
||||
|
||||
|
||||
1. **Describe the bug** (mandatory)
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
Also, if applicable, **do you reproduce it with linphone-android latest release from the Play Store?**
|
||||
|
||||
**If the issue is about the SDK (build, issue, etc...) open the ticket in the [linphone-sdk](https://github.com/BelledonneCommunications/linphone-sdk) repository or one of it's submodules!**
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
2. **To Reproduce** (mandatory)
|
||||
|
||||
Please detail steps to reproduce the behavior.
|
||||
|
||||
3. **Expected behavior** (mandatory)
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Please complete the following information**
|
||||
4. **Please complete the following information** (mandatory)
|
||||
|
||||
- Device: [e.g. Samsung Note 20 Ultra]
|
||||
- OS: [e.g. Android 11]
|
||||
- Version of the App [e.g. 4.3.1]
|
||||
- Version of the SDK [e.g 4.4.16]
|
||||
- OS: [e.g. Android 12]
|
||||
- Version of the App [e.g. 4.6.11]
|
||||
- Version of the SDK [e.g 5.1.48]
|
||||
- Where you did got it from (Play Store, F-Droid, local build)
|
||||
- Please tell us if your Android is a Lineage OS or another variant.
|
||||
|
||||
**SDK logs**
|
||||
Enable debug logs in advanced section of the settings, restart the app, reproduce the issue and then go to About page, click on "Send logs" and copy/paste the link here.
|
||||
If you are using a SDK that isn't the latest release, please update first as it's likely your issue is already solved.
|
||||
|
||||
**Adb logcat logs**
|
||||
In case of a crash of the app, please also provide the stack trace of the crash using adb logcat.
|
||||
5. **SDK logs** (mandatory)
|
||||
|
||||
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).
|
||||
|
||||
In case of a call issue, please attach logs from both devices!
|
||||
|
||||
6. **Adb logcat logs** (mandatory if native crash)
|
||||
|
||||
In case of a crash of the app, please also provide the symbolized stack trace of the crash using adb logcat.
|
||||
|
||||
Here's the command for a arm64 device: `adb logcat | grep ndk-stack -sym <sdk build directory>/libs-debug/arm64-v8a/`
|
||||
|
||||
For more information, please refer to [this section of the README](https://github.com/BelledonneCommunications/linphone-android#native-crash) file.
|
||||
|
||||
7. **Screenshots** (optionnal)
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
8. **Additional context** (optionnal)
|
||||
|
||||
Add any other context about the problem here.
|
||||
|
||||
|
||||
Thank you in advance for filling bug reports properly!
|
||||
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:20210903_java11_android31
|
||||
image: gitlab.linphone.org:4567/bc/public/linphone-android/bc-dev-android-36:20260120_trixie_java21_android36_gradle9
|
||||
|
||||
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="21" />
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/misc.xml
generated
Normal file
10
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" 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>
|
||||
868
CHANGELOG.md
868
CHANGELOG.md
|
|
@ -10,7 +10,873 @@ 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.22] - 2026-01-20
|
||||
|
||||
### Changed
|
||||
- Close search bar when opening bottom sheet and vice versa
|
||||
|
||||
### Fixed
|
||||
- Sending a file from another app using Android shortcut not working if conversation was already opened
|
||||
- Trying to workaround an issue where ForegroundService notification isn't displayed in the allowed timeframe, causing an Exception
|
||||
|
||||
## [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
|
||||
- Conference creation with scheduling, video, different layouts, showing who is speaking and who is muted, etc...
|
||||
- Group calls directly from group chat rooms
|
||||
- Chat rooms can be individually muted (no notification when receiving a chat message)
|
||||
- When a message is received wait a short amount of time to check if more are to be received to notify them all at once
|
||||
- Outgoing call video in early-media if requested by callee
|
||||
- Image & Video in-app viewers allow for full-screen display
|
||||
- Display name can be set during assistant when creating / logging in a sip.linphone.org account
|
||||
- Android 13 support, using new post notifications & media permissions
|
||||
- Call recordings can be exported
|
||||
- Setting to prevent international prefix from account to be applied to call & chat
|
||||
- Themed app icon is now supported for Android 13+
|
||||
|
||||
### 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
|
||||
- Phone numbers & email addresses are now clickable links in chat messages
|
||||
- Go to call activity when you click on launcher icon if there is at least one active call
|
||||
|
||||
### Fixed
|
||||
- Multiple file download attempt from the same chat bubble at the same time needed app restart to properly download each file
|
||||
- Call stopped when removing app from recent tasks
|
||||
- Generated avatars in dark mode
|
||||
- Call state in self-managed TelecomManager service if it takes longer to be created than the call to be answered
|
||||
- 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
|
||||
|
||||
## [4.6.14] - 2022-09-19
|
||||
|
||||
### Fixed
|
||||
- ANR that happens sometimes when playing voice recording
|
||||
|
||||
### Changed
|
||||
- Improved contact loader by querying only relevant fields
|
||||
|
||||
## [4.6.13] - 2022-08-25
|
||||
|
||||
### Fixed
|
||||
- Disable Telecom Manager feature on Android < 10 to prevent crash due to Android 9 OS bug
|
||||
- Fixed crash due to AAudio's waitForStateChange (SDK fix)
|
||||
|
||||
## [4.6.12] - 2022-07-29
|
||||
|
||||
### Fixed
|
||||
- Call notification not being removed if service channel is disabled & background mode is enabled
|
||||
- Wrong display name in chat notification sometimes
|
||||
- Removed secure chat button if no LIME server configured or no conference factory URI set
|
||||
- Disable TelecomManager feature when the device doesn't support it
|
||||
|
||||
### Changed
|
||||
- ContactsLoader have been updated, shouldn't crash anymore
|
||||
|
||||
## [4.6.11] - 2022-06-27
|
||||
|
||||
### Fixed
|
||||
- Various crashes due to unhandled exceptions
|
||||
- Echo canceller calibration not using speaker (SDK fix)
|
||||
|
||||
## [4.6.10] - 2022-06-07
|
||||
|
||||
### Fixed
|
||||
- Fixed contact address used instead of identity address when creating a basic chat room from history or contact details
|
||||
- Fixed call notification still visible after call ended on some devices
|
||||
- Fixed incoming call activity not displayed on some devices
|
||||
- Fixed Malaysian dial plan (SDK fix)
|
||||
- Fixed incoming call ringing even if Do not disturb mode is enabled (SDK fix)
|
||||
|
||||
## [4.6.9] - 2022-05-30
|
||||
|
||||
### Fixed
|
||||
- ANR when screen turns OFF/ON while app is in foreground
|
||||
- Crash due to missing CoreContext instance in TelecomManager service
|
||||
- One-to-One encrypted chat room creation if it already exists
|
||||
- Crash if ConnectionService feature isn't supported by the device
|
||||
|
||||
### Changed
|
||||
- Updated translations from Weblate
|
||||
- Improved audio devices logs
|
||||
|
||||
## [4.6.8] - 2022-05-23
|
||||
|
||||
### Fixed
|
||||
- Crash due to missing CoreContext in CoreService
|
||||
- Crash in BootReceiver if auto start is disabled
|
||||
- Other crashes
|
||||
|
||||
## [4.6.7] - 2022-05-04
|
||||
|
||||
### Changed
|
||||
- Do not start Core in Application, prevents service notification from appearing by itself
|
||||
- When switching from bluetooth or headset device to earpiece/speaker, also change microphone
|
||||
- Prevent empty chat bubble by sending only space character(s)
|
||||
|
||||
### Fixed
|
||||
- Phone numbers with non-ASCII labels missing from address book
|
||||
- Wrong audio device displayed in call statistics
|
||||
- Various issues from Crashlytics
|
||||
|
||||
## [4.6.6] - 2022-04-26
|
||||
|
||||
### Changed
|
||||
- Prevent requests to LIME X3DH & long term presence servers when not using a sip.linphone.org account
|
||||
- Updated DE & RU translations
|
||||
- Improved UI on landscape tablets
|
||||
|
||||
### Fixed
|
||||
- Catching exceptions in new ContactsLoader reported on PlayStore
|
||||
- Missing phone numbers in contacts when label contains a space character (5.1.24 SDK fix)
|
||||
- Prevent app from starting by itself due to DummySyncService
|
||||
- Hide chat rooms settings not working properly
|
||||
|
||||
## [4.6.5] - 2022-04-11
|
||||
|
||||
### Changed
|
||||
- Only display phone number if it matches SIP address username
|
||||
- Using new MagicSearch API to improve contacts list performances
|
||||
|
||||
### Fixed
|
||||
- Prevent concurrent exception while loading native address book contacts
|
||||
|
||||
## [4.6.4] - 2022-04-06
|
||||
|
||||
### Added
|
||||
- Set video information in CallStyle incoming call notification
|
||||
|
||||
### Changed
|
||||
- Massive rework of how native contacts from address book are handled to improve performances
|
||||
- Only display phone number from LDAP search result if it matches SIP address' username
|
||||
|
||||
### Fixed
|
||||
- Do not use CallStyle notification on Samsung devices, they are currently displayed badly
|
||||
- Fixed microphone muted when starting a new call if microphone was muted at the end of the previous one
|
||||
- Added LDAP contact display name to SIP address
|
||||
- Prevent read-only 1-1 chat room
|
||||
- Fixed chat room last updated time not updated sometimes
|
||||
|
||||
## [4.6.3] - 2022-03-08
|
||||
|
||||
### Added
|
||||
- Improvements in contacts matching
|
||||
|
||||
### Changed
|
||||
- "Operation in progress" spinner hidden when contacts display/filter takes less than 200ms
|
||||
|
||||
### Fixed
|
||||
- Contacts order when multiple address book contacts share the same number / SIP address
|
||||
- Wrongly formatted phone numbers not displayed anymore
|
||||
- Incoming call activity not displayed on LineageOS sometimes
|
||||
- Various crashes related to Telecom Manager exceptions not being caught
|
||||
|
||||
## [4.6.2] - 2022-03-01
|
||||
|
||||
### Added
|
||||
- Request BLUETOOTH_CONNECT permission on Android 12+ devices, if not we won't be notified when a BT device is being connected/disconnected while app is alive.
|
||||
- LDAP settings if SDK is built with OpenLDAP (requires 5.1.1 or higher linphone-sdk), will add contacts if any
|
||||
- SIP addresses & phone numbers can be selected in history & contact details view
|
||||
- Text can be selected in file viewer & config viewer
|
||||
- Prevent screen to turn off while recording a voice message
|
||||
|
||||
### Changed
|
||||
- Contacts lists now show LDAP contacts if any
|
||||
|
||||
### Fixed
|
||||
- Negative gain in audio settings is allowed again
|
||||
- STUN server URL setting not enabling it for non sip.linphone.org accounts
|
||||
- Contacts list header case comparison
|
||||
- Stop voice recording playback when sending chat message
|
||||
- Call activity not finishing when hanging up sometimes
|
||||
- Auto start setting disabled not working if background mode setting was enabled
|
||||
|
||||
## [4.6.1] - 2022-02-14
|
||||
|
||||
### Fixed
|
||||
- Quit button not working when background mode was enabled
|
||||
- Crash when background mode was enabled and service notification channel was disabled
|
||||
- Crashes while changing audio route
|
||||
- Crash while fetching contacts
|
||||
- Crash when rotating the device (SDK fix)
|
||||
|
||||
## [4.6.0] - 2022-02-09
|
||||
|
||||
|
|
|
|||
73
README.md
73
README.md
|
|
@ -1,12 +1,18 @@
|
|||
|
||||
[](https://gitlab.linphone.org/BC/public/linphone-android/commits/master)
|
||||
|
||||
[](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
|
||||
|
||||
[<img src="metadata/google-play-badge.png" height="60" alt="Get it on Google Play">](https://play.google.com/store/apps/details?id=org.linphone)[<img src="metadata/f-droid-badge.png" height="60" alt="Get it on F-Droid">](https://f-droid.org/en/packages/org.linphone/)
|
||||
|
||||
You can also download APKs signed with our key from [our website](https://download.linphone.org/releases/android/?C=M;O=D).
|
||||
|
||||
### License
|
||||
|
||||
|
|
@ -16,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/
|
||||
|
||||
|
|
@ -28,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 detailled 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
|
||||
|
||||
|
|
@ -92,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.
|
||||
|
||||
|
|
@ -101,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 About 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
|
||||
|
||||
|
|
@ -115,33 +114,43 @@ 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.
|
||||
|
||||
Here's how to get the stacktrace and the right architecture from a device plugged to your computer:
|
||||
If you know the CPU architecture of your device (most probably arm64 if it's a recent device) you can use the following to get the stacktrace from a device plugged to a computer:
|
||||
```
|
||||
adb logcat -d | ndk-stack -sym ./libs-debug/arm64-v8a/
|
||||
```
|
||||
If you don't know the CPU architecture, use the following instead:
|
||||
```
|
||||
adb logcat -d | ndk-stack -sym ./libs-debug/`adb shell getprop ro.product.cpu.abi | tr -d '\r'`
|
||||
```
|
||||
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
|
||||
|
||||
|
|
@ -159,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:
|
||||
|
|
|
|||
2
app/.gitignore
vendored
2
app/.gitignore
vendored
|
|
@ -1 +1 @@
|
|||
/build
|
||||
/build
|
||||
272
app/build.gradle
272
app/build.gradle
|
|
@ -1,272 +0,0 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'org.jlleitschuh.gradle.ktlint'
|
||||
}
|
||||
|
||||
def appVersionName = "4.6.0"
|
||||
def appVersionCode = 40600 // 4.06.00
|
||||
|
||||
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 31
|
||||
buildToolsVersion '31.0.0'
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 31
|
||||
versionCode appVersionCode
|
||||
versionName "${project.version}"
|
||||
applicationId getPackageName()
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.all {
|
||||
outputFileName = "linphone-android-${variant.buildType.name}-${project.version}.apk"
|
||||
}
|
||||
|
||||
// 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",
|
||||
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",
|
||||
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
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'androidx.media:media:1.4.3'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
|
||||
def nav_version = "2.4.0"
|
||||
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.window:window:1.0.0"
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha03"
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-beta01'
|
||||
|
||||
implementation 'com.google.android.material:material:1.5.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'
|
||||
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.12.0'
|
||||
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
|
||||
implementation platform('com.google.firebase:firebase-bom:26.4.0')
|
||||
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.1+'
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
320
app/build.gradle.kts
Normal file
320
app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
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("Debug flavor app version is [$appVersion], app branch is [$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("Release flavor app version is [$appVersion], app branch is [$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_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
buildConfig = true
|
||||
resValues = 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>
|
||||
8
app/proguard-rules.pro
vendored
8
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
|
||||
|
|
@ -18,8 +18,4 @@
|
|||
|
||||
# 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 { *; }
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -1,63 +1,87 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.linphone">
|
||||
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" />
|
||||
<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 -->
|
||||
<!-- To vibrate while receiving an incoming call -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<!-- 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 -->
|
||||
<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 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:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="${appLabel}"
|
||||
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>
|
||||
|
|
@ -82,68 +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.call.CallActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true" />
|
||||
|
||||
<activity android:name=".activities.call.IncomingCallActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:noHistory="true" />
|
||||
|
||||
<activity android:name=".activities.call.OutgoingCallActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:noHistory="true" />
|
||||
<activity
|
||||
android:name=".ui.welcome.WelcomeActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.chat_bubble.ChatBubbleActivity"
|
||||
android:allowEmbedded="true"
|
||||
android:documentLaunchMode="always"
|
||||
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" />
|
||||
|
||||
<!-- 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="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
|
|
@ -151,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 -->
|
||||
|
||||
|
|
@ -210,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>
|
||||
|
|
@ -15,27 +16,20 @@
|
|||
<entry name="nat_policy_ref" overwrite="true">nat_policy_default_values</entry>
|
||||
<entry name="realm" overwrite="true">sip.linphone.org</entry>
|
||||
<entry name="conference_factory_uri" overwrite="true">sip:conference-factory@sip.linphone.org</entry>
|
||||
<entry name="audio_video_conference_factory_uri" overwrite="true">sip:videoconference-factory@sip.linphone.org</entry>
|
||||
<entry name="push_notification_allowed" overwrite="true">1</entry>
|
||||
<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="sip">
|
||||
<entry name="rls_uri" overwrite="true">sips:rls@sip.linphone.org</entry>
|
||||
</section>
|
||||
<section name="lime">
|
||||
<entry name="x3dh_server_url" overwrite="true">https://lime.linphone.org/lime-server/lime-server.php</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>
|
||||
<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>
|
||||
|
|
@ -15,21 +16,23 @@
|
|||
<entry name="nat_policy_ref" overwrite="true"></entry>
|
||||
<entry name="realm" overwrite="true"></entry>
|
||||
<entry name="conference_factory_uri" overwrite="true"></entry>
|
||||
<entry name="audio_video_conference_factory_uri" overwrite="true"></entry>
|
||||
<entry name="push_notification_allowed" overwrite="true">0</entry>
|
||||
<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="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="sip">
|
||||
<entry name="media_encryption">srtp</entry>
|
||||
<entry name="media_encryption_mandatory" overwrite="true">0</entry>
|
||||
</section>
|
||||
<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,24 +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
|
||||
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,32 +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_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_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
|
||||
|
||||
[assistant]
|
||||
xmlrpc_url=https://subscribe.linphone.org:444/wizard.php
|
||||
record_aware=1
|
||||
|
||||
[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,53 +22,142 @@ package org.linphone
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
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
|
||||
|
||||
class LinphoneApplication : Application() {
|
||||
@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
|
||||
|
||||
fun ensureCoreExists(context: Context, pushReceived: Boolean = false) {
|
||||
if (::coreContext.isInitialized && !coreContext.stopped) {
|
||||
Log.d("[Application] Skipping Core creation (push received? $pushReceived)")
|
||||
return
|
||||
}
|
||||
|
||||
Factory.instance().setLogCollectionPath(context.filesDir.absolutePath)
|
||||
Factory.instance().enableLogCollection(LogCollectionState.Enabled)
|
||||
|
||||
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 context created ${if (pushReceived) "from push" else ""}")
|
||||
coreContext = CoreContext(context, config)
|
||||
coreContext.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val appName = getString(R.string.app_name)
|
||||
android.util.Log.i("[$appName]", "Application is being created")
|
||||
ensureCoreExists(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 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())
|
||||
}
|
||||
.memoryCache {
|
||||
MemoryCache.Builder()
|
||||
.maxSizePercent(context, 0.25)
|
||||
.build()
|
||||
}
|
||||
.diskCache {
|
||||
val cache = cacheDir.resolve("image_cache")
|
||||
DiskCache.Builder()
|
||||
.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.flow.collect
|
||||
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)
|
||||
|
||||
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,113 +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 androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class GenericFragment<T : ViewDataBinding> : Fragment() {
|
||||
private var _binding: T? = null
|
||||
protected val binding get() = _binding!!
|
||||
protected var useMaterialSharedAxisXForwardAnimation = true
|
||||
|
||||
protected fun isBindingAvailable(): Boolean {
|
||||
return _binding != null
|
||||
}
|
||||
|
||||
protected val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getLayoutId(): Int
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
|
||||
return _binding!!.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackPressedCallback)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
onBackPressedCallback.remove()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
protected open fun goBack() {
|
||||
try {
|
||||
if (!findNavController().popBackStack()) {
|
||||
if (!findNavController().navigateUp()) {
|
||||
onBackPressedCallback.isEnabled = false
|
||||
requireActivity().onBackPressed()
|
||||
}
|
||||
}
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e("[Generic Fragment] [$this] Can't go back: $ise")
|
||||
onBackPressedCallback.isEnabled = false
|
||||
requireActivity().onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,951 +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.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.fragments.*
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.fragments.ChatRoomCreationFragment
|
||||
import org.linphone.activities.main.chat.fragments.DetailChatRoomFragment
|
||||
import org.linphone.activities.main.chat.fragments.GroupInfoFragment
|
||||
import org.linphone.activities.main.chat.fragments.MasterChatRoomsFragment
|
||||
import org.linphone.activities.main.contact.fragments.ContactEditorFragment
|
||||
import org.linphone.activities.main.contact.fragments.DetailContactFragment
|
||||
import org.linphone.activities.main.contact.fragments.MasterContactsFragment
|
||||
import org.linphone.activities.main.dialer.fragments.DialerFragment
|
||||
import org.linphone.activities.main.fragments.TabsFragment
|
||||
import org.linphone.activities.main.history.fragments.DetailCallLogFragment
|
||||
import org.linphone.activities.main.history.fragments.MasterCallLogsFragment
|
||||
import org.linphone.activities.main.settings.fragments.*
|
||||
import org.linphone.activities.main.sidemenu.fragments.SideMenuFragment
|
||||
import org.linphone.contact.NativeContact
|
||||
import org.linphone.core.Address
|
||||
|
||||
internal fun Fragment.findMasterNavController(): NavController {
|
||||
return parentFragment?.parentFragment?.findNavController() ?: findNavController()
|
||||
}
|
||||
|
||||
fun popupTo(
|
||||
popUpTo: Int = -1,
|
||||
popUpInclusive: Boolean = false,
|
||||
singleTop: Boolean = true
|
||||
): NavOptions {
|
||||
val builder = NavOptions.Builder()
|
||||
builder.setPopUpTo(popUpTo, popUpInclusive).setLaunchSingleTop(singleTop)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/* Main activity related */
|
||||
|
||||
internal fun MainActivity.navigateToDialer(args: Bundle?) {
|
||||
findNavController(R.id.nav_host_fragment).navigate(
|
||||
R.id.action_global_dialerFragment,
|
||||
args,
|
||||
popupTo(R.id.dialerFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun MainActivity.navigateToChatRooms(args: Bundle? = null) {
|
||||
findNavController(R.id.nav_host_fragment).navigate(
|
||||
R.id.action_global_masterChatRoomsFragment,
|
||||
args,
|
||||
popupTo(R.id.masterChatRoomsFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun MainActivity.navigateToChatRoom(localAddress: String?, peerAddress: String?) {
|
||||
val deepLink = "linphone-android://chat-room/$localAddress/$peerAddress"
|
||||
findNavController(R.id.nav_host_fragment).navigate(
|
||||
Uri.parse(deepLink),
|
||||
popupTo(R.id.masterChatRoomsFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun MainActivity.navigateToContact(contactId: String?) {
|
||||
val deepLink = "linphone-android://contact/view/$contactId"
|
||||
findNavController(R.id.nav_host_fragment).navigate(
|
||||
Uri.parse(deepLink),
|
||||
popupTo(R.id.masterContactsFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
/* Tabs fragment related */
|
||||
|
||||
internal fun TabsFragment.navigateToCallHistory() {
|
||||
val action = when (findNavController().currentDestination?.id) {
|
||||
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterCallLogsFragment
|
||||
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterCallLogsFragment
|
||||
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterCallLogsFragment
|
||||
else -> R.id.action_global_masterCallLogsFragment
|
||||
}
|
||||
findNavController().navigate(
|
||||
action,
|
||||
null,
|
||||
popupTo(R.id.masterCallLogsFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun TabsFragment.navigateToContacts() {
|
||||
val action = when (findNavController().currentDestination?.id) {
|
||||
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterContactsFragment
|
||||
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterContactsFragment
|
||||
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterContactsFragment
|
||||
else -> R.id.action_global_masterContactsFragment
|
||||
}
|
||||
findNavController().navigate(
|
||||
action,
|
||||
null,
|
||||
popupTo(R.id.masterContactsFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun TabsFragment.navigateToDialer() {
|
||||
val action = when (findNavController().currentDestination?.id) {
|
||||
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_dialerFragment
|
||||
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_dialerFragment
|
||||
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_dialerFragment
|
||||
else -> R.id.action_global_dialerFragment
|
||||
}
|
||||
findNavController().navigate(
|
||||
action,
|
||||
null,
|
||||
popupTo(R.id.dialerFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun TabsFragment.navigateToChatRooms() {
|
||||
val action = when (findNavController().currentDestination?.id) {
|
||||
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterChatRoomsFragment
|
||||
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterChatRoomsFragment
|
||||
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterChatRoomsFragment
|
||||
else -> R.id.action_global_masterChatRoomsFragment
|
||||
}
|
||||
findNavController().navigate(
|
||||
action,
|
||||
null,
|
||||
popupTo(R.id.masterChatRoomsFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
/* Dialer related */
|
||||
|
||||
internal fun DialerFragment.navigateToContacts(uriToAdd: String?) {
|
||||
val deepLink = "linphone-android://contact/new/$uriToAdd"
|
||||
findNavController().navigate(
|
||||
Uri.parse(deepLink),
|
||||
popupTo(R.id.masterContactsFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DialerFragment.navigateToConfigFileViewer() {
|
||||
val bundle = bundleOf("Secure" to true)
|
||||
findMasterNavController().navigate(
|
||||
R.id.action_global_configViewerFragment,
|
||||
bundle,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
|
||||
/* Chat related */
|
||||
|
||||
internal fun MasterChatRoomsFragment.navigateToChatRoom(args: Bundle) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
|
||||
val previousBackStackEntry = navHostFragment.navController.currentBackStackEntry
|
||||
val popUpToFragmentId = when (previousBackStackEntry?.destination?.id) {
|
||||
R.id.detailChatRoomFragment -> R.id.detailChatRoomFragment
|
||||
R.id.chatRoomCreationFragment -> R.id.chatRoomCreationFragment
|
||||
else -> R.id.emptyChatFragment
|
||||
}
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_detailChatRoomFragment,
|
||||
args,
|
||||
popupTo(popUpToFragmentId, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun MasterChatRoomsFragment.navigateToChatRoomCreation(
|
||||
createGroupChatRoom: Boolean = false,
|
||||
slidingPane: SlidingPaneLayout
|
||||
) {
|
||||
val bundle = bundleOf("createGroup" to createGroupChatRoom)
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
|
||||
val previousBackStackEntry = navHostFragment.navController.currentBackStackEntry
|
||||
val popUpToFragmentId = when (previousBackStackEntry?.destination?.id) {
|
||||
R.id.detailChatRoomFragment -> R.id.detailChatRoomFragment
|
||||
R.id.chatRoomCreationFragment -> R.id.chatRoomCreationFragment
|
||||
else -> R.id.emptyChatFragment
|
||||
}
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_chatRoomCreationFragment,
|
||||
bundle,
|
||||
popupTo(popUpToFragmentId, true)
|
||||
)
|
||||
if (!slidingPane.isOpen) slidingPane.openPane()
|
||||
}
|
||||
|
||||
internal fun MasterChatRoomsFragment.clearDisplayedChatRoom() {
|
||||
if (findNavController().currentDestination?.id == R.id.masterChatRoomsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_emptyChatFragment,
|
||||
null,
|
||||
popupTo(R.id.emptyChatFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToContacts(sipUriToAdd: String) {
|
||||
val deepLink = "linphone-android://contact/new/$sipUriToAdd"
|
||||
findMasterNavController().navigate(Uri.parse(deepLink))
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToImdn(args: Bundle?) {
|
||||
if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_detailChatRoomFragment_to_imdnFragment,
|
||||
args,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToDevices() {
|
||||
if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_detailChatRoomFragment_to_devicesFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToGroupInfo() {
|
||||
if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_detailChatRoomFragment_to_groupInfoFragment,
|
||||
null,
|
||||
popupTo(R.id.groupInfoFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToEphemeralInfo() {
|
||||
if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_detailChatRoomFragment_to_ephemeralFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToTextFileViewer(secure: Boolean) {
|
||||
val bundle = bundleOf("Secure" to secure)
|
||||
findMasterNavController().navigate(
|
||||
R.id.action_global_textViewerFragment,
|
||||
bundle,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToPdfFileViewer(secure: Boolean) {
|
||||
val bundle = bundleOf("Secure" to secure)
|
||||
findMasterNavController().navigate(
|
||||
R.id.action_global_pdfViewerFragment,
|
||||
bundle,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToImageFileViewer(secure: Boolean) {
|
||||
val bundle = bundleOf("Secure" to secure)
|
||||
findMasterNavController().navigate(
|
||||
R.id.action_global_imageViewerFragment,
|
||||
bundle,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToVideoFileViewer(secure: Boolean) {
|
||||
val bundle = bundleOf("Secure" to secure)
|
||||
findMasterNavController().navigate(
|
||||
R.id.action_global_videoViewerFragment,
|
||||
bundle,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToAudioFileViewer(secure: Boolean) {
|
||||
val bundle = bundleOf("Secure" to secure)
|
||||
findMasterNavController().navigate(
|
||||
R.id.action_global_audioViewerFragment,
|
||||
bundle,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToEmptyChatRoom() {
|
||||
findNavController().navigate(
|
||||
R.id.action_global_emptyChatFragment,
|
||||
null,
|
||||
popupTo(R.id.detailChatRoomFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DetailChatRoomFragment.navigateToDialer(args: Bundle?) {
|
||||
findMasterNavController().navigate(
|
||||
R.id.action_global_dialerFragment,
|
||||
args,
|
||||
popupTo(R.id.dialerFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun ChatRoomCreationFragment.navigateToGroupInfo() {
|
||||
if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_chatRoomCreationFragment_to_groupInfoFragment,
|
||||
null,
|
||||
popupTo(R.id.groupInfoFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ChatRoomCreationFragment.navigateToChatRoom(args: Bundle) {
|
||||
if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_chatRoomCreationFragment_to_detailChatRoomFragment,
|
||||
args,
|
||||
popupTo(R.id.chatRoomCreationFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ChatRoomCreationFragment.navigateToEmptyChatRoom() {
|
||||
findNavController().navigate(
|
||||
R.id.action_global_emptyChatFragment,
|
||||
null,
|
||||
popupTo(R.id.chatRoomCreationFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun GroupInfoFragment.navigateToChatRoomCreation(args: Bundle?) {
|
||||
if (findNavController().currentDestination?.id == R.id.groupInfoFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_groupInfoFragment_to_chatRoomCreationFragment,
|
||||
args,
|
||||
popupTo(R.id.chatRoomCreationFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun GroupInfoFragment.navigateToChatRoom(args: Bundle?, created: Boolean) {
|
||||
if (findNavController().currentDestination?.id == R.id.groupInfoFragment) {
|
||||
val popUpToFragmentId = if (created) { // To remove all creation fragments from back stack
|
||||
R.id.chatRoomCreationFragment
|
||||
} else {
|
||||
R.id.detailChatRoomFragment
|
||||
}
|
||||
findNavController().navigate(
|
||||
R.id.action_groupInfoFragment_to_detailChatRoomFragment,
|
||||
args,
|
||||
popupTo(popUpToFragmentId, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* Contacts related */
|
||||
|
||||
internal fun MasterContactsFragment.navigateToContact() {
|
||||
if (findNavController().currentDestination?.id == R.id.masterContactsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment
|
||||
val previousBackStackEntry = navHostFragment.navController.currentBackStackEntry
|
||||
val popUpToFragmentId = when (previousBackStackEntry?.destination?.id) {
|
||||
R.id.detailContactFragment -> R.id.detailContactFragment
|
||||
R.id.contactEditorFragment -> R.id.contactEditorFragment
|
||||
else -> R.id.emptyContactFragment
|
||||
}
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_detailContactFragment,
|
||||
null,
|
||||
popupTo(popUpToFragmentId, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MasterContactsFragment.navigateToContactEditor(
|
||||
sipUriToAdd: String? = null,
|
||||
slidingPane: SlidingPaneLayout
|
||||
) {
|
||||
if (findNavController().currentDestination?.id == R.id.masterContactsFragment) {
|
||||
val bundle = if (sipUriToAdd != null) bundleOf("SipUri" to sipUriToAdd) else Bundle()
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_contactEditorFragment,
|
||||
bundle,
|
||||
popupTo(R.id.emptyContactFragment, true)
|
||||
)
|
||||
if (!slidingPane.isOpen) slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MasterContactsFragment.clearDisplayedContact() {
|
||||
if (findNavController().currentDestination?.id == R.id.masterContactsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_emptyContactFragment,
|
||||
null,
|
||||
popupTo(R.id.emptyContactFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ContactEditorFragment.navigateToContact(contact: NativeContact) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString("id", contact.nativeId)
|
||||
findNavController().navigate(
|
||||
R.id.action_contactEditorFragment_to_detailContactFragment,
|
||||
bundle,
|
||||
popupTo(R.id.contactEditorFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun ContactEditorFragment.navigateToEmptyContact() {
|
||||
findNavController().navigate(
|
||||
R.id.action_global_emptyContactFragment,
|
||||
null,
|
||||
popupTo(R.id.emptyContactFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DetailContactFragment.navigateToChatRoom(args: Bundle?) {
|
||||
findMasterNavController().navigate(
|
||||
R.id.action_global_masterChatRoomsFragment,
|
||||
args,
|
||||
popupTo(R.id.masterChatRoomsFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DetailContactFragment.navigateToDialer(args: Bundle?) {
|
||||
findMasterNavController().navigate(
|
||||
R.id.action_global_dialerFragment,
|
||||
args,
|
||||
popupTo(R.id.dialerFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DetailContactFragment.navigateToContactEditor() {
|
||||
if (findNavController().currentDestination?.id == R.id.detailContactFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_detailContactFragment_to_contactEditorFragment,
|
||||
null,
|
||||
popupTo(R.id.contactEditorFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DetailContactFragment.navigateToEmptyContact() {
|
||||
findNavController().navigate(
|
||||
R.id.action_global_emptyContactFragment,
|
||||
null,
|
||||
popupTo(R.id.emptyContactFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
/* History related */
|
||||
|
||||
internal fun MasterCallLogsFragment.navigateToCallHistory(slidingPane: SlidingPaneLayout) {
|
||||
if (findNavController().currentDestination?.id == R.id.masterCallLogsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_detailCallLogFragment,
|
||||
null,
|
||||
popupTo(R.id.detailCallLogFragment, true)
|
||||
)
|
||||
if (!slidingPane.isOpen) slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MasterCallLogsFragment.clearDisplayedCallHistory() {
|
||||
if (findNavController().currentDestination?.id == R.id.masterCallLogsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_emptyFragment,
|
||||
null,
|
||||
popupTo(R.id.emptyCallHistoryFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MasterCallLogsFragment.navigateToDialer(args: Bundle?) {
|
||||
findNavController().navigate(
|
||||
R.id.action_global_dialerFragment,
|
||||
args,
|
||||
popupTo(R.id.dialerFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun DetailCallLogFragment.navigateToContacts(sipUriToAdd: String) {
|
||||
val deepLink = "linphone-android://contact/new/$sipUriToAdd"
|
||||
findMasterNavController().navigate(Uri.parse(deepLink))
|
||||
}
|
||||
|
||||
internal fun DetailCallLogFragment.navigateToContact(contact: NativeContact) {
|
||||
val deepLink = "linphone-android://contact/view/${contact.nativeId}"
|
||||
findMasterNavController().navigate(Uri.parse(deepLink))
|
||||
}
|
||||
|
||||
internal fun DetailCallLogFragment.navigateToFriend(friendAddress: Address) {
|
||||
val deepLink = "linphone-android://contact/new/${friendAddress.asStringUriOnly()}"
|
||||
findMasterNavController().navigate(Uri.parse(deepLink))
|
||||
}
|
||||
|
||||
internal fun DetailCallLogFragment.navigateToChatRoom(args: Bundle?) {
|
||||
if (findNavController().currentDestination?.id == R.id.detailCallLogFragment) {
|
||||
findMasterNavController().navigate(
|
||||
R.id.action_global_masterChatRoomsFragment,
|
||||
args,
|
||||
popupTo(R.id.masterChatRoomsFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DetailCallLogFragment.navigateToDialer(args: Bundle?) {
|
||||
if (findNavController().currentDestination?.id == R.id.detailCallLogFragment) {
|
||||
findMasterNavController().navigate(
|
||||
R.id.action_global_dialerFragment,
|
||||
args,
|
||||
popupTo(R.id.dialerFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DetailCallLogFragment.navigateToEmptyCallHistory() {
|
||||
if (findNavController().currentDestination?.id == R.id.detailCallLogFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_global_emptyFragment,
|
||||
null,
|
||||
popupTo(R.id.emptyCallHistoryFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* Settings related */
|
||||
|
||||
internal fun SettingsFragment.navigateToAccountSettings(identity: String) {
|
||||
if (findNavController().currentDestination?.id == R.id.settingsFragment) {
|
||||
val bundle = bundleOf("Identity" to identity)
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_accountSettingsFragment,
|
||||
bundle,
|
||||
popupTo(R.id.accountSettingsFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun SettingsFragment.navigateToTunnelSettings(slidingPane: SlidingPaneLayout) {
|
||||
if (findNavController().currentDestination?.id == R.id.settingsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_tunnelSettingsFragment,
|
||||
null,
|
||||
popupTo(R.id.tunnelSettingsFragment, true)
|
||||
)
|
||||
if (!slidingPane.isOpen) slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun SettingsFragment.navigateToAudioSettings(slidingPane: SlidingPaneLayout) {
|
||||
if (findNavController().currentDestination?.id == R.id.settingsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_audioSettingsFragment,
|
||||
null,
|
||||
popupTo(R.id.audioSettingsFragment, true)
|
||||
)
|
||||
if (!slidingPane.isOpen) slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun SettingsFragment.navigateToVideoSettings(slidingPane: SlidingPaneLayout) {
|
||||
if (findNavController().currentDestination?.id == R.id.settingsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_videoSettingsFragment,
|
||||
null,
|
||||
popupTo(R.id.videoSettingsFragment, true)
|
||||
)
|
||||
if (!slidingPane.isOpen) slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun SettingsFragment.navigateToCallSettings(slidingPane: SlidingPaneLayout) {
|
||||
if (findNavController().currentDestination?.id == R.id.settingsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_callSettingsFragment,
|
||||
null,
|
||||
popupTo(R.id.callSettingsFragment, true)
|
||||
)
|
||||
if (!slidingPane.isOpen) slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun SettingsFragment.navigateToChatSettings(slidingPane: SlidingPaneLayout) {
|
||||
if (findNavController().currentDestination?.id == R.id.settingsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_chatSettingsFragment,
|
||||
null,
|
||||
popupTo(R.id.chatSettingsFragment, true)
|
||||
)
|
||||
if (!slidingPane.isOpen) slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun SettingsFragment.navigateToNetworkSettings(slidingPane: SlidingPaneLayout) {
|
||||
if (findNavController().currentDestination?.id == R.id.settingsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_networkSettingsFragment,
|
||||
null,
|
||||
popupTo(R.id.networkSettingsFragment, true)
|
||||
)
|
||||
if (!slidingPane.isOpen) slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun SettingsFragment.navigateToContactsSettings(slidingPane: SlidingPaneLayout) {
|
||||
if (findNavController().currentDestination?.id == R.id.settingsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_contactsSettingsFragment,
|
||||
null,
|
||||
popupTo(R.id.contactsSettingsFragment, true)
|
||||
)
|
||||
if (!slidingPane.isOpen) slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun SettingsFragment.navigateToAdvancedSettings(slidingPane: SlidingPaneLayout) {
|
||||
if (findNavController().currentDestination?.id == R.id.settingsFragment) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(
|
||||
R.id.action_global_advancedSettingsFragment,
|
||||
null,
|
||||
popupTo(R.id.advancedSettingsFragment, true)
|
||||
)
|
||||
if (!slidingPane.isOpen) slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun AccountSettingsFragment.navigateToPhoneLinking(args: Bundle?) {
|
||||
if (findNavController().currentDestination?.id == R.id.accountSettingsFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_accountSettingsFragment_to_phoneAccountLinkingFragment,
|
||||
args,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PhoneAccountLinkingFragment.navigateToPhoneAccountValidation(args: Bundle?) {
|
||||
if (findNavController().currentDestination?.id == R.id.phoneAccountLinkingFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_phoneAccountLinkingFragment_to_phoneAccountValidationFragment,
|
||||
args,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun navigateToEmptySetting(navController: NavController) {
|
||||
navController.navigate(
|
||||
R.id.action_global_emptySettingsFragment,
|
||||
null,
|
||||
popupTo(R.id.emptySettingsFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun AccountSettingsFragment.navigateToEmptySetting() {
|
||||
navigateToEmptySetting(findNavController())
|
||||
}
|
||||
|
||||
internal fun AdvancedSettingsFragment.navigateToEmptySetting() {
|
||||
navigateToEmptySetting(findNavController())
|
||||
}
|
||||
|
||||
internal fun AudioSettingsFragment.navigateToEmptySetting() {
|
||||
navigateToEmptySetting(findNavController())
|
||||
}
|
||||
|
||||
internal fun CallSettingsFragment.navigateToEmptySetting() {
|
||||
navigateToEmptySetting(findNavController())
|
||||
}
|
||||
|
||||
internal fun ChatSettingsFragment.navigateToEmptySetting() {
|
||||
navigateToEmptySetting(findNavController())
|
||||
}
|
||||
|
||||
internal fun ContactsSettingsFragment.navigateToEmptySetting() {
|
||||
navigateToEmptySetting(findNavController())
|
||||
}
|
||||
|
||||
internal fun NetworkSettingsFragment.navigateToEmptySetting() {
|
||||
navigateToEmptySetting(findNavController())
|
||||
}
|
||||
|
||||
internal fun TunnelSettingsFragment.navigateToEmptySetting() {
|
||||
navigateToEmptySetting(findNavController())
|
||||
}
|
||||
|
||||
internal fun VideoSettingsFragment.navigateToEmptySetting() {
|
||||
navigateToEmptySetting(findNavController())
|
||||
}
|
||||
|
||||
/* Side menu related */
|
||||
|
||||
internal fun SideMenuFragment.navigateToAccountSettings(identity: String) {
|
||||
val deepLink = "linphone-android://settings/$identity"
|
||||
findNavController().navigate(Uri.parse(deepLink))
|
||||
}
|
||||
|
||||
internal fun SideMenuFragment.navigateToSettings() {
|
||||
findNavController().navigate(
|
||||
R.id.action_global_settingsFragment,
|
||||
null,
|
||||
popupTo(R.id.settingsFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun SideMenuFragment.navigateToAbout() {
|
||||
findNavController().navigate(
|
||||
R.id.action_global_aboutFragment,
|
||||
null,
|
||||
popupTo(R.id.aboutFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
internal fun SideMenuFragment.navigateToRecordings() {
|
||||
findNavController().navigate(
|
||||
R.id.action_global_recordingsFragment,
|
||||
null,
|
||||
popupTo(R.id.recordingsFragment, true)
|
||||
)
|
||||
}
|
||||
|
||||
/* Assistant related */
|
||||
|
||||
internal fun WelcomeFragment.navigateToEmailAccountCreation() {
|
||||
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_welcomeFragment_to_emailAccountCreationFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WelcomeFragment.navigateToPhoneAccountCreation() {
|
||||
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_welcomeFragment_to_phoneAccountCreationFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WelcomeFragment.navigateToAccountLogin() {
|
||||
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_welcomeFragment_to_accountLoginFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WelcomeFragment.navigateToGenericLoginWarning() {
|
||||
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_welcomeFragment_to_genericAccountWarningFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun WelcomeFragment.navigateToRemoteProvisioning() {
|
||||
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_welcomeFragment_to_remoteProvisioningFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun AccountLoginFragment.navigateToEchoCancellerCalibration() {
|
||||
if (findNavController().currentDestination?.id == R.id.accountLoginFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_accountLoginFragment_to_echoCancellerCalibrationFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun AccountLoginFragment.navigateToPhoneAccountValidation(args: Bundle?) {
|
||||
if (findNavController().currentDestination?.id == R.id.accountLoginFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_accountLoginFragment_to_phoneAccountValidationFragment,
|
||||
args,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun GenericAccountWarningFragment.navigateToGenericLogin() {
|
||||
if (findNavController().currentDestination?.id == R.id.genericAccountWarningFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_genericAccountWarningFragment_to_genericAccountLoginFragment,
|
||||
null,
|
||||
popupTo(R.id.welcomeFragment, popUpInclusive = false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun GenericAccountLoginFragment.navigateToEchoCancellerCalibration() {
|
||||
if (findNavController().currentDestination?.id == R.id.genericAccountLoginFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_genericAccountLoginFragment_to_echoCancellerCalibrationFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RemoteProvisioningFragment.navigateToQrCode() {
|
||||
if (findNavController().currentDestination?.id == R.id.remoteProvisioningFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_remoteProvisioningFragment_to_qrCodeFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RemoteProvisioningFragment.navigateToEchoCancellerCalibration() {
|
||||
if (findNavController().currentDestination?.id == R.id.remoteProvisioningFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_remoteProvisioningFragment_to_echoCancellerCalibrationFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun EmailAccountCreationFragment.navigateToEmailAccountValidation() {
|
||||
if (findNavController().currentDestination?.id == R.id.emailAccountCreationFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_emailAccountCreationFragment_to_emailAccountValidationFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun EmailAccountValidationFragment.navigateToAccountLinking(args: Bundle?) {
|
||||
if (findNavController().currentDestination?.id == R.id.emailAccountValidationFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_emailAccountValidationFragment_to_phoneAccountLinkingFragment,
|
||||
args,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PhoneAccountCreationFragment.navigateToPhoneAccountValidation(args: Bundle?) {
|
||||
if (findNavController().currentDestination?.id == R.id.phoneAccountCreationFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_phoneAccountCreationFragment_to_phoneAccountValidationFragment,
|
||||
args,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PhoneAccountValidationFragment.navigateToAccountSettings(args: Bundle?) {
|
||||
if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_phoneAccountValidationFragment_to_accountSettingsFragment,
|
||||
args,
|
||||
popupTo(R.id.accountSettingsFragment, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PhoneAccountValidationFragment.navigateToEchoCancellerCalibration() {
|
||||
if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_phoneAccountValidationFragment_to_echoCancellerCalibrationFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun PhoneAccountLinkingFragment.navigateToEchoCancellerCalibration() {
|
||||
if (findNavController().currentDestination?.id == R.id.phoneAccountLinkingFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_phoneAccountLinkingFragment_to_echoCancellerCalibrationFragment,
|
||||
null,
|
||||
popupTo()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.assistant
|
||||
|
||||
import android.os.Bundle
|
||||
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(resourceId: Int) {
|
||||
Snackbar.make(coordinator, resourceId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(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,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.assistant.fragments
|
||||
|
||||
import android.annotation.TargetApi
|
||||
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.mediastream.Version
|
||||
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
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Version.API23_MARSHMALLOW_60)
|
||||
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,143 +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.mediastream.Version
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragmentBinding>() {
|
||||
override lateinit var viewModel: AccountLoginViewModel
|
||||
private lateinit var sharedViewModel: 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
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
AccountLoginViewModelFactory(sharedViewModel.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.contactsManager.updateLocalContacts()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||
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,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.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()
|
||||
}
|
||||
}
|
||||
|
||||
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 sharedViewModel: 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
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, EmailAccountCreationViewModelFactory(sharedViewModel.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 sharedViewModel: 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
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, EmailAccountValidationViewModelFactory(sharedViewModel.getAccountCreator()))[EmailAccountValidationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
coreContext.contactsManager.updateLocalContacts()
|
||||
|
||||
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,103 +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.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 sharedViewModel: 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
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, GenericLoginViewModelFactory(sharedViewModel.getAccountCreator(true)))[GenericLoginViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
coreContext.contactsManager.updateLocalContacts()
|
||||
|
||||
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,41 +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.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.navigateToGenericLogin
|
||||
import org.linphone.databinding.AssistantGenericAccountWarningFragmentBinding
|
||||
|
||||
class GenericAccountWarningFragment : GenericFragment<AssistantGenericAccountWarningFragmentBinding>() {
|
||||
override fun getLayoutId(): Int = R.layout.assistant_generic_account_warning_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
binding.setUnderstoodClickListener {
|
||||
navigateToGenericLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.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
|
||||
import org.linphone.mediastream.Version
|
||||
|
||||
class PhoneAccountCreationFragment :
|
||||
AbstractPhoneFragment<AssistantPhoneAccountCreationFragmentBinding>() {
|
||||
private lateinit var sharedViewModel: 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
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
PhoneAccountCreationViewModelFactory(sharedViewModel.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)
|
||||
}
|
||||
}
|
||||
|
||||
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +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
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
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
|
||||
import org.linphone.mediastream.Version
|
||||
|
||||
class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountLinkingFragmentBinding>() {
|
||||
private lateinit var sharedViewModel: 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
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
val accountCreator = sharedViewModel.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 (LinphoneApplication.coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,106 +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 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.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.databinding.AssistantPhoneAccountValidationFragmentBinding
|
||||
|
||||
class PhoneAccountValidationFragment : GenericFragment<AssistantPhoneAccountValidationFragmentBinding>() {
|
||||
private lateinit var sharedViewModel: 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
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this, PhoneAccountValidationViewModelFactory(sharedViewModel.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.contactsManager.updateLocalContacts()
|
||||
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
viewModel.isLinking.value == true -> {
|
||||
val args = Bundle()
|
||||
args.putString(
|
||||
"Identity",
|
||||
"sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}"
|
||||
)
|
||||
navigateToAccountSettings(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
viewModel.code.value = clip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +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 sharedViewModel: 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
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this)[QrCodeViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.qrCodeFoundEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { url ->
|
||||
sharedViewModel.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()
|
||||
}
|
||||
|
||||
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,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.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 sharedViewModel: 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
|
||||
|
||||
sharedViewModel = 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 = sharedViewModel.remoteProvisioningUrl.value ?: coreContext.core.provisioningUri
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
sharedViewModel.remoteProvisioningUrl.value = null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,125 +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.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))
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
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))
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
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,239 +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 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() {
|
||||
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() {
|
||||
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,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.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")
|
||||
}
|
||||
}
|
||||
echoCalibrationTerminated.value = Event(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,170 +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 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
|
||||
|
||||
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,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.MediatorLiveData
|
||||
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.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() {
|
||||
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")
|
||||
// The following is required to keep the app alive
|
||||
// and be able to receive calls while in background
|
||||
if (domain.value.orEmpty() != corePreferences.defaultDomain) {
|
||||
Log.i("[Assistant] [Generic Login] Background mode with foreground service automatically enabled")
|
||||
corePreferences.keepServiceAlive = true
|
||||
coreContext.notificationsManager.startForeground()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLoginButtonEnabled(): Boolean {
|
||||
return username.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,170 +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 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.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,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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,208 +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.call
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.window.layout.FoldingFeature
|
||||
import kotlinx.coroutines.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.call.viewmodels.*
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallActivityBinding
|
||||
|
||||
class CallActivity : ProximitySensorActivity() {
|
||||
private lateinit var binding: CallActivityBinding
|
||||
private lateinit var viewModel: ControlsFadingViewModel
|
||||
private lateinit var sharedViewModel: SharedCallViewModel
|
||||
|
||||
private var foldingFeature: FoldingFeature? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Compatibility.setShowWhenLocked(this, true)
|
||||
Compatibility.setTurnScreenOn(this, true)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.call_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
viewModel = ViewModelProvider(this)[ControlsFadingViewModel::class.java]
|
||||
binding.controlsFadingViewModel = viewModel
|
||||
|
||||
sharedViewModel = ViewModelProvider(this)[SharedCallViewModel::class.java]
|
||||
|
||||
sharedViewModel.toggleDrawerEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume {
|
||||
if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) {
|
||||
binding.statsMenu.closeDrawer(binding.sideMenuContent, true)
|
||||
} else {
|
||||
binding.statsMenu.openDrawer(binding.sideMenuContent, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume {
|
||||
viewModel.showMomentarily()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.proximitySensorEnabled.observe(
|
||||
this
|
||||
) {
|
||||
enableProximitySensor(it)
|
||||
}
|
||||
|
||||
viewModel.videoEnabled.observe(
|
||||
this
|
||||
) {
|
||||
updateConstraintSetDependingOnFoldingState()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
|
||||
this.foldingFeature = foldingFeature
|
||||
updateConstraintSetDependingOnFoldingState()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (coreContext.core.callsNb == 0) {
|
||||
Log.w("[Call Activity] Resuming but no call found...")
|
||||
if (isTaskRoot) {
|
||||
// When resuming app from recent tasks make sure MainActivity will be launched if there is no call
|
||||
val intent = Intent()
|
||||
intent.setClass(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
} else {
|
||||
coreContext.removeCallOverlay()
|
||||
}
|
||||
|
||||
if (corePreferences.fullScreenCallUI) {
|
||||
hideSystemUI()
|
||||
window.decorView.setOnSystemUiVisibilityChangeListener { visibility ->
|
||||
if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
|
||||
GlobalScope.launch {
|
||||
delay(2000)
|
||||
withContext(Dispatchers.Main) {
|
||||
hideSystemUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
val core = coreContext.core
|
||||
if (core.callsNb > 0) {
|
||||
coreContext.createCallOverlay()
|
||||
}
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
coreContext.core.nativeVideoWindowId = null
|
||||
coreContext.core.nativePreviewWindowId = null
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
|
||||
if (coreContext.isVideoCallOrConferenceActive()) {
|
||||
Compatibility.enterPipMode(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration
|
||||
) {
|
||||
if (isInPictureInPictureMode) {
|
||||
viewModel.areControlsHidden.value = true
|
||||
}
|
||||
|
||||
if (corePreferences.hideCameraPreviewInPipMode) {
|
||||
viewModel.isVideoPreviewHidden.value = isInPictureInPictureMode
|
||||
} else {
|
||||
viewModel.isVideoPreviewResizedForPip.value = isInPictureInPictureMode
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTheme(): Resources.Theme {
|
||||
val theme = super.getTheme()
|
||||
if (corePreferences.fullScreenCallUI) {
|
||||
theme.applyStyle(R.style.FullScreenTheme, true)
|
||||
}
|
||||
return theme
|
||||
}
|
||||
|
||||
private fun hideSystemUI() {
|
||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
}
|
||||
|
||||
private fun updateConstraintSetDependingOnFoldingState() {
|
||||
val feature = foldingFeature ?: return
|
||||
val constraintLayout = binding.constraintLayout
|
||||
val set = ConstraintSet()
|
||||
set.clone(constraintLayout)
|
||||
|
||||
if (feature.state == FoldingFeature.State.HALF_OPENED && viewModel.videoEnabled.value == true) {
|
||||
set.setGuidelinePercent(R.id.hinge_top, 0.5f)
|
||||
set.setGuidelinePercent(R.id.hinge_bottom, 0.5f)
|
||||
viewModel.disable(true)
|
||||
} else {
|
||||
set.setGuidelinePercent(R.id.hinge_top, 0f)
|
||||
set.setGuidelinePercent(R.id.hinge_bottom, 1f)
|
||||
viewModel.disable(false)
|
||||
}
|
||||
|
||||
set.applyTo(constraintLayout)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,183 +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.call
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.TargetApi
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.call.viewmodels.IncomingCallViewModel
|
||||
import org.linphone.activities.call.viewmodels.IncomingCallViewModelFactory
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallIncomingActivityBinding
|
||||
import org.linphone.mediastream.Version
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class IncomingCallActivity : GenericActivity() {
|
||||
private lateinit var binding: CallIncomingActivityBinding
|
||||
private lateinit var viewModel: IncomingCallViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Compatibility.setShowWhenLocked(this, true)
|
||||
Compatibility.setTurnScreenOn(this, true)
|
||||
// Leaks on API 27+: https://stackoverflow.com/questions/60477120/keyguardmanager-memory-leak
|
||||
Compatibility.requestDismissKeyguard(this)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.call_incoming_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
val incomingCall: Call? = findIncomingCall()
|
||||
if (incomingCall == null) {
|
||||
Log.e("[Incoming Call Activity] Couldn't find call in state Incoming")
|
||||
if (isTaskRoot) {
|
||||
// When resuming app from recent tasks make sure MainActivity will be launched if there is no call
|
||||
val intent = Intent()
|
||||
intent.setClass(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
IncomingCallViewModelFactory(incomingCall)
|
||||
)[IncomingCallViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.callEndedEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume {
|
||||
Log.i("[Incoming Call Activity] Call ended, finish activity")
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.earlyMediaVideoEnabled.observe(
|
||||
this
|
||||
) {
|
||||
if (it) {
|
||||
Log.i("[Incoming Call Activity] Early media video being received, set native window id")
|
||||
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
|
||||
}
|
||||
}
|
||||
|
||||
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
val keyguardLocked = keyguardManager.isKeyguardLocked
|
||||
viewModel.screenLocked.value = keyguardLocked
|
||||
if (keyguardLocked) {
|
||||
// Forbid screen rotation to prevent keyguard to show up above incoming call view
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
}
|
||||
|
||||
binding.buttons.setViewModel(viewModel)
|
||||
|
||||
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val incomingCall: Call? = findIncomingCall()
|
||||
if (incomingCall == null) {
|
||||
Log.e("[Incoming Call Activity] Couldn't find call in state Incoming")
|
||||
if (isTaskRoot) {
|
||||
// When resuming app from recent tasks make sure MainActivity will be launched if there is no call
|
||||
val intent = Intent()
|
||||
intent.setClass(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Version.API23_MARSHMALLOW_60)
|
||||
private fun checkPermissions() {
|
||||
val permissionsRequiredList = arrayListOf<String>()
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
Log.i("[Incoming Call Activity] Asking for RECORD_AUDIO permission")
|
||||
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
|
||||
if (viewModel.call.currentParams.isVideoEnabled && !PermissionHelper.get().hasCameraPermission()) {
|
||||
Log.i("[Incoming Call Activity] Asking for CAMERA permission")
|
||||
permissionsRequiredList.add(Manifest.permission.CAMERA)
|
||||
}
|
||||
|
||||
if (permissionsRequiredList.isNotEmpty()) {
|
||||
val permissionsRequired = arrayOfNulls<String>(permissionsRequiredList.size)
|
||||
permissionsRequiredList.toArray(permissionsRequired)
|
||||
requestPermissions(permissionsRequired, 0)
|
||||
}
|
||||
}
|
||||
|
||||
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("[Incoming Call Activity] RECORD_AUDIO permission has been granted")
|
||||
}
|
||||
Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i("[Incoming Call Activity] CAMERA permission has been granted")
|
||||
coreContext.core.reloadVideoDevices()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
private fun findIncomingCall(): Call? {
|
||||
for (call in coreContext.core.calls) {
|
||||
if (call.state == Call.State.IncomingReceived ||
|
||||
call.state == Call.State.IncomingEarlyMedia
|
||||
) {
|
||||
return call
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,236 +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.call
|
||||
|
||||
import android.Manifest
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import org.linphone.LinphoneApplication
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.call.viewmodels.CallViewModel
|
||||
import org.linphone.activities.call.viewmodels.CallViewModelFactory
|
||||
import org.linphone.activities.call.viewmodels.ControlsViewModel
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallOutgoingActivityBinding
|
||||
import org.linphone.mediastream.Version
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class OutgoingCallActivity : ProximitySensorActivity() {
|
||||
private lateinit var binding: CallOutgoingActivityBinding
|
||||
private lateinit var viewModel: CallViewModel
|
||||
private lateinit var controlsViewModel: ControlsViewModel
|
||||
|
||||
// We have to use lateinit here because we need to compute the screen width first
|
||||
private lateinit var numpadAnimator: ValueAnimator
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.call_outgoing_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
val outgoingCall: Call? = findOutgoingCall()
|
||||
if (outgoingCall == null) {
|
||||
Log.e("[Outgoing Call Activity] Couldn't find call in state Outgoing")
|
||||
if (isTaskRoot) {
|
||||
// When resuming app from recent tasks make sure MainActivity will be launched if there is no call
|
||||
val intent = Intent()
|
||||
intent.setClass(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
CallViewModelFactory(outgoingCall)
|
||||
)[CallViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
controlsViewModel = ViewModelProvider(this)[ControlsViewModel::class.java]
|
||||
binding.controlsViewModel = controlsViewModel
|
||||
|
||||
viewModel.callEndedEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume {
|
||||
Log.i("[Outgoing Call Activity] Call ended, finish activity")
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.callConnectedEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume {
|
||||
Log.i("[Outgoing Call Activity] Call connected, finish activity")
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
controlsViewModel.isSpeakerSelected.observe(
|
||||
this
|
||||
) {
|
||||
enableProximitySensor(!it)
|
||||
}
|
||||
|
||||
controlsViewModel.askAudioRecordPermissionEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume { permission ->
|
||||
requestPermissions(arrayOf(permission), 0)
|
||||
}
|
||||
}
|
||||
|
||||
controlsViewModel.askCameraPermissionEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume { permission ->
|
||||
requestPermissions(arrayOf(permission), 0)
|
||||
}
|
||||
}
|
||||
|
||||
controlsViewModel.toggleNumpadEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume { open ->
|
||||
if (this::numpadAnimator.isInitialized) {
|
||||
if (open) {
|
||||
numpadAnimator.start()
|
||||
} else {
|
||||
numpadAnimator.reverse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
initNumpadLayout()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
numpadAnimator.end()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val outgoingCall: Call? = findOutgoingCall()
|
||||
if (outgoingCall == null) {
|
||||
Log.e("[Outgoing Call Activity] Couldn't find call in state Outgoing")
|
||||
if (isTaskRoot) {
|
||||
// When resuming app from recent tasks make sure MainActivity will be launched if there is no call
|
||||
val intent = Intent()
|
||||
intent.setClass(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Version.API23_MARSHMALLOW_60)
|
||||
private fun checkPermissions() {
|
||||
val permissionsRequiredList = arrayListOf<String>()
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
Log.i("[Outgoing Call Activity] Asking for RECORD_AUDIO permission")
|
||||
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
if (viewModel.call.currentParams.isVideoEnabled && !PermissionHelper.get().hasCameraPermission()) {
|
||||
Log.i("[Outgoing Call Activity] Asking for CAMERA permission")
|
||||
permissionsRequiredList.add(Manifest.permission.CAMERA)
|
||||
}
|
||||
if (permissionsRequiredList.isNotEmpty()) {
|
||||
val permissionsRequired = arrayOfNulls<String>(permissionsRequiredList.size)
|
||||
permissionsRequiredList.toArray(permissionsRequired)
|
||||
requestPermissions(permissionsRequired, 0)
|
||||
}
|
||||
}
|
||||
|
||||
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("[Outgoing Call Activity] RECORD_AUDIO permission has been granted")
|
||||
controlsViewModel.updateMuteMicState()
|
||||
}
|
||||
Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i("[Outgoing Call Activity] CAMERA permission has been granted")
|
||||
coreContext.core.reloadVideoDevices()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
private fun findOutgoingCall(): Call? {
|
||||
for (call in coreContext.core.calls) {
|
||||
if (call.state == Call.State.OutgoingInit ||
|
||||
call.state == Call.State.OutgoingProgress ||
|
||||
call.state == Call.State.OutgoingRinging ||
|
||||
call.state == Call.State.OutgoingEarlyMedia
|
||||
) {
|
||||
return call
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun initNumpadLayout() {
|
||||
val screenWidth = coreContext.screenWidth
|
||||
numpadAnimator = ValueAnimator.ofFloat(screenWidth, 0f).apply {
|
||||
addUpdateListener {
|
||||
val value = it.animatedValue as Float
|
||||
findViewById<FlexboxLayout>(R.id.numpad)?.translationX = -value
|
||||
duration = if (LinphoneApplication.corePreferences.enableAnimations) 500 else 0
|
||||
}
|
||||
}
|
||||
// Hide the numpad here as we can't set the translationX property on include tag in layout
|
||||
if (this::controlsViewModel.isInitialized && controlsViewModel.numpadVisibility.value == false) {
|
||||
findViewById<FlexboxLayout>(R.id.numpad)?.translationX = -screenWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +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.call
|
||||
|
||||
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.activities.GenericActivity
|
||||
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.isVideoCallOrConferenceActive()
|
||||
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,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.call
|
||||
|
||||
import android.content.Context
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.View
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Call
|
||||
|
||||
class VideoZoomHelper(context: Context, private var videoDisplayView: View) : GestureDetector.SimpleOnGestureListener() {
|
||||
private var scaleDetector: ScaleGestureDetector
|
||||
|
||||
private var zoomFactor = 1f
|
||||
private var zoomCenterX = 0f
|
||||
private var zoomCenterY = 0f
|
||||
|
||||
init {
|
||||
val gestureDetector = GestureDetector(context, this)
|
||||
|
||||
scaleDetector = ScaleGestureDetector(
|
||||
context,
|
||||
object :
|
||||
ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
zoomFactor *= detector.scaleFactor
|
||||
// Don't let the object get too small or too large.
|
||||
// Zoom to make the video fill the screen vertically
|
||||
val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4)
|
||||
// Zoom to make the video fill the screen horizontally
|
||||
val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4)
|
||||
zoomFactor = max(0.1f, min(zoomFactor, max(portraitZoomFactor, landscapeZoomFactor)))
|
||||
|
||||
val currentCall: Call? = coreContext.core.currentCall
|
||||
if (currentCall != null) {
|
||||
currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
videoDisplayView.setOnTouchListener { _, event ->
|
||||
val currentZoomFactor = zoomFactor
|
||||
scaleDetector.onTouchEvent(event)
|
||||
|
||||
if (currentZoomFactor != zoomFactor) {
|
||||
// We did scale, prevent touch event from going further
|
||||
return@setOnTouchListener true
|
||||
}
|
||||
|
||||
// If true, gesture detected, prevent touch event from going further
|
||||
// Otherwise it seems we didn't use event,
|
||||
// allow it to be dispatched somewhere else
|
||||
gestureDetector.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
val currentCall: Call? = coreContext.core.currentCall
|
||||
if (currentCall != null) {
|
||||
if (zoomFactor > 1) {
|
||||
// Video is zoomed, slide is used to change center of zoom
|
||||
if (distanceX > 0 && zoomCenterX < 1) {
|
||||
zoomCenterX += 0.01f
|
||||
} else if (distanceX < 0 && zoomCenterX > 0) {
|
||||
zoomCenterX -= 0.01f
|
||||
}
|
||||
|
||||
if (distanceY < 0 && zoomCenterY < 1) {
|
||||
zoomCenterY += 0.01f
|
||||
} else if (distanceY > 0 && zoomCenterY > 0) {
|
||||
zoomCenterY -= 0.01f
|
||||
}
|
||||
|
||||
if (zoomCenterX > 1) zoomCenterX = 1f
|
||||
if (zoomCenterX < 0) zoomCenterX = 0f
|
||||
if (zoomCenterY > 1) zoomCenterY = 1f
|
||||
if (zoomCenterY < 0) zoomCenterY = 0f
|
||||
|
||||
currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
||||
val currentCall: Call? = coreContext.core.currentCall
|
||||
if (currentCall != null) {
|
||||
if (zoomFactor == 1f) {
|
||||
// Zoom to make the video fill the screen vertically
|
||||
val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4)
|
||||
// Zoom to make the video fill the screen horizontally
|
||||
val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4)
|
||||
zoomFactor = max(portraitZoomFactor, landscapeZoomFactor)
|
||||
} else {
|
||||
resetZoom()
|
||||
}
|
||||
|
||||
currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun resetZoom() {
|
||||
zoomFactor = 1f
|
||||
zoomCenterY = 0.5f
|
||||
zoomCenterX = zoomCenterY
|
||||
}
|
||||
}
|
||||
|
|
@ -1,115 +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.call.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.*
|
||||
|
||||
class CallStatisticsData(val call: Call) : GenericContactData(call.remoteAddress) {
|
||||
val audioStats = MutableLiveData<ArrayList<StatItemData>>()
|
||||
|
||||
val videoStats = MutableLiveData<ArrayList<StatItemData>>()
|
||||
|
||||
val isVideoEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isExpanded = MutableLiveData<Boolean>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
|
||||
if (call == this@CallStatisticsData.call) {
|
||||
isVideoEnabled.value = call.currentParams.isVideoEnabled
|
||||
updateCallStats(stats)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
audioStats.value = arrayListOf()
|
||||
videoStats.value = arrayListOf()
|
||||
|
||||
initCallStats()
|
||||
|
||||
val videoEnabled = call.currentParams.isVideoEnabled
|
||||
isVideoEnabled.value = videoEnabled
|
||||
|
||||
isExpanded.value = coreContext.core.currentCall == call
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
coreContext.core.removeListener(listener)
|
||||
super.destroy()
|
||||
}
|
||||
|
||||
fun toggleExpanded() {
|
||||
isExpanded.value = isExpanded.value != true
|
||||
}
|
||||
|
||||
private fun initCallStats() {
|
||||
val audioList = arrayListOf<StatItemData>()
|
||||
audioList.add(StatItemData(StatType.CAPTURE))
|
||||
audioList.add(StatItemData(StatType.PLAYBACK))
|
||||
audioList.add(StatItemData(StatType.PAYLOAD))
|
||||
audioList.add(StatItemData(StatType.ENCODER))
|
||||
audioList.add(StatItemData(StatType.DECODER))
|
||||
audioList.add(StatItemData(StatType.DOWNLOAD_BW))
|
||||
audioList.add(StatItemData(StatType.UPLOAD_BW))
|
||||
audioList.add(StatItemData(StatType.ICE))
|
||||
audioList.add(StatItemData(StatType.IP_FAM))
|
||||
audioList.add(StatItemData(StatType.SENDER_LOSS))
|
||||
audioList.add(StatItemData(StatType.RECEIVER_LOSS))
|
||||
audioList.add(StatItemData(StatType.JITTER))
|
||||
audioStats.value = audioList
|
||||
|
||||
val videoList = arrayListOf<StatItemData>()
|
||||
videoList.add(StatItemData(StatType.CAPTURE))
|
||||
videoList.add(StatItemData(StatType.PLAYBACK))
|
||||
videoList.add(StatItemData(StatType.PAYLOAD))
|
||||
videoList.add(StatItemData(StatType.ENCODER))
|
||||
videoList.add(StatItemData(StatType.DECODER))
|
||||
videoList.add(StatItemData(StatType.DOWNLOAD_BW))
|
||||
videoList.add(StatItemData(StatType.UPLOAD_BW))
|
||||
videoList.add(StatItemData(StatType.ESTIMATED_AVAILABLE_DOWNLOAD_BW))
|
||||
videoList.add(StatItemData(StatType.ICE))
|
||||
videoList.add(StatItemData(StatType.IP_FAM))
|
||||
videoList.add(StatItemData(StatType.SENDER_LOSS))
|
||||
videoList.add(StatItemData(StatType.RECEIVER_LOSS))
|
||||
videoList.add(StatItemData(StatType.SENT_RESOLUTION))
|
||||
videoList.add(StatItemData(StatType.RECEIVED_RESOLUTION))
|
||||
videoList.add(StatItemData(StatType.SENT_FPS))
|
||||
videoList.add(StatItemData(StatType.RECEIVED_FPS))
|
||||
videoStats.value = videoList
|
||||
}
|
||||
|
||||
private fun updateCallStats(stats: CallStats) {
|
||||
if (stats.type == StreamType.Audio) {
|
||||
for (stat in audioStats.value.orEmpty()) {
|
||||
stat.update(call, stats)
|
||||
}
|
||||
} else if (stats.type == StreamType.Video) {
|
||||
for (stat in videoStats.value.orEmpty()) {
|
||||
stat.update(call, stats)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +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.call.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.Conference
|
||||
import org.linphone.core.Participant
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class ConferenceParticipantData(
|
||||
private val conference: Conference,
|
||||
val participant: Participant
|
||||
) :
|
||||
GenericContactData(participant.address) {
|
||||
private val isAdmin = MutableLiveData<Boolean>()
|
||||
val isMeAdmin = MutableLiveData<Boolean>()
|
||||
|
||||
init {
|
||||
isAdmin.value = participant.isAdmin
|
||||
isMeAdmin.value = conference.me.isAdmin
|
||||
Log.i("[Conference Participant VM] Participant ${participant.address.asStringUriOnly()} is ${if (participant.isAdmin) "admin" else "not admin"}")
|
||||
Log.i("[Conference Participant VM] Me is ${if (conference.me.isAdmin) "admin" else "not admin"} and is ${if (conference.me.isFocus) "focus" else "not focus"}")
|
||||
}
|
||||
|
||||
fun removeFromConference() {
|
||||
Log.i("[Conference Participant VM] Removing participant ${participant.address.asStringUriOnly()} from conference $conference")
|
||||
conference.removeParticipant(participant)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +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.call.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.text.DecimalFormat
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AddressFamily
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.CallStats
|
||||
import org.linphone.core.StreamType
|
||||
|
||||
enum class StatType(val nameResource: Int) {
|
||||
CAPTURE(R.string.call_stats_capture_filter),
|
||||
PLAYBACK(R.string.call_stats_player_filter),
|
||||
PAYLOAD(R.string.call_stats_codec),
|
||||
ENCODER(R.string.call_stats_encoder_name),
|
||||
DECODER(R.string.call_stats_decoder_name),
|
||||
DOWNLOAD_BW(R.string.call_stats_download),
|
||||
UPLOAD_BW(R.string.call_stats_upload),
|
||||
ICE(R.string.call_stats_ice),
|
||||
IP_FAM(R.string.call_stats_ip),
|
||||
SENDER_LOSS(R.string.call_stats_sender_loss_rate),
|
||||
RECEIVER_LOSS(R.string.call_stats_receiver_loss_rate),
|
||||
JITTER(R.string.call_stats_jitter_buffer),
|
||||
SENT_RESOLUTION(R.string.call_stats_video_resolution_sent),
|
||||
RECEIVED_RESOLUTION(R.string.call_stats_video_resolution_received),
|
||||
SENT_FPS(R.string.call_stats_video_fps_sent),
|
||||
RECEIVED_FPS(R.string.call_stats_video_fps_received),
|
||||
ESTIMATED_AVAILABLE_DOWNLOAD_BW(R.string.call_stats_estimated_download)
|
||||
}
|
||||
|
||||
class StatItemData(val type: StatType) {
|
||||
val value = MutableLiveData<String>()
|
||||
|
||||
fun update(call: Call, stats: CallStats) {
|
||||
val payloadType = if (stats.type == StreamType.Audio) call.currentParams.usedAudioPayloadType else call.currentParams.usedVideoPayloadType
|
||||
payloadType ?: return
|
||||
value.value = when (type) {
|
||||
StatType.CAPTURE -> if (stats.type == StreamType.Audio) call.core.captureDevice else call.core.videoDevice
|
||||
StatType.PLAYBACK -> if (stats.type == StreamType.Audio) call.core.playbackDevice else call.core.videoDisplayFilter
|
||||
StatType.PAYLOAD -> "${payloadType.mimeType}/${payloadType.clockRate / 1000} kHz"
|
||||
StatType.ENCODER -> call.core.mediastreamerFactory.getDecoderText(payloadType.mimeType)
|
||||
StatType.DECODER -> call.core.mediastreamerFactory.getEncoderText(payloadType.mimeType)
|
||||
StatType.DOWNLOAD_BW -> "${stats.downloadBandwidth} kbits/s"
|
||||
StatType.UPLOAD_BW -> "${stats.uploadBandwidth} kbits/s"
|
||||
StatType.ICE -> stats.iceState.toString()
|
||||
StatType.IP_FAM -> if (stats.ipFamilyOfRemote == AddressFamily.Inet6) "IPv6" else "IPv4"
|
||||
StatType.SENDER_LOSS -> DecimalFormat("##.##%").format(stats.senderLossRate)
|
||||
StatType.RECEIVER_LOSS -> DecimalFormat("##.##%").format(stats.receiverLossRate)
|
||||
StatType.JITTER -> DecimalFormat("##.## ms").format(stats.jitterBufferSizeMs)
|
||||
StatType.SENT_RESOLUTION -> call.currentParams.sentVideoDefinition?.name
|
||||
StatType.RECEIVED_RESOLUTION -> call.currentParams.receivedVideoDefinition?.name
|
||||
StatType.SENT_FPS -> "${call.currentParams.sentFramerate}"
|
||||
StatType.RECEIVED_FPS -> "${call.currentParams.receivedFramerate}"
|
||||
StatType.ESTIMATED_AVAILABLE_DOWNLOAD_BW -> "${stats.estimatedDownloadBandwidth} kbit/s"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,323 +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.call.fragments
|
||||
|
||||
import android.Manifest
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
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.call.viewmodels.CallsViewModel
|
||||
import org.linphone.activities.call.viewmodels.ConferenceViewModel
|
||||
import org.linphone.activities.call.viewmodels.ControlsViewModel
|
||||
import org.linphone.activities.call.viewmodels.SharedCallViewModel
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallControlsFragmentBinding
|
||||
import org.linphone.mediastream.Version
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.DialogUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
|
||||
private lateinit var callsViewModel: CallsViewModel
|
||||
private lateinit var controlsViewModel: ControlsViewModel
|
||||
private lateinit var conferenceViewModel: ConferenceViewModel
|
||||
private lateinit var sharedViewModel: SharedCallViewModel
|
||||
|
||||
private var dialog: Dialog? = null
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.call_controls_fragment
|
||||
|
||||
// We have to use lateinit here because we need to compute the screen width first
|
||||
private lateinit var numpadAnimator: ValueAnimator
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
useMaterialSharedAxisXForwardAnimation = false
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedCallViewModel::class.java]
|
||||
}
|
||||
|
||||
callsViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CallsViewModel::class.java]
|
||||
}
|
||||
binding.viewModel = callsViewModel
|
||||
|
||||
controlsViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[ControlsViewModel::class.java]
|
||||
}
|
||||
binding.controlsViewModel = controlsViewModel
|
||||
|
||||
conferenceViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[ConferenceViewModel::class.java]
|
||||
}
|
||||
binding.conferenceViewModel = conferenceViewModel
|
||||
|
||||
callsViewModel.currentCallViewModel.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
if (it != null) {
|
||||
binding.activeCallTimer.base =
|
||||
SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds
|
||||
binding.activeCallTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
callsViewModel.noMoreCallEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
|
||||
callsViewModel.askWriteExternalStoragePermissionEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
||||
Log.i("[Controls Fragment] Asking for WRITE_EXTERNAL_STORAGE permission")
|
||||
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callsViewModel.callUpdateEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { call ->
|
||||
if (call.state == Call.State.StreamsRunning) {
|
||||
dialog?.dismiss()
|
||||
} else if (call.state == Call.State.UpdatedByRemote) {
|
||||
if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) {
|
||||
if (call.currentParams.isVideoEnabled != call.remoteParams?.isVideoEnabled) {
|
||||
showCallVideoUpdateDialog(call)
|
||||
}
|
||||
} else {
|
||||
Log.w("[Controls Fragment] Video display & capture are disabled, don't show video dialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
controlsViewModel.chatClickedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val intent = Intent()
|
||||
intent.setClass(requireContext(), MainActivity::class.java)
|
||||
intent.putExtra("Chat", true)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
controlsViewModel.addCallClickedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val intent = Intent()
|
||||
intent.setClass(requireContext(), MainActivity::class.java)
|
||||
intent.putExtra("Dialer", true)
|
||||
intent.putExtra("Transfer", false)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
controlsViewModel.transferCallClickedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val intent = Intent()
|
||||
intent.setClass(requireContext(), MainActivity::class.java)
|
||||
intent.putExtra("Dialer", true)
|
||||
intent.putExtra("Transfer", true)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
controlsViewModel.askAudioRecordPermissionEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { permission ->
|
||||
Log.i("[Controls Fragment] Asking for $permission permission")
|
||||
requestPermissions(arrayOf(permission), 0)
|
||||
}
|
||||
}
|
||||
|
||||
controlsViewModel.askCameraPermissionEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { permission ->
|
||||
Log.i("[Controls Fragment] Asking for $permission permission")
|
||||
requestPermissions(arrayOf(permission), 1)
|
||||
}
|
||||
}
|
||||
|
||||
controlsViewModel.toggleNumpadEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { open ->
|
||||
if (this::numpadAnimator.isInitialized) {
|
||||
if (open) {
|
||||
numpadAnimator.start()
|
||||
} else {
|
||||
numpadAnimator.reverse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
controlsViewModel.somethingClickedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
|
||||
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
onBackPressedCallback.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
initNumpadLayout()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
numpadAnimator.end()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
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] == PERMISSION_GRANTED) {
|
||||
Log.i("[Controls Fragment] RECORD_AUDIO permission has been granted")
|
||||
controlsViewModel.updateMuteMicState()
|
||||
}
|
||||
Manifest.permission.CAMERA -> if (grantResults[i] == PERMISSION_GRANTED) {
|
||||
Log.i("[Controls Fragment] CAMERA permission has been granted")
|
||||
coreContext.core.reloadVideoDevices()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (requestCode == 1) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
|
||||
Log.i("[Controls Fragment] CAMERA permission has been granted")
|
||||
coreContext.core.reloadVideoDevices()
|
||||
controlsViewModel.toggleVideo()
|
||||
}
|
||||
} else if (requestCode == 2 && grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
|
||||
callsViewModel.takeScreenshot()
|
||||
}
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
@TargetApi(Version.API23_MARSHMALLOW_60)
|
||||
private fun checkPermissions() {
|
||||
val permissionsRequiredList = arrayListOf<String>()
|
||||
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
Log.i("[Controls Fragment] Asking for RECORD_AUDIO permission")
|
||||
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
|
||||
if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) {
|
||||
Log.i("[Controls Fragment] Asking for CAMERA permission")
|
||||
permissionsRequiredList.add(Manifest.permission.CAMERA)
|
||||
}
|
||||
|
||||
if (permissionsRequiredList.isNotEmpty()) {
|
||||
val permissionsRequired = arrayOfNulls<String>(permissionsRequiredList.size)
|
||||
permissionsRequiredList.toArray(permissionsRequired)
|
||||
requestPermissions(permissionsRequired, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCallVideoUpdateDialog(call: Call) {
|
||||
val viewModel = DialogViewModel(AppUtils.getString(R.string.call_video_update_requested_dialog))
|
||||
dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||
|
||||
viewModel.showCancelButton(
|
||||
{
|
||||
callsViewModel.answerCallVideoUpdateRequest(call, false)
|
||||
dialog?.dismiss()
|
||||
},
|
||||
getString(R.string.dialog_decline)
|
||||
)
|
||||
|
||||
viewModel.showOkButton(
|
||||
{
|
||||
callsViewModel.answerCallVideoUpdateRequest(call, true)
|
||||
dialog?.dismiss()
|
||||
},
|
||||
getString(R.string.dialog_accept)
|
||||
)
|
||||
|
||||
dialog?.show()
|
||||
}
|
||||
|
||||
private fun initNumpadLayout() {
|
||||
val screenWidth = coreContext.screenWidth
|
||||
numpadAnimator = ValueAnimator.ofFloat(screenWidth, 0f).apply {
|
||||
addUpdateListener {
|
||||
val value = it.animatedValue as Float
|
||||
view?.findViewById<FlexboxLayout>(R.id.numpad)?.translationX = -value
|
||||
duration = if (corePreferences.enableAnimations) 500 else 0
|
||||
}
|
||||
}
|
||||
// Hide the numpad here as we can't set the translationX property on include tag in layout
|
||||
if (controlsViewModel.numpadVisibility.value == false) {
|
||||
view?.findViewById<FlexboxLayout>(R.id.numpad)?.translationX = -screenWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.call.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.call.viewmodels.StatisticsListViewModel
|
||||
import org.linphone.databinding.CallStatisticsFragmentBinding
|
||||
|
||||
class StatisticsFragment : GenericFragment<CallStatisticsFragmentBinding>() {
|
||||
private lateinit var viewModel: StatisticsListViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.call_statistics_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
useMaterialSharedAxisXForwardAnimation = false
|
||||
|
||||
viewModel = ViewModelProvider(this)[StatisticsListViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
onBackPressedCallback.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,146 +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.call.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import java.util.*
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.call.viewmodels.SharedCallViewModel
|
||||
import org.linphone.activities.call.viewmodels.StatusViewModel
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallStatusFragmentBinding
|
||||
import org.linphone.utils.DialogUtils
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class StatusFragment : GenericFragment<CallStatusFragmentBinding>() {
|
||||
private lateinit var viewModel: StatusViewModel
|
||||
private lateinit var sharedViewModel: SharedCallViewModel
|
||||
private var zrtpDialog: Dialog? = null
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.call_status_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
useMaterialSharedAxisXForwardAnimation = false
|
||||
|
||||
viewModel = ViewModelProvider(this)[StatusViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedCallViewModel::class.java]
|
||||
}
|
||||
|
||||
binding.setStatsClickListener {
|
||||
sharedViewModel.toggleDrawerEvent.value = Event(true)
|
||||
}
|
||||
|
||||
binding.setRefreshClickListener {
|
||||
viewModel.refreshRegister()
|
||||
}
|
||||
|
||||
viewModel.showZrtpDialogEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { call ->
|
||||
if (call.state == Call.State.Connected || call.state == Call.State.StreamsRunning) {
|
||||
showZrtpDialog(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
onBackPressedCallback.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (zrtpDialog != null) {
|
||||
zrtpDialog?.dismiss()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun showZrtpDialog(call: Call) {
|
||||
if (zrtpDialog != null && zrtpDialog?.isShowing == true) {
|
||||
Log.e("[Status Fragment] ZRTP dialog already visible")
|
||||
return
|
||||
}
|
||||
|
||||
val token = call.authenticationToken
|
||||
if (token == null || token.length < 4) {
|
||||
Log.e("[Status Fragment] ZRTP token is invalid: $token")
|
||||
return
|
||||
}
|
||||
|
||||
val toRead: String
|
||||
val toListen: String
|
||||
when (call.dir) {
|
||||
Call.Dir.Incoming -> {
|
||||
toRead = token.substring(0, 2)
|
||||
toListen = token.substring(2)
|
||||
}
|
||||
else -> {
|
||||
toRead = token.substring(2)
|
||||
toListen = token.substring(0, 2)
|
||||
}
|
||||
}
|
||||
|
||||
val viewModel = DialogViewModel(getString(R.string.zrtp_dialog_message), getString(R.string.zrtp_dialog_title))
|
||||
viewModel.showZrtp = true
|
||||
viewModel.zrtpReadSas = toRead.uppercase(Locale.getDefault())
|
||||
viewModel.zrtpListenSas = toListen.uppercase(Locale.getDefault())
|
||||
viewModel.showIcon = true
|
||||
viewModel.iconResource = R.drawable.security_2_indicator
|
||||
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||
|
||||
viewModel.showDeleteButton(
|
||||
{
|
||||
call.authenticationTokenVerified = false
|
||||
this@StatusFragment.viewModel.updateEncryptionInfo(call)
|
||||
dialog.dismiss()
|
||||
zrtpDialog = null
|
||||
},
|
||||
getString(R.string.zrtp_dialog_deny_button_label)
|
||||
)
|
||||
|
||||
viewModel.showOkButton(
|
||||
{
|
||||
call.authenticationTokenVerified = true
|
||||
this@StatusFragment.viewModel.updateEncryptionInfo(call)
|
||||
dialog.dismiss()
|
||||
zrtpDialog = null
|
||||
},
|
||||
getString(R.string.zrtp_dialog_ok_button_label)
|
||||
)
|
||||
|
||||
zrtpDialog = dialog
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +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.call.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
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.call.VideoZoomHelper
|
||||
import org.linphone.activities.call.viewmodels.CallsViewModel
|
||||
import org.linphone.activities.call.viewmodels.ConferenceViewModel
|
||||
import org.linphone.activities.call.viewmodels.ControlsFadingViewModel
|
||||
import org.linphone.databinding.CallVideoFragmentBinding
|
||||
|
||||
class VideoRenderingFragment : GenericFragment<CallVideoFragmentBinding>() {
|
||||
private lateinit var controlsFadingViewModel: ControlsFadingViewModel
|
||||
private lateinit var callsViewModel: CallsViewModel
|
||||
private lateinit var conferenceViewModel: ConferenceViewModel
|
||||
|
||||
private var previewX: Float = 0f
|
||||
private var previewY: Float = 0f
|
||||
private lateinit var videoZoomHelper: VideoZoomHelper
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.call_video_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
controlsFadingViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[ControlsFadingViewModel::class.java]
|
||||
}
|
||||
binding.controlsFadingViewModel = controlsFadingViewModel
|
||||
|
||||
callsViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[CallsViewModel::class.java]
|
||||
}
|
||||
|
||||
conferenceViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[ConferenceViewModel::class.java]
|
||||
}
|
||||
binding.conferenceViewModel = conferenceViewModel
|
||||
|
||||
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
|
||||
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
|
||||
|
||||
binding.setPreviewTouchListener { v, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
previewX = v.x - event.rawX
|
||||
previewY = v.y - event.rawY
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
v.animate()
|
||||
.x(event.rawX + previewX)
|
||||
.y(event.rawY + previewY)
|
||||
.setDuration(0)
|
||||
.start()
|
||||
}
|
||||
else -> {
|
||||
v.performClick()
|
||||
false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
videoZoomHelper = VideoZoomHelper(requireContext(), binding.remoteVideoSurface)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,166 +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.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.contact.GenericContactViewModel
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.CallListenerStub
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class CallViewModelFactory(private val call: Call) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return CallViewModel(call) as T
|
||||
}
|
||||
}
|
||||
|
||||
open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAddress) {
|
||||
val address: String by lazy {
|
||||
LinphoneUtils.getDisplayableAddress(call.remoteAddress)
|
||||
}
|
||||
|
||||
val isPaused = MutableLiveData<Boolean>()
|
||||
|
||||
val isOutgoingEarlyMedia = MutableLiveData<Boolean>()
|
||||
|
||||
val callEndedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val callConnectedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private var timer: Timer? = null
|
||||
|
||||
private val listener = object : CallListenerStub() {
|
||||
override fun onStateChanged(call: Call, state: Call.State, message: String) {
|
||||
if (call != this@CallViewModel.call) return
|
||||
|
||||
isPaused.value = state == Call.State.Paused
|
||||
isOutgoingEarlyMedia.value = state == Call.State.OutgoingEarlyMedia
|
||||
|
||||
if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
|
||||
timer?.cancel()
|
||||
callEndedEvent.value = Event(true)
|
||||
|
||||
if (state == Call.State.Error) {
|
||||
Log.e("[Call View Model] Error state reason is ${call.reason}")
|
||||
}
|
||||
} else if (call.state == Call.State.Connected) {
|
||||
callConnectedEvent.value = Event(true)
|
||||
} else if (call.state == Call.State.StreamsRunning) {
|
||||
// Stop call update timer once user has accepted or declined call update
|
||||
timer?.cancel()
|
||||
} else if (call.state == Call.State.UpdatedByRemote) {
|
||||
// User has 30 secs to accept or decline call update
|
||||
// Dialog to accept or decline is handled by CallsViewModel & ControlsFragment
|
||||
startTimer(call)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSnapshotTaken(call: Call, filePath: String) {
|
||||
Log.i("[Call View Model] Snapshot taken, saved at $filePath")
|
||||
val content = Factory.instance().createContent()
|
||||
content.filePath = filePath
|
||||
content.type = "image"
|
||||
content.subtype = "jpeg"
|
||||
content.name = filePath.substring(filePath.indexOf("/") + 1)
|
||||
|
||||
viewModelScope.launch {
|
||||
if (Compatibility.addImageToMediaStore(coreContext.context, content)) {
|
||||
Log.i("[Call View Model] Adding snapshot ${content.name} to Media Store terminated")
|
||||
} else {
|
||||
Log.e("[Call View Model] Something went wrong while copying file to Media Store...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
call.addListener(listener)
|
||||
|
||||
isPaused.value = call.state == Call.State.Paused
|
||||
isOutgoingEarlyMedia.value = call.state == Call.State.OutgoingEarlyMedia
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
destroy()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
call.removeListener(listener)
|
||||
}
|
||||
|
||||
fun terminateCall() {
|
||||
coreContext.terminateCall(call)
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
call.pause()
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
call.resume()
|
||||
}
|
||||
|
||||
fun takeScreenshot() {
|
||||
if (call.currentParams.isVideoEnabled) {
|
||||
val fileName = System.currentTimeMillis().toString() + ".jpeg"
|
||||
call.takeVideoSnapshot(FileUtils.getFileStoragePath(fileName).absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTimer(call: Call) {
|
||||
timer?.cancel()
|
||||
|
||||
timer = Timer("Call update timeout")
|
||||
timer?.schedule(
|
||||
object : TimerTask() {
|
||||
override fun run() {
|
||||
// Decline call update
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
coreContext.answerCallVideoUpdateRequest(call, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
30000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,165 +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.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class CallsViewModel : ViewModel() {
|
||||
val currentCallViewModel = MutableLiveData<CallViewModel>()
|
||||
|
||||
val noActiveCall = MutableLiveData<Boolean>()
|
||||
|
||||
val callPausedByRemote = MutableLiveData<Boolean>()
|
||||
|
||||
val pausedCalls = MutableLiveData<ArrayList<CallViewModel>>()
|
||||
|
||||
val noMoreCallEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val callUpdateEvent: MutableLiveData<Event<Call>> by lazy {
|
||||
MutableLiveData<Event<Call>>()
|
||||
}
|
||||
|
||||
val askWriteExternalStoragePermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onCallStateChanged(core: Core, call: Call, state: Call.State, message: String) {
|
||||
Log.i("[Calls VM] Call state changed: $state")
|
||||
callPausedByRemote.value = (state == Call.State.PausedByRemote) and (call.conference == null)
|
||||
|
||||
val currentCall = core.currentCall
|
||||
noActiveCall.value = currentCall == null
|
||||
if (currentCall == null) {
|
||||
currentCallViewModel.value?.destroy()
|
||||
} else if (currentCallViewModel.value?.call != currentCall) {
|
||||
val viewModel = CallViewModel(currentCall)
|
||||
currentCallViewModel.value = viewModel
|
||||
}
|
||||
|
||||
if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
|
||||
if (core.callsNb == 0) {
|
||||
noMoreCallEvent.value = Event(true)
|
||||
} else {
|
||||
removeCallFromPausedListIfPresent(call)
|
||||
}
|
||||
} else if (state == Call.State.Paused) {
|
||||
addCallToPausedList(call)
|
||||
} else if (state == Call.State.Resuming) {
|
||||
removeCallFromPausedListIfPresent(call)
|
||||
} else if (call.state == Call.State.UpdatedByRemote) {
|
||||
// If the correspondent asks to turn on video while audio call,
|
||||
// defer update until user has chosen whether to accept it or not
|
||||
val remoteVideo = call.remoteParams?.isVideoEnabled ?: false
|
||||
val localVideo = call.currentParams.isVideoEnabled
|
||||
val autoAccept = call.core.videoActivationPolicy.automaticallyAccept
|
||||
if (remoteVideo && !localVideo && !autoAccept) {
|
||||
if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) {
|
||||
call.deferUpdate()
|
||||
callUpdateEvent.value = Event(call)
|
||||
} else {
|
||||
coreContext.answerCallVideoUpdateRequest(call, false)
|
||||
}
|
||||
}
|
||||
} else if (state == Call.State.StreamsRunning) {
|
||||
callUpdateEvent.value = Event(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
val currentCall = coreContext.core.currentCall
|
||||
noActiveCall.value = currentCall == null
|
||||
if (currentCall != null) {
|
||||
currentCallViewModel.value?.destroy()
|
||||
|
||||
val viewModel = CallViewModel(currentCall)
|
||||
currentCallViewModel.value = viewModel
|
||||
}
|
||||
|
||||
callPausedByRemote.value = currentCall?.state == Call.State.PausedByRemote
|
||||
|
||||
for (call in coreContext.core.calls) {
|
||||
if (call.state == Call.State.Paused || call.state == Call.State.Pausing) {
|
||||
addCallToPausedList(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun answerCallVideoUpdateRequest(call: Call, accept: Boolean) {
|
||||
coreContext.answerCallVideoUpdateRequest(call, accept)
|
||||
}
|
||||
|
||||
fun takeScreenshot() {
|
||||
if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
||||
askWriteExternalStoragePermissionEvent.value = Event(true)
|
||||
} else {
|
||||
currentCallViewModel.value?.takeScreenshot()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCallToPausedList(call: Call) {
|
||||
if (call.conference != null) return // Conference will be displayed as paused, no need to display the call as well
|
||||
|
||||
val list = arrayListOf<CallViewModel>()
|
||||
list.addAll(pausedCalls.value.orEmpty())
|
||||
|
||||
for (pausedCallViewModel in list) {
|
||||
if (pausedCallViewModel.call == call) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val viewModel = CallViewModel(call)
|
||||
list.add(viewModel)
|
||||
pausedCalls.value = list
|
||||
}
|
||||
|
||||
private fun removeCallFromPausedListIfPresent(call: Call) {
|
||||
val list = arrayListOf<CallViewModel>()
|
||||
list.addAll(pausedCalls.value.orEmpty())
|
||||
|
||||
for (pausedCallViewModel in list) {
|
||||
if (pausedCallViewModel.call == call) {
|
||||
pausedCallViewModel.destroy()
|
||||
list.remove(pausedCallViewModel)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pausedCalls.value = list
|
||||
}
|
||||
}
|
||||
|
|
@ -1,157 +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.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.activities.call.data.ConferenceParticipantData
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class ConferenceViewModel : ViewModel() {
|
||||
val isConferencePaused = MutableLiveData<Boolean>()
|
||||
|
||||
val isMeConferenceFocus = MutableLiveData<Boolean>()
|
||||
|
||||
val conferenceAddress = MutableLiveData<Address>()
|
||||
|
||||
val conferenceParticipants = MutableLiveData<List<ConferenceParticipantData>>()
|
||||
|
||||
val isInConference = MutableLiveData<Boolean>()
|
||||
|
||||
private val conferenceListener = object : ConferenceListenerStub() {
|
||||
override fun onParticipantAdded(conference: Conference, participant: Participant) {
|
||||
if (conference.isMe(participant.address)) {
|
||||
Log.i("[Conference VM] Entered conference")
|
||||
isConferencePaused.value = false
|
||||
} else {
|
||||
Log.i("[Conference VM] Participant added")
|
||||
updateParticipantsList(conference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(conference: Conference, participant: Participant) {
|
||||
if (conference.isMe(participant.address)) {
|
||||
Log.i("[Conference VM] Left conference")
|
||||
isConferencePaused.value = true
|
||||
} else {
|
||||
Log.i("[Conference VM] Participant removed")
|
||||
updateParticipantsList(conference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onParticipantAdminStatusChanged(
|
||||
conference: Conference,
|
||||
participant: Participant
|
||||
) {
|
||||
Log.i("[Conference VM] Participant admin status changed")
|
||||
updateParticipantsList(conference)
|
||||
}
|
||||
}
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onConferenceStateChanged(
|
||||
core: Core,
|
||||
conference: Conference,
|
||||
state: Conference.State
|
||||
) {
|
||||
Log.i("[Conference VM] Conference state changed: $state")
|
||||
isConferencePaused.value = !conference.isIn
|
||||
|
||||
if (state == Conference.State.Instantiated) {
|
||||
conference.addListener(conferenceListener)
|
||||
} else if (state == Conference.State.Created) {
|
||||
updateParticipantsList(conference)
|
||||
isMeConferenceFocus.value = conference.me.isFocus
|
||||
conferenceAddress.value = conference.conferenceAddress
|
||||
} else if (state == Conference.State.Terminated || state == Conference.State.TerminationFailed) {
|
||||
isInConference.value = false
|
||||
conference.removeListener(conferenceListener)
|
||||
conferenceParticipants.value = arrayListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
isConferencePaused.value = coreContext.core.conference?.isIn != true
|
||||
isMeConferenceFocus.value = false
|
||||
conferenceParticipants.value = arrayListOf()
|
||||
isInConference.value = false
|
||||
|
||||
val conference = coreContext.core.conference
|
||||
if (conference != null) {
|
||||
conference.addListener(conferenceListener)
|
||||
isMeConferenceFocus.value = conference.me.isFocus
|
||||
updateParticipantsList(conference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun pauseConference() {
|
||||
val defaultProxyConfig = coreContext.core.defaultProxyConfig
|
||||
val localAddress = defaultProxyConfig?.identityAddress
|
||||
val participants = arrayOf<Address>()
|
||||
val remoteConference = coreContext.core.searchConference(null, localAddress, conferenceAddress.value, participants)
|
||||
val localConference = coreContext.core.searchConference(null, conferenceAddress.value, conferenceAddress.value, participants)
|
||||
val conference = remoteConference ?: localConference
|
||||
|
||||
if (conference != null) {
|
||||
Log.i("[Conference VM] Leaving conference with address ${conferenceAddress.value?.asStringUriOnly()} temporarily")
|
||||
conference.leave()
|
||||
} else {
|
||||
Log.w("[Conference VM] Unable to find conference with address ${conferenceAddress.value?.asStringUriOnly()}")
|
||||
}
|
||||
}
|
||||
|
||||
fun resumeConference() {
|
||||
val defaultProxyConfig = coreContext.core.defaultProxyConfig
|
||||
val localAddress = defaultProxyConfig?.identityAddress
|
||||
val participants = arrayOf<Address>()
|
||||
val remoteConference = coreContext.core.searchConference(null, localAddress, conferenceAddress.value, participants)
|
||||
val localConference = coreContext.core.searchConference(null, conferenceAddress.value, conferenceAddress.value, participants)
|
||||
val conference = remoteConference ?: localConference
|
||||
|
||||
if (conference != null) {
|
||||
Log.i("[Conference VM] Entering again conference with address ${conferenceAddress.value?.asStringUriOnly()}")
|
||||
conference.enter()
|
||||
} else {
|
||||
Log.w("[Conference VM] Unable to find conference with address ${conferenceAddress.value?.asStringUriOnly()}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateParticipantsList(conference: Conference) {
|
||||
val participants = arrayListOf<ConferenceParticipantData>()
|
||||
for (participant in conference.participantList) {
|
||||
Log.i("[Conference VM] Participant found: ${participant.address.asStringUriOnly()}")
|
||||
val viewModel = ConferenceParticipantData(conference, participant)
|
||||
participants.add(viewModel)
|
||||
}
|
||||
conferenceParticipants.value = participants
|
||||
isInConference.value = participants.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,154 +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.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.AudioDevice
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class ControlsFadingViewModel : ViewModel() {
|
||||
val areControlsHidden = MutableLiveData<Boolean>()
|
||||
|
||||
val isVideoPreviewHidden = MutableLiveData<Boolean>()
|
||||
val isVideoPreviewResizedForPip = MutableLiveData<Boolean>()
|
||||
|
||||
val videoEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val proximitySensorEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
private val nonEarpieceOutputAudioDevice = MutableLiveData<Boolean>()
|
||||
|
||||
private var timer: Timer? = null
|
||||
|
||||
private var disabled: Boolean = false
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String
|
||||
) {
|
||||
if (state == Call.State.StreamsRunning || state == Call.State.Updating || state == Call.State.UpdatedByRemote) {
|
||||
val isVideoCall = coreContext.isVideoCallOrConferenceActive()
|
||||
Log.i("[Controls Fading] Call is in state $state, video is ${if (isVideoCall) "enabled" else "disabled"}")
|
||||
if (isVideoCall) {
|
||||
videoEnabled.value = true
|
||||
startTimer()
|
||||
} else {
|
||||
videoEnabled.value = false
|
||||
stopTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) {
|
||||
if (audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
|
||||
Log.i("[Controls Fading] Output audio device changed to: ${audioDevice.id}")
|
||||
nonEarpieceOutputAudioDevice.value = audioDevice.type != AudioDevice.Type.Earpiece
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
areControlsHidden.value = false
|
||||
isVideoPreviewHidden.value = false
|
||||
isVideoPreviewResizedForPip.value = false
|
||||
nonEarpieceOutputAudioDevice.value = coreContext.core.outputAudioDevice?.type != AudioDevice.Type.Earpiece
|
||||
|
||||
val isVideoCall = coreContext.isVideoCallOrConferenceActive()
|
||||
videoEnabled.value = isVideoCall
|
||||
if (isVideoCall) {
|
||||
startTimer()
|
||||
}
|
||||
|
||||
proximitySensorEnabled.value = shouldEnableProximitySensor()
|
||||
proximitySensorEnabled.addSource(videoEnabled) {
|
||||
proximitySensorEnabled.value = shouldEnableProximitySensor()
|
||||
}
|
||||
proximitySensorEnabled.addSource(nonEarpieceOutputAudioDevice) {
|
||||
proximitySensorEnabled.value = shouldEnableProximitySensor()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
stopTimer()
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun showMomentarily() {
|
||||
stopTimer()
|
||||
startTimer()
|
||||
}
|
||||
|
||||
fun disable(disable: Boolean) {
|
||||
disabled = disable
|
||||
if (disabled) {
|
||||
stopTimer()
|
||||
} else {
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldEnableProximitySensor(): Boolean {
|
||||
return !(videoEnabled.value ?: false) && !(nonEarpieceOutputAudioDevice.value ?: false)
|
||||
}
|
||||
|
||||
private fun stopTimer() {
|
||||
timer?.cancel()
|
||||
|
||||
areControlsHidden.value = false
|
||||
}
|
||||
|
||||
private fun startTimer() {
|
||||
timer?.cancel()
|
||||
if (disabled) return
|
||||
|
||||
timer = Timer("Hide UI controls scheduler")
|
||||
timer?.schedule(
|
||||
object : TimerTask() {
|
||||
override fun run() {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
val videoEnabled = coreContext.isVideoCallOrConferenceActive()
|
||||
areControlsHidden.postValue(videoEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
3000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,484 +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.call.viewmodels
|
||||
|
||||
import android.Manifest
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.os.Vibrator
|
||||
import android.view.animation.LinearInterpolator
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlin.math.max
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.dialer.NumpadDigitListener
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.*
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class ControlsViewModel : ViewModel() {
|
||||
val isMicrophoneMuted = MutableLiveData<Boolean>()
|
||||
|
||||
val isMuteMicrophoneEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isSpeakerSelected = MutableLiveData<Boolean>()
|
||||
|
||||
val isBluetoothHeadsetSelected = MutableLiveData<Boolean>()
|
||||
|
||||
val isVideoAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val isVideoEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isVideoUpdateInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val showSwitchCamera = MutableLiveData<Boolean>()
|
||||
|
||||
val isPauseEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val isConferencingAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val unreadMessagesCount = MutableLiveData<Int>()
|
||||
|
||||
val numpadVisibility = MutableLiveData<Boolean>()
|
||||
|
||||
val optionsVisibility = MutableLiveData<Boolean>()
|
||||
|
||||
val audioRoutesSelected = MutableLiveData<Boolean>()
|
||||
|
||||
val audioRoutesEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val takeScreenshotEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val chatClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val addCallClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val transferCallClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val askAudioRecordPermissionEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val askCameraPermissionEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val somethingClickedEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val chatAllowed = !corePreferences.disableChat
|
||||
|
||||
private val vibrator = coreContext.context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
val chatUnreadCountTranslateY = MutableLiveData<Float>()
|
||||
|
||||
val optionsMenuTranslateY = MutableLiveData<Float>()
|
||||
|
||||
val audioRoutesMenuTranslateY = MutableLiveData<Float>()
|
||||
|
||||
val toggleNumpadEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
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 optionsMenuAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.call_options_menu_translate_y), 0f).apply {
|
||||
addUpdateListener {
|
||||
val value = it.animatedValue as Float
|
||||
optionsMenuTranslateY.value = value
|
||||
}
|
||||
duration = if (corePreferences.enableAnimations) 500 else 0
|
||||
}
|
||||
}
|
||||
|
||||
private val audioRoutesMenuAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.call_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 onKeyClick: NumpadDigitListener = object : NumpadDigitListener {
|
||||
override fun handleClick(key: Char) {
|
||||
coreContext.core.playDtmf(key, 1)
|
||||
somethingClickedEvent.value = Event(true)
|
||||
coreContext.core.currentCall?.sendDtmf(key)
|
||||
|
||||
if (vibrator.hasVibrator() && corePreferences.dtmfKeypadVibration) {
|
||||
Compatibility.eventVibration(vibrator)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleLongClick(key: Char): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
|
||||
updateUnreadChatCount()
|
||||
}
|
||||
|
||||
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
|
||||
updateUnreadChatCount()
|
||||
}
|
||||
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String
|
||||
) {
|
||||
if (state == Call.State.StreamsRunning) {
|
||||
isVideoUpdateInProgress.value = false
|
||||
}
|
||||
|
||||
if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) {
|
||||
askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA)
|
||||
}
|
||||
|
||||
updateUI()
|
||||
}
|
||||
|
||||
override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) {
|
||||
Log.i("[Call] Audio device changed: ${audioDevice.deviceName}")
|
||||
updateSpeakerState()
|
||||
updateBluetoothHeadsetState()
|
||||
}
|
||||
|
||||
override fun onAudioDevicesListUpdated(core: Core) {
|
||||
Log.i("[Call] Audio devices list updated")
|
||||
val wasBluetoothPreviouslyAvailable = audioRoutesEnabled.value == true
|
||||
updateAudioRoutesState()
|
||||
|
||||
if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) {
|
||||
AudioRouteUtils.routeAudioToHeadset()
|
||||
} else if (!wasBluetoothPreviouslyAvailable && corePreferences.routeAudioToBluetoothIfAvailable) {
|
||||
// Only attempt to route audio to bluetooth automatically when bluetooth device is connected
|
||||
if (AudioRouteUtils.isBluetoothAudioRouteAvailable()) {
|
||||
AudioRouteUtils.routeAudioToBluetooth()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
val currentCall = coreContext.core.currentCall
|
||||
|
||||
updateMuteMicState()
|
||||
updateAudioRelated()
|
||||
updateUnreadChatCount()
|
||||
|
||||
numpadVisibility.value = false
|
||||
optionsVisibility.value = false
|
||||
audioRoutesSelected.value = false
|
||||
|
||||
isRecording.value = currentCall?.isRecording
|
||||
isVideoUpdateInProgress.value = false
|
||||
showSwitchCamera.value = coreContext.showSwitchCameraButton()
|
||||
|
||||
chatUnreadCountTranslateY.value = 0f
|
||||
optionsMenuTranslateY.value = AppUtils.getDimension(R.dimen.call_options_menu_translate_y)
|
||||
audioRoutesMenuTranslateY.value = AppUtils.getDimension(R.dimen.call_audio_routes_menu_translate_y)
|
||||
|
||||
takeScreenshotEnabled.value = corePreferences.showScreenshotButton
|
||||
|
||||
updateUI()
|
||||
if (corePreferences.enableAnimations) bounceAnimator.start()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
if (corePreferences.enableAnimations) bounceAnimator.end()
|
||||
optionsMenuAnimator.end()
|
||||
audioRoutesMenuAnimator.end()
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun updateUnreadChatCount() {
|
||||
unreadMessagesCount.value = coreContext.core.unreadChatMessageCountFromActiveLocals
|
||||
}
|
||||
|
||||
fun toggleMuteMicrophone() {
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
askAudioRecordPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO)
|
||||
return
|
||||
}
|
||||
|
||||
somethingClickedEvent.value = Event(true)
|
||||
val micEnabled = coreContext.core.isMicEnabled
|
||||
coreContext.core.isMicEnabled = !micEnabled
|
||||
updateMuteMicState()
|
||||
}
|
||||
|
||||
fun toggleSpeaker() {
|
||||
somethingClickedEvent.value = Event(true)
|
||||
if (AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed()) {
|
||||
forceEarpieceAudioRoute()
|
||||
} else {
|
||||
forceSpeakerAudioRoute()
|
||||
}
|
||||
}
|
||||
|
||||
fun switchCamera() {
|
||||
somethingClickedEvent.value = Event(true)
|
||||
coreContext.switchCamera()
|
||||
}
|
||||
|
||||
fun terminateCall() {
|
||||
val core = coreContext.core
|
||||
when {
|
||||
core.currentCall != null -> core.currentCall?.terminate()
|
||||
core.conference?.isIn == true -> core.terminateConference()
|
||||
else -> core.terminateAllCalls()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleVideo() {
|
||||
if (!PermissionHelper.get().hasCameraPermission()) {
|
||||
askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA)
|
||||
return
|
||||
}
|
||||
|
||||
val core = coreContext.core
|
||||
val currentCall = core.currentCall
|
||||
val conference = core.conference
|
||||
|
||||
if (conference != null && conference.isIn) {
|
||||
val params = core.createConferenceParams()
|
||||
val videoEnabled = conference.currentParams.isVideoEnabled
|
||||
params.isVideoEnabled = !videoEnabled
|
||||
Log.i("[Controls VM] Conference current param for video is $videoEnabled")
|
||||
conference.updateParams(params)
|
||||
} else if (currentCall != null) {
|
||||
val state = currentCall.state
|
||||
if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error)
|
||||
return
|
||||
|
||||
isVideoUpdateInProgress.value = true
|
||||
val params = core.createCallParams(currentCall)
|
||||
params?.isVideoEnabled = !currentCall.currentParams.isVideoEnabled
|
||||
currentCall.update(params)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleOptionsMenu() {
|
||||
somethingClickedEvent.value = Event(true)
|
||||
optionsVisibility.value = optionsVisibility.value != true
|
||||
if (optionsVisibility.value == true) {
|
||||
optionsMenuAnimator.start()
|
||||
} else {
|
||||
optionsMenuAnimator.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleNumpadVisibility() {
|
||||
somethingClickedEvent.value = Event(true)
|
||||
numpadVisibility.value = numpadVisibility.value != true
|
||||
toggleNumpadEvent.value = Event(numpadVisibility.value ?: true)
|
||||
}
|
||||
|
||||
fun toggleRoutesMenu() {
|
||||
somethingClickedEvent.value = Event(true)
|
||||
audioRoutesSelected.value = audioRoutesSelected.value != true
|
||||
if (audioRoutesSelected.value == true) {
|
||||
audioRoutesMenuAnimator.start()
|
||||
} else {
|
||||
audioRoutesMenuAnimator.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleRecording(closeMenu: Boolean) {
|
||||
somethingClickedEvent.value = Event(true)
|
||||
|
||||
val core = coreContext.core
|
||||
val currentCall = core.currentCall
|
||||
val conference = core.conference
|
||||
|
||||
when {
|
||||
currentCall != null -> {
|
||||
if (currentCall.isRecording) {
|
||||
currentCall.stopRecording()
|
||||
} else {
|
||||
currentCall.startRecording()
|
||||
}
|
||||
isRecording.value = currentCall.isRecording
|
||||
}
|
||||
conference != null -> {
|
||||
val path = LinphoneUtils.getRecordingFilePathForConference()
|
||||
if (conference.isRecording) {
|
||||
conference.stopRecording()
|
||||
} else {
|
||||
conference.startRecording(path)
|
||||
}
|
||||
isRecording.value = conference.isRecording
|
||||
}
|
||||
else -> {
|
||||
isRecording.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (closeMenu) toggleOptionsMenu()
|
||||
}
|
||||
|
||||
fun onChatClicked() {
|
||||
chatClickedEvent.value = Event(true)
|
||||
}
|
||||
|
||||
fun onAddCallClicked() {
|
||||
addCallClickedEvent.value = Event(true)
|
||||
toggleOptionsMenu()
|
||||
}
|
||||
|
||||
fun onTransferCallClicked() {
|
||||
transferCallClickedEvent.value = Event(true)
|
||||
toggleOptionsMenu()
|
||||
}
|
||||
|
||||
fun startConference() {
|
||||
somethingClickedEvent.value = Event(true)
|
||||
|
||||
val core = coreContext.core
|
||||
val currentCallVideoEnabled = core.currentCall?.currentParams?.isVideoEnabled ?: false
|
||||
|
||||
val params = core.createConferenceParams()
|
||||
params.isVideoEnabled = currentCallVideoEnabled
|
||||
Log.i("[Call] Setting videoEnabled to [$currentCallVideoEnabled] in conference params")
|
||||
|
||||
val conference = core.conference ?: core.createConferenceWithParams(params)
|
||||
conference?.addParticipants(core.calls)
|
||||
|
||||
toggleOptionsMenu()
|
||||
}
|
||||
|
||||
fun forceEarpieceAudioRoute() {
|
||||
somethingClickedEvent.value = Event(true)
|
||||
if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) {
|
||||
Log.i("[Call] Headset found, route audio to it instead of earpiece")
|
||||
AudioRouteUtils.routeAudioToHeadset()
|
||||
} else {
|
||||
AudioRouteUtils.routeAudioToEarpiece()
|
||||
}
|
||||
}
|
||||
|
||||
fun forceSpeakerAudioRoute() {
|
||||
somethingClickedEvent.value = Event(true)
|
||||
AudioRouteUtils.routeAudioToSpeaker()
|
||||
}
|
||||
|
||||
fun forceBluetoothAudioRoute() {
|
||||
somethingClickedEvent.value = Event(true)
|
||||
AudioRouteUtils.routeAudioToBluetooth()
|
||||
}
|
||||
|
||||
fun updateMuteMicState() {
|
||||
isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || !coreContext.core.isMicEnabled
|
||||
isMuteMicrophoneEnabled.value = coreContext.core.currentCall != null || coreContext.core.conference?.isIn == true
|
||||
}
|
||||
|
||||
private fun updateAudioRelated() {
|
||||
updateSpeakerState()
|
||||
updateBluetoothHeadsetState()
|
||||
updateAudioRoutesState()
|
||||
}
|
||||
|
||||
private fun updateUI() {
|
||||
val currentCall = coreContext.core.currentCall
|
||||
updateVideoAvailable()
|
||||
updateVideoEnabled()
|
||||
isPauseEnabled.value = currentCall != null && !currentCall.mediaInProgress()
|
||||
isMuteMicrophoneEnabled.value = currentCall != null || coreContext.core.conference?.isIn == true
|
||||
updateConferenceState()
|
||||
|
||||
// Check periodically until mediaInProgress is false
|
||||
if (currentCall != null && currentCall.mediaInProgress()) {
|
||||
viewModelScope.launch {
|
||||
delay(1000)
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSpeakerState() {
|
||||
isSpeakerSelected.value = AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed()
|
||||
}
|
||||
|
||||
private fun updateAudioRoutesState() {
|
||||
val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable()
|
||||
audioRoutesEnabled.value = bluetoothDeviceAvailable
|
||||
if (!bluetoothDeviceAvailable) {
|
||||
audioRoutesSelected.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBluetoothHeadsetState() {
|
||||
isBluetoothHeadsetSelected.value = AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed()
|
||||
}
|
||||
|
||||
private fun updateVideoAvailable() {
|
||||
val core = coreContext.core
|
||||
val currentCall = core.currentCall
|
||||
isVideoAvailable.value = (core.isVideoCaptureEnabled || core.isVideoPreviewEnabled) &&
|
||||
(
|
||||
(currentCall != null && !currentCall.mediaInProgress()) ||
|
||||
core.conference?.isIn == true
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateVideoEnabled() {
|
||||
val enabled = coreContext.isVideoCallOrConferenceActive()
|
||||
isVideoEnabled.value = enabled
|
||||
}
|
||||
|
||||
private fun updateConferenceState() {
|
||||
val core = coreContext.core
|
||||
isConferencingAvailable.value = core.callsNb > max(1, core.conference?.participantCount ?: 0) && !core.soundResourcesLocked()
|
||||
}
|
||||
}
|
||||
|
|
@ -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.call.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.core.*
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class IncomingCallViewModelFactory(private val call: Call) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return IncomingCallViewModel(call) as T
|
||||
}
|
||||
}
|
||||
|
||||
class IncomingCallViewModel(call: Call) : CallViewModel(call) {
|
||||
val screenLocked = MutableLiveData<Boolean>()
|
||||
|
||||
val earlyMediaVideoEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val inviteWithVideo = MutableLiveData<Boolean>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String
|
||||
) {
|
||||
if (core.callsNb == 0) {
|
||||
callEndedEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
screenLocked.value = false
|
||||
inviteWithVideo.value = call.remoteParams?.isVideoEnabled == true && coreContext.core.videoActivationPolicy.automaticallyAccept
|
||||
earlyMediaVideoEnabled.value = corePreferences.acceptEarlyMedia &&
|
||||
call.state == Call.State.IncomingEarlyMedia &&
|
||||
call.currentParams.isVideoEnabled
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun answer(doAction: Boolean) {
|
||||
if (doAction) coreContext.answerCall(call)
|
||||
}
|
||||
|
||||
fun decline(doAction: Boolean) {
|
||||
if (doAction) coreContext.declineCall(call)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.activities.call.data.CallStatisticsData
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
|
||||
class StatisticsListViewModel : ViewModel() {
|
||||
val callStatsList = MutableLiveData<ArrayList<CallStatisticsData>>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String
|
||||
) {
|
||||
if (state == Call.State.End || state == Call.State.Error || state == Call.State.Connected) {
|
||||
computeCallsList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
computeCallsList()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
callStatsList.value.orEmpty().forEach(CallStatisticsData::destroy)
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun computeCallsList() {
|
||||
val list = arrayListOf<CallStatisticsData>()
|
||||
for (call in coreContext.core.calls) {
|
||||
if (call.state != Call.State.End && call.state != Call.State.Released && call.state != Call.State.Error) {
|
||||
list.add(CallStatisticsData(call))
|
||||
}
|
||||
}
|
||||
callStatsList.value = list
|
||||
}
|
||||
}
|
||||
|
|
@ -1,153 +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.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.viewmodels.StatusViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class StatusViewModel : StatusViewModel() {
|
||||
val callQualityIcon = MutableLiveData<Int>()
|
||||
val callQualityContentDescription = MutableLiveData<Int>()
|
||||
|
||||
val encryptionIcon = MutableLiveData<Int>()
|
||||
val encryptionContentDescription = MutableLiveData<Int>()
|
||||
val encryptionIconVisible = MutableLiveData<Boolean>()
|
||||
|
||||
val showZrtpDialogEvent: MutableLiveData<Event<Call>> by lazy {
|
||||
MutableLiveData<Event<Call>>()
|
||||
}
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
|
||||
updateCallQualityIcon()
|
||||
}
|
||||
|
||||
override fun onCallEncryptionChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
on: Boolean,
|
||||
authenticationToken: String?
|
||||
) {
|
||||
if (call.currentParams.mediaEncryption == MediaEncryption.ZRTP && !call.authenticationTokenVerified) {
|
||||
showZrtpDialogEvent.value = Event(call)
|
||||
} else {
|
||||
updateEncryptionInfo(call)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String
|
||||
) {
|
||||
if (call == core.currentCall) {
|
||||
updateEncryptionInfo(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
updateCallQualityIcon()
|
||||
|
||||
val currentCall = coreContext.core.currentCall
|
||||
if (currentCall != null) {
|
||||
updateEncryptionInfo(currentCall)
|
||||
|
||||
if (currentCall.currentParams.mediaEncryption == MediaEncryption.ZRTP && !currentCall.authenticationTokenVerified) {
|
||||
showZrtpDialogEvent.value = Event(currentCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun showZrtpDialog() {
|
||||
val currentCall = coreContext.core.currentCall
|
||||
if (currentCall?.currentParams?.mediaEncryption == MediaEncryption.ZRTP) {
|
||||
showZrtpDialogEvent.value = Event(currentCall)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateEncryptionInfo(call: Call) {
|
||||
if (call.dir == Call.Dir.Incoming && call.state == Call.State.IncomingReceived && call.core.isMediaEncryptionMandatory) {
|
||||
// If the incoming call view is displayed while encryption is mandatory,
|
||||
// we can safely show the security_ok icon
|
||||
encryptionIcon.value = R.drawable.security_ok
|
||||
encryptionIconVisible.value = true
|
||||
encryptionContentDescription.value = R.string.content_description_call_secured
|
||||
return
|
||||
}
|
||||
|
||||
when (call.currentParams.mediaEncryption ?: MediaEncryption.None) {
|
||||
MediaEncryption.SRTP, MediaEncryption.DTLS -> {
|
||||
encryptionIcon.value = R.drawable.security_ok
|
||||
encryptionIconVisible.value = true
|
||||
encryptionContentDescription.value = R.string.content_description_call_secured
|
||||
}
|
||||
MediaEncryption.ZRTP -> {
|
||||
encryptionIcon.value = when (call.authenticationTokenVerified) {
|
||||
true -> R.drawable.security_ok
|
||||
else -> R.drawable.security_pending
|
||||
}
|
||||
encryptionContentDescription.value = when (call.authenticationTokenVerified) {
|
||||
true -> R.string.content_description_call_secured
|
||||
else -> R.string.content_description_call_security_pending
|
||||
}
|
||||
encryptionIconVisible.value = true
|
||||
}
|
||||
MediaEncryption.None -> {
|
||||
encryptionIcon.value = R.drawable.security_ko
|
||||
// Do not show unsecure icon if user doesn't want to do call encryption
|
||||
encryptionIconVisible.value = call.core.mediaEncryption != MediaEncryption.None
|
||||
encryptionContentDescription.value = R.string.content_description_call_not_secured
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCallQualityIcon() {
|
||||
val call = coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull()
|
||||
val quality = call?.currentQuality ?: 0f
|
||||
callQualityIcon.value = when {
|
||||
quality >= 4 -> R.drawable.call_quality_indicator_4
|
||||
quality >= 3 -> R.drawable.call_quality_indicator_3
|
||||
quality >= 2 -> R.drawable.call_quality_indicator_2
|
||||
quality >= 1 -> R.drawable.call_quality_indicator_1
|
||||
else -> R.drawable.call_quality_indicator_0
|
||||
}
|
||||
callQualityContentDescription.value = when {
|
||||
quality >= 4 -> R.string.content_description_call_quality_4
|
||||
quality >= 3 -> R.string.content_description_call_quality_3
|
||||
quality >= 2 -> R.string.content_description_call_quality_2
|
||||
quality >= 1 -> R.string.content_description_call_quality_1
|
||||
else -> R.string.content_description_call_quality_0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,180 +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.call.views
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.View.OnTouchListener
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.LinearLayout
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.call.viewmodels.IncomingCallViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallIncomingAnswerDeclineButtonsBinding
|
||||
|
||||
class AnswerDeclineIncomingCallButtons : LinearLayout {
|
||||
private lateinit var binding: CallIncomingAnswerDeclineButtonsBinding
|
||||
private var mBegin = false
|
||||
private var mDeclineX = 0f
|
||||
private var mAnswerX = 0f
|
||||
private var mOldSize = 0f
|
||||
|
||||
private val mAnswerTouchListener = OnTouchListener { view, motionEvent ->
|
||||
val curX: Float
|
||||
|
||||
when (motionEvent.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
binding.declineButton.visibility = View.GONE
|
||||
mAnswerX = motionEvent.x - view.width
|
||||
mBegin = true
|
||||
mOldSize = 0f
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
curX = motionEvent.x - view.width
|
||||
view.scrollBy((mAnswerX - curX).toInt(), view.scrollY)
|
||||
mOldSize -= mAnswerX - curX
|
||||
mAnswerX = curX
|
||||
if (mOldSize < -25) mBegin = false
|
||||
if (curX < (width / 4) - view.width && !mBegin) {
|
||||
binding.viewModel?.answer(true)
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
binding.declineButton.visibility = View.VISIBLE
|
||||
view.scrollTo(0, view.scrollY)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
private val mDeclineTouchListener = OnTouchListener { view, motionEvent ->
|
||||
val curX: Float
|
||||
|
||||
when (motionEvent.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
binding.answerButton.visibility = View.GONE
|
||||
mDeclineX = motionEvent.x
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
curX = motionEvent.x
|
||||
view.scrollBy((mDeclineX - curX).toInt(), view.scrollY)
|
||||
mDeclineX = curX
|
||||
if (curX > 3 * width / 4) {
|
||||
binding.viewModel?.decline(true)
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
binding.answerButton.visibility = View.VISIBLE
|
||||
view.scrollTo(0, view.scrollY)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(
|
||||
context,
|
||||
attrs
|
||||
) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
fun setViewModel(viewModel: IncomingCallViewModel) {
|
||||
binding.viewModel = viewModel
|
||||
|
||||
updateSlideMode()
|
||||
}
|
||||
|
||||
private fun init(context: Context) {
|
||||
binding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context), R.layout.call_incoming_answer_decline_buttons, this, true
|
||||
)
|
||||
|
||||
updateSlideMode()
|
||||
configureAnimation()
|
||||
}
|
||||
|
||||
private fun updateSlideMode() {
|
||||
val slideMode = binding.viewModel?.screenLocked?.value == true
|
||||
Log.i("[Call Incoming Decline Button] Slide mode is $slideMode")
|
||||
if (slideMode) {
|
||||
binding.answerButton.setOnTouchListener(mAnswerTouchListener)
|
||||
binding.declineButton.setOnTouchListener(mDeclineTouchListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureAnimation() {
|
||||
if (!corePreferences.enableAnimations) return
|
||||
|
||||
val accept1 = ObjectAnimator.ofFloat(binding.arrowAccept1, "alpha", 1f, 0.6f, 0.4f, 1f).apply {
|
||||
repeatCount = ObjectAnimator.INFINITE
|
||||
repeatMode = ObjectAnimator.RESTART
|
||||
}
|
||||
|
||||
val accept2 = ObjectAnimator.ofFloat(binding.arrowAccept2, "alpha", 0.6f, 1f, 0.4f, 0.6f).apply {
|
||||
repeatCount = ObjectAnimator.INFINITE
|
||||
repeatMode = ObjectAnimator.RESTART
|
||||
}
|
||||
|
||||
val accept3 = ObjectAnimator.ofFloat(binding.arrowAccept3, "alpha", 0.4f, 0.6f, 1f, 0.4f).apply {
|
||||
repeatCount = ObjectAnimator.INFINITE
|
||||
repeatMode = ObjectAnimator.RESTART
|
||||
}
|
||||
|
||||
val hangup1 = ObjectAnimator.ofFloat(binding.arrowHangup1, "alpha", 1f, 0.6f, 0.4f, 1f).apply {
|
||||
repeatCount = ObjectAnimator.INFINITE
|
||||
repeatMode = ObjectAnimator.RESTART
|
||||
}
|
||||
|
||||
val hangup2 = ObjectAnimator.ofFloat(binding.arrowHangup2, "alpha", 0.6f, 1f, 0.4f, 0.6f).apply {
|
||||
repeatCount = ObjectAnimator.INFINITE
|
||||
repeatMode = ObjectAnimator.RESTART
|
||||
}
|
||||
|
||||
val hangup3 = ObjectAnimator.ofFloat(binding.arrowHangup3, "alpha", 0.4f, 0.6f, 1f, 0.4f).apply {
|
||||
repeatCount = ObjectAnimator.INFINITE
|
||||
repeatMode = ObjectAnimator.RESTART
|
||||
}
|
||||
|
||||
AnimatorSet().apply {
|
||||
duration = 2000
|
||||
interpolator = LinearInterpolator()
|
||||
playTogether(accept1, accept2, accept3, hangup1, hangup2, hangup3)
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.call.views
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.call.data.ConferenceParticipantData
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallConferenceParticipantBinding
|
||||
|
||||
class ConferenceParticipantView : LinearLayout {
|
||||
private lateinit var binding: CallConferenceParticipantBinding
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(
|
||||
context,
|
||||
attrs
|
||||
) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
binding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context), R.layout.call_conference_participant, this, true
|
||||
)
|
||||
}
|
||||
|
||||
fun setData(data: ConferenceParticipantData) {
|
||||
binding.data = data
|
||||
|
||||
val currentTimeSecs = System.currentTimeMillis()
|
||||
val participantTime = data.participant.creationTime * 1000 // Linphone timestamps are in seconds
|
||||
val diff = currentTimeSecs - participantTime
|
||||
Log.i("[Conference Participant] Participant joined conference at $participantTime == ${diff / 1000} seconds ago.")
|
||||
binding.callTimer.base = SystemClock.elapsedRealtime() - diff
|
||||
binding.callTimer.start()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +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.call.views
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.call.viewmodels.CallViewModel
|
||||
import org.linphone.databinding.CallPausedBinding
|
||||
|
||||
class PausedCallView : LinearLayout {
|
||||
private lateinit var binding: CallPausedBinding
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(
|
||||
context,
|
||||
attrs
|
||||
) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
binding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context), R.layout.call_paused, this, true
|
||||
)
|
||||
}
|
||||
|
||||
fun setViewModel(viewModel: CallViewModel) {
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.callTimer.base =
|
||||
SystemClock.elapsedRealtime() - (1000 * viewModel.call.duration) // Linphone timestamps are in seconds
|
||||
binding.callTimer.start()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 onChatMessageReceived(chatRoom: ChatRoom, eventLog: 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.disableContextMenu()
|
||||
|
||||
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,517 +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.app.Activity
|
||||
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 android.view.inputmethod.InputMethodManager
|
||||
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 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.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.GlideApp
|
||||
|
||||
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) {
|
||||
Compatibility.createShortcutsToContacts(this@MainActivity)
|
||||
} else if (corePreferences.chatRoomShortcuts) {
|
||||
Compatibility.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 !")
|
||||
GlideApp.get(this@MainActivity).clearMemory()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
|
||||
sharedViewModel.layoutChangedEvent.value = Event(true)
|
||||
}
|
||||
|
||||
private var tabsFragmentVisible1 = true
|
||||
private var tabsFragmentVisible2 = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val splashScreen = 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(resourceId: Int) {
|
||||
Snackbar.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(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"}")
|
||||
tabsFragmentVisible2 = !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
|
||||
}
|
||||
|
||||
tabsFragmentVisible1 = when (destination.id) {
|
||||
R.id.masterCallLogsFragment, R.id.masterContactsFragment, R.id.dialerFragment, R.id.masterChatRoomsFragment ->
|
||||
true
|
||||
else -> false
|
||||
}
|
||||
updateTabsFragmentVisibility()
|
||||
}
|
||||
|
||||
fun hideKeyboard() {
|
||||
currentFocus?.hideKeyboard()
|
||||
}
|
||||
|
||||
private fun updateTabsFragmentVisibility() {
|
||||
tabsFragment.visibility = if (tabsFragmentVisible1 && tabsFragmentVisible2) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun View.hideKeyboard() {
|
||||
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(windowToken, 0)
|
||||
}
|
||||
|
||||
private fun handleIntentParams(intent: Intent) {
|
||||
when (intent.action) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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)?.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,70 +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.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.setBackClickListener { goBack() }
|
||||
|
||||
binding.setPrivacyPolicyClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_privacy_policy_link))
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
|
||||
binding.setLicenseClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_license_link))
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
|
||||
binding.setWeblateClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_weblate_link))
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,474 +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.ChatMessage
|
||||
import org.linphone.core.ChatRoomCapabilities
|
||||
import org.linphone.core.Content
|
||||
import org.linphone.core.EventLog
|
||||
import org.linphone.databinding.*
|
||||
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 sipUriClickedEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val scrollToChatMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
private val contentClickedListener = object : OnContentClickedListener {
|
||||
override fun onContentClicked(content: Content) {
|
||||
openContentEvent.value = Event(content)
|
||||
}
|
||||
|
||||
override fun onSipAddressClicked(sipUri: String) {
|
||||
sipUriClickedEvent.value = Event(sipUri)
|
||||
}
|
||||
}
|
||||
|
||||
private var contextMenuDisabled: Boolean = false
|
||||
|
||||
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 disableContextMenu() {
|
||||
contextMenuDisabled = 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()
|
||||
|
||||
if (contextMenuDisabled) return
|
||||
|
||||
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) {
|
||||
popupView.addToContactsHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.chatRoom.hasBeenLeft()) {
|
||||
popupView.replyHidden = 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
|
||||
)
|
||||
// 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.eventLog.type == EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.eventLog.type == EventLog.Type.ConferenceChatMessage
|
||||
) {
|
||||
oldItem.eventLog.chatMessage?.time == newItem.eventLog.chatMessage?.time &&
|
||||
oldItem.eventLog.chatMessage?.isOutgoing == newItem.eventLog.chatMessage?.isOutgoing
|
||||
} else oldItem.eventLog.notifyId == newItem.eventLog.notifyId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: EventLogData,
|
||||
newItem: EventLogData
|
||||
): Boolean {
|
||||
return if (oldItem.eventLog.type == EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.eventLog.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
|
||||
newItem.eventLog.chatMessage?.state == ChatMessage.State.Displayed && previous && next
|
||||
} else {
|
||||
oldItem.eventLog.type != EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.eventLog.type != EventLog.Type.ConferenceChatMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +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.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.data.ChatRoomCreationContactData
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.FriendCapability
|
||||
import org.linphone.core.SearchResult
|
||||
import org.linphone.databinding.ChatRoomCreationContactCellBinding
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class ChatRoomCreationContactsAdapter(
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : ListAdapter<SearchResult, RecyclerView.ViewHolder>(SearchResultDiffCallback()) {
|
||||
val selectedContact = MutableLiveData<Event<SearchResult>>()
|
||||
|
||||
var groupChatEnabled: Boolean = false
|
||||
|
||||
private var selectedAddresses = ArrayList<Address>()
|
||||
|
||||
private var securityEnabled: Boolean = false
|
||||
|
||||
fun updateSelectedAddresses(selection: ArrayList<Address>) {
|
||||
selectedAddresses = selection
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun updateSecurity(enabled: Boolean) {
|
||||
securityEnabled = enabled
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val binding: ChatRoomCreationContactCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_creation_contact_cell, parent, false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ChatRoomCreationContactCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(searchResult: SearchResult) {
|
||||
with(binding) {
|
||||
val searchResultViewModel = ChatRoomCreationContactData(searchResult)
|
||||
data = searchResultViewModel
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
updateSecurity(searchResult, searchResultViewModel, securityEnabled)
|
||||
|
||||
val selected = selectedAddresses.find { address ->
|
||||
val searchAddress = searchResult.address
|
||||
if (searchAddress != null) address.weakEqual(searchAddress) else false
|
||||
}
|
||||
searchResultViewModel.isSelected.value = selected != null
|
||||
|
||||
setClickListener {
|
||||
selectedContact.value = Event(searchResult)
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSecurity(
|
||||
searchResult: SearchResult,
|
||||
viewModel: ChatRoomCreationContactData,
|
||||
securityEnabled: Boolean
|
||||
) {
|
||||
val searchAddress = searchResult.address
|
||||
val isMyself = securityEnabled && searchAddress != null && coreContext.core.defaultAccount?.params?.identityAddress?.weakEqual(searchAddress) ?: false
|
||||
val limeCheck = !securityEnabled || (securityEnabled && searchResult.hasCapability(FriendCapability.LimeX3Dh))
|
||||
val groupCheck = !groupChatEnabled || (groupChatEnabled && searchResult.hasCapability(FriendCapability.GroupChat))
|
||||
val disabled = if (searchResult.friend != null) !limeCheck || !groupCheck || isMyself else false // Generated entry from search filter
|
||||
|
||||
viewModel.isDisabled.value = disabled
|
||||
|
||||
if (disabled && viewModel.isSelected.value == true) {
|
||||
// Remove item from selection if both selected and disabled
|
||||
selectedContact.postValue(Event(searchResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchResultDiffCallback : DiffUtil.ItemCallback<SearchResult>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: SearchResult,
|
||||
newItem: SearchResult
|
||||
): Boolean {
|
||||
val oldAddress = oldItem.address
|
||||
val newAddress = newItem.address
|
||||
return if (oldAddress != null && newAddress != null) oldAddress.weakEqual(newAddress) else false
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: SearchResult,
|
||||
newItem: SearchResult
|
||||
): Boolean {
|
||||
return newItem.friend != null
|
||||
}
|
||||
}
|
||||
|
|
@ -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.viewmodels.ChatRoomViewModel
|
||||
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<ChatRoomViewModel, 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
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ChatRoomListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(chatRoomViewModel: ChatRoomViewModel) {
|
||||
with(binding) {
|
||||
viewModel = chatRoomViewModel
|
||||
|
||||
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(chatRoomViewModel.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<ChatRoomViewModel>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ChatRoomViewModel,
|
||||
newItem: ChatRoomViewModel
|
||||
): Boolean {
|
||||
return oldItem.chatRoom == newItem.chatRoom
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ChatRoomViewModel,
|
||||
newItem: ChatRoomViewModel
|
||||
): Boolean {
|
||||
return false // To force redraw when contacts are updated
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
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,123 +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
|
||||
}
|
||||
}
|
||||
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,58 +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.graphics.Bitmap
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.*
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.ImageUtils
|
||||
|
||||
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)
|
||||
val videoPreview = MutableLiveData<Bitmap>()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
init {
|
||||
if (isVideo) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
videoPreview.postValue(ImageUtils.getVideoPreview(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
fun delete() {
|
||||
deleteCallback(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,362 +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.graphics.Bitmap
|
||||
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.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
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.ImageUtils
|
||||
|
||||
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 videoPreview = MutableLiveData<Bitmap>()
|
||||
val isPdf = MutableLiveData<Boolean>()
|
||||
val isGenericFile = MutableLiveData<Boolean>()
|
||||
val isVoiceRecording = 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 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() {
|
||||
val content = getContent()
|
||||
val filePath = content.filePath
|
||||
if (content.isFileTransfer && (filePath == null || filePath.isEmpty())) {
|
||||
val contentName = content.name
|
||||
if (contentName != null) {
|
||||
val file = FileUtils.getFileStoragePath(contentName)
|
||||
content.filePath = file.path
|
||||
downloadEnabled.value = false
|
||||
|
||||
Log.i("[Content] Started downloading $contentName into ${content.filePath}")
|
||||
chatMessage.downloadContent(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
|
||||
val path = if (isFileEncrypted) {
|
||||
Log.i("[Content] Content is encrypted, requesting plain file path")
|
||||
content.plainFilePath
|
||||
} else {
|
||||
content.filePath ?: ""
|
||||
}
|
||||
downloadable.value = content.filePath.orEmpty().isEmpty()
|
||||
|
||||
if (path.isNotEmpty()) {
|
||||
Log.i("[Content] Found displayable content: $path")
|
||||
val isVoiceRecord = content.isVoiceRecording
|
||||
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)
|
||||
isVoiceRecording.value = isVoiceRecord
|
||||
|
||||
if (isVoiceRecord) {
|
||||
val duration = content.fileDuration // duration is in ms
|
||||
voiceRecordDuration.value = duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration)
|
||||
Log.i("[Voice Recording] Duration is ${voiceRecordDuration.value} ($duration)")
|
||||
}
|
||||
|
||||
if (isVideo.value == true) {
|
||||
scope.launch {
|
||||
videoPreview.postValue(ImageUtils.getVideoPreview(path))
|
||||
}
|
||||
}
|
||||
} 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
|
||||
}
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
isGenericFile.value = !isPdf.value!! && !isAudio.value!! && !isVideo.value!! && !isImage.value!! && !isVoiceRecording.value!!
|
||||
downloadEnabled.value = !chatMessage.isFileTransferInProgress
|
||||
downloadProgressInt.value = 0
|
||||
downloadProgressString.value = "0%"
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
voiceRecordPlayingPosition.postValue(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)) {
|
||||
if (device.type == AudioDevice.Type.Speaker) {
|
||||
speakerCard = device.id
|
||||
} else if (device.type == AudioDevice.Type.Earpiece) {
|
||||
earpieceCard = device.id
|
||||
} else if (device.type == AudioDevice.Type.Headphones || device.type == AudioDevice.Type.Headset) {
|
||||
headphonesCard = device.id
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
@ -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.data
|
||||
|
||||
import android.os.CountDownTimer
|
||||
import android.text.Spannable
|
||||
import android.text.util.Linkify
|
||||
import androidx.core.text.util.LinkifyCompat
|
||||
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>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
val data = ChatMessageContentData(chatMessage, index)
|
||||
data.listener = contentListener
|
||||
list.add(data)
|
||||
} else if (content.isText) {
|
||||
val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text?.trim())
|
||||
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS)
|
||||
text.value = PatternClickableSpan()
|
||||
.add(
|
||||
Pattern.compile("(sips?):([^@]+)(?:@([^ ]+))?"),
|
||||
object : PatternClickableSpan.SpannableClickedListener {
|
||||
override fun onSpanClicked(text: String) {
|
||||
Log.i("[Chat Message Data] Clicked on SIP URI: $text")
|
||||
contentListener?.onSipAddressClicked(text)
|
||||
}
|
||||
}
|
||||
).build(spannable)
|
||||
}
|
||||
}
|
||||
|
||||
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,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.contact.Contact
|
||||
import org.linphone.contact.ContactDataInterface
|
||||
import org.linphone.core.*
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomCreationContactData(private val searchResult: SearchResult) : ContactDataInterface {
|
||||
override val contact: MutableLiveData<Contact> = MutableLiveData<Contact>()
|
||||
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
|
||||
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
|
||||
|
||||
val isDisabled: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>()
|
||||
}
|
||||
|
||||
val isSelected: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>()
|
||||
}
|
||||
|
||||
val isLinphoneUser: Boolean by lazy {
|
||||
searchResult.friend?.getPresenceModelForUriOrTel(searchResult.phoneNumber ?: searchResult.address?.asStringUriOnly() ?: "")?.basicStatus == PresenceBasicStatus.Open
|
||||
}
|
||||
|
||||
val sipUri: String by lazy {
|
||||
searchResult.phoneNumber ?: LinphoneUtils.getDisplayableAddress(searchResult.address)
|
||||
}
|
||||
|
||||
val address: Address? by lazy {
|
||||
searchResult.address
|
||||
}
|
||||
|
||||
val hasLimeX3DHCapability: Boolean
|
||||
get() = searchResult.hasCapability(FriendCapability.LimeX3Dh)
|
||||
|
||||
init {
|
||||
isDisabled.value = false
|
||||
isSelected.value = false
|
||||
searchMatchingContact()
|
||||
}
|
||||
|
||||
private fun searchMatchingContact() {
|
||||
val address = searchResult.address
|
||||
if (address != null) {
|
||||
contact.value = coreContext.contactsManager.findContactByAddress(address)
|
||||
displayName.value = searchResult.friend?.name ?: LinphoneUtils.getDisplayName(address)
|
||||
} else if (searchResult.phoneNumber != null) {
|
||||
contact.value = coreContext.contactsManager.findContactByPhoneNumber(searchResult.phoneNumber.orEmpty())
|
||||
displayName.value = searchResult.friend?.name ?: searchResult.phoneNumber.orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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, 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, 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?.fullName ?: 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,57 +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.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.contact.GenericContactData
|
||||
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>()
|
||||
|
||||
init {
|
||||
securityLevel.value = participant.securityLevel
|
||||
isAdmin.value = participant.isAdmin
|
||||
showAdminControls.value = false
|
||||
canBeSetAdmin.value = participant.canBeSetAdmin
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
super.destroy()
|
||||
}
|
||||
|
||||
fun setAdmin() {
|
||||
isAdmin.value = true
|
||||
participant.isAdmin = true
|
||||
}
|
||||
|
||||
fun unSetAdmin() {
|
||||
isAdmin.value = false
|
||||
participant.isAdmin = false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,202 +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.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.LinphoneApplication
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.adapters.ChatRoomCreationContactsAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.ChatRoomCreationViewModel
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.activities.navigateToChatRoom
|
||||
import org.linphone.activities.navigateToEmptyChatRoom
|
||||
import org.linphone.activities.navigateToGroupInfo
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomCreationFragmentBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>() {
|
||||
private lateinit var viewModel: ChatRoomCreationViewModel
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
private lateinit var adapter: ChatRoomCreationContactsAdapter
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_creation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedMainViewModel::class.java]
|
||||
}
|
||||
|
||||
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 = ChatRoomCreationContactsAdapter(viewLifecycleOwner)
|
||||
adapter.groupChatEnabled = viewModel.createGroupChat.value == true
|
||||
adapter.updateSecurity(viewModel.isEncrypted.value == true)
|
||||
binding.contactsList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(activity)
|
||||
binding.contactsList.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
binding.contactsList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||
|
||||
binding.setBackClickListener {
|
||||
goBack()
|
||||
}
|
||||
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.updateSecurity(it)
|
||||
}
|
||||
|
||||
viewModel.sipContactsSelected.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
viewModel.updateContactsList()
|
||||
}
|
||||
|
||||
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.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
}
|
||||
|
||||
if (!PermissionHelper.get().hasReadContactsPermission()) {
|
||||
Log.i("[Chat Room Creation] Asking for READ_CONTACTS permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun goBack() {
|
||||
if (!findNavController().popBackStack()) {
|
||||
if (sharedViewModel.isSlidingPaneSlideable.value == true) {
|
||||
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
|
||||
} else {
|
||||
navigateToEmptyChatRoom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
LinphoneApplication.coreContext.contactsManager.onReadContactsPermissionGranted()
|
||||
LinphoneApplication.coreContext.contactsManager.fetchContactsAsync()
|
||||
} else {
|
||||
Log.w("[Chat Room Creation] READ_CONTACTS permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,75 +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.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomDevicesFragmentBinding
|
||||
|
||||
class DevicesFragment : SecureFragment<ChatRoomDevicesFragmentBinding>() {
|
||||
private lateinit var listViewModel: DevicesListViewModel
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_devices_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedMainViewModel::class.java]
|
||||
}
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Devices] Chat room is null, aborting!")
|
||||
// (activity as MainActivity).showSnackBar(R.string.error)
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
||||
|
||||
listViewModel = ViewModelProvider(
|
||||
this,
|
||||
DevicesListViewModelFactory(chatRoom)
|
||||
)[DevicesListViewModel::class.java]
|
||||
binding.viewModel = listViewModel
|
||||
|
||||
binding.setBackClickListener {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
listViewModel.updateParticipants()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +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.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomEphemeralFragmentBinding
|
||||
|
||||
class EphemeralFragment : SecureFragment<ChatRoomEphemeralFragmentBinding>() {
|
||||
private lateinit var viewModel: EphemeralViewModel
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
|
||||
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
|
||||
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedMainViewModel::class.java]
|
||||
}
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Ephemeral] Chat room is null, aborting!")
|
||||
// (activity as MainActivity).showSnackBar(R.string.error)
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
EphemeralViewModelFactory(chatRoom)
|
||||
)[EphemeralViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setBackClickListener {
|
||||
goBack()
|
||||
}
|
||||
|
||||
binding.setValidClickListener {
|
||||
viewModel.updateChatRoomEphemeralDuration()
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue