mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Compare commits
725 commits
v4.2.0-bet
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63f621cb56 | ||
|
|
e81f916626 | ||
|
|
28fa7b1a20 | ||
|
|
aae9d4a387 | ||
|
|
6873f63cf8 | ||
|
|
2d0255e5fb | ||
|
|
32ae406cca | ||
|
|
3e04a1f036 | ||
|
|
6487191394 | ||
|
|
beaf211f39 | ||
|
|
3549789cdf | ||
|
|
def33552f9 | ||
|
|
3a55583460 | ||
|
|
717a855149 | ||
|
|
29b6d0f8fe | ||
|
|
b5b5d8a8e4 | ||
|
|
714e5f8a4b | ||
|
|
7d96e94689 | ||
|
|
7a865df909 | ||
|
|
864884e7b2 | ||
|
|
1ecaf09f21 | ||
|
|
1ff2a28326 | ||
|
|
b48905a153 | ||
|
|
09c8d987e1 | ||
|
|
2e52adbef8 | ||
|
|
61c9de6fcc | ||
|
|
41d95814c9 | ||
|
|
c4cb65fc3c | ||
|
|
a1c5974e93 | ||
|
|
0c244f369c | ||
|
|
b6014b017c | ||
|
|
91ea4a6e7b | ||
|
|
1e51c4c5d0 | ||
|
|
fbd28a0564 | ||
|
|
d0965206cd | ||
|
|
bb330c1771 | ||
|
|
14d6c80241 | ||
|
|
4c621364c9 | ||
|
|
2a9d5db51e | ||
|
|
b8d340fbe8 | ||
|
|
dd1814c793 | ||
|
|
adb6181e9f | ||
|
|
0a4b179db5 | ||
|
|
e78db7fa08 | ||
|
|
7be615bacb | ||
|
|
95d58023c7 | ||
|
|
7b8fbc239b | ||
|
|
30d1107cef | ||
|
|
fe16c44caa | ||
|
|
4ed9ad5085 | ||
|
|
755d8311dc | ||
|
|
b6457cc6b9 | ||
|
|
2d51a7ce9a | ||
|
|
0ade0705e2 | ||
|
|
6bc25ccd9b | ||
|
|
ed7007fc8c | ||
|
|
71ad6a2ce5 | ||
|
|
e9a1af0f52 | ||
|
|
10c384ffa7 | ||
|
|
4e51977fb6 | ||
|
|
d632c268ae | ||
|
|
be371e5236 | ||
|
|
25d3068faf | ||
|
|
179c7c1855 | ||
|
|
8018000584 | ||
|
|
657af4fe04 | ||
|
|
219fcd3dd8 | ||
|
|
2e9726b84f | ||
|
|
64c6b0c8d0 | ||
|
|
fcc63b9f09 | ||
|
|
a283ffe2bc | ||
|
|
2811b181b7 | ||
|
|
730f314200 | ||
|
|
81da5c9a1a | ||
|
|
a59bf64677 | ||
|
|
e2c8f85a5b | ||
|
|
dd96c64182 | ||
|
|
9ba702eaa9 | ||
|
|
296b4c1f52 | ||
|
|
48e7effd0a | ||
|
|
b9f353bb5a | ||
|
|
c22e8447b3 | ||
|
|
f810a2d49b | ||
|
|
4f3f7b97fd | ||
|
|
718c466505 | ||
|
|
b8a558303b | ||
|
|
a892aa6dee | ||
|
|
5a6b3cbf09 | ||
|
|
5bdfbf5f6f | ||
|
|
1d7d2801e4 | ||
|
|
5201af70cd | ||
|
|
d0e95bc3c2 | ||
|
|
ffb9af1f1c | ||
|
|
6dcce45c59 | ||
|
|
6f36cae767 | ||
|
|
516039c91d | ||
|
|
8de57304bf | ||
|
|
869371b485 | ||
|
|
929711da98 | ||
|
|
b2816e1459 | ||
|
|
532bd8baa6 | ||
|
|
90ab7a2766 | ||
|
|
ee33a9350f | ||
|
|
f1e6f1ad31 | ||
|
|
11e3e37263 | ||
|
|
da694022ac | ||
|
|
29ade1e5b7 | ||
|
|
88565b70c5 | ||
|
|
e5dbcfc2a1 | ||
|
|
0cda8e4d70 | ||
|
|
7500b6d374 | ||
|
|
a4c7a9c4f7 | ||
|
|
8fc7e1039b | ||
|
|
79f52db929 | ||
|
|
13048cc2fd | ||
|
|
66395b9871 | ||
|
|
65f41beed8 | ||
|
|
f98b49608e | ||
|
|
3bd0ec4466 | ||
|
|
4befff8f42 | ||
|
|
89436b0a75 | ||
|
|
6de5a07e0d | ||
|
|
27b9d70333 | ||
|
|
9a94dc2548 | ||
|
|
b1a8308aaf | ||
|
|
ad7dddaac4 | ||
|
|
5d7f42d127 | ||
|
|
d9e8917418 | ||
|
|
09da7b8d68 | ||
|
|
ca5c7ec966 | ||
|
|
9eff9e8e82 | ||
|
|
5665bc7f93 | ||
|
|
20e5df7d49 | ||
|
|
d3ae925567 | ||
|
|
af82cb2123 | ||
|
|
7df52e3f9c | ||
|
|
6b40560dfc | ||
|
|
54bb789461 | ||
|
|
7979be17c1 | ||
|
|
91564a1dff | ||
|
|
2b5f0e4ac9 | ||
|
|
9b04031c91 | ||
|
|
8ff52e6815 | ||
|
|
c41b5cc9da | ||
|
|
767b625289 | ||
|
|
f45f26e602 | ||
|
|
06a613e855 | ||
|
|
62c5231dc9 | ||
|
|
7a224a9120 | ||
|
|
593335aea3 | ||
|
|
6edc6a22e4 | ||
|
|
230604f5ef | ||
|
|
73f5200c2d | ||
|
|
95b8ac74b9 | ||
|
|
cfc2cfcca1 | ||
|
|
ed1485ca22 | ||
|
|
c49c85e68b | ||
|
|
91ca2e6672 | ||
|
|
8849f8984b | ||
|
|
bb21e4bdcd | ||
|
|
eb617ae8ca | ||
|
|
b3c1474b31 | ||
|
|
21ffcb56fd | ||
|
|
f977e16774 | ||
|
|
012020735f | ||
|
|
3f2077a6db | ||
|
|
f06ae4ebfe | ||
|
|
865824a8e3 | ||
|
|
4d2170257a | ||
|
|
0024e72a2e | ||
|
|
60aca9a5e3 | ||
|
|
d0f6c16878 | ||
|
|
8fded5ef6e | ||
|
|
329a68216e | ||
|
|
30762971db | ||
|
|
7479d96675 | ||
|
|
ed42d85f67 | ||
|
|
78d29bcf20 | ||
|
|
1a13cb3383 | ||
|
|
9289dcc42c | ||
|
|
efdc9c5548 | ||
|
|
69b3544107 | ||
|
|
5b5aeead88 | ||
|
|
4bacac1f8b | ||
|
|
6aeb3c07cc | ||
|
|
2c41176a6e | ||
|
|
e3dd00bcfa | ||
|
|
262efe4d8c | ||
|
|
2eed441462 | ||
|
|
56fa8ceb5a | ||
|
|
7bf9276d1a | ||
|
|
51da9e4dd6 | ||
|
|
731ff62faf | ||
|
|
fdfd7781e9 | ||
|
|
6e090c8d7a | ||
|
|
44966645ca | ||
|
|
669f3043ae | ||
|
|
5a5e660a43 | ||
|
|
2e05a58e8b | ||
|
|
f1f4e8baff | ||
|
|
828f69fc46 | ||
|
|
954a7aee91 | ||
|
|
fa0bdf5747 | ||
|
|
67ac92ff57 | ||
|
|
e1466c866b | ||
|
|
ba89894dc4 | ||
|
|
c46c1d2353 | ||
|
|
30322707fc | ||
|
|
972bf785f1 | ||
|
|
32d485cc51 | ||
|
|
d11439f85d | ||
|
|
681881f4f6 | ||
|
|
939d01b267 | ||
|
|
d233de6103 | ||
|
|
139a296bd3 | ||
|
|
6b56075df8 | ||
|
|
218476acbc | ||
|
|
fa24b93830 | ||
|
|
b2f655522e | ||
|
|
88eedc3506 | ||
|
|
aa84dedd64 | ||
|
|
1c7dce9e12 | ||
|
|
1c4797d3aa | ||
|
|
b2927483fa | ||
|
|
fda87b7823 | ||
|
|
71d3d12020 | ||
|
|
50eb13a850 | ||
|
|
a8e38f4329 | ||
|
|
d32ab15d42 | ||
|
|
8dd1091608 | ||
|
|
44f69fcabd | ||
|
|
8d0da86569 | ||
|
|
98b25acab9 | ||
|
|
40241b4142 | ||
|
|
7a685b1241 | ||
|
|
34943542bf | ||
|
|
6345fef6bf | ||
|
|
a529ba8032 | ||
|
|
e9e2697369 | ||
|
|
12cadd0186 | ||
|
|
1e77b1457a | ||
|
|
43dca1dd14 | ||
|
|
30a7f702a1 | ||
|
|
0293b865b4 | ||
|
|
7566ddf529 | ||
|
|
e653857437 | ||
|
|
7b291535e0 | ||
|
|
9dc9a3b8ab | ||
|
|
5d4474ead6 | ||
|
|
36f844a709 | ||
|
|
e01ecb20fa | ||
|
|
41170d81d9 | ||
|
|
7400872f87 | ||
|
|
aedcd7f9b9 | ||
|
|
bb974f8935 | ||
|
|
77bad3380c | ||
|
|
3570377678 | ||
|
|
d4ababc0a5 | ||
|
|
1c6ebafb29 | ||
|
|
23e1f01783 | ||
|
|
ef032b0f93 | ||
|
|
35a2fe87db | ||
|
|
9f1fe8737f | ||
|
|
2d6583fea6 | ||
|
|
1e64acdf1d | ||
|
|
1f33926ed5 | ||
|
|
16ac08fe21 | ||
|
|
70291a0cb2 | ||
|
|
62136b5b09 | ||
|
|
76078cf3b5 | ||
|
|
be0b1db193 | ||
|
|
a796a8adcf | ||
|
|
efc9ae8fb6 | ||
|
|
d4a9bacd91 | ||
|
|
0e735512bb | ||
|
|
6d64357d45 | ||
|
|
78666ccbde | ||
|
|
39b513da12 | ||
|
|
18f599b554 | ||
|
|
3e7565c7e3 | ||
|
|
87a453cb72 | ||
|
|
fdbe504ca9 | ||
|
|
b2159ed87f | ||
|
|
ecb19d6984 | ||
|
|
940c0740b0 | ||
|
|
cebe1c2a1f | ||
|
|
1d8d1d6b03 | ||
|
|
25e467b3a5 | ||
|
|
038ae9acd4 | ||
|
|
ea20a64b34 | ||
|
|
411184fde8 | ||
|
|
bf89f11606 | ||
|
|
391408ed17 | ||
|
|
9bbb1a98db | ||
|
|
faa58a19de | ||
|
|
d2751595cb | ||
|
|
46cefa4899 | ||
|
|
5bc58284aa | ||
|
|
a6444968fa | ||
|
|
dec56a3342 | ||
|
|
91e41c4c18 | ||
|
|
b714b45bfd | ||
|
|
22238f55cd | ||
|
|
4244373a5d | ||
|
|
75ca96a526 | ||
|
|
0d71da106f | ||
|
|
86cdf96f3d | ||
|
|
e7864ac1dd | ||
|
|
a9058d129e | ||
|
|
369e79be5e | ||
|
|
c963cd9ea4 | ||
|
|
7479767266 | ||
|
|
b55c61ddb8 | ||
|
|
f1e8e48769 | ||
|
|
d0bde4a3fe | ||
|
|
6a32454347 | ||
|
|
4dd16054ca | ||
|
|
4b152fc15f | ||
|
|
c891c2b0df | ||
|
|
70b4f78a5d | ||
|
|
4c9637c821 | ||
|
|
a4b74794cb | ||
|
|
5500b03976 | ||
|
|
0153cbe0ed | ||
|
|
e8970ab7f2 | ||
|
|
a933b92efa | ||
|
|
f2d1f7dbbb | ||
|
|
235e8cdba2 | ||
|
|
c1acdbe31a | ||
|
|
2c8c441f25 | ||
|
|
cb007608d9 | ||
|
|
8a55b5e613 | ||
|
|
b2810bcef1 | ||
|
|
f51b607312 | ||
|
|
3bfa3612c6 | ||
|
|
9a876fa5e2 | ||
|
|
c175a4ee03 | ||
|
|
3030a6fca7 | ||
|
|
73311970c5 | ||
|
|
56ada36b83 | ||
|
|
85d9aef2f3 | ||
|
|
04a07ed655 | ||
|
|
cc74707894 | ||
|
|
64fd10d00e | ||
|
|
015c5d5c63 | ||
|
|
64354fb9e4 | ||
|
|
a8387f01c9 | ||
|
|
ae52267a27 | ||
|
|
f8d519e8eb | ||
|
|
3777f18bf9 | ||
|
|
9dd504e560 | ||
|
|
33548fa57d | ||
|
|
8265cc6306 | ||
|
|
771f370f9a | ||
|
|
fb1ef3212d | ||
|
|
1e5521b434 | ||
|
|
dac3657536 | ||
|
|
d6c4cab207 | ||
|
|
1afff73c24 | ||
|
|
a6152f937e | ||
|
|
794dbb8f92 | ||
|
|
0c969c365b | ||
|
|
238023056f | ||
|
|
381f9eca0c | ||
|
|
874773b881 | ||
|
|
00cfd83521 | ||
|
|
bafae821e2 | ||
|
|
e070c5dbe8 | ||
|
|
fe347c21fd | ||
|
|
088dd2479e | ||
|
|
cf88f9b796 | ||
|
|
5f1d284309 | ||
|
|
ed18a37577 | ||
|
|
cb4ffd8ca8 | ||
|
|
0fdb0044b9 | ||
|
|
c439143dd3 | ||
|
|
5c8c4032e9 | ||
|
|
248c7b0ceb | ||
|
|
183e84c098 | ||
|
|
634bc3ede1 | ||
|
|
17a8845dfd | ||
|
|
a70d585df8 | ||
|
|
3bd7b533d4 | ||
|
|
e388f456dc | ||
|
|
c46928252c | ||
|
|
091ddb5db1 | ||
|
|
9ca285be53 | ||
|
|
707997bd1f | ||
|
|
72a80f0616 | ||
|
|
f011abef1d | ||
|
|
7c826502b6 | ||
|
|
4a46488e4e | ||
|
|
197855af0e | ||
|
|
610919ec67 | ||
|
|
d21c63f1db | ||
|
|
6adedd9789 | ||
|
|
522f1fe192 | ||
|
|
cdc4f89da5 | ||
|
|
bc065c8792 | ||
|
|
7c58891892 | ||
|
|
3e020ed973 | ||
|
|
becc07d26b | ||
|
|
1659a4ce22 | ||
|
|
1e7aabad16 | ||
|
|
f1205c19be | ||
|
|
9c1c95f5cf | ||
|
|
ba7348f83f | ||
|
|
7b0b604834 | ||
|
|
014feb54e5 | ||
|
|
56d0beb22a | ||
|
|
63f1ed8a2d | ||
|
|
c77e5abba7 | ||
|
|
817e07b921 | ||
|
|
82c7dcfe18 | ||
|
|
0a7fe662d4 | ||
|
|
ef3f6b7977 | ||
|
|
3ac608c82c | ||
|
|
f889ed1821 | ||
|
|
95e0a0d143 | ||
|
|
dadc4b6a11 | ||
|
|
ba7a559714 | ||
|
|
c7065e103b | ||
|
|
f47a0dd49c | ||
|
|
d0e64d7886 | ||
|
|
3d49b1f79a | ||
|
|
16a1375800 | ||
|
|
249e284a93 | ||
|
|
2d82a430c4 | ||
|
|
950539c55c | ||
|
|
f751ab4a75 | ||
|
|
3e915f9848 | ||
|
|
eb027b74ce | ||
|
|
9393dda9a4 | ||
|
|
c3cd30ce5c | ||
|
|
9edde234ec | ||
|
|
76e4c38299 | ||
|
|
65ae5060c5 | ||
|
|
b9bb9bcf34 | ||
|
|
93f1e1ec29 | ||
|
|
62d6dea219 | ||
|
|
190135d36c | ||
|
|
ec4a6bc0c4 | ||
|
|
39a0b88e3a | ||
|
|
46df64d208 | ||
|
|
31bb1a73c0 | ||
|
|
096c075548 | ||
|
|
a81d48cc9d | ||
|
|
a3a5980ebd | ||
|
|
060d41f973 | ||
|
|
dc6cc82751 | ||
|
|
9a31d2318e | ||
|
|
862f8f0b04 | ||
|
|
2d63f351ed | ||
|
|
ba6c8fe8d0 | ||
|
|
dbfe3b50f9 | ||
|
|
59da70aca1 | ||
|
|
d2d8eb9153 | ||
|
|
7d9f8d27bc | ||
|
|
7f6b45aeb6 | ||
|
|
34addbe33a | ||
|
|
5be76044b1 | ||
|
|
73e9ed8e26 | ||
|
|
4d8a7dc138 | ||
|
|
5bcbaa1beb | ||
|
|
018a924c53 | ||
|
|
3779cfb6a5 | ||
|
|
36905711d0 | ||
|
|
05ffd123e4 | ||
|
|
923acd802c | ||
|
|
f8e1030164 | ||
|
|
63ec5c8433 | ||
|
|
69cd9c94d1 | ||
|
|
1808699e89 | ||
|
|
0e39d93721 | ||
|
|
c2ac0f659a | ||
|
|
68df749ad2 | ||
|
|
5cdfb85a9c | ||
|
|
48bd3c07b8 | ||
|
|
3dc7180784 | ||
|
|
bb4cfe421a | ||
|
|
22dd69cabb | ||
|
|
bde09072fb | ||
|
|
8fdfb8e6dc | ||
|
|
06e25a074c | ||
|
|
fe7a2f2a8c | ||
|
|
18e03ed038 | ||
|
|
ff21e73928 | ||
|
|
cd045a2a2a | ||
|
|
9a2a56c1cf | ||
|
|
041c293808 | ||
|
|
4545059035 | ||
|
|
7e84a447d4 | ||
|
|
c67cf4b07e | ||
|
|
6b93c34f9e | ||
|
|
d6ac307f63 | ||
|
|
c178c5de41 | ||
|
|
6aa9303d0f | ||
|
|
af028cbddc | ||
|
|
da0b2c28e3 | ||
|
|
4cc5224556 | ||
|
|
c692766c41 | ||
|
|
12ac709aba | ||
|
|
9f904a9515 | ||
|
|
e7504b8344 | ||
|
|
2e0c57b3ce | ||
|
|
e23d03b2db | ||
|
|
feabb6bc20 | ||
|
|
4eb8a82191 | ||
|
|
4bd48aa432 | ||
|
|
04f9ef4819 | ||
|
|
1f2e31d45b | ||
|
|
de95c46627 | ||
|
|
8ce7485153 | ||
|
|
2d29fbe885 | ||
|
|
a595500921 | ||
|
|
200894665b | ||
|
|
9aa1875bc4 | ||
|
|
aa39e509c3 | ||
|
|
6cbeec2119 | ||
|
|
54312fc2ac | ||
|
|
3d1efecb55 | ||
|
|
e56de2c343 | ||
|
|
82b97fc49f | ||
|
|
5a508ae417 | ||
|
|
0a6257b27b | ||
|
|
7dd00efa64 | ||
|
|
5ca3b7834d | ||
|
|
4c687b4335 | ||
|
|
a7a2125e1d | ||
|
|
1f064e29fa | ||
|
|
6d4ba12775 | ||
|
|
c5c5a52a09 | ||
|
|
d408134b7e | ||
|
|
4c43bf2bd4 | ||
|
|
42641644cb | ||
|
|
16960a369a | ||
|
|
ce9dc33045 | ||
|
|
c9317d891e | ||
|
|
30b8968199 | ||
|
|
71a45c02e2 | ||
|
|
21fdd39d7c | ||
|
|
c41940241b | ||
|
|
dae1f2557e | ||
|
|
01a3e14154 | ||
|
|
2a2780a4d2 | ||
|
|
a45ab9cf16 | ||
|
|
cd337b000e | ||
|
|
724e4db0fd | ||
|
|
f404ac9b47 | ||
|
|
78a53fd40d | ||
|
|
e5c4230f97 | ||
|
|
7e5789d539 | ||
|
|
751bc6ce6b | ||
|
|
2d333a2af0 | ||
|
|
72cdb5d0dd | ||
|
|
2a6ab66c11 | ||
|
|
152e824aa6 | ||
|
|
d112be04db | ||
|
|
9041e1bc0c | ||
|
|
7e9aa5b72a | ||
|
|
8df0055a5a | ||
|
|
23492ab11f | ||
|
|
192a17156c | ||
|
|
bd06a74caa | ||
|
|
fec6dba341 | ||
|
|
161e2edc31 | ||
|
|
3e5424e18d | ||
|
|
f73c9dc4d3 | ||
|
|
1b5df47362 | ||
|
|
c94f607107 | ||
|
|
2076bf9b29 | ||
|
|
7b63185d5a | ||
|
|
6ed5deac65 | ||
|
|
dbe739e755 | ||
|
|
b5ce7c735d | ||
|
|
28ea11127c | ||
|
|
2c376da46e | ||
|
|
9e5854766e | ||
|
|
55346c125b | ||
|
|
f5a5159f8c | ||
|
|
535792390e | ||
|
|
0656c88fbe | ||
|
|
f0a1d036a5 | ||
|
|
728712c4e1 | ||
|
|
c43405267a | ||
|
|
9a8b89c19a | ||
|
|
74f2e9cd60 | ||
|
|
26658e91b3 | ||
|
|
4c5d690a9e | ||
|
|
013bc7db39 | ||
|
|
2d5b7cc579 | ||
|
|
1cbce77d5f | ||
|
|
b18117bc07 | ||
|
|
a308a1c8b5 | ||
|
|
8df7f66992 | ||
|
|
d8b7e27b48 | ||
|
|
1895bb3ec2 | ||
|
|
f9090b0c2c | ||
|
|
c6cb97e199 | ||
|
|
ba11f0d06e | ||
|
|
a534c11bbb | ||
|
|
0c3085257d | ||
|
|
72a6fd2c90 | ||
|
|
60046f4881 | ||
|
|
8db0b54929 | ||
|
|
ab9e57f5be | ||
|
|
7ec2a22fce | ||
|
|
8bafee3801 | ||
|
|
a8545abff2 | ||
|
|
9f7e407181 | ||
|
|
84ffffbbe7 | ||
|
|
5661e8c332 | ||
|
|
f1276e4697 | ||
|
|
805076ee6b | ||
|
|
611f40801e | ||
|
|
9e970123fd | ||
|
|
8222c4a42c | ||
|
|
71de19f27b | ||
|
|
96b2608eb1 | ||
|
|
02ce017c98 | ||
|
|
0541aacdff | ||
|
|
8789879f10 | ||
|
|
5225f3e7f2 | ||
|
|
97a208dcfa | ||
|
|
e8e87b1d1c | ||
|
|
8b8eb84fae | ||
|
|
3d0e65c92c | ||
|
|
ab3540312a | ||
|
|
495d001dc9 | ||
|
|
3c1cdf18a1 | ||
|
|
1948bab873 | ||
|
|
ef47b7025e | ||
|
|
2c086b3d79 | ||
|
|
cbf022d2f2 | ||
|
|
b7323d0913 | ||
|
|
1ebbe63fc7 | ||
|
|
86608ede54 | ||
|
|
6319da5445 | ||
|
|
11e7b1cde7 | ||
|
|
1aa07f9368 | ||
|
|
56e21ab7c8 | ||
|
|
87d1255323 | ||
|
|
38b1a014dd | ||
|
|
dd605fba87 | ||
|
|
54c7187bba | ||
|
|
a16d9f7473 | ||
|
|
b3a4e8731e | ||
|
|
1f738eb7c4 | ||
|
|
f2e7c7645c | ||
|
|
b7090d90c4 | ||
|
|
e5c789e874 | ||
|
|
e99ff1c044 | ||
|
|
31b7357bdb | ||
|
|
908d3c43a4 | ||
|
|
94b2f891ab | ||
|
|
54ab87013c | ||
|
|
6a70c90643 | ||
|
|
050a06b258 | ||
|
|
4b2d6d3280 | ||
|
|
9a52e7f86a | ||
|
|
5df18fb4a6 | ||
|
|
3d525d4eb3 | ||
|
|
8b054926fe | ||
|
|
c818e8b7c2 | ||
|
|
77dbafaa9b | ||
|
|
26400c930d | ||
|
|
d9e41b9d2b | ||
|
|
3ae5bb5cd7 | ||
|
|
eef82d04eb | ||
|
|
7e5e17ea9e | ||
|
|
b3c908583e | ||
|
|
96431dd01d | ||
|
|
e1e4f9329a | ||
|
|
9028e0ed32 | ||
|
|
76337454da | ||
|
|
2dafc0d2ad | ||
|
|
e7b15b1160 | ||
|
|
60764f6f73 | ||
|
|
3118a8368b | ||
|
|
d967279abc | ||
|
|
a85decd2cd | ||
|
|
d87f7dec3c | ||
|
|
8be5526b7a | ||
|
|
5e35db159d | ||
|
|
861d2b9bf6 | ||
|
|
9620f6eee0 | ||
|
|
b113f3c986 | ||
|
|
2ab100d99f | ||
|
|
c901140796 | ||
|
|
327541c7b1 | ||
|
|
fb166b52ec | ||
|
|
187872114c | ||
|
|
6f96e8959b | ||
|
|
b7913fa027 | ||
|
|
39a2fbe3d5 | ||
|
|
6166fde7cd | ||
|
|
14b6c455bf | ||
|
|
2ddb6b2e5e | ||
|
|
6b8954b4a9 | ||
|
|
6c14163c66 | ||
|
|
57d7159ee0 | ||
|
|
c086ff264f | ||
|
|
1490710a51 | ||
|
|
d4917b5455 | ||
|
|
c9dfc03a20 | ||
|
|
af581612f6 | ||
|
|
04aec20fc9 | ||
|
|
988b83dc32 | ||
|
|
f7164d0b78 | ||
|
|
64652b987d | ||
|
|
f13085147f | ||
|
|
26eaa4650d | ||
|
|
870d138d11 | ||
|
|
f06dedaebc | ||
|
|
8553a96358 | ||
|
|
a708c811d6 | ||
|
|
b18bc8ff4b | ||
|
|
0d90ac3c53 | ||
|
|
f69ecde713 | ||
|
|
733c8709fc | ||
|
|
05fbfce865 | ||
|
|
048b78a03a | ||
|
|
8aee7a680d | ||
|
|
fd6ba00fbd | ||
|
|
373c6201bd |
1624 changed files with 87745 additions and 94207 deletions
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
name: "\U0001F41E Bug report"
|
name: "\U0001F41E Bug report"
|
||||||
description: Create a report to help us improve.
|
description: Create a report to help us improve.
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug"]
|
type: Bug # Retained to categorize the issue as per organization-level type
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|
@ -70,7 +70,7 @@ body:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Screen-shots
|
label: Screenshots
|
||||||
description: Add screenshots related to the issue (if available). Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher.
|
description: Add screenshots related to the issue (if available). Can be created by pressing the Volume Down and Power Button at the same time on Android 4.0 and higher.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
|
||||||
22
.github/workflows/android.yml
vendored
22
.github/workflows/android.yml
vendored
|
|
@ -12,17 +12,17 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|
||||||
- name: Cache packages
|
- name: Cache packages
|
||||||
id: cache-packages
|
id: cache-packages
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
|
|
@ -37,7 +37,7 @@ jobs:
|
||||||
|
|
||||||
- name: AVD cache
|
- name: AVD cache
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
id: avd-cache
|
id: avd-cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -89,7 +89,7 @@ jobs:
|
||||||
run: bash ./gradlew assembleBetaDebug --stacktrace
|
run: bash ./gradlew assembleBetaDebug --stacktrace
|
||||||
|
|
||||||
- name: Upload betaDebug APK
|
- name: Upload betaDebug APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: betaDebugAPK
|
name: betaDebugAPK
|
||||||
path: app/build/outputs/apk/beta/debug/app-*.apk
|
path: app/build/outputs/apk/beta/debug/app-*.apk
|
||||||
|
|
@ -98,7 +98,17 @@ jobs:
|
||||||
run: bash ./gradlew assembleProdDebug --stacktrace
|
run: bash ./gradlew assembleProdDebug --stacktrace
|
||||||
|
|
||||||
- name: Upload prodDebug APK
|
- name: Upload prodDebug APK
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: prodDebugAPK
|
name: prodDebugAPK
|
||||||
path: app/build/outputs/apk/prod/debug/app-*.apk
|
path: app/build/outputs/apk/prod/debug/app-*.apk
|
||||||
|
|
||||||
|
- name: Create and PR number artifact
|
||||||
|
run: |
|
||||||
|
echo "{\"pr_number\": ${{ github.event.pull_request.number || 'null' }}}" > pr_number.json
|
||||||
|
|
||||||
|
- name: Upload PR number artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: pr_number
|
||||||
|
path: ./pr_number.json
|
||||||
|
|
|
||||||
41
.github/workflows/build-beta.yml
vendored
Normal file
41
.github/workflows/build-beta.yml
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
name: Build beta only
|
||||||
|
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
cache: gradle
|
||||||
|
|
||||||
|
- name: Access test login credentials
|
||||||
|
run: |
|
||||||
|
echo "TEST_USER_NAME=${{ secrets.TEST_USER_NAME }}" >> local.properties
|
||||||
|
echo "TEST_USER_PASSWORD=${{ secrets.TEST_USER_PASSWORD }}" >> local.properties
|
||||||
|
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Set env
|
||||||
|
run: echo "COMMIT_SHA=$(git log -n 1 --format='%h')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Generate betaDebug APK
|
||||||
|
run: ./gradlew assembleBetaDebug --stacktrace
|
||||||
|
|
||||||
|
- name: Rename betaDebug APK
|
||||||
|
run: mv app/build/outputs/apk/beta/debug/app-*.apk app/build/outputs/apk/beta/debug/apps-android-commons-betaDebug-$COMMIT_SHA.apk
|
||||||
|
|
||||||
|
- name: Upload betaDebug APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: apps-android-commons-betaDebugAPK-${{ env.COMMIT_SHA }}
|
||||||
|
path: app/build/outputs/apk/beta/debug/*.apk
|
||||||
|
retention-days: 30
|
||||||
96
.github/workflows/comment_artifacts_on_PR.yml
vendored
Normal file
96
.github/workflows/comment_artifacts_on_PR.yml
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
name: Comment Artifacts on PR
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [ "Android CI" ]
|
||||||
|
types: [ completed ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: comment-${{ github.event.workflow_run.id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
|
||||||
|
steps:
|
||||||
|
- name: Download and process artifacts
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const runId = context.payload.workflow_run.id;
|
||||||
|
|
||||||
|
const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: runId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prNumberArtifact = allArtifacts.data.artifacts.find(artifact => artifact.name === "pr_number");
|
||||||
|
if (!prNumberArtifact) {
|
||||||
|
console.log("pr_number artifact not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const download = await github.rest.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: prNumberArtifact.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr_number.zip`, Buffer.from(download.data));
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
execSync('unzip -q pr_number.zip -d ./pr_number/');
|
||||||
|
fs.unlinkSync('pr_number.zip');
|
||||||
|
|
||||||
|
const prData = JSON.parse(fs.readFileSync('./pr_number/pr_number.json', 'utf8'));
|
||||||
|
const prNumber = prData.pr_number;
|
||||||
|
|
||||||
|
if (!prNumber || prNumber === 'null') {
|
||||||
|
console.log("No valid PR number found in pr_number.json. Skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const artifactsToLink = allArtifacts.data.artifacts.filter(artifact => artifact.name !== "pr_number");
|
||||||
|
if (artifactsToLink.length === 0) {
|
||||||
|
console.log("No artifacts to link found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const comments = await github.rest.issues.listComments({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: Number(prNumber),
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldComments = comments.data.filter(comment =>
|
||||||
|
comment.body.startsWith("✅ Generated APK variants!")
|
||||||
|
);
|
||||||
|
for (const comment of oldComments) {
|
||||||
|
await github.rest.issues.deleteComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: comment.id,
|
||||||
|
});
|
||||||
|
console.log(`Deleted old comment ID: ${comment.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commentBody = `✅ Generated APK variants!\n` +
|
||||||
|
artifactsToLink.map(artifact => {
|
||||||
|
const artifactUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/artifacts/${artifact.id}`;
|
||||||
|
return `- 🤖 [Download ${artifact.name}](${artifactUrl})`;
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: Number(prNumber),
|
||||||
|
body: commentBody
|
||||||
|
});
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -43,3 +43,8 @@ app/src/main/jniLibs
|
||||||
#https://docs.opencv.org/3.3.0/
|
#https://docs.opencv.org/3.3.0/
|
||||||
/libraries/opencv/javadoc/
|
/libraries/opencv/javadoc/
|
||||||
captures/*
|
captures/*
|
||||||
|
|
||||||
|
# Test and other output
|
||||||
|
app/jacoco.exec
|
||||||
|
app/CommonsContributions
|
||||||
|
app/.*
|
||||||
|
|
|
||||||
3
.idea/codeStyles/Project.xml
generated
3
.idea/codeStyles/Project.xml
generated
|
|
@ -16,6 +16,7 @@
|
||||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
|
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
|
||||||
<option name="IMPORT_LAYOUT_TABLE">
|
<option name="IMPORT_LAYOUT_TABLE">
|
||||||
<value>
|
<value>
|
||||||
|
<package name="" withSubpackages="true" static="false" module="true" />
|
||||||
<package name="" withSubpackages="true" static="true" />
|
<package name="" withSubpackages="true" static="true" />
|
||||||
<emptyLine />
|
<emptyLine />
|
||||||
<package name="" withSubpackages="true" static="false" />
|
<package name="" withSubpackages="true" static="false" />
|
||||||
|
|
@ -315,9 +316,7 @@
|
||||||
<codeStyleSettings language="protobuf">
|
<codeStyleSettings language="protobuf">
|
||||||
<option name="RIGHT_MARGIN" value="80" />
|
<option name="RIGHT_MARGIN" value="80" />
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
<option name="INDENT_SIZE" value="2" />
|
|
||||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
<option name="TAB_SIZE" value="2" />
|
|
||||||
</indentOptions>
|
</indentOptions>
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
</code_scheme>
|
</code_scheme>
|
||||||
|
|
|
||||||
52
.idea/inspectionProfiles/Project_Default.xml
generated
52
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -1,16 +1,36 @@
|
||||||
<component name="InspectionProjectProfileManager">
|
<component name="InspectionProjectProfileManager">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
<inspection_tool class="AndroidLintNewerVersionAvailable" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
<inspection_tool class="ConfusingElse" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="ConfusingElse" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="reportWhenNoStatementFollow" value="true" />
|
<option name="reportWhenNoStatementFollow" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="ERROR" enabled_by_default="true" />
|
<inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
<inspection_tool class="DefaultNotLastCaseInSwitch" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="ExplicitThis" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
<inspection_tool class="ExplicitThis" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="FieldMayBeFinal" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="REPORT_VARIABLES" value="true" />
|
<option name="REPORT_VARIABLES" value="true" />
|
||||||
<option name="REPORT_PARAMETERS" value="true" />
|
<option name="REPORT_PARAMETERS" value="true" />
|
||||||
|
|
@ -24,14 +44,33 @@
|
||||||
<inspection_tool class="OverlyStrongTypeCast" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="OverlyStrongTypeCast" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="ignoreInMatchingInstanceof" value="false" />
|
<option name="ignoreInMatchingInstanceof" value="false" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
<inspection_tool class="ProblematicWhitespace" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="ProblematicWhitespace" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="ProtectedMemberInFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="RedundantFieldInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="ignoreSerializable" value="false" />
|
<option name="ignoreSerializable" value="false" />
|
||||||
<option name="ignoreCloneable" value="false" />
|
<option name="ignoreCloneable" value="false" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="RedundantMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="SimplifiableEqualsExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="SimplifiableEqualsExpression" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="TypeParameterExtendsFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="TypeParameterExtendsFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="UnnecessarilyQualifiedStaticUsage" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="UnnecessarilyQualifiedStaticUsage" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
|
@ -47,6 +86,5 @@
|
||||||
<inspection_tool class="UnnecessaryQualifierForThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="UnnecessaryQualifierForThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="UnnecessarySuperConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="UnnecessarySuperConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="UnnecessaryThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="UnnecessaryThis" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
<inspection_tool class="UnnecessaryToStringCall" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
5
.mailmap
Normal file
5
.mailmap
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# See: https://git-scm.com/docs/git-shortlog#_mapping_authors
|
||||||
|
#
|
||||||
|
Brooke Vibber <bvibber@wikimedia.org>
|
||||||
|
Brooke Vibber <bvibber@wikimedia.org> <brion@wikimedia.org>
|
||||||
|
Brooke Vibber <bvibber@wikimedia.org> <brion@pobox.com>
|
||||||
286
CHANGELOG.md
286
CHANGELOG.md
|
|
@ -1,5 +1,291 @@
|
||||||
# Wikimedia Commons for Android
|
# Wikimedia Commons for Android
|
||||||
|
|
||||||
|
## v6.0.2
|
||||||
|
|
||||||
|
### What's changed
|
||||||
|
* Addressed a bug that prevented the keyboard from appearing in various text fields, such as on the upload wizard
|
||||||
|
* Links in the "File usages" list are now clickable and will take you to the correct page.
|
||||||
|
* Titles for file usages are now clearer and easier to understand
|
||||||
|
* Bug fixes and stability improvements
|
||||||
|
|
||||||
|
## v6.0.1
|
||||||
|
|
||||||
|
### What's changed
|
||||||
|
* The app now supports Android 15 with an improved user interface
|
||||||
|
* Enhanced Nearby with robust and more reliable labels
|
||||||
|
* Bug fixes and stability improvements
|
||||||
|
|
||||||
|
## v5.6.1
|
||||||
|
|
||||||
|
### What's changed
|
||||||
|
* The app no longer uploads images to Wikidata if one exists already for a given item
|
||||||
|
* File usage displays correctly now
|
||||||
|
* No more infinite circular progress bar on nominating an image for deletion
|
||||||
|
* Enhanced location updates while using GPS
|
||||||
|
* Author/uploader names are now available in Media Details for Commons licensing compliance
|
||||||
|
* Improved usage of popups in Nearby
|
||||||
|
* Bug fixes and stability improvements
|
||||||
|
|
||||||
|
## v5.5.0
|
||||||
|
|
||||||
|
### What's changed
|
||||||
|
* Explore images will now be shown based on the map location and not at your current location
|
||||||
|
* Enhanced Wikidata feedback message
|
||||||
|
* Green labels in Explore map will no longer be hidden by other pins thumbnails
|
||||||
|
* Upload wizard's language drop-down now reflects the language used in the pin label
|
||||||
|
* Users can now pick only one image at a time while using the custom selector
|
||||||
|
* Bug fixes and stability improvements
|
||||||
|
|
||||||
|
## v5.4.1
|
||||||
|
|
||||||
|
### What's changed
|
||||||
|
* Custom picker now detects images that are already available on Commons
|
||||||
|
* Improve credit line in image list
|
||||||
|
* Show place cards with loaded names only in the Nearby list
|
||||||
|
* Fix the error that occurs while loading images in Explore
|
||||||
|
|
||||||
|
## v5.3.0
|
||||||
|
|
||||||
|
### What's changed
|
||||||
|
* Enable EmailAuth support
|
||||||
|
* Explore map images no longer show "Unknown"
|
||||||
|
* Fix crash when removing last two images of multiupload
|
||||||
|
* Mark ❌ for closed locations (P3999) in Nearby
|
||||||
|
* Fix two pin labels staying visible at the same time in Explore map
|
||||||
|
* Refactoring and minor UI improvements
|
||||||
|
|
||||||
|
## v5.2.0
|
||||||
|
|
||||||
|
v5.2.0 boasts several new functionalities like:
|
||||||
|
|
||||||
|
* A new refresh button lets you quickly reload the Nearby map
|
||||||
|
* Bookmarks now support categories
|
||||||
|
* Improved feedback and consistency in the user interface
|
||||||
|
* Bug fixes and performance improvements
|
||||||
|
|
||||||
|
### What's changed
|
||||||
|
* Implement "Refresh" button to clear the cache and reload the Nearby map.
|
||||||
|
* `CommonsApplication` migrate to kotlin & some lint fixes.
|
||||||
|
* Revert back to MainScope for database and UI updates and make database operations thread safe.
|
||||||
|
* Hide edit options for logged-out users in Explore screen.
|
||||||
|
* Introduced a button to delete the current folder in custom selector.
|
||||||
|
* Improve Unique File Name Search.
|
||||||
|
* Migration of several modules from Java to Kotlin.
|
||||||
|
* Fix modification on bottom sheet's data when coming from Nearby Banner and clicked on other pins.
|
||||||
|
* Bug fixes and enhancement of Achievements screen.
|
||||||
|
* Show where file is being used on Commons and other wikis.
|
||||||
|
* Migrate android.media.ExifInterface to androidx.exifinterface.media.ExifInterface as android.media.ExifInterface had security flaws on older devices.
|
||||||
|
* Make dialogs modal and always show the upload icon.
|
||||||
|
* Fix unintentional deletion of subfolders and non-images by custom selector.
|
||||||
|
* Bookmark categories.
|
||||||
|
* Add pull down to refresh in the Contributions screen.
|
||||||
|
* Fix race condition and lag when loading pin details, faster overlay management.
|
||||||
|
* Show cached pins in Nearby even when internet is unavailable
|
||||||
|
|
||||||
|
Full changelog with the list of contributors: [`v5.1.2...v5.2.0`](https://github.com/commons-app/apps-android-commons/compare/v5.1.2...v5.2.0).
|
||||||
|
|
||||||
|
|
||||||
|
## v5.1.2
|
||||||
|
|
||||||
|
### What's changed
|
||||||
|
|
||||||
|
* Fix the broken category search in the explore screen
|
||||||
|
|
||||||
|
## v5.1.1
|
||||||
|
|
||||||
|
### What's changed
|
||||||
|
|
||||||
|
* Use Android's new EXIF interface to mitigate security issues in old
|
||||||
|
EXIF interface.
|
||||||
|
* Make the icon that helps view the upload queue always visible as it ensures
|
||||||
|
that the queue accessible at all times.
|
||||||
|
|
||||||
|
## v5.1.0
|
||||||
|
|
||||||
|
### What's Changed
|
||||||
|
|
||||||
|
* Enhanced **upload queue management** in the Commons app for smoother, sequential
|
||||||
|
processing, clearer progress tracking, prevention of stuck or duplicate
|
||||||
|
uploads. As part of this improvement, the "Limited Connection mode" has been
|
||||||
|
removed.
|
||||||
|
* Added an option in "Nearby" feature enabling users to **provide feedback on
|
||||||
|
Wikidata items**. Users can report if an item doesn’t exist, is at a different
|
||||||
|
location, or has other issues, with submissions tagged for easy tracking and
|
||||||
|
updates.
|
||||||
|
* Improved the "Nearby" feature by splitting the query into two parts for faster
|
||||||
|
loading and **better performance, especially in areas with dense amount of
|
||||||
|
places**. This update also resolves issues with pins overlapping place names.
|
||||||
|
* Upgraded AGP and **target/compile SDK to 34** and make necessary adjustments to
|
||||||
|
the app such as adding **"Partial Access" support**. Also includes some minor
|
||||||
|
refactoring, and replacement of deprecated circular progress bars.
|
||||||
|
* Fixed an **UI issue where the 'Subcategories' and 'Parent Categories' tabs
|
||||||
|
appeared blank** in the Category Details screen. Resolved by optimizing view
|
||||||
|
binding handling in the parent fragments.
|
||||||
|
* Fixed an issue where editing depictions removed all other structured data from
|
||||||
|
images. Now, **only depictions are updated, preserving other associated data**.
|
||||||
|
* Fixed **map centering** in the image upload flow to **use GPS EXIF tag location**
|
||||||
|
from pictures and ensured "Show in map app" accurately reflects this location.
|
||||||
|
* Fixed navigation **after uploading via Nearby by directing users to the Uploads
|
||||||
|
activity** instead of returning to Nearby, preventing confusion about needing to
|
||||||
|
upload again.
|
||||||
|
|
||||||
|
### Bug fixes and various changes
|
||||||
|
|
||||||
|
* Improved the "Nearby" feature to fetch labels based on the user's preferred
|
||||||
|
language instead of defaulting to English.
|
||||||
|
* Added a legend to the "Nearby" feature indicating pin statuses: red for items
|
||||||
|
without pictures, green for those with pictures, and grey for items being
|
||||||
|
checked. A floating action button now allows users to toggle the legend's
|
||||||
|
visibility.
|
||||||
|
* Fixed an issue where the "Nominate for deletion" option is shown to logged out
|
||||||
|
users, preventing app errors and crashes.
|
||||||
|
* Updated the regex pattern that filters categories with an year in it to also
|
||||||
|
filter the 2020s.
|
||||||
|
* Fix an issue where past depictions were not shown as suggestions, despite
|
||||||
|
being saved correctly.
|
||||||
|
* Fixed an issue in custom image picker where exiting the media preview showed
|
||||||
|
only the first image and cleared selections. Now, previously selected images
|
||||||
|
are restored correctly after exiting the preview. This was contributed.
|
||||||
|
* Fixed an issue in custom image picker where scrolling behavior did not
|
||||||
|
maintain position after exiting fullscreen preview, ensuring users remain at
|
||||||
|
the same point in their image roll unless actioned images are filtered. This
|
||||||
|
was contributed.
|
||||||
|
* Fixed Nearby map not showing new pins on map move by removing the 2000m scroll
|
||||||
|
threshold and adding an 800ms debounce for smoother pin updates when the map
|
||||||
|
is moved. Queued searches are now canceled on fragment destruction.
|
||||||
|
* Revised author information retrieval to emphasize the custom author name from
|
||||||
|
the metadata instead of the default registered username.
|
||||||
|
* Enhanced notification classification to properly identify "email" type
|
||||||
|
notifications and prompting users to check their e-mail inbox when such
|
||||||
|
notifications are clicked.
|
||||||
|
* Resolved a bug in the language chooser that incorrectly greyed-out previously
|
||||||
|
selected languages, ensuring only the current language is non-selectable during
|
||||||
|
image upload.
|
||||||
|
* Resolved pin color update issue in "Nearby" feature where the pin colour
|
||||||
|
failed to be updated after a successful image upload.
|
||||||
|
|
||||||
|
What's listed here is only a subset of all the changes. Check the full-list of
|
||||||
|
the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v5.0.2...v5.1.0).
|
||||||
|
Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.1.0)
|
||||||
|
for an exhaustive list of changes and the various contributors who contributed the same.
|
||||||
|
|
||||||
|
## v5.0.2
|
||||||
|
|
||||||
|
- Enhanced multi-upload functionality with user prompts to clarify that all images would share the
|
||||||
|
same category and depictions.
|
||||||
|
- Show Wikidata description on currently active Nearby pin to provide more useful information.
|
||||||
|
- Improve the visibility of map markers by dynamically adjusting their colors based on the app's
|
||||||
|
theme. The map markers will now appear lighter when the app is in dark mode and darker when the
|
||||||
|
app is in light mode. This change aims to enhance marker visibility and improve the overall user
|
||||||
|
experience.
|
||||||
|
- Added information on where user feedback is posted, helping users track existing feedback and
|
||||||
|
monitor their own submissions.
|
||||||
|
- Enhanced the edit location screen of the upload screen by centering the map on the picture's
|
||||||
|
location from metadata when editing, or on the device's GPS location if metadata is unavailable,
|
||||||
|
improving accuracy and user experience.
|
||||||
|
- Ensured the 'Add Location' button is renamed to 'Edit Location' when copying the location of a
|
||||||
|
recently uploaded image, enhancing clarity and user experience.
|
||||||
|
- Added a ProgressBar to the media detail screen to indicate image loading status, enhancing user
|
||||||
|
experience by showing loading progress until the image is fully loaded.
|
||||||
|
- Fixed an issue where caption and description fields would intermittently disappear when using
|
||||||
|
voice input, ensuring text remains visible and stable across all entries.
|
||||||
|
- Fixed a crash that occurred when attempting to remove multiple instances of caption/description
|
||||||
|
fields after initially adding them.
|
||||||
|
- Improve the text in the prompt shown when skipping login to sound more natural.
|
||||||
|
- Modified feedback addition logic to append new sections at the bottom of the page, ensuring
|
||||||
|
auto-archiving of sections functions correctly on the feedback page.
|
||||||
|
- Resolved issue where the app failed to clear cookies upon logout.
|
||||||
|
|
||||||
|
## v5.0.1
|
||||||
|
|
||||||
|
Same as v5.0.0 except this fixes some R8 rules to ensure that the release
|
||||||
|
variants of the app work as intended.
|
||||||
|
|
||||||
|
## v5.0.0
|
||||||
|
|
||||||
|
### What's Changed
|
||||||
|
|
||||||
|
- Redesigned the map feature to **replace Mapbox with the osmdroid library**.
|
||||||
|
Key elements like pin visualization and user-centered display are still
|
||||||
|
included in this redesign. This is done to guard against possible misuse of
|
||||||
|
the Mapbox token and, more crucially, to keep the app from becoming dependent
|
||||||
|
on a service that charges for usage but offers a free tier.
|
||||||
|
|
||||||
|
With this change, the app retrieves the map tiles from [Wikimedia maps](https://maps.wikimedia.org).
|
||||||
|
- Add the ability to **export locations of nearby missing pictures in GPX and
|
||||||
|
KML formats**. This allows users to browse the locations with desired radius
|
||||||
|
for offline use in their favourite map apps like OsmAnd or Maps.me, enhancing
|
||||||
|
accessibility and offline functionality.
|
||||||
|
- **Limited the uploads via the custom image picker** to a maximum of 20.
|
||||||
|
- Added two menu choices for **transparent image backgrounds**, giving users the
|
||||||
|
option of either a black or white background, increasing adaptability to
|
||||||
|
various theme settings.
|
||||||
|
|
||||||
|
User customization option has been provided with the
|
||||||
|
ability to save background color selections permanently on a per image basis.
|
||||||
|
- Implemented functionality to **automatically resume uploads** that become
|
||||||
|
stuck due to app termination or device reboot.
|
||||||
|
- Added a **compass arrow in the Nearby banner** shown in the "Contributions"
|
||||||
|
screen to guide users towards the nearest item, thus providing the missing
|
||||||
|
directional cues. The arrow dynamically adjusts based on device rotation,
|
||||||
|
aligning with the calculated bearing towards the target location. Further,
|
||||||
|
the distance and direction are updated as the user moves.
|
||||||
|
- Implemented **voice input feature** for caption and description fields,
|
||||||
|
enabling users to dictate text directly into these fields.
|
||||||
|
- Improved various flows in the app to **redirect users to the login page** and
|
||||||
|
display a persistent message **if their session becomes invalid** due to a
|
||||||
|
password change, enhancing user guidance and security measures.
|
||||||
|
|
||||||
|
### Revamps and refactorings
|
||||||
|
|
||||||
|
- **Revamped initial upload screen layout and the description edit screen layout**
|
||||||
|
for enhanced user experience and ensuring better symmetry in the design.
|
||||||
|
- **Replaced Butterknife with ViewBinding** in various places of the app.
|
||||||
|
- Transferred essential code from **the redundant data-client module** to the
|
||||||
|
main Commons app code, enabling its integration and facilitating the removal
|
||||||
|
of the redundant module. Further, convert various parts of the code to Kotlin.
|
||||||
|
- **Revamped the various location permission flows** to ensure consistency for
|
||||||
|
the sake of a better user experience.
|
||||||
|
|
||||||
|
### Bug fixes and various changes
|
||||||
|
|
||||||
|
- Resolved an issue where paused uploads that were subsequently cancelled were
|
||||||
|
still being uploaded.
|
||||||
|
- Fixed an issue where some user information such as upload count were not
|
||||||
|
displayed in the "Contributions" and "Profile" screens.
|
||||||
|
- Fixed the long-standing broken *"Picture of the Day" widget* to restore its
|
||||||
|
usability.
|
||||||
|
- Resolved an issue where some categories were hidden at the top of Upload
|
||||||
|
Wizard suggestions.
|
||||||
|
- Resolved an issue where there was a grey empty screen at Upload wizard when
|
||||||
|
the app was denied the files permission.
|
||||||
|
- Implemented logic to bypass media in Peer Review if the current reviewer is
|
||||||
|
also the user who uploaded the media.
|
||||||
|
- Corrected arrow image behaviour in the first upload screen: now displays down
|
||||||
|
arrow when details card is fully visible, aligning with expected user
|
||||||
|
interaction.
|
||||||
|
- Updated app icon to improve visibility and recognition on F-Droid.
|
||||||
|
- Fixed issue causing all pictures to disappear and activity to reload fully in
|
||||||
|
the custom image selector after marking a picture as 'not for upload', now
|
||||||
|
ensuring only the selected picture is removed as expected.
|
||||||
|
|
||||||
|
What's listed here is only a subset of all the changes. Check the full-list of
|
||||||
|
the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v4.2.1...v5.0.0).
|
||||||
|
Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.0.0)
|
||||||
|
for an exhaustive list of changes and the various contributors who contributed the same.
|
||||||
|
|
||||||
|
## v4.2.1
|
||||||
|
|
||||||
|
- Provide the ability to edit an image to losslessly rotate it while uploading
|
||||||
|
- Fix a bug in v4.2.0 where the nearby places were not loading
|
||||||
|
- Fix a bug where editing depictions was showing a progress bar indefinitely
|
||||||
|
- In the upload screen, use different map icons to indicate if image is being uploaded with location
|
||||||
|
metadata
|
||||||
|
- For nearby uploads, it is no longer possible to deselect the item's category and depiction
|
||||||
|
- The Mapbox account key used by the app has been changed
|
||||||
|
- Category search now shows exact matches without any discrepancies
|
||||||
|
- Various bug and crash fixes
|
||||||
|
|
||||||
## v4.2.0
|
## v4.2.0
|
||||||
- Dark mode colour improvements
|
- Dark mode colour improvements
|
||||||
- Enhancements done to address location metadata loss including the metadata loss that occurs in
|
- Enhancements done to address location metadata loss including the metadata loss that occurs in
|
||||||
|
|
|
||||||
1
CREDITS
1
CREDITS
|
|
@ -53,7 +53,6 @@ their contribution to the product.
|
||||||
* Butterknife
|
* Butterknife
|
||||||
* GSON
|
* GSON
|
||||||
* Timber
|
* Timber
|
||||||
* MapBox
|
|
||||||
|
|
||||||
3rd party open source apps from which significant code has been reused:
|
3rd party open source apps from which significant code has been reused:
|
||||||
* Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia
|
* Android Wikipedia app https://github.com/wikimedia/apps-android-wikipedia
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Wikimedia Commons Android app
|
# Wikimedia Commons Android app
|
||||||

|

|
||||||
[](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amaster)
|
[](https://github.com/commons-app/apps-android-commons/actions?query=branch%3Amain)
|
||||||
[](https://appetize.io/app/8ywtpe9f8tb8h6bey11c92vkcw)
|
[](https://appetize.io/app/8ywtpe9f8tb8h6bey11c92vkcw)
|
||||||
[](https://codecov.io/gh/commons-app/apps-android-commons)
|
[](https://codecov.io/gh/commons-app/apps-android-commons)
|
||||||
|
|
||||||
|
|
@ -29,11 +29,12 @@ Thank you all for your work!
|
||||||
|
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/3611199?v=4" width="100px;"/><br /><sub><b>misaochan</b></sub>](https://github.com/misaochan) | [<img src="https://avatars.githubusercontent.com/u/24829418?v=4" width="100px;"/><br /><sub><b>translatewiki</b></sub>](https://github.com/translatewiki) | [<img src="https://avatars.githubusercontent.com/u/3127881?v=4" width="100px;"/><br /><sub><b>neslihanturan</b></sub>](https://github.com/neslihanturan) | [<img src="https://avatars.githubusercontent.com/u/30430?v=4" width="100px;"/><br /><sub><b>yuvipanda</b></sub>](https://github.com/yuvipanda) | [<img src="https://avatars.githubusercontent.com/u/99590?v=4" width="100px;"/><br /><sub><b>nicolas-raoul</b></sub>](https://github.com/nicolas-raoul) |
|
| [<img src="https://avatars.githubusercontent.com/u/3611199?v=4" width="100px;"/><br /><sub><b>misaochan</b></sub>](https://github.com/misaochan) | [<img src="https://avatars.githubusercontent.com/u/24829418?v=4" width="100px;"/><br /><sub><b>translatewiki</b></sub>](https://github.com/translatewiki) | [<img src="https://avatars.githubusercontent.com/u/3127881?v=4" width="100px;"/><br /><sub><b>neslihanturan</b></sub>](https://github.com/neslihanturan) | [<img src="https://avatars.githubusercontent.com/u/30430?v=4" width="100px;"/><br /><sub><b>yuvipanda</b></sub>](https://github.com/yuvipanda) | [<img src="https://avatars.githubusercontent.com/u/99590?v=4" width="100px;"/><br /><sub><b>nicolas-raoul</b></sub>](https://github.com/nicolas-raoul) |
|
||||||
| :---: | :---: | :---: | :---: | :---: |
|
| :---: | :---: | :---: | :---: | :---: |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) | [<img src="https://avatars.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>brion</b></sub>](https://github.com/brion) | [<img src="https://avatars.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) |
|
| [<img src="https://avatars.githubusercontent.com/u/407647?v=4" width="100px;"/><br /><sub><b>psh</b></sub>](https://github.com/psh) | [<img src="https://avatars.githubusercontent.com/u/4953590?v=4" width="100px;"/><br /><sub><b>domdomegg</b></sub>](https://github.com/domdomegg) | [<img src="https://avatars.githubusercontent.com/u/3069373?v=4" width="100px;"/><br /><sub><b>maskaravivek</b></sub>](https://github.com/maskaravivek) | [<img src="https://avatars.githubusercontent.com/u/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<img src="https://avatars.githubusercontent.com/u/17375274?v=4" width="100px;"/><br /><sub><b>ashishkumar468</b></sub>](https://github.com/ashishkumar468) |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars.githubusercontent.com/u/30932899?v=4" width="100px;"/><br /><sub><b>madhurgupta10</b></sub>](https://github.com/madhurgupta10) | [<img src="https://avatars.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) | [<img src="https://avatars.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) |
|
| [<img src="https://avatars.githubusercontent.com/u/103075?v=4" width="100px;"/><br /><sub><b>bvibber</b></sub>](https://github.com/bvibber) | [<img src="https://avatars.githubusercontent.com/u/10674?v=4" width="100px;"/><br /><sub><b>whym</b></sub>](https://github.com/whym) | [<img src="https://avatars.githubusercontent.com/u/10153800?v=4" width="100px;"/><br /><sub><b>akaita</b></sub>](https://github.com/akaita) | [<img src="https://avatars.githubusercontent.com/u/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<img src="https://avatars.githubusercontent.com/u/6900601?v=4" width="100px;"/><br /><sub><b>veyndan</b></sub>](https://github.com/veyndan) |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/3358282?v=4" width="100px;"/><br /><sub><b>macgills</b></sub>](https://github.com/macgills) | [<img src="https://avatars.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars.githubusercontent.com/u/34261945?v=4" width="100px;"/><br /><sub><b>vanshikaarora</b></sub>](https://github.com/vanshikaarora) | [<img src="https://avatars.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) | [<img src="https://avatars.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) |
|
| [<img src="https://avatars.githubusercontent.com/u/19607555?v=4" width="100px;"/><br /><sub><b>ujjwalagrawal17</b></sub>](https://github.com/ujjwalagrawal17) | [<img src="https://avatars.githubusercontent.com/u/3358282?v=4" width="100px;"/><br /><sub><b>macgills</b></sub>](https://github.com/macgills) | [<img src="https://avatars.githubusercontent.com/u/346271?v=4" width="100px;"/><br /><sub><b>amire80</b></sub>](https://github.com/amire80) | [<img src="https://avatars.githubusercontent.com/u/1682214?v=4" width="100px;"/><br /><sub><b>dbrant</b></sub>](https://github.com/dbrant) | [<img src="https://avatars.githubusercontent.com/u/34261945?v=4" width="100px;"/><br /><sub><b>vanshikaarora</b></sub>](https://github.com/vanshikaarora) |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/39745544?v=4" width="100px;"/><br /><sub><b>cypherop</b></sub>](https://github.com/cypherop) | [<img src="https://avatars.githubusercontent.com/u/6953323?v=4" width="100px;"/><br /><sub><b>tobias47n9e</b></sub>](https://github.com/tobias47n9e) | [<img src="https://avatars.githubusercontent.com/u/25305892?v=4" width="100px;"/><br /><sub><b>hismaeel</b></sub>](https://github.com/hismaeel) | [<img src="https://avatars.githubusercontent.com/u/12574756?v=4" width="100px;"/><br /><sub><b>tshradheya</b></sub>](https://github.com/tshradheya) | [<img src="https://avatars.githubusercontent.com/u/3308769?v=4" width="100px;"/><br /><sub><b>addshore</b></sub>](https://github.com/addshore) |
|
| [<img src="https://avatars.githubusercontent.com/u/83745993?v=4" width="100px;"/><br /><sub><b>RitikaPahwa4444</b></sub>](https://github.com/RitikaPahwa4444) | [<img src="https://avatars.githubusercontent.com/u/71203077?v=4" width="100px;"/><br /><sub><b>Ayan-10</b></sub>](https://github.com/Ayan-10) | [<img src="https://avatars.githubusercontent.com/u/101377978?v=4" width="100px;"/><br /><sub><b>rohit9625</b></sub>](https://github.com/rohit9625) | [<img src="https://avatars.githubusercontent.com/u/126143257?v=4" width="100px;"/><br /><sub><b>shashankiitbhu</b></sub>](https://github.com/shashankiitbhu) | [<img src="https://avatars.githubusercontent.com/u/54663429?v=4" width="100px;"/><br /><sub><b>Pratham2305</b></sub>](https://github.com/Pratham2305) |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/20313518?v=4" width="100px;"/><br /><sub><b>knight-shade</b></sub>](https://github.com/knight-shade) | [<img src="https://avatars.githubusercontent.com/u/210297?v=4" width="100px;"/><br /><sub><b>siebrand</b></sub>](https://github.com/siebrand) | [<img src="https://avatars.githubusercontent.com/u/12448084?v=4" width="100px;"/><br /><sub><b>sivaraam</b></sub>](https://github.com/sivaraam) | [<img src="https://avatars.githubusercontent.com/u/5329780?v=4" width="100px;"/><br /><sub><b>Bluesir9</b></sub>](https://github.com/Bluesir9) | [<img src="https://avatars.githubusercontent.com/u/44129798?v=4" width="100px;"/><br /><sub><b>kbhardwaj123</b></sub>](https://github.com/kbhardwaj123) |
|
| [<img src="https://avatars.githubusercontent.com/u/111801812?v=4" width="100px;"/><br /><sub><b>parneet-guraya</b></sub>](https://github.com/parneet-guraya) | [<img src="https://avatars.githubusercontent.com/u/1345681?v=4" width="100px;"/><br /><sub><b>sandarumk</b></sub>](https://github.com/sandarumk) | [<img src="https://avatars.githubusercontent.com/u/29161745?v=4" width="100px;"/><br /><sub><b>tanvidadu</b></sub>](https://github.com/tanvidadu) | [<img src="https://avatars.githubusercontent.com/u/39745544?v=4" width="100px;"/><br /><sub><b>cypherop</b></sub>](https://github.com/cypherop) | [<img src="https://avatars.githubusercontent.com/u/65972015?v=4" width="100px;"/><br /><sub><b>Prince-kushwaha</b></sub>](https://github.com/Prince-kushwaha) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors).
|
.. and [many more](https://github.com/commons-app/apps-android-commons/graphs/contributors).
|
||||||
|
|
@ -45,7 +46,7 @@ This software is open source, licensed under the [Apache License 2.0][10].
|
||||||
|
|
||||||
[1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
|
[1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
|
||||||
[2]: https://commons-app.github.io/
|
[2]: https://commons-app.github.io/
|
||||||
[3]: https://github.com/commons-app/apps-android-commons/issues
|
[3]: https://github.com/commons-app/apps-android-commons/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+-label%3Adebated+label%3Abug+-label%3A%22low+priority%22+-label%3Aupstream
|
||||||
|
|
||||||
[4]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-android-documentation
|
[4]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-android-documentation
|
||||||
[5]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-user-documentation
|
[5]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-user-documentation
|
||||||
|
|
|
||||||
383
app/build.gradle
383
app/build.gradle
|
|
@ -1,383 +0,0 @@
|
||||||
plugins {
|
|
||||||
id 'com.github.triplet.play' version '2.7.2' apply false
|
|
||||||
}
|
|
||||||
apply from: '../gitutils.gradle'
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply plugin: 'kotlin-kapt'
|
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
apply from: "$rootDir/jacoco.gradle"
|
|
||||||
|
|
||||||
def isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file('../play.p12').exists()
|
|
||||||
|
|
||||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
|
||||||
apply plugin: 'com.github.triplet.play'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
implementation project(':wikimedia-data-client')
|
|
||||||
// Utils
|
|
||||||
implementation 'in.yuvi:http.fluent:1.3'
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.5'
|
|
||||||
implementation ("com.squareup.okhttp3:okhttp:$OKHTTP_VERSION!!"){
|
|
||||||
// Forcing dependency versions using force = true on a first-level dependency has been deprecated.
|
|
||||||
// Ref: https://docs.gradle.org/7.5/userguide/upgrading_version_5.html#forced_dependencies
|
|
||||||
//force = true //API 19 support
|
|
||||||
}
|
|
||||||
implementation 'com.squareup.okio:okio:2.2.2'
|
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
|
||||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
|
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
|
|
||||||
implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0'
|
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.1.1'
|
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.1.1'
|
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1'
|
|
||||||
implementation 'com.facebook.fresco:fresco:1.13.0'
|
|
||||||
implementation 'org.apache.commons:commons-lang3:3.8.1'
|
|
||||||
|
|
||||||
// UI
|
|
||||||
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
|
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
|
|
||||||
implementation 'com.github.pedrovgs:renderers:3.3.3'
|
|
||||||
implementation "org.maplibre.gl:android-sdk:$MAPLIBRE_VERSION"
|
|
||||||
implementation 'org.maplibre.gl:android-plugin-scalebar-v9:1.0.0'
|
|
||||||
|
|
||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
|
||||||
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
|
|
||||||
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
|
|
||||||
implementation 'com.karumi:dexter:5.0.0'
|
|
||||||
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
|
||||||
|
|
||||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
|
||||||
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:$ADAPTER_DELEGATES_VERSION"
|
|
||||||
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
|
|
||||||
implementation "androidx.paging:paging-runtime-ktx:$PAGING_VERSION"
|
|
||||||
testImplementation "androidx.paging:paging-common-ktx:$PAGING_VERSION"
|
|
||||||
implementation "androidx.paging:paging-rxjava2-ktx:$PAGING_VERSION"
|
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
|
|
||||||
implementation "com.squareup.okhttp3:okhttp-ws:$OKHTTP_VERSION"
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
implementation 'ch.acra:acra-dialog:5.8.4'
|
|
||||||
implementation 'ch.acra:acra-mail:5.8.4'
|
|
||||||
implementation 'org.slf4j:slf4j-api:1.7.25'
|
|
||||||
api('com.github.tony19:logback-android-classic:1.1.1-6') {
|
|
||||||
exclude group: 'com.google.android', module: 'android'
|
|
||||||
}
|
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$OKHTTP_VERSION"
|
|
||||||
|
|
||||||
// Dependency injector
|
|
||||||
implementation "com.google.dagger:dagger-android:$DAGGER_VERSION"
|
|
||||||
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
|
|
||||||
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
|
||||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
|
||||||
annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
|
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
|
|
||||||
|
|
||||||
//Mocking
|
|
||||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
|
||||||
testImplementation 'org.mockito:mockito-inline:2.13.0'
|
|
||||||
testImplementation 'org.mockito:mockito-core:2.25.1'
|
|
||||||
testImplementation "org.powermock:powermock-module-junit4:2.0.2"
|
|
||||||
testImplementation "org.powermock:powermock-api-mockito2:2.0.2"
|
|
||||||
|
|
||||||
// Unit testing
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
|
||||||
testImplementation 'org.robolectric:robolectric:4.10.3'
|
|
||||||
testImplementation 'androidx.test:core:1.4.0'
|
|
||||||
testImplementation "com.squareup.okhttp3:mockwebserver:$OKHTTP_VERSION"
|
|
||||||
testImplementation "com.jraska.livedata:testing-ktx:1.1.2"
|
|
||||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.0"
|
|
||||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.0"
|
|
||||||
testImplementation 'com.facebook.soloader:soloader:0.10.1'
|
|
||||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0"
|
|
||||||
|
|
||||||
// Android testing
|
|
||||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha04'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.0-alpha04'
|
|
||||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
|
||||||
androidTestImplementation 'androidx.test:rules:1.4.1-alpha04'
|
|
||||||
androidTestImplementation 'androidx.test:core:1.4.0'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
|
||||||
androidTestImplementation 'androidx.annotation:annotation:1.3.0'
|
|
||||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.0'
|
|
||||||
androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0"
|
|
||||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
|
||||||
|
|
||||||
// Debugging
|
|
||||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY_VERSION"
|
|
||||||
|
|
||||||
// Support libraries
|
|
||||||
implementation "com.google.android.material:material:1.1.0-alpha04"
|
|
||||||
implementation "androidx.browser:browser:1.3.0"
|
|
||||||
implementation "androidx.cardview:cardview:1.0.0"
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
|
||||||
implementation "androidx.exifinterface:exifinterface:1.3.2"
|
|
||||||
implementation "androidx.core:core-ktx:$CORE_KTX_VERSION"
|
|
||||||
implementation "androidx.multidex:multidex:2.0.1"
|
|
||||||
implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1'
|
|
||||||
|
|
||||||
//swipe_layout
|
|
||||||
implementation 'com.daimajia.swipelayout:library:1.2.0@aar'
|
|
||||||
|
|
||||||
//Room
|
|
||||||
implementation "androidx.room:room-runtime:$ROOM_VERSION"
|
|
||||||
implementation "androidx.room:room-ktx:$ROOM_VERSION"
|
|
||||||
implementation "androidx.room:room-rxjava2:$ROOM_VERSION"
|
|
||||||
kapt "androidx.room:room-compiler:$ROOM_VERSION"
|
|
||||||
// For Kotlin use kapt instead of annotationProcessor
|
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
|
|
||||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
|
||||||
|
|
||||||
// Pref
|
|
||||||
// Java language implementation
|
|
||||||
implementation "androidx.preference:preference:$PREFERENCE_VERSION"
|
|
||||||
// Kotlin
|
|
||||||
implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION"
|
|
||||||
|
|
||||||
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
|
|
||||||
|
|
||||||
def work_version = "2.8.1"
|
|
||||||
// Kotlin + coroutines
|
|
||||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
|
||||||
implementation("androidx.work:work-runtime:$work_version")
|
|
||||||
testImplementation "androidx.work:work-testing:$work_version"
|
|
||||||
|
|
||||||
//Glide
|
|
||||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
|
||||||
|
|
||||||
implementation("io.github.coordinates2country:coordinates2country-android:1.3") { exclude group: 'com.google.android', module: 'android' }
|
|
||||||
}
|
|
||||||
|
|
||||||
task disableAnimations(type: Exec) {
|
|
||||||
def adb = "$System.env.ANDROID_HOME/platform-tools/adb"
|
|
||||||
commandLine "$adb", 'shell', 'settings', 'put', 'global', 'window_animation_scale', '0'
|
|
||||||
commandLine "$adb", 'shell', 'settings', 'put', 'global', 'transition_animation_scale', '0'
|
|
||||||
commandLine "$adb", 'shell', 'settings', 'put', 'global', 'animator_duration_scale', '0'
|
|
||||||
}
|
|
||||||
|
|
||||||
project.gradle.taskGraph.whenReady {
|
|
||||||
connectedBetaDebugAndroidTest.dependsOn disableAnimations
|
|
||||||
connectedProdDebugAndroidTest.dependsOn disableAnimations
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 33
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
//applicationId 'fr.free.nrw.commons'
|
|
||||||
|
|
||||||
versionCode 1035
|
|
||||||
versionName '4.2.0'
|
|
||||||
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
|
||||||
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion 33
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
|
||||||
|
|
||||||
multiDexEnabled true
|
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
|
||||||
}
|
|
||||||
packagingOptions {
|
|
||||||
jniLibs {
|
|
||||||
excludes += ['META-INF/androidx.*']
|
|
||||||
}
|
|
||||||
resources {
|
|
||||||
excludes += ['META-INF/androidx.*', 'META-INF/proguard/androidx-annotations.pro']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
animationsDisabled true
|
|
||||||
|
|
||||||
unitTests.returnDefaultValues = true
|
|
||||||
unitTests.includeAndroidResources = true
|
|
||||||
|
|
||||||
unitTests.all {
|
|
||||||
jvmArgs '-noverify'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
// use kotlin only in tests (for now)
|
|
||||||
test.java.srcDirs += 'src/test/kotlin'
|
|
||||||
|
|
||||||
// use main assets and resources in test
|
|
||||||
test.assets.srcDirs += 'src/main/assets'
|
|
||||||
test.resources.srcDirs += 'src/main/resoures'
|
|
||||||
}
|
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
release
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
|
||||||
testProguardFile 'test-proguard-rules.txt'
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
minifyEnabled false
|
|
||||||
testCoverageEnabled true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
|
||||||
testProguardFile 'test-proguard-rules.txt'
|
|
||||||
versionNameSuffix "-debug-" + getBranchName()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
|
||||||
// configure keystore based on env vars in Travis for automated alpha builds
|
|
||||||
signingConfigs.release.storeFile = file("../nr-commons.keystore")
|
|
||||||
signingConfigs.release.storePassword = System.getenv("keystore_password")
|
|
||||||
signingConfigs.release.keyAlias = System.getenv("key_alias")
|
|
||||||
signingConfigs.release.keyPassword = System.getenv("key_password")
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations.all {
|
|
||||||
resolutionStrategy.force 'androidx.annotation:annotation:1.1.0'
|
|
||||||
resolutionStrategy.force 'com.jakewharton.timber:timber:4.7.1'
|
|
||||||
resolutionStrategy.force 'androidx.fragment:fragment:1.3.6'
|
|
||||||
exclude module: 'okhttp-ws'
|
|
||||||
}
|
|
||||||
flavorDimensions 'tier'
|
|
||||||
productFlavors {
|
|
||||||
prod {
|
|
||||||
|
|
||||||
applicationId 'fr.free.nrw.commons'
|
|
||||||
|
|
||||||
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
|
|
||||||
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
|
|
||||||
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
|
|
||||||
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
|
|
||||||
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
|
|
||||||
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\""
|
|
||||||
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
|
|
||||||
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
|
|
||||||
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\""
|
|
||||||
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
|
|
||||||
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\""
|
|
||||||
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
|
||||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
|
|
||||||
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\""
|
|
||||||
buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\""
|
|
||||||
buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\""
|
|
||||||
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\""
|
|
||||||
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\""
|
|
||||||
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\""
|
|
||||||
buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\""
|
|
||||||
buildConfigField "String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.recentlanguages.contentprovider\""
|
|
||||||
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\""
|
|
||||||
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\""
|
|
||||||
buildConfigField "String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.items.contentprovider\""
|
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
|
|
||||||
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
|
|
||||||
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
|
|
||||||
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
|
|
||||||
|
|
||||||
dimension 'tier'
|
|
||||||
}
|
|
||||||
|
|
||||||
beta {
|
|
||||||
applicationId 'fr.free.nrw.commons.beta'
|
|
||||||
|
|
||||||
// What values do we need to hit the BETA versions of the site / api ?
|
|
||||||
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
|
|
||||||
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\""
|
|
||||||
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
|
|
||||||
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
|
|
||||||
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
|
|
||||||
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\""
|
|
||||||
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
|
|
||||||
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
|
|
||||||
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\""
|
|
||||||
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
|
|
||||||
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\""
|
|
||||||
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
|
|
||||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
|
|
||||||
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\""
|
|
||||||
buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\""
|
|
||||||
buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\""
|
|
||||||
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\""
|
|
||||||
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\""
|
|
||||||
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\""
|
|
||||||
buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\""
|
|
||||||
buildConfigField "String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.beta.recentlanguages.contentprovider\""
|
|
||||||
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\""
|
|
||||||
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\""
|
|
||||||
buildConfigField "String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.items.contentprovider\""
|
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
|
|
||||||
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
|
|
||||||
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
|
|
||||||
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
|
|
||||||
|
|
||||||
dimension 'tier'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
buildToolsVersion buildToolsVersion
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
}
|
|
||||||
namespace 'fr.free.nrw.commons'
|
|
||||||
lint {
|
|
||||||
abortOnError false
|
|
||||||
disable 'MissingTranslation', 'ExtraTranslation'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String getTestUserName() {
|
|
||||||
def propFile = rootProject.file("./local.properties")
|
|
||||||
def properties = new Properties()
|
|
||||||
properties.load(new FileInputStream(propFile))
|
|
||||||
return properties['TEST_USER_NAME']
|
|
||||||
}
|
|
||||||
|
|
||||||
String getTestPassword() {
|
|
||||||
def propFile = rootProject.file("./local.properties")
|
|
||||||
def properties = new Properties()
|
|
||||||
properties.load(new FileInputStream(propFile))
|
|
||||||
return properties['TEST_USER_PASSWORD']
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRunningOnTravisAndIsNotPRBuild) {
|
|
||||||
play {
|
|
||||||
track = "alpha"
|
|
||||||
userFraction = 1
|
|
||||||
serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME")
|
|
||||||
serviceAccountCredentials = file("../play.p12")
|
|
||||||
|
|
||||||
resolutionStrategy = "auto"
|
|
||||||
outputProcessor { // this: ApkVariantOutput
|
|
||||||
versionNameOverride = "$versionNameOverride.$versionCode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
androidExtensions {
|
|
||||||
experimental = true
|
|
||||||
}
|
|
||||||
447
app/build.gradle.kts
Normal file
447
app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,447 @@
|
||||||
|
import java.util.Properties
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.jetbrains.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.kapt)
|
||||||
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "$rootDir/jacoco.gradle")
|
||||||
|
|
||||||
|
val isRunningOnTravisAndIsNotPRBuild = System.getenv("CI") == "true" && file("../play.p12").exists()
|
||||||
|
|
||||||
|
if (isRunningOnTravisAndIsNotPRBuild) {
|
||||||
|
apply(plugin = "com.github.triplet.play")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "fr.free.nrw.commons"
|
||||||
|
compileSdk = 35
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "fr.free.nrw.commons"
|
||||||
|
minSdk = 21
|
||||||
|
targetSdk = 35
|
||||||
|
versionCode = 1059
|
||||||
|
versionName = "6.1.0"
|
||||||
|
|
||||||
|
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
testInstrumentationRunnerArguments["clearPackageData"] = "true"
|
||||||
|
|
||||||
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
vectorDrawables {
|
||||||
|
useSupportLibrary = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("test") {
|
||||||
|
// Use kotlin only in tests (for now)
|
||||||
|
java.srcDirs("src/test/kotlin")
|
||||||
|
|
||||||
|
// Use main assets and resources in test
|
||||||
|
assets.srcDirs("src/main/assets")
|
||||||
|
resources.srcDirs("src/main/resources")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
// Configure keystore based on env vars in Travis for automated alpha builds
|
||||||
|
if(isRunningOnTravisAndIsNotPRBuild) {
|
||||||
|
storeFile = file("../nr-commons.keystore")
|
||||||
|
storePassword = System.getenv("keystore_password")
|
||||||
|
keyAlias = System.getenv("key_alias")
|
||||||
|
keyPassword = System.getenv("key_password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt")
|
||||||
|
testProguardFile("test-proguard-rules.txt")
|
||||||
|
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
if (isRunningOnTravisAndIsNotPRBuild) {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.txt")
|
||||||
|
testProguardFile("test-proguard-rules.txt")
|
||||||
|
|
||||||
|
versionNameSuffix = "-debug-" + getBranchName()
|
||||||
|
enableUnitTestCoverage = true
|
||||||
|
enableAndroidTestCoverage = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy {
|
||||||
|
force("androidx.annotation:annotation:1.1.0")
|
||||||
|
force("com.jakewharton.timber:timber:4.7.1")
|
||||||
|
force("androidx.fragment:fragment:1.3.6")
|
||||||
|
}
|
||||||
|
exclude(module = "okhttp-ws")
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions += "tier"
|
||||||
|
productFlavors {
|
||||||
|
create("prod") {
|
||||||
|
dimension = "tier"
|
||||||
|
applicationId = "fr.free.nrw.commons"
|
||||||
|
|
||||||
|
buildConfigField("String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"")
|
||||||
|
buildConfigField("String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"")
|
||||||
|
buildConfigField("String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"")
|
||||||
|
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
|
||||||
|
buildConfigField("String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"")
|
||||||
|
buildConfigField("String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\"")
|
||||||
|
buildConfigField("String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"")
|
||||||
|
buildConfigField("String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"")
|
||||||
|
buildConfigField("String", "COMMONS_URL", "\"https://commons.wikimedia.org\"")
|
||||||
|
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
|
||||||
|
buildConfigField("String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\"")
|
||||||
|
buildConfigField("String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.org/wiki/\"")
|
||||||
|
buildConfigField("String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"")
|
||||||
|
buildConfigField("String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\"")
|
||||||
|
buildConfigField("String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\"")
|
||||||
|
buildConfigField("String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"")
|
||||||
|
buildConfigField("String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"")
|
||||||
|
buildConfigField("String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\"")
|
||||||
|
buildConfigField("String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\"")
|
||||||
|
buildConfigField("String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\"")
|
||||||
|
buildConfigField("String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\"")
|
||||||
|
buildConfigField("String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\"")
|
||||||
|
buildConfigField("String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.recentlanguages.contentprovider\"")
|
||||||
|
buildConfigField("String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\"")
|
||||||
|
buildConfigField("String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\"")
|
||||||
|
buildConfigField("String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.items.contentprovider\"")
|
||||||
|
buildConfigField("String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"")
|
||||||
|
buildConfigField("String", "TEST_USERNAME", "\"" + getTestUserName() + "\"")
|
||||||
|
buildConfigField("String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"")
|
||||||
|
buildConfigField("String", "DEPICTS_PROPERTY", "\"P180\"")
|
||||||
|
buildConfigField("String", "CREATOR_PROPERTY", "\"P170\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
create("beta") {
|
||||||
|
dimension = "tier"
|
||||||
|
applicationId = "fr.free.nrw.commons.beta"
|
||||||
|
|
||||||
|
// What values do we need to hit the BETA versions of the site / api ?
|
||||||
|
buildConfigField("String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"")
|
||||||
|
buildConfigField("String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\"")
|
||||||
|
buildConfigField("String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"")
|
||||||
|
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
|
||||||
|
buildConfigField("String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"")
|
||||||
|
buildConfigField("String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\"")
|
||||||
|
buildConfigField("String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"")
|
||||||
|
buildConfigField("String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"")
|
||||||
|
buildConfigField("String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"")
|
||||||
|
buildConfigField("String", "WIKIDATA_URL", "\"https://www.wikidata.org\"")
|
||||||
|
buildConfigField("String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\"")
|
||||||
|
buildConfigField("String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.beta.wmflabs.org/wiki/\"")
|
||||||
|
buildConfigField("String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"")
|
||||||
|
buildConfigField("String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\"")
|
||||||
|
buildConfigField("String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\"")
|
||||||
|
buildConfigField("String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"")
|
||||||
|
buildConfigField("String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"")
|
||||||
|
buildConfigField("String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\"")
|
||||||
|
buildConfigField("String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\"")
|
||||||
|
buildConfigField("String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\"")
|
||||||
|
buildConfigField("String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\"")
|
||||||
|
buildConfigField("String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\"")
|
||||||
|
buildConfigField("String", "RECENT_LANGUAGE_AUTHORITY", "\"fr.free.nrw.commons.beta.recentlanguages.contentprovider\"")
|
||||||
|
buildConfigField("String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\"")
|
||||||
|
buildConfigField("String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\"")
|
||||||
|
buildConfigField("String", "BOOKMARK_ITEMS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.items.contentprovider\"")
|
||||||
|
buildConfigField("String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\"")
|
||||||
|
buildConfigField("String", "TEST_USERNAME", "\"" + getTestUserName() + "\"")
|
||||||
|
buildConfigField("String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"")
|
||||||
|
buildConfigField("String", "DEPICTS_PROPERTY", "\"P245962\"")
|
||||||
|
buildConfigField("String", "CREATOR_PROPERTY", "\"P253075\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
viewBinding = true
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
buildToolsVersion = buildToolsVersion
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.5.8"
|
||||||
|
}
|
||||||
|
packaging {
|
||||||
|
jniLibs {
|
||||||
|
excludes += listOf("META-INF/androidx.*")
|
||||||
|
}
|
||||||
|
resources {
|
||||||
|
excludes += listOf(
|
||||||
|
"META-INF/androidx.*",
|
||||||
|
"META-INF/proguard/androidx-annotations.pro",
|
||||||
|
"/META-INF/LICENSE.md",
|
||||||
|
"/META-INF/LICENSE-notice.md"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
testOptions {
|
||||||
|
animationsDisabled = true
|
||||||
|
unitTests {
|
||||||
|
isReturnDefaultValues = true
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
unitTests.all {
|
||||||
|
it.jvmArgs("-noverify")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lint {
|
||||||
|
abortOnError = false
|
||||||
|
disable += listOf("MissingTranslation", "ExtraTranslation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Utils
|
||||||
|
implementation(libs.gson)
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.retrofit)
|
||||||
|
implementation(libs.retrofit.converter.gson)
|
||||||
|
implementation(libs.retrofit.adapter.rxjava)
|
||||||
|
implementation(libs.rxandroid)
|
||||||
|
implementation(libs.rxjava)
|
||||||
|
implementation(libs.rxbinding)
|
||||||
|
implementation(libs.rxbinding.appcompat)
|
||||||
|
implementation(libs.facebook.fresco)
|
||||||
|
implementation(libs.facebook.fresco.middleware)
|
||||||
|
implementation(libs.apache.commons.lang3)
|
||||||
|
|
||||||
|
// UI
|
||||||
|
implementation("${libs.viewpagerindicator.library.get()}@aar")
|
||||||
|
implementation(libs.photoview)
|
||||||
|
implementation(libs.android.sdk)
|
||||||
|
implementation(libs.android.plugin.scalebar)
|
||||||
|
|
||||||
|
implementation(libs.timber)
|
||||||
|
implementation(libs.android.material)
|
||||||
|
implementation(libs.dexter)
|
||||||
|
|
||||||
|
// Jetpack Compose
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
|
implementation(libs.androidx.compose.runtime)
|
||||||
|
implementation(libs.androidx.ui)
|
||||||
|
implementation(libs.androidx.ui.graphics)
|
||||||
|
implementation(libs.androidx.ui.tooling.preview)
|
||||||
|
implementation(libs.androidx.ui.viewbinding)
|
||||||
|
implementation(libs.androidx.material3)
|
||||||
|
implementation(libs.androidx.foundation)
|
||||||
|
implementation(libs.androidx.foundation.layout)
|
||||||
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
|
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||||
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
|
debugImplementation(libs.androidx.ui.test.manifest)
|
||||||
|
|
||||||
|
implementation(libs.adapterdelegates4.kotlin.dsl.viewbinding)
|
||||||
|
implementation(libs.adapterdelegates4.pagination)
|
||||||
|
implementation(libs.androidx.paging.runtime.ktx)
|
||||||
|
testImplementation(libs.androidx.paging.common.ktx)
|
||||||
|
implementation(libs.androidx.paging.rxjava2.ktx)
|
||||||
|
implementation(libs.androidx.recyclerview)
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
implementation(libs.acra.dialog)
|
||||||
|
implementation(libs.acra.mail)
|
||||||
|
implementation(libs.slf4j.api)
|
||||||
|
implementation(libs.logback.android.classic) {
|
||||||
|
exclude(group = "com.google.android", module = "android")
|
||||||
|
}
|
||||||
|
implementation(libs.logging.interceptor)
|
||||||
|
|
||||||
|
// Dependency injector
|
||||||
|
implementation(libs.dagger.android)
|
||||||
|
implementation(libs.dagger.android.support)
|
||||||
|
kapt(libs.dagger.android.processor)
|
||||||
|
kapt(libs.dagger.compiler)
|
||||||
|
annotationProcessor(libs.dagger.android.processor)
|
||||||
|
|
||||||
|
implementation(libs.kotlin.reflect)
|
||||||
|
|
||||||
|
//Mocking
|
||||||
|
testImplementation(libs.mockito.kotlin)
|
||||||
|
testImplementation(libs.mockito.core)
|
||||||
|
testImplementation(libs.powermock.module.junit)
|
||||||
|
testImplementation(libs.powermock.api.mockito)
|
||||||
|
testImplementation(libs.mockk)
|
||||||
|
|
||||||
|
// Unit testing
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
testImplementation(libs.robolectric)
|
||||||
|
testImplementation(libs.androidx.test.core)
|
||||||
|
testImplementation(libs.androidx.runner)
|
||||||
|
testImplementation(libs.androidx.test.ext.junit)
|
||||||
|
testImplementation(libs.androidx.test.rules)
|
||||||
|
testImplementation(libs.mockwebserver)
|
||||||
|
testImplementation(libs.livedata.testing.ktx)
|
||||||
|
testImplementation(libs.androidx.core.testing)
|
||||||
|
testImplementation(libs.junit.jupiter.api)
|
||||||
|
testRuntimeOnly(libs.junit.jupiter.engine)
|
||||||
|
testImplementation(libs.soloader)
|
||||||
|
testImplementation(libs.kotlinx.coroutines.test)
|
||||||
|
debugImplementation(libs.androidx.fragment.testing)
|
||||||
|
testImplementation(libs.commons.io)
|
||||||
|
|
||||||
|
// Android testing
|
||||||
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
androidTestImplementation(libs.androidx.espresso.intents)
|
||||||
|
androidTestImplementation(libs.androidx.espresso.contrib)
|
||||||
|
androidTestImplementation(libs.androidx.runner)
|
||||||
|
androidTestImplementation(libs.androidx.test.rules)
|
||||||
|
androidTestImplementation(libs.androidx.test.core)
|
||||||
|
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||||
|
androidTestImplementation(libs.androidx.annotation)
|
||||||
|
androidTestImplementation(libs.mockwebserver)
|
||||||
|
androidTestImplementation(libs.androidx.uiautomator)
|
||||||
|
|
||||||
|
// Debugging
|
||||||
|
debugImplementation(libs.leakcanary.android)
|
||||||
|
|
||||||
|
// Support libraries
|
||||||
|
implementation(libs.androidx.browser)
|
||||||
|
implementation(libs.androidx.cardview)
|
||||||
|
implementation(libs.androidx.constraintlayout)
|
||||||
|
implementation(libs.androidx.exifinterface)
|
||||||
|
implementation(libs.recyclerview.fastscroll)
|
||||||
|
|
||||||
|
//swipe_layout
|
||||||
|
implementation(libs.swipelayout.library)
|
||||||
|
|
||||||
|
//Room
|
||||||
|
implementation(libs.androidx.room.runtime)
|
||||||
|
implementation(libs.androidx.room.ktx)
|
||||||
|
implementation(libs.androidx.room.rxjava)
|
||||||
|
kapt(libs.androidx.room.compiler)
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
implementation(libs.androidx.preference)
|
||||||
|
implementation(libs.androidx.preference.ktx)
|
||||||
|
|
||||||
|
//Android Media
|
||||||
|
implementation(libs.juanitobananas.androidDmediaUtil)
|
||||||
|
implementation(libs.androidx.multidex)
|
||||||
|
|
||||||
|
// Kotlin + coroutines
|
||||||
|
implementation(libs.androidx.work.runtime.ktx)
|
||||||
|
implementation(libs.androidx.work.runtime)
|
||||||
|
implementation(libs.kotlinx.coroutines.rx2)
|
||||||
|
testImplementation(libs.androidx.work.testing)
|
||||||
|
|
||||||
|
//Glide
|
||||||
|
implementation(libs.glide)
|
||||||
|
annotationProcessor(libs.glide.compiler)
|
||||||
|
kaptTest(libs.androidx.databinding.compiler)
|
||||||
|
kaptAndroidTest(libs.androidx.databinding.compiler)
|
||||||
|
|
||||||
|
implementation(libs.coordinates2country.android) {
|
||||||
|
exclude(group = "com.google.android", module = "android")
|
||||||
|
}
|
||||||
|
|
||||||
|
//OSMDroid
|
||||||
|
implementation(libs.osmdroid.android)
|
||||||
|
constraints {
|
||||||
|
implementation(libs.kotlin.stdlib.jdk7) {
|
||||||
|
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
|
||||||
|
}
|
||||||
|
implementation(libs.kotlin.stdlib.jdk8) {
|
||||||
|
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Exec>("disableAnimations") {
|
||||||
|
val adb = "${System.getenv("ANDROID_HOME")}/platform-tools/adb"
|
||||||
|
commandLine(adb, "shell", "settings", "put", "global", "window_animation_scale", "0")
|
||||||
|
commandLine(adb, "shell", "settings", "put", "global", "transition_animation_scale", "0")
|
||||||
|
commandLine(adb, "shell", "settings", "put", "global", "animator_duration_scale", "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
project.gradle.taskGraph.whenReady {
|
||||||
|
val connectedBetaDebugAndroidTest = tasks.named("connectedBetaDebugAndroidTest")
|
||||||
|
val connectedProdDebugAndroidTest = tasks.named("connectedProdDebugAndroidTest")
|
||||||
|
|
||||||
|
connectedBetaDebugAndroidTest.configure {
|
||||||
|
dependsOn("disableAnimations")
|
||||||
|
}
|
||||||
|
connectedProdDebugAndroidTest.configure {
|
||||||
|
dependsOn("disableAnimations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTestUserName(): String? {
|
||||||
|
val propFile = rootProject.file("./local.properties")
|
||||||
|
val properties = Properties()
|
||||||
|
propFile.inputStream().use { properties.load(it) }
|
||||||
|
return properties.getProperty("TEST_USER_NAME")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTestPassword(): String? {
|
||||||
|
val propFile = rootProject.file("./local.properties")
|
||||||
|
val properties = Properties()
|
||||||
|
propFile.inputStream().use { properties.load(it) }
|
||||||
|
return properties.getProperty("TEST_USER_PASSWORD")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunningOnTravisAndIsNotPRBuild) {
|
||||||
|
configure<com.github.triplet.gradle.play.PlayPublisherExtension> {
|
||||||
|
track = "alpha"
|
||||||
|
userFraction = 1.0
|
||||||
|
serviceAccountEmail = System.getenv("SERVICE_ACCOUNT_NAME")
|
||||||
|
serviceAccountCredentials = file("../play.p12")
|
||||||
|
|
||||||
|
resolutionStrategy = "auto"
|
||||||
|
outputProcessor { // this: ApkVariantOutput
|
||||||
|
versionNameOverride = "$versionNameOverride.$versionCode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBuildVersion(): String? {
|
||||||
|
return try {
|
||||||
|
val stdout = ByteArrayOutputStream()
|
||||||
|
exec {
|
||||||
|
commandLine("git", "rev-parse", "--short", "HEAD")
|
||||||
|
standardOutput = stdout
|
||||||
|
}
|
||||||
|
stdout.toString().trim()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBranchName(): String? {
|
||||||
|
return try {
|
||||||
|
val stdout = ByteArrayOutputStream()
|
||||||
|
exec {
|
||||||
|
commandLine("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
standardOutput = stdout
|
||||||
|
}
|
||||||
|
stdout.toString().trim()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,8 +31,17 @@
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
# Retain declared checked exceptions for use by a Proxy instance.
|
# Retain declared checked exceptions for use by a Proxy instance.
|
||||||
-keepattributes Exceptions
|
-keepattributes Exceptions
|
||||||
# Classes used by retrofit to fetch API repsonse
|
|
||||||
-keepclasseswithmembers class org.wikipedia.** { *; }
|
# Note: The model package right now seems to include some other classes that
|
||||||
|
# are not used for serialization / deserialization over Gson. Hopefully
|
||||||
|
# that's not a problem since it only prevents R8 from avoiding trimming
|
||||||
|
# of few more classes.
|
||||||
|
-keepclasseswithmembers class fr.free.nrw.commons.*.model.** { *; }
|
||||||
|
-keepclasseswithmembers class fr.free.nrw.commons.actions.** { *; }
|
||||||
|
-keepclasseswithmembers class fr.free.nrw.commons.auth.csrf.** { *; }
|
||||||
|
-keepclasseswithmembers class fr.free.nrw.commons.auth.login.** { *; }
|
||||||
|
-keepclasseswithmembers class fr.free.nrw.commons.wikidata.mwapi.** { *; }
|
||||||
|
|
||||||
# --- /Retrofit ---
|
# --- /Retrofit ---
|
||||||
|
|
||||||
# --- OkHttp + Okio ---
|
# --- OkHttp + Okio ---
|
||||||
|
|
@ -57,6 +66,9 @@
|
||||||
# Application classes that will be serialized/deserialized over Gson
|
# Application classes that will be serialized/deserialized over Gson
|
||||||
-keep class com.google.gson.examples.android.model.** { *; }
|
-keep class com.google.gson.examples.android.model.** { *; }
|
||||||
|
|
||||||
|
# Prevent R8 from obfuscating project classes used by Gson for parsing
|
||||||
|
-keep class fr.free.nrw.commons.fileusages.** { *; }
|
||||||
|
|
||||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class AboutActivityTest {
|
class AboutActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java)
|
var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -36,7 +35,8 @@ class AboutActivityTest {
|
||||||
device.setOrientationNatural()
|
device.setOrientationNatural()
|
||||||
device.freezeRotation()
|
device.freezeRotation()
|
||||||
Intents.init()
|
Intents.init()
|
||||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
Intents
|
||||||
|
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,11 +47,12 @@ class AboutActivityTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testBuildNumber() {
|
fun testBuildNumber() {
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.about_version))
|
Espresso
|
||||||
|
.onView(ViewMatchers.withId(R.id.about_version))
|
||||||
.check(
|
.check(
|
||||||
ViewAssertions.matches(
|
ViewAssertions.matches(
|
||||||
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha())
|
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha()),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,8 +62,8 @@ class AboutActivityTest {
|
||||||
Intents.intended(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
IntentMatchers.hasData(Urls.WEBSITE_URL)
|
IntentMatchers.hasData(Urls.WEBSITE_URL),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,8 +74,8 @@ class AboutActivityTest {
|
||||||
CoreMatchers.anyOf(
|
CoreMatchers.anyOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL),
|
IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL),
|
||||||
IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME)
|
IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,8 +85,8 @@ class AboutActivityTest {
|
||||||
Intents.intended(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
IntentMatchers.hasData(Urls.GITHUB_REPO_URL)
|
IntentMatchers.hasData(Urls.GITHUB_REPO_URL),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,8 +96,8 @@ class AboutActivityTest {
|
||||||
Intents.intended(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL)
|
IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,12 +105,12 @@ class AboutActivityTest {
|
||||||
fun testLaunchTranslate() {
|
fun testLaunchTranslate() {
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.about_translate)).perform(ViewActions.click())
|
Espresso.onView(ViewMatchers.withId(R.id.about_translate)).perform(ViewActions.click())
|
||||||
Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click())
|
Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click())
|
||||||
val langCode = CommonsApplication.getInstance().languageLookUpTable.codes[0]
|
val langCode = CommonsApplication.instance.languageLookUpTable!!.getCodes()[0]
|
||||||
Intents.intended(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode")
|
IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode"),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,27 +120,30 @@ class AboutActivityTest {
|
||||||
Intents.intended(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
IntentMatchers.hasData(Urls.CREDITS_URL)
|
IntentMatchers.hasData(Urls.CREDITS_URL),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLaunchUserGuide() {
|
fun testLaunchUserGuide() {
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click())
|
Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click())
|
||||||
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
Intents.intended(
|
||||||
IntentMatchers.hasData(Urls.USER_GUIDE_URL)))
|
CoreMatchers.allOf(
|
||||||
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
|
IntentMatchers.hasData(Urls.USER_GUIDE_URL),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLaunchAboutFaq() {
|
fun testLaunchAboutFaq() {
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click())
|
Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click())
|
||||||
Intents.intended(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
IntentMatchers.hasData(Urls.FAQ_URL)
|
IntentMatchers.hasData(Urls.FAQ_URL),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,14 @@ import fr.free.nrw.commons.auth.LoginActivity
|
||||||
import fr.free.nrw.commons.auth.SignupActivity
|
import fr.free.nrw.commons.auth.SignupActivity
|
||||||
import org.hamcrest.CoreMatchers
|
import org.hamcrest.CoreMatchers
|
||||||
import org.hamcrest.CoreMatchers.not
|
import org.hamcrest.CoreMatchers.not
|
||||||
import org.junit.*
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class LoginActivityTest {
|
class LoginActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -49,8 +51,8 @@ class LoginActivityTest {
|
||||||
Intents.intended(
|
Intents.intended(
|
||||||
CoreMatchers.allOf(
|
CoreMatchers.allOf(
|
||||||
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
IntentMatchers.hasAction(Intent.ACTION_VIEW),
|
||||||
IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL)
|
IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,4 +66,4 @@ class LoginActivityTest {
|
||||||
fun orientationChange() {
|
fun orientationChange() {
|
||||||
UITestHelper.changeOrientation(activityRule)
|
UITestHelper.changeOrientation(activityRule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,20 +21,23 @@ import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
import fr.free.nrw.commons.notification.NotificationActivity
|
import fr.free.nrw.commons.notification.NotificationActivity
|
||||||
import org.hamcrest.CoreMatchers
|
import org.hamcrest.CoreMatchers
|
||||||
import org.hamcrest.Matchers
|
import org.hamcrest.Matchers
|
||||||
import org.junit.*
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class MainActivityTest {
|
class MainActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var mGrantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
|
var mGrantPermissionRule: GrantPermissionRule =
|
||||||
"android.permission.ACCESS_FINE_LOCATION"
|
GrantPermissionRule.grant(
|
||||||
)
|
"android.permission.ACCESS_FINE_LOCATION",
|
||||||
|
)
|
||||||
|
|
||||||
private val device: UiDevice =
|
private val device: UiDevice =
|
||||||
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||||
|
|
@ -48,7 +51,8 @@ class MainActivityTest {
|
||||||
UITestHelper.loginUser()
|
UITestHelper.loginUser()
|
||||||
UITestHelper.skipWelcome()
|
UITestHelper.skipWelcome()
|
||||||
Intents.init()
|
Intents.init()
|
||||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
Intents
|
||||||
|
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
val storeName = context.packageName + "_preferences"
|
val storeName = context.packageName + "_preferences"
|
||||||
|
|
@ -62,169 +66,149 @@ class MainActivityTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testNearby() {
|
fun testNearby() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
childAtPosition(
|
Matchers.allOf(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
1
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
).perform(ViewActions.click())
|
||||||
)
|
Espresso
|
||||||
).perform(ViewActions.click())
|
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
|
||||||
.check(matches(ViewMatchers.isDisplayed()))
|
.check(matches(ViewMatchers.isDisplayed()))
|
||||||
UITestHelper.sleep(10000)
|
UITestHelper.sleep(10000)
|
||||||
val actionMenuItemView2 = Espresso.onView(
|
val actionMenuItemView2 =
|
||||||
Matchers.allOf(
|
Espresso.onView(
|
||||||
ViewMatchers.withId(R.id.list_sheet), ViewMatchers.withContentDescription("List"),
|
Matchers.allOf(
|
||||||
childAtPosition(
|
ViewMatchers.withId(R.id.list_sheet),
|
||||||
|
ViewMatchers.withContentDescription("List"),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.toolbar),
|
childAtPosition(
|
||||||
1
|
ViewMatchers.withId(R.id.toolbar),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
actionMenuItemView2.perform(ViewActions.click())
|
actionMenuItemView2.perform(ViewActions.click())
|
||||||
UITestHelper.sleep(1000)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testExplore() {
|
fun testExplore() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
childAtPosition(
|
Matchers.allOf(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
2,
|
||||||
),
|
),
|
||||||
2
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
).perform(ViewActions.click())
|
||||||
)
|
Espresso
|
||||||
).perform(ViewActions.click())
|
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
|
||||||
.check(matches(ViewMatchers.isDisplayed()))
|
.check(matches(ViewMatchers.isDisplayed()))
|
||||||
UITestHelper.sleep(1000)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testContributions() {
|
fun testContributions() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
childAtPosition(
|
Matchers.allOf(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
).perform(ViewActions.click())
|
||||||
)
|
Espresso
|
||||||
).perform(ViewActions.click())
|
.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
||||||
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer))
|
|
||||||
.check(matches(ViewMatchers.isDisplayed()))
|
.check(matches(ViewMatchers.isDisplayed()))
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
ViewMatchers.withId(R.id.contributionImage),
|
Matchers.allOf(
|
||||||
childAtPosition(
|
ViewMatchers.withId(R.id.contributionImage),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.contributionsList),
|
childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.contributionsList),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
1
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
).perform(ViewActions.click())
|
||||||
)
|
val actionMenuItemView =
|
||||||
).perform(ViewActions.click())
|
Espresso.onView(
|
||||||
val actionMenuItemView = Espresso.onView(
|
Matchers.allOf(
|
||||||
Matchers.allOf(
|
ViewMatchers.withId(R.id.menu_bookmark_current_image),
|
||||||
ViewMatchers.withId(R.id.menu_bookmark_current_image),
|
|
||||||
childAtPosition(
|
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.toolbar),
|
childAtPosition(
|
||||||
1
|
ViewMatchers.withId(R.id.toolbar),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
actionMenuItemView.perform(ViewActions.click())
|
actionMenuItemView.perform(ViewActions.click())
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testBookmarks() {
|
fun testBookmarks() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
childAtPosition(
|
Matchers.allOf(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
3,
|
||||||
),
|
),
|
||||||
3
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
).perform(ViewActions.click())
|
||||||
)
|
|
||||||
).perform(ViewActions.click())
|
|
||||||
UITestHelper.sleep(1000)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testNotifications() {
|
fun testNotifications() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
ViewMatchers.withId(R.id.notifications),
|
Matchers.allOf(
|
||||||
childAtPosition(
|
ViewMatchers.withId(R.id.notifications),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.toolbar),
|
childAtPosition(
|
||||||
1
|
ViewMatchers.withId(R.id.toolbar),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
1
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
).perform(ViewActions.click())
|
||||||
)
|
|
||||||
).perform(ViewActions.click())
|
|
||||||
Intents.intended(IntentMatchers.hasComponent(NotificationActivity::class.java.name))
|
Intents.intended(IntentMatchers.hasComponent(NotificationActivity::class.java.name))
|
||||||
Espresso.pressBack()
|
Espresso.pressBack()
|
||||||
UITestHelper.sleep(1000)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@Test
|
|
||||||
fun testLimitedConnectionModeToggle() {
|
|
||||||
val isEnabled = defaultKvStore
|
|
||||||
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)
|
|
||||||
Espresso.onView(
|
|
||||||
Matchers.allOf(
|
|
||||||
ViewMatchers.withId(R.id.toggle_limited_connection_mode),
|
|
||||||
childAtPosition(
|
|
||||||
childAtPosition(
|
|
||||||
ViewMatchers.withId(R.id.toolbar),
|
|
||||||
1
|
|
||||||
),
|
|
||||||
0
|
|
||||||
),
|
|
||||||
ViewMatchers.isDisplayed()
|
|
||||||
)
|
|
||||||
).perform(ViewActions.click())
|
|
||||||
UITestHelper.sleep(1000)
|
|
||||||
if (isEnabled) {
|
|
||||||
Assert.assertFalse(
|
|
||||||
defaultKvStore
|
|
||||||
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Assert.assertTrue(
|
|
||||||
defaultKvStore
|
|
||||||
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import android.app.Activity
|
||||||
import android.app.Instrumentation
|
import android.app.Instrumentation
|
||||||
import androidx.test.espresso.Espresso.onView
|
import androidx.test.espresso.Espresso.onView
|
||||||
import androidx.test.espresso.action.ViewActions
|
import androidx.test.espresso.action.ViewActions
|
||||||
import androidx.test.espresso.action.ViewActions.swipeRight
|
|
||||||
import androidx.test.espresso.intent.Intents
|
import androidx.test.espresso.intent.Intents
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||||
|
|
@ -26,7 +25,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ProfileActivityTest {
|
class ProfileActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule = IntentsTestRule(LoginActivity::class.java)
|
var activityRule = IntentsTestRule(LoginActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -38,7 +36,8 @@ class ProfileActivityTest {
|
||||||
device.freezeRotation()
|
device.freezeRotation()
|
||||||
UITestHelper.loginUser()
|
UITestHelper.loginUser()
|
||||||
UITestHelper.skipWelcome()
|
UITestHelper.skipWelcome()
|
||||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
Intents
|
||||||
|
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,20 +49,19 @@ class ProfileActivityTest {
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.fragment_main_nav_tab_layout),
|
withId(R.id.fragment_main_nav_tab_layout),
|
||||||
0
|
0,
|
||||||
),
|
),
|
||||||
4
|
4,
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
ViewMatchers.isDisplayed(),
|
||||||
)
|
),
|
||||||
).perform(ViewActions.click())
|
).perform(ViewActions.click())
|
||||||
onView(Matchers.allOf(withId(R.id.more_profile))).perform(
|
onView(Matchers.allOf(withId(R.id.more_profile))).perform(
|
||||||
ViewActions.scrollTo(),
|
ViewActions.scrollTo(),
|
||||||
ViewActions.click()
|
ViewActions.click(),
|
||||||
)
|
)
|
||||||
device.swipe(1033,1346,531,1346,20)
|
device.swipe(1033, 1346, 531, 1346, 20)
|
||||||
UITestHelper.sleep(5000)
|
UITestHelper.sleep(5000)
|
||||||
Intents.intended(hasComponent(ProfileActivity::class.java.name))
|
Intents.intended(hasComponent(ProfileActivity::class.java.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ReviewActivityTest {
|
class ReviewActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(ReviewActivity::class.java)
|
var activityRule: ActivityTestRule<*> = ActivityTestRule(ReviewActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -17,5 +16,4 @@ class ReviewActivityTest {
|
||||||
fun orientationChange() {
|
fun orientationChange() {
|
||||||
UITestHelper.changeOrientation(activityRule)
|
UITestHelper.changeOrientation(activityRule)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.room.Room;
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
import fr.free.nrw.commons.db.AppDatabase;
|
|
||||||
import fr.free.nrw.commons.review.ReviewDao;
|
|
||||||
import fr.free.nrw.commons.review.ReviewEntity;
|
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class ReviewDaoTest {
|
|
||||||
|
|
||||||
private ReviewDao reviewDao;
|
|
||||||
private AppDatabase database;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up the application database
|
|
||||||
*/
|
|
||||||
@Before
|
|
||||||
public void createDb() {
|
|
||||||
Context context = ApplicationProvider.getApplicationContext();
|
|
||||||
database = Room.inMemoryDatabaseBuilder(
|
|
||||||
context, AppDatabase.class)
|
|
||||||
.allowMainThreadQueries()
|
|
||||||
.build();
|
|
||||||
reviewDao = database.ReviewDao();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the database
|
|
||||||
*/
|
|
||||||
@After
|
|
||||||
public void closeDb() {
|
|
||||||
database.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test insertion
|
|
||||||
* Also checks isReviewedAlready():
|
|
||||||
* Case 1: When image has been reviewed/skipped by the user
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void insert() {
|
|
||||||
// Insert data
|
|
||||||
String imageId = "1234";
|
|
||||||
ReviewEntity reviewEntity = new ReviewEntity(imageId);
|
|
||||||
reviewDao.insert(reviewEntity);
|
|
||||||
|
|
||||||
// Check insertion
|
|
||||||
// Covers the case where the image exists in the database
|
|
||||||
// And isReviewedAlready() returns true
|
|
||||||
Boolean isInserted = reviewDao.isReviewedAlready(imageId);
|
|
||||||
assertThat(isInserted, equalTo(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test review status of the image
|
|
||||||
* Case 2: When image has not been reviewed/skipped
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void isReviewedAlready(){
|
|
||||||
String imageId = "5856";
|
|
||||||
Boolean isInserted = reviewDao.isReviewedAlready(imageId);
|
|
||||||
assertThat(isInserted, equalTo(false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -16,7 +16,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class SearchActivityTest {
|
class SearchActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule = ActivityTestRule(SearchActivity::class.java)
|
var activityRule = ActivityTestRule(SearchActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -31,21 +30,22 @@ class SearchActivityTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun exploreActivityTest() {
|
fun exploreActivityTest() {
|
||||||
val searchAutoComplete = Espresso.onView(
|
val searchAutoComplete =
|
||||||
Matchers.allOf(
|
Espresso.onView(
|
||||||
UITestHelper.childAtPosition(
|
Matchers.allOf(
|
||||||
Matchers.allOf(
|
UITestHelper.childAtPosition(
|
||||||
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
|
Matchers.allOf(
|
||||||
UITestHelper.childAtPosition(
|
|
||||||
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
|
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
|
||||||
1
|
UITestHelper.childAtPosition(
|
||||||
)
|
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
searchAutoComplete.perform(ViewActions.replaceText("cat"), ViewActions.closeSoftKeyboard())
|
searchAutoComplete.perform(ViewActions.replaceText("cat"), ViewActions.closeSoftKeyboard())
|
||||||
UITestHelper.sleep(5000)
|
UITestHelper.sleep(5000)
|
||||||
device.swipe(1000, 1400, 500, 1400, 20)
|
device.swipe(1000, 1400, 500, 1400, 20)
|
||||||
|
|
@ -56,4 +56,4 @@ class SearchActivityTest {
|
||||||
device.swipe(800, 1400, 600, 1400, 20)
|
device.swipe(800, 1400, 600, 1400, 20)
|
||||||
UITestHelper.sleep(1000)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class SettingsActivityLoggedInTest {
|
class SettingsActivityLoggedInTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -35,31 +34,32 @@ class SettingsActivityLoggedInTest {
|
||||||
device.freezeRotation()
|
device.freezeRotation()
|
||||||
UITestHelper.loginUser()
|
UITestHelper.loginUser()
|
||||||
UITestHelper.skipWelcome()
|
UITestHelper.skipWelcome()
|
||||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
Intents
|
||||||
|
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSettings() {
|
fun testSettings() {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
Matchers.allOf(
|
.onView(
|
||||||
ViewMatchers.withContentDescription("More"),
|
Matchers.allOf(
|
||||||
UITestHelper.childAtPosition(
|
ViewMatchers.withContentDescription("More"),
|
||||||
UITestHelper.childAtPosition(
|
UITestHelper.childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
UITestHelper.childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
4,
|
||||||
),
|
),
|
||||||
4
|
ViewMatchers.isDisplayed(),
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
).perform(ViewActions.click())
|
||||||
)
|
|
||||||
).perform(ViewActions.click())
|
|
||||||
Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.more_settings))).perform(
|
Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.more_settings))).perform(
|
||||||
ViewActions.scrollTo(),
|
ViewActions.scrollTo(),
|
||||||
ViewActions.click()
|
ViewActions.click(),
|
||||||
)
|
)
|
||||||
Intents.intended(IntentMatchers.hasComponent(SettingsActivity::class.java.name))
|
Intents.intended(IntentMatchers.hasComponent(SettingsActivity::class.java.name))
|
||||||
UITestHelper.sleep(1000)
|
UITestHelper.sleep(1000)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class SettingsActivityTest {
|
class SettingsActivityTest {
|
||||||
|
|
||||||
private lateinit var defaultKvStore: JsonKvStore
|
private lateinit var defaultKvStore: JsonKvStore
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
|
@ -44,22 +43,24 @@ class SettingsActivityTest {
|
||||||
fun useAuthorNameTogglesOn() {
|
fun useAuthorNameTogglesOn() {
|
||||||
// Turn on "Use author name" preference if currently off
|
// Turn on "Use author name" preference if currently off
|
||||||
if (!defaultKvStore.getBoolean("useAuthorName", false)) {
|
if (!defaultKvStore.getBoolean("useAuthorName", false)) {
|
||||||
Espresso.onView(
|
Espresso
|
||||||
allOf(
|
.onView(
|
||||||
withId(R.id.recycler_view),
|
allOf(
|
||||||
childAtPosition(withId(android.R.id.list_container), 0)
|
withId(R.id.recycler_view),
|
||||||
|
childAtPosition(withId(android.R.id.list_container), 0),
|
||||||
|
),
|
||||||
|
).perform(
|
||||||
|
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(6, click()),
|
||||||
)
|
)
|
||||||
).perform(
|
|
||||||
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(6, click())
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// Check authorName preference is enabled
|
// Check authorName preference is enabled
|
||||||
Espresso.onView(
|
Espresso
|
||||||
allOf(
|
.onView(
|
||||||
withId(R.id.recycler_view),
|
allOf(
|
||||||
childAtPosition(withId(android.R.id.list_container), 0)
|
withId(R.id.recycler_view),
|
||||||
)
|
childAtPosition(withId(android.R.id.list_container), 0),
|
||||||
).check(matches(isEnabled()))
|
),
|
||||||
|
).check(matches(isEnabled()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,20 @@ import androidx.test.espresso.action.ViewActions
|
||||||
import androidx.test.espresso.matcher.ViewMatchers
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
import androidx.test.rule.ActivityTestRule
|
import androidx.test.rule.ActivityTestRule
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.hamcrest.*
|
import org.hamcrest.BaseMatcher
|
||||||
|
import org.hamcrest.Description
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
import org.hamcrest.TypeSafeMatcher
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
class UITestHelper {
|
class UITestHelper {
|
||||||
companion object {
|
companion object {
|
||||||
fun skipWelcome() {
|
fun skipWelcome() {
|
||||||
try {
|
try {
|
||||||
onView(ViewMatchers.withId(R.id.button_ok))
|
onView(ViewMatchers.withId(R.id.button_ok))
|
||||||
.perform(ViewActions.click())
|
.perform(ViewActions.click())
|
||||||
//Skip tutorial
|
// Skip tutorial
|
||||||
onView(ViewMatchers.withId(R.id.finishTutorialButton))
|
onView(ViewMatchers.withId(R.id.finishTutorialButton))
|
||||||
.perform(ViewActions.click())
|
.perform(ViewActions.click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
|
|
@ -29,27 +32,31 @@ class UITestHelper {
|
||||||
|
|
||||||
fun skipLogin() {
|
fun skipLogin() {
|
||||||
try {
|
try {
|
||||||
//Skip Login
|
// Skip Login
|
||||||
val htmlTextView = onView(
|
val htmlTextView =
|
||||||
Matchers.allOf(
|
onView(
|
||||||
ViewMatchers.withId(R.id.skip_login), ViewMatchers.withText("Skip"),
|
Matchers.allOf(
|
||||||
ViewMatchers.isDisplayed()
|
ViewMatchers.withId(R.id.skip_login),
|
||||||
|
ViewMatchers.withText("Skip"),
|
||||||
|
ViewMatchers.isDisplayed(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
htmlTextView.perform(ViewActions.click())
|
htmlTextView.perform(ViewActions.click())
|
||||||
|
|
||||||
val appCompatButton = onView(
|
val appCompatButton =
|
||||||
Matchers.allOf(
|
onView(
|
||||||
ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"),
|
Matchers.allOf(
|
||||||
childAtPosition(
|
ViewMatchers.withId(android.R.id.button1),
|
||||||
|
ViewMatchers.withText("Yes"),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.buttonPanel),
|
childAtPosition(
|
||||||
0
|
ViewMatchers.withId(R.id.buttonPanel),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
3,
|
||||||
),
|
),
|
||||||
3
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
appCompatButton.perform(ViewActions.scrollTo(), ViewActions.click())
|
appCompatButton.perform(ViewActions.scrollTo(), ViewActions.click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
|
|
@ -57,18 +64,18 @@ class UITestHelper {
|
||||||
|
|
||||||
fun loginUser() {
|
fun loginUser() {
|
||||||
try {
|
try {
|
||||||
//Perform Login
|
// Perform Login
|
||||||
sleep(3000)
|
sleep(3000)
|
||||||
onView(ViewMatchers.withId(R.id.login_username))
|
onView(ViewMatchers.withId(R.id.login_username))
|
||||||
.perform(
|
.perform(
|
||||||
ViewActions.replaceText(getTestUsername()),
|
ViewActions.replaceText(getTestUsername()),
|
||||||
ViewActions.closeSoftKeyboard()
|
ViewActions.closeSoftKeyboard(),
|
||||||
)
|
)
|
||||||
sleep(2000)
|
sleep(2000)
|
||||||
onView(ViewMatchers.withId(R.id.login_password))
|
onView(ViewMatchers.withId(R.id.login_password))
|
||||||
.perform(
|
.perform(
|
||||||
ViewActions.replaceText(getTestUserPassword()),
|
ViewActions.replaceText(getTestUserPassword()),
|
||||||
ViewActions.closeSoftKeyboard()
|
ViewActions.closeSoftKeyboard(),
|
||||||
)
|
)
|
||||||
sleep(2000)
|
sleep(2000)
|
||||||
onView(ViewMatchers.withId(R.id.login_button))
|
onView(ViewMatchers.withId(R.id.login_button))
|
||||||
|
|
@ -76,7 +83,6 @@ class UITestHelper {
|
||||||
sleep(10000)
|
sleep(10000)
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logoutUser() {
|
fun logoutUser() {
|
||||||
|
|
@ -87,36 +93,38 @@ class UITestHelper {
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
|
||||||
0
|
0,
|
||||||
),
|
),
|
||||||
4
|
4,
|
||||||
),
|
),
|
||||||
ViewMatchers.isDisplayed()
|
ViewMatchers.isDisplayed(),
|
||||||
)
|
),
|
||||||
).perform(ViewActions.click())
|
).perform(ViewActions.click())
|
||||||
onView(
|
onView(
|
||||||
Matchers.allOf(
|
Matchers.allOf(
|
||||||
ViewMatchers.withId(R.id.more_logout), ViewMatchers.withText("Logout"),
|
ViewMatchers.withId(R.id.more_logout),
|
||||||
|
ViewMatchers.withText("Logout"),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.scroll_view_more_bottom_sheet),
|
ViewMatchers.withId(R.id.scroll_view_more_bottom_sheet),
|
||||||
0
|
0,
|
||||||
),
|
),
|
||||||
6
|
6,
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
).perform(ViewActions.scrollTo(), ViewActions.click())
|
).perform(ViewActions.scrollTo(), ViewActions.click())
|
||||||
onView(
|
onView(
|
||||||
Matchers.allOf(
|
Matchers.allOf(
|
||||||
ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"),
|
ViewMatchers.withId(android.R.id.button1),
|
||||||
|
ViewMatchers.withText("Yes"),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
ViewMatchers.withId(R.id.buttonPanel),
|
ViewMatchers.withId(R.id.buttonPanel),
|
||||||
0
|
0,
|
||||||
),
|
),
|
||||||
3
|
3,
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
).perform(ViewActions.scrollTo(), ViewActions.click())
|
).perform(ViewActions.scrollTo(), ViewActions.click())
|
||||||
sleep(5000)
|
sleep(5000)
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
|
|
@ -124,9 +132,9 @@ class UITestHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun childAtPosition(
|
fun childAtPosition(
|
||||||
parentMatcher: Matcher<View>, position: Int
|
parentMatcher: Matcher<View>,
|
||||||
|
position: Int,
|
||||||
): Matcher<View> {
|
): Matcher<View> {
|
||||||
|
|
||||||
return object : TypeSafeMatcher<View>() {
|
return object : TypeSafeMatcher<View>() {
|
||||||
override fun describeTo(description: Description) {
|
override fun describeTo(description: Description) {
|
||||||
description.appendText("Child at position $position in parent ")
|
description.appendText("Child at position $position in parent ")
|
||||||
|
|
@ -135,8 +143,9 @@ class UITestHelper {
|
||||||
|
|
||||||
public override fun matchesSafely(view: View): Boolean {
|
public override fun matchesSafely(view: View): Boolean {
|
||||||
val parent = view.parent
|
val parent = view.parent
|
||||||
return parent is ViewGroup && parentMatcher.matches(parent)
|
return parent is ViewGroup &&
|
||||||
&& view == parent.getChildAt(position)
|
parentMatcher.matches(parent) &&
|
||||||
|
view == parent.getChildAt(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,14 +163,18 @@ class UITestHelper {
|
||||||
val username = BuildConfig.TEST_USERNAME
|
val username = BuildConfig.TEST_USERNAME
|
||||||
if (StringUtils.isEmpty(username) || username == "null") {
|
if (StringUtils.isEmpty(username) || username == "null") {
|
||||||
throw NotImplementedError("Configure your beta account's username")
|
throw NotImplementedError("Configure your beta account's username")
|
||||||
} else return username
|
} else {
|
||||||
|
return username
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTestUserPassword(): String {
|
private fun getTestUserPassword(): String {
|
||||||
val password = BuildConfig.TEST_PASSWORD
|
val password = BuildConfig.TEST_PASSWORD
|
||||||
if (StringUtils.isEmpty(password) || password == "null") {
|
if (StringUtils.isEmpty(password) || password == "null") {
|
||||||
throw NotImplementedError("Configure your beta account's password")
|
throw NotImplementedError("Configure your beta account's password")
|
||||||
} else return password
|
} else {
|
||||||
|
return password
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Activity> changeOrientation(activityRule: ActivityTestRule<T>) {
|
fun <T : Activity> changeOrientation(activityRule: ActivityTestRule<T>) {
|
||||||
|
|
@ -174,6 +187,7 @@ class UITestHelper {
|
||||||
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
|
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
|
||||||
return object : BaseMatcher<T>() {
|
return object : BaseMatcher<T>() {
|
||||||
var isFirst = true
|
var isFirst = true
|
||||||
|
|
||||||
override fun matches(item: Any): Boolean {
|
override fun matches(item: Any): Boolean {
|
||||||
if (isFirst && matcher.matches(item)) {
|
if (isFirst && matcher.matches(item)) {
|
||||||
isFirst = false
|
isFirst = false
|
||||||
|
|
@ -188,4 +202,4 @@ class UITestHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ import android.app.Activity
|
||||||
import android.app.Instrumentation
|
import android.app.Instrumentation
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.test.espresso.Espresso.onView
|
import androidx.test.espresso.Espresso.onView
|
||||||
import androidx.test.espresso.action.ViewActions.*
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||||
|
import androidx.test.espresso.action.ViewActions.replaceText
|
||||||
|
import androidx.test.espresso.action.ViewActions.scrollTo
|
||||||
import androidx.test.espresso.contrib.RecyclerViewActions
|
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||||
import androidx.test.espresso.intent.Intents
|
import androidx.test.espresso.intent.Intents
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||||
|
|
@ -15,7 +18,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.rule.ActivityTestRule
|
import androidx.test.rule.ActivityTestRule
|
||||||
import androidx.test.rule.GrantPermissionRule
|
import androidx.test.rule.GrantPermissionRule
|
||||||
import androidx.test.uiautomator.UiDevice
|
import androidx.test.uiautomator.UiDevice
|
||||||
import fr.free.nrw.commons.LocationPicker.LocationPickerActivity
|
import fr.free.nrw.commons.locationpicker.LocationPickerActivity
|
||||||
import fr.free.nrw.commons.UITestHelper.Companion.childAtPosition
|
import fr.free.nrw.commons.UITestHelper.Companion.childAtPosition
|
||||||
import fr.free.nrw.commons.auth.LoginActivity
|
import fr.free.nrw.commons.auth.LoginActivity
|
||||||
import org.hamcrest.CoreMatchers
|
import org.hamcrest.CoreMatchers
|
||||||
|
|
@ -28,7 +31,6 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class UploadCancelledTest {
|
class UploadCancelledTest {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
var mActivityTestRule = ActivityTestRule(LoginActivity::class.java)
|
var mActivityTestRule = ActivityTestRule(LoginActivity::class.java)
|
||||||
|
|
@ -37,7 +39,7 @@ class UploadCancelledTest {
|
||||||
@JvmField
|
@JvmField
|
||||||
var mGrantPermissionRule: GrantPermissionRule =
|
var mGrantPermissionRule: GrantPermissionRule =
|
||||||
GrantPermissionRule.grant(
|
GrantPermissionRule.grant(
|
||||||
"android.permission.WRITE_EXTERNAL_STORAGE"
|
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||||
)
|
)
|
||||||
|
|
||||||
private val device: UiDevice =
|
private val device: UiDevice =
|
||||||
|
|
@ -47,15 +49,15 @@ class UploadCancelledTest {
|
||||||
fun setup() {
|
fun setup() {
|
||||||
try {
|
try {
|
||||||
Intents.init()
|
Intents.init()
|
||||||
} catch (ex: IllegalStateException) {
|
} catch (_: IllegalStateException) {
|
||||||
|
|
||||||
}
|
}
|
||||||
device.unfreezeRotation()
|
device.unfreezeRotation()
|
||||||
device.setOrientationNatural()
|
device.setOrientationNatural()
|
||||||
device.freezeRotation()
|
device.freezeRotation()
|
||||||
UITestHelper.loginUser()
|
UITestHelper.loginUser()
|
||||||
UITestHelper.skipWelcome()
|
UITestHelper.skipWelcome()
|
||||||
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
Intents
|
||||||
|
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
|
||||||
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,131 +65,138 @@ class UploadCancelledTest {
|
||||||
fun teardown() {
|
fun teardown() {
|
||||||
try {
|
try {
|
||||||
Intents.release()
|
Intents.release()
|
||||||
} catch (ex: IllegalStateException) {
|
} catch (_: IllegalStateException) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun uploadCancelledAfterLocationPickedTest() {
|
fun uploadCancelledAfterLocationPickedTest() {
|
||||||
|
val bottomNavigationItemView =
|
||||||
val bottomNavigationItemView = onView(
|
onView(
|
||||||
allOf(
|
allOf(
|
||||||
childAtPosition(
|
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.fragment_main_nav_tab_layout),
|
childAtPosition(
|
||||||
0
|
withId(R.id.fragment_main_nav_tab_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
1
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
bottomNavigationItemView.perform(click())
|
bottomNavigationItemView.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(12000)
|
UITestHelper.sleep(12000)
|
||||||
|
|
||||||
val actionMenuItemView = onView(
|
val actionMenuItemView =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.list_sheet),
|
allOf(
|
||||||
childAtPosition(
|
withId(R.id.list_sheet),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.toolbar),
|
childAtPosition(
|
||||||
1
|
withId(R.id.toolbar),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
actionMenuItemView.perform(click())
|
actionMenuItemView.perform(click())
|
||||||
|
|
||||||
val recyclerView = onView(
|
val recyclerView =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.rv_nearby_list),
|
allOf(
|
||||||
|
withId(R.id.rv_nearby_list),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
recyclerView.perform(
|
recyclerView.perform(
|
||||||
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
|
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
|
||||||
0,
|
0,
|
||||||
click()
|
click(),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
val linearLayout3 = onView(
|
val linearLayout3 =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.cameraButton),
|
allOf(
|
||||||
childAtPosition(
|
withId(R.id.cameraButton),
|
||||||
allOf(
|
childAtPosition(
|
||||||
withId(R.id.nearby_button_layout),
|
allOf(
|
||||||
|
withId(R.id.nearby_button_layout),
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
linearLayout3.perform(click())
|
linearLayout3.perform(click())
|
||||||
|
|
||||||
val pasteSensitiveTextInputEditText = onView(
|
val pasteSensitiveTextInputEditText =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.caption_item_edit_text),
|
allOf(
|
||||||
childAtPosition(
|
withId(R.id.caption_item_edit_text),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.caption_item_edit_text_input_layout),
|
childAtPosition(
|
||||||
0
|
withId(R.id.caption_item_edit_text_input_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
pasteSensitiveTextInputEditText.perform(replaceText("test"), closeSoftKeyboard())
|
pasteSensitiveTextInputEditText.perform(replaceText("test"), closeSoftKeyboard())
|
||||||
|
|
||||||
val pasteSensitiveTextInputEditText2 = onView(
|
val pasteSensitiveTextInputEditText2 =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.description_item_edit_text),
|
allOf(
|
||||||
childAtPosition(
|
withId(R.id.description_item_edit_text),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.description_item_edit_text_input_layout),
|
childAtPosition(
|
||||||
0
|
withId(R.id.description_item_edit_text_input_layout),
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
0
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
pasteSensitiveTextInputEditText2.perform(replaceText("test"), closeSoftKeyboard())
|
pasteSensitiveTextInputEditText2.perform(replaceText("test"), closeSoftKeyboard())
|
||||||
|
|
||||||
val appCompatButton2 = onView(
|
val appCompatButton2 =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.btn_next),
|
allOf(
|
||||||
childAtPosition(
|
withId(R.id.btn_next),
|
||||||
childAtPosition(
|
childAtPosition(
|
||||||
withId(R.id.ll_container_media_detail),
|
childAtPosition(
|
||||||
2
|
withId(R.id.ll_container_media_detail),
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
1,
|
||||||
),
|
),
|
||||||
1
|
isDisplayed(),
|
||||||
),
|
),
|
||||||
isDisplayed()
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
appCompatButton2.perform(click())
|
appCompatButton2.perform(click())
|
||||||
|
|
||||||
val appCompatButton3 = onView(
|
val appCompatButton3 =
|
||||||
allOf(
|
onView(
|
||||||
withId(android.R.id.button1),
|
allOf(
|
||||||
|
withId(android.R.id.button1),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
appCompatButton3.perform(scrollTo(), click())
|
appCompatButton3.perform(scrollTo(), click())
|
||||||
|
|
||||||
Intents.intended(IntentMatchers.hasComponent(LocationPickerActivity::class.java.name))
|
Intents.intended(IntentMatchers.hasComponent(LocationPickerActivity::class.java.name))
|
||||||
|
|
||||||
val floatingActionButton3 = onView(
|
val floatingActionButton3 =
|
||||||
allOf(
|
onView(
|
||||||
withId(R.id.location_chosen_button),
|
allOf(
|
||||||
isDisplayed()
|
withId(R.id.location_chosen_button),
|
||||||
|
isDisplayed(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
UITestHelper.sleep(2000)
|
UITestHelper.sleep(2000)
|
||||||
floatingActionButton3.perform(click())
|
floatingActionButton3.perform(click())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@ import androidx.test.espresso.intent.Intents.intended
|
||||||
import androidx.test.espresso.intent.Intents.intending
|
import androidx.test.espresso.intent.Intents.intending
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasType
|
import androidx.test.espresso.intent.matcher.IntentMatchers.hasType
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withParent
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import androidx.test.rule.ActivityTestRule
|
import androidx.test.rule.ActivityTestRule
|
||||||
|
|
@ -29,21 +32,29 @@ import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
|
||||||
import fr.free.nrw.commons.util.MyViewAction
|
import fr.free.nrw.commons.util.MyViewAction
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils
|
import fr.free.nrw.commons.utils.ConfigUtils
|
||||||
import org.hamcrest.core.AllOf.allOf
|
import org.hamcrest.core.AllOf.allOf
|
||||||
import org.junit.*
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
import java.util.Random
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class UploadTest {
|
class UploadTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
var permissionRule =
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION)!!
|
GrantPermissionRule.grant(
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
)!!
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
var activityRule = ActivityTestRule(LoginActivity::class.java)
|
||||||
|
|
@ -60,8 +71,7 @@ class UploadTest {
|
||||||
fun setup() {
|
fun setup() {
|
||||||
try {
|
try {
|
||||||
Intents.init()
|
Intents.init()
|
||||||
} catch (ex: IllegalStateException) {
|
} catch (_: IllegalStateException) {
|
||||||
|
|
||||||
}
|
}
|
||||||
UITestHelper.loginUser()
|
UITestHelper.loginUser()
|
||||||
UITestHelper.skipWelcome()
|
UITestHelper.skipWelcome()
|
||||||
|
|
@ -94,14 +104,13 @@ class UploadTest {
|
||||||
dismissWarning("Yes")
|
dismissWarning("Yes")
|
||||||
|
|
||||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||||
.perform(replaceText(commonsFileName))
|
.perform(replaceText(commonsFileName))
|
||||||
|
|
||||||
onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text)))
|
onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text)))
|
||||||
.perform(replaceText(commonsFileName))
|
.perform(replaceText(commonsFileName))
|
||||||
|
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(5000)
|
UITestHelper.sleep(5000)
|
||||||
dismissWarning("Yes")
|
dismissWarning("Yes")
|
||||||
|
|
@ -109,29 +118,30 @@ class UploadTest {
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||||
.perform(replaceText("Uploaded with Mobile/Android Tests"))
|
.perform(replaceText("Uploaded with Mobile/Android Tests"))
|
||||||
|
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
dismissWarning("Yes, Submit")
|
dismissWarning("Yes, Submit")
|
||||||
|
|
||||||
UITestHelper.sleep(500)
|
UITestHelper.sleep(500)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(10000)
|
UITestHelper.sleep(10000)
|
||||||
|
|
||||||
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
val fileUrl =
|
||||||
|
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||||
commonsFileName.replace(' ', '_') + ".jpg"
|
commonsFileName.replace(' ', '_') + ".jpg"
|
||||||
Timber.i("File should be uploaded to $fileUrl")
|
Timber.i("File should be uploaded to $fileUrl")
|
||||||
}
|
}
|
||||||
|
|
@ -139,8 +149,8 @@ class UploadTest {
|
||||||
private fun dismissWarning(warningText: String) {
|
private fun dismissWarning(warningText: String) {
|
||||||
try {
|
try {
|
||||||
onView(withText(warningText))
|
onView(withText(warningText))
|
||||||
.check(matches(isDisplayed()))
|
.check(matches(isDisplayed()))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,10 +177,10 @@ class UploadTest {
|
||||||
dismissWarning("Yes")
|
dismissWarning("Yes")
|
||||||
|
|
||||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||||
.perform(replaceText(commonsFileName))
|
.perform(replaceText(commonsFileName))
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(10000)
|
UITestHelper.sleep(10000)
|
||||||
dismissWarning("Yes")
|
dismissWarning("Yes")
|
||||||
|
|
@ -178,29 +188,30 @@ class UploadTest {
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||||
.perform(replaceText("Test"))
|
.perform(replaceText("Test"))
|
||||||
|
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
dismissWarning("Yes, Submit")
|
dismissWarning("Yes, Submit")
|
||||||
|
|
||||||
UITestHelper.sleep(500)
|
UITestHelper.sleep(500)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(10000)
|
UITestHelper.sleep(10000)
|
||||||
|
|
||||||
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
val fileUrl =
|
||||||
|
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||||
commonsFileName.replace(' ', '_') + ".jpg"
|
commonsFileName.replace(' ', '_') + ".jpg"
|
||||||
Timber.i("File should be uploaded to $fileUrl")
|
Timber.i("File should be uploaded to $fileUrl")
|
||||||
}
|
}
|
||||||
|
|
@ -227,23 +238,29 @@ class UploadTest {
|
||||||
dismissWarningDialog()
|
dismissWarningDialog()
|
||||||
|
|
||||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||||
.perform(replaceText(commonsFileName))
|
.perform(replaceText(commonsFileName))
|
||||||
|
|
||||||
onView(withId(R.id.rv_descriptions)).perform(
|
onView(withId(R.id.rv_descriptions)).perform(
|
||||||
RecyclerViewActions
|
RecyclerViewActions
|
||||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0,
|
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
|
||||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
|
0,
|
||||||
|
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
onView(withId(R.id.btn_add_description))
|
onView(withId(R.id.btn_add))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
onView(withId(R.id.rv_descriptions)).perform(
|
onView(withId(R.id.rv_descriptions)).perform(
|
||||||
RecyclerViewActions
|
RecyclerViewActions
|
||||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1,
|
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
|
||||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))
|
1,
|
||||||
|
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(5000)
|
UITestHelper.sleep(5000)
|
||||||
dismissWarning("Yes")
|
dismissWarning("Yes")
|
||||||
|
|
@ -251,29 +268,30 @@ class UploadTest {
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
onView(allOf(isDisplayed(), withId(R.id.et_search)))
|
||||||
.perform(replaceText("Test"))
|
.perform(replaceText("Test"))
|
||||||
|
|
||||||
UITestHelper.sleep(3000)
|
UITestHelper.sleep(3000)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
onView(allOf(isDisplayed(), UITestHelper.first(withParent(withId(R.id.rv_categories)))))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
dismissWarning("Yes, Submit")
|
dismissWarning("Yes, Submit")
|
||||||
|
|
||||||
UITestHelper.sleep(500)
|
UITestHelper.sleep(500)
|
||||||
|
|
||||||
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
onView(allOf(isDisplayed(), withId(R.id.btn_submit)))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
UITestHelper.sleep(10000)
|
UITestHelper.sleep(10000)
|
||||||
|
|
||||||
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
val fileUrl =
|
||||||
|
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
|
||||||
commonsFileName.replace(' ', '_') + ".jpg"
|
commonsFileName.replace(' ', '_') + ".jpg"
|
||||||
Timber.i("File should be uploaded to $fileUrl")
|
Timber.i("File should be uploaded to $fileUrl")
|
||||||
}
|
}
|
||||||
|
|
@ -306,7 +324,6 @@ class UploadTest {
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,8 +345,8 @@ class UploadTest {
|
||||||
private fun dismissWarningDialog() {
|
private fun dismissWarningDialog() {
|
||||||
try {
|
try {
|
||||||
onView(withText("Yes"))
|
onView(withText("Yes"))
|
||||||
.check(matches(isDisplayed()))
|
.check(matches(isDisplayed()))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
} catch (ignored: NoMatchingViewException) {
|
} catch (ignored: NoMatchingViewException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -337,10 +354,10 @@ class UploadTest {
|
||||||
private fun openGallery() {
|
private fun openGallery() {
|
||||||
// Open FAB
|
// Open FAB
|
||||||
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
|
onView(allOf<View>(withId(R.id.fab_plus), isDisplayed()))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
|
|
||||||
// Click gallery
|
// Click gallery
|
||||||
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
|
onView(allOf<View>(withId(R.id.fab_gallery), isDisplayed()))
|
||||||
.perform(click())
|
.perform(click())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons
|
||||||
import androidx.test.espresso.Espresso.onView
|
import androidx.test.espresso.Espresso.onView
|
||||||
import androidx.test.espresso.action.ViewActions
|
import androidx.test.espresso.action.ViewActions
|
||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
import androidx.test.espresso.matcher.ViewMatchers
|
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
@ -18,11 +17,12 @@ import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class WelcomeActivityTest {
|
class WelcomeActivityTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var activityRule: ActivityTestRule<*> = ActivityTestRule(WelcomeActivity::class.java)
|
var activityRule: ActivityTestRule<*> = ActivityTestRule(WelcomeActivity::class.java)
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ class WelcomeActivityTest {
|
||||||
.perform(ViewActions.click())
|
.perform(ViewActions.click())
|
||||||
onView(withId(R.id.finishTutorialButton))
|
onView(withId(R.id.finishTutorialButton))
|
||||||
.perform(ViewActions.click())
|
.perform(ViewActions.click())
|
||||||
assert(activityRule.activity.isDestroyed)
|
assertThat(activityRule.activity.isDestroyed, equalTo(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,10 +71,10 @@ class WelcomeActivityTest {
|
||||||
.perform(ViewActions.click())
|
.perform(ViewActions.click())
|
||||||
onView(withId(R.id.welcomePager))
|
onView(withId(R.id.welcomePager))
|
||||||
.perform(ViewActions.swipeLeft())
|
.perform(ViewActions.swipeLeft())
|
||||||
assert(true)
|
assertThat(true, equalTo(true))
|
||||||
onView(withId(R.id.welcomePager))
|
onView(withId(R.id.welcomePager))
|
||||||
.perform(ViewActions.swipeRight())
|
.perform(ViewActions.swipeRight())
|
||||||
assert(true)
|
assertThat(true, equalTo(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -86,13 +86,13 @@ class WelcomeActivityTest {
|
||||||
.perform(ViewActions.swipeLeft())
|
.perform(ViewActions.swipeLeft())
|
||||||
.perform(ViewActions.swipeLeft())
|
.perform(ViewActions.swipeLeft())
|
||||||
.perform(ViewActions.swipeLeft())
|
.perform(ViewActions.swipeLeft())
|
||||||
assert(true)
|
assertThat(true, equalTo(true))
|
||||||
onView(withId(R.id.welcomePager))
|
onView(withId(R.id.welcomePager))
|
||||||
.perform(ViewActions.swipeRight())
|
.perform(ViewActions.swipeRight())
|
||||||
.perform(ViewActions.swipeRight())
|
.perform(ViewActions.swipeRight())
|
||||||
.perform(ViewActions.swipeRight())
|
.perform(ViewActions.swipeRight())
|
||||||
.perform(ViewActions.swipeRight())
|
.perform(ViewActions.swipeRight())
|
||||||
assert(true)
|
assertThat(true, equalTo(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -103,10 +103,10 @@ class WelcomeActivityTest {
|
||||||
if (viewPager.currentItem == 3) {
|
if (viewPager.currentItem == 3) {
|
||||||
onView(withId(R.id.welcomePager))
|
onView(withId(R.id.welcomePager))
|
||||||
.perform(ViewActions.swipeLeft())
|
.perform(ViewActions.swipeLeft())
|
||||||
assert(true)
|
assertThat(true, equalTo(true))
|
||||||
onView(withId(R.id.welcomePager))
|
onView(withId(R.id.welcomePager))
|
||||||
.perform(ViewActions.swipeRight())
|
.perform(ViewActions.swipeRight())
|
||||||
assert(false)
|
assertThat(true, equalTo(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +121,7 @@ class WelcomeActivityTest {
|
||||||
.perform(ViewActions.click())
|
.perform(ViewActions.click())
|
||||||
onView(withId(R.id.finishTutorialButton))
|
onView(withId(R.id.finishTutorialButton))
|
||||||
.perform(ViewActions.click())
|
.perform(ViewActions.click())
|
||||||
assert(activityRule.activity.isDestroyed)
|
assertThat(activityRule.activity.isDestroyed, equalTo(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,4 +130,4 @@ class WelcomeActivityTest {
|
||||||
fun orientationChange() {
|
fun orientationChange() {
|
||||||
UITestHelper.changeOrientation(activityRule)
|
UITestHelper.changeOrientation(activityRule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
package fr.free.nrw.commons.contributions
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.fragment.app.testing.FragmentScenario
|
||||||
|
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
import fr.free.nrw.commons.Media
|
||||||
|
import fr.free.nrw.commons.OkHttpConnectionFactory
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.TestCommonsApplication
|
||||||
|
import fr.free.nrw.commons.createTestClient
|
||||||
|
import fr.free.nrw.commons.upload.WikidataPlace
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.ArgumentMatchers.anyInt
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.`when`
|
||||||
|
import org.robolectric.Shadows
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.LooperMode
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@Config(sdk = [21], application = TestCommonsApplication::class)
|
||||||
|
@LooperMode(LooperMode.Mode.PAUSED)
|
||||||
|
class ContributionsListFragmentUnitTests {
|
||||||
|
private lateinit var scenario: FragmentScenario<ContributionsListFragment>
|
||||||
|
private lateinit var fragment: ContributionsListFragment
|
||||||
|
|
||||||
|
private val adapter: ContributionsListAdapter = mock()
|
||||||
|
private val contribution: Contribution = mock()
|
||||||
|
private val media: Media = mock()
|
||||||
|
private val wikidataPlace: WikidataPlace = mock()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
OkHttpConnectionFactory.CLIENT = createTestClient()
|
||||||
|
|
||||||
|
scenario =
|
||||||
|
launchFragmentInContainer(
|
||||||
|
initialState = Lifecycle.State.RESUMED,
|
||||||
|
themeResId = R.style.LightAppTheme,
|
||||||
|
) {
|
||||||
|
ContributionsListFragment()
|
||||||
|
.apply {
|
||||||
|
contributionsListPresenter = mock()
|
||||||
|
callback = mock()
|
||||||
|
}.also {
|
||||||
|
fragment = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scenario.onFragment {
|
||||||
|
it.adapter = adapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun checkFragmentNotNull() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
Assert.assertNotNull(fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testOnDetach() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
fragment.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testGetContributionStateAt() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
`when`(adapter.getContributionForPosition(anyInt())).thenReturn(contribution)
|
||||||
|
fragment.getContributionStateAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testOnScrollToTop() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
fragment.rvContributionsList = mock()
|
||||||
|
fragment.scrollToTop()
|
||||||
|
verify(fragment.rvContributionsList)?.smoothScrollToPosition(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testOnConfirmClicked() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
`when`(contribution.media).thenReturn(media)
|
||||||
|
`when`(media.wikiCode).thenReturn("")
|
||||||
|
`when`(contribution.wikidataPlace).thenReturn(wikidataPlace)
|
||||||
|
fragment.onConfirmClicked(contribution, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testGetTotalMediaCount() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
fragment.totalMediaCount
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testGetMediaAtPositionCaseNonNull() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
`when`(adapter.getContributionForPosition(anyInt())).thenReturn(contribution)
|
||||||
|
`when`(contribution.media).thenReturn(media)
|
||||||
|
fragment.getMediaAtPosition(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testGetMediaAtPositionCaseNull() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
`when`(adapter.getContributionForPosition(anyInt())).thenReturn(null)
|
||||||
|
fragment.getMediaAtPosition(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testShowAddImageToWikipediaInstructions() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
val method: Method =
|
||||||
|
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||||
|
"showAddImageToWikipediaInstructions",
|
||||||
|
Contribution::class.java,
|
||||||
|
)
|
||||||
|
method.isAccessible = true
|
||||||
|
method.invoke(fragment, contribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testAddImageToWikipedia() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
fragment.addImageToWikipedia(contribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testOpenMediaDetail() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
fragment.openMediaDetail(0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testOnViewStateRestored() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
fragment.onViewStateRestored(mock())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testOnSaveInstanceState() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
fragment.onSaveInstanceState(mock())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testShowNoContributionsUI() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
fragment.showNoContributionsUI(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testShowProgress() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
fragment.showProgress(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testShowWelcomeTip() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
fragment.showWelcomeTip(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testAnimateFAB() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
scenario.onFragment {
|
||||||
|
it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).hide()
|
||||||
|
}
|
||||||
|
val method: Method =
|
||||||
|
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||||
|
"animateFAB",
|
||||||
|
Boolean::class.java,
|
||||||
|
)
|
||||||
|
method.isAccessible = true
|
||||||
|
method.invoke(fragment, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testAnimateFABCaseShownAndOpen() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
scenario.onFragment {
|
||||||
|
it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).show()
|
||||||
|
}
|
||||||
|
val method: Method =
|
||||||
|
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||||
|
"animateFAB",
|
||||||
|
Boolean::class.java,
|
||||||
|
)
|
||||||
|
method.isAccessible = true
|
||||||
|
method.invoke(fragment, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testAnimateFABCaseShownAndClose() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
scenario.onFragment {
|
||||||
|
it.requireView().findViewById<FloatingActionButton>(R.id.fab_plus).show()
|
||||||
|
}
|
||||||
|
val method: Method =
|
||||||
|
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||||
|
"animateFAB",
|
||||||
|
Boolean::class.java,
|
||||||
|
)
|
||||||
|
method.isAccessible = true
|
||||||
|
method.invoke(fragment, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testSetListeners() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
val method: Method =
|
||||||
|
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||||
|
"setListeners",
|
||||||
|
)
|
||||||
|
method.isAccessible = true
|
||||||
|
method.invoke(fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testInitializeAnimations() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
val method: Method =
|
||||||
|
ContributionsListFragment::class.java.getDeclaredMethod(
|
||||||
|
"initializeAnimations",
|
||||||
|
)
|
||||||
|
method.isAccessible = true
|
||||||
|
method.invoke(fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testOnConfigurationChanged() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
val newConfig: Configuration = mock()
|
||||||
|
newConfig.orientation = Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
fragment.onConfigurationChanged(newConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package fr.free.nrw.commons.navtab
|
||||||
|
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.fragment.app.testing.FragmentScenario
|
||||||
|
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.TestCommonsApplication
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.Shadows
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import org.robolectric.annotation.LooperMode
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@Config(sdk = [21], application = TestCommonsApplication::class)
|
||||||
|
@LooperMode(LooperMode.Mode.PAUSED)
|
||||||
|
class MoreBottomSheetLoggedOutFragmentUnitTests {
|
||||||
|
private lateinit var scenario: FragmentScenario<MoreBottomSheetLoggedOutFragment>
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
scenario =
|
||||||
|
launchFragmentInContainer(
|
||||||
|
initialState = Lifecycle.State.RESUMED,
|
||||||
|
themeResId = R.style.LightAppTheme,
|
||||||
|
) {
|
||||||
|
MoreBottomSheetLoggedOutFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testOnSettingsClicked() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
scenario.onFragment { it.onSettingsClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testOnAboutClicked() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
scenario.onFragment { it.onAboutClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testOnFeedbackClicked() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
scenario.onFragment { it.onFeedbackClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testOnLogoutClicked() {
|
||||||
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
|
scenario.onFragment { it.onLogoutClicked() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,21 +11,24 @@ import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class PasteSensitiveTextInputEditTextTest {
|
class PasteSensitiveTextInputEditTextTest {
|
||||||
|
|
||||||
private var context: Context? = null
|
private var context: Context? = null
|
||||||
private var textView: PasteSensitiveTextInputEditText? = null
|
private var textView: PasteSensitiveTextInputEditText? = null
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
context = ApplicationProvider.getApplicationContext()
|
context = ApplicationProvider.getApplicationContext()
|
||||||
textView = PasteSensitiveTextInputEditText(context)
|
textView = PasteSensitiveTextInputEditText(context!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
// this test has no real value, just % for test code coverage
|
// this test has no real value, just % for test code coverage
|
||||||
@Test
|
@Test
|
||||||
fun extractFormattingAttributeSet(){
|
fun extractFormattingAttributeSet() {
|
||||||
val methodExtractFormattingAttribute = textView!!.javaClass.getDeclaredMethod(
|
val methodExtractFormattingAttribute =
|
||||||
"extractFormattingAttribute", Context::class.java, AttributeSet::class.java)
|
textView!!.javaClass.getDeclaredMethod(
|
||||||
|
"extractFormattingAttribute",
|
||||||
|
Context::class.java,
|
||||||
|
AttributeSet::class.java,
|
||||||
|
)
|
||||||
methodExtractFormattingAttribute.isAccessible = true
|
methodExtractFormattingAttribute.isAccessible = true
|
||||||
methodExtractFormattingAttribute.invoke(textView, context, null)
|
methodExtractFormattingAttribute.invoke(textView, context, null)
|
||||||
}
|
}
|
||||||
|
|
@ -40,4 +43,4 @@ class PasteSensitiveTextInputEditTextTest {
|
||||||
textView!!.setFormattingAllowed(false)
|
textView!!.setFormattingAllowed(false)
|
||||||
Assert.assertFalse(fieldFormattingAllowed.getBoolean(textView))
|
Assert.assertFalse(fieldFormattingAllowed.getBoolean(textView))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,56 +9,58 @@ import org.hamcrest.Matcher
|
||||||
|
|
||||||
class MyViewAction {
|
class MyViewAction {
|
||||||
companion object {
|
companion object {
|
||||||
fun typeTextInChildViewWithId(id: Int, textToBeTyped: String): ViewAction {
|
fun typeTextInChildViewWithId(
|
||||||
return object : ViewAction {
|
id: Int,
|
||||||
override fun getConstraints(): Matcher<View>? {
|
textToBeTyped: String,
|
||||||
return null
|
): ViewAction =
|
||||||
}
|
object : ViewAction {
|
||||||
|
override fun getConstraints(): Matcher<View>? = null
|
||||||
|
|
||||||
override fun getDescription(): String {
|
override fun getDescription(): String = "Click on a child view with specified id."
|
||||||
return "Click on a child view with specified id."
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun perform(uiController: UiController, view: View) {
|
override fun perform(
|
||||||
|
uiController: UiController,
|
||||||
|
view: View,
|
||||||
|
) {
|
||||||
val v = view.findViewById<View>(id) as EditText
|
val v = view.findViewById<View>(id) as EditText
|
||||||
v.setText(textToBeTyped)
|
v.setText(textToBeTyped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun selectSpinnerItemInChildViewWithId(id: Int, position: Int): ViewAction {
|
fun selectSpinnerItemInChildViewWithId(
|
||||||
return object : ViewAction {
|
id: Int,
|
||||||
override fun getConstraints(): Matcher<View>? {
|
position: Int,
|
||||||
return null
|
): ViewAction =
|
||||||
}
|
object : ViewAction {
|
||||||
|
override fun getConstraints(): Matcher<View>? = null
|
||||||
|
|
||||||
override fun getDescription(): String {
|
override fun getDescription(): String = "Click on a child view with specified id."
|
||||||
return "Click on a child view with specified id."
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun perform(uiController: UiController, view: View) {
|
override fun perform(
|
||||||
|
uiController: UiController,
|
||||||
|
view: View,
|
||||||
|
) {
|
||||||
val v = view.findViewById<View>(id) as AppCompatSpinner
|
val v = view.findViewById<View>(id) as AppCompatSpinner
|
||||||
v.setSelection(position)
|
v.setSelection(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun clickItemWithId(id: Int, position: Int): ViewAction {
|
fun clickItemWithId(
|
||||||
return object : ViewAction {
|
id: Int,
|
||||||
override fun getConstraints(): Matcher<View>? {
|
position: Int,
|
||||||
return null
|
): ViewAction =
|
||||||
}
|
object : ViewAction {
|
||||||
|
override fun getConstraints(): Matcher<View>? = null
|
||||||
|
|
||||||
override fun getDescription(): String {
|
override fun getDescription(): String = "Click on a child view with specified id."
|
||||||
return "Click on a child view with specified id."
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun perform(uiController: UiController, view: View) {
|
override fun perform(
|
||||||
|
uiController: UiController,
|
||||||
|
view: View,
|
||||||
|
) {
|
||||||
val v = view.findViewById<View>(id) as View
|
val v = view.findViewById<View>(id) as View
|
||||||
v.performClick()
|
v.performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,257 +1,259 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
|
||||||
<uses-permission android:name="android.permission.REORDER_TASKS" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
|
||||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
|
||||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
|
||||||
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
|
||||||
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
|
|
||||||
|
|
||||||
<queries>
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<!-- Browser -->
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
<intent>
|
android:maxSdkVersion="32" />
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||||
<data android:scheme="https" />
|
<uses-permission android:name="android.permission.REORDER_TASKS" />
|
||||||
</intent>
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
<!-- Google Maps -->
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
<package android:name="com.google.android.apps.maps" />
|
android:maxSdkVersion="29"/>
|
||||||
</queries>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||||
|
<!-- Permission needed up to Android 5.1, see https://github.com/commons-app/apps-android-commons/pull/5863 -->
|
||||||
|
<uses-permission android:name="android.permission.GET_ACCOUNTS"
|
||||||
|
android:maxSdkVersion="22"/>
|
||||||
|
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"
|
||||||
|
android:minSdkVersion="33"/>
|
||||||
|
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
||||||
|
<uses-permission android:name="android.permission.SET_WALLPAPER" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
|
||||||
|
android:minSdkVersion="34"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
|
||||||
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
|
<!-- Browser -->
|
||||||
<uses-feature android:name="android.hardware.location.gps" />
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<application
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
android:name=".CommonsApplication"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/LightAppTheme"
|
|
||||||
android:largeHeap="true"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
tools:replace="android:appComponentFactory"
|
|
||||||
android:appComponentFactory="commons"
|
|
||||||
android:requestLegacyExternalStorage = "true"
|
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
|
||||||
|
|
||||||
<activity
|
<data android:scheme="https" />
|
||||||
android:name=".description.DescriptionEditActivity"
|
</intent>
|
||||||
android:exported="true" />
|
<!-- Google Maps -->
|
||||||
|
<package android:name="com.google.android.apps.maps" />
|
||||||
|
</queries> <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
|
||||||
|
<uses-feature android:name="android.hardware.location.gps" />
|
||||||
|
|
||||||
<activity android:name="org.acra.dialog.CrashReportDialog"
|
<application
|
||||||
android:process=":acra"
|
android:name=".CommonsApplication"
|
||||||
android:launchMode="singleInstance"
|
android:appComponentFactory="commons"
|
||||||
android:excludeFromRecents="true"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:finishOnTaskLaunch="true" />
|
android:label="@string/app_name"
|
||||||
|
android:largeHeap="true"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/LightAppTheme"
|
||||||
|
tools:ignore="GoogleAppIndexingWarning"
|
||||||
|
tools:replace="android:appComponentFactory">
|
||||||
|
<activity
|
||||||
|
android:name=".activity.SingleWebViewActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".nearby.WikidataFeedback"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".upload.UploadProgressActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".description.DescriptionEditActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/EditActivityTheme" />
|
||||||
|
<activity
|
||||||
|
android:name=".edit.EditActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name="org.acra.dialog.CrashReportDialog"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:finishOnTaskLaunch="true"
|
||||||
|
android:launchMode="singleInstance"
|
||||||
|
android:process=":acra" />
|
||||||
|
<activity
|
||||||
|
android:name=".media.ZoomableActivity"
|
||||||
|
android:configChanges="screenSize|keyboard|orientation"
|
||||||
|
android:label="Zoomable Activity"
|
||||||
|
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".auth.LoginActivity"
|
||||||
|
android:windowSoftInputMode="adjustPan"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|
||||||
<activity
|
<action android:name="android.intent.action.MAIN" />
|
||||||
android:name=".media.ZoomableActivity"
|
</intent-filter>
|
||||||
android:label="Zoomable Activity"
|
|
||||||
android:configChanges="screenSize|keyboard|orientation"
|
|
||||||
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
|
|
||||||
|
|
||||||
<activity android:name=".auth.LoginActivity"
|
<meta-data
|
||||||
android:exported="true">
|
android:name="android.app.shortcuts"
|
||||||
<intent-filter>
|
android:resource="@xml/shortcuts" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
</activity>
|
||||||
|
<activity android:name=".WelcomeActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".upload.UploadActivity"
|
||||||
|
android:configChanges="orientation|screenSize|keyboard"
|
||||||
|
android:exported="true"
|
||||||
|
android:hardwareAccelerated="false"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:windowSoftInputMode="adjustPan">
|
||||||
|
<intent-filter android:label="@string/intent_share_upload_label">
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data android:name="android.app.shortcuts"
|
<data android:mimeType="image/*" />
|
||||||
android:resource="@xml/shortcuts" />
|
<data android:mimeType="audio/ogg" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:label="@string/intent_share_upload_label">
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
|
||||||
</activity>
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<activity android:name=".WelcomeActivity" />
|
|
||||||
|
|
||||||
<activity
|
<data android:mimeType="image/*" />
|
||||||
android:hardwareAccelerated="false"
|
<data android:mimeType="audio/ogg" />
|
||||||
android:name=".upload.UploadActivity"
|
</intent-filter>
|
||||||
android:exported="true"
|
</activity>
|
||||||
android:configChanges="orientation|screenSize|keyboard"
|
<activity
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:name=".contributions.MainActivity"
|
||||||
android:label="@string/app_name"
|
android:configChanges="screenSize|keyboard|orientation"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:icon="@mipmap/ic_launcher"
|
||||||
>
|
/>
|
||||||
<intent-filter android:label="@string/intent_share_upload_label">
|
<activity
|
||||||
<action android:name="android.intent.action.SEND" />
|
android:name=".settings.SettingsActivity"
|
||||||
|
android:label="@string/title_activity_settings" />
|
||||||
|
<activity
|
||||||
|
android:name=".AboutActivity"
|
||||||
|
android:label="@string/title_activity_about"
|
||||||
|
android:parentActivityName=".contributions.MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".auth.SignupActivity"
|
||||||
|
android:configChanges="orientation|screenLayout|screenSize"
|
||||||
|
android:label="@string/title_activity_signup" />
|
||||||
|
<activity
|
||||||
|
android:name=".notification.NotificationActivity"
|
||||||
|
android:label="@string/navigation_item_notification" />
|
||||||
|
<activity
|
||||||
|
android:name=".quiz.QuizActivity"
|
||||||
|
android:label="@string/quiz" />
|
||||||
|
<activity
|
||||||
|
android:name=".quiz.QuizResultActivity"
|
||||||
|
android:label="@string/result" />
|
||||||
|
<activity
|
||||||
|
android:name=".customselector.ui.selector.CustomSelectorActivity"
|
||||||
|
android:configChanges="screenSize|keyboard|orientation"
|
||||||
|
android:label="@string/title_activity_custom_selector"
|
||||||
|
android:parentActivityName=".contributions.MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".category.CategoryDetailsActivity"
|
||||||
|
android:configChanges="screenSize|keyboard|orientation"
|
||||||
|
android:label="@string/title_activity_featured_images"
|
||||||
|
android:parentActivityName=".contributions.MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".explore.depictions.WikidataItemDetailsActivity"
|
||||||
|
android:configChanges="screenSize|keyboard|orientation"
|
||||||
|
android:label="@string/title_activity_featured_images"
|
||||||
|
android:parentActivityName=".contributions.MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".explore.SearchActivity"
|
||||||
|
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||||
|
android:label="@string/title_activity_search"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:parentActivityName=".contributions.MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".profile.ProfileActivity"
|
||||||
|
android:configChanges="orientation|screenSize|keyboard"
|
||||||
|
android:label="@string/Profile" />
|
||||||
|
<activity
|
||||||
|
android:name=".review.ReviewActivity"
|
||||||
|
android:label="@string/title_activity_review" />
|
||||||
|
<activity
|
||||||
|
android:name=".locationpicker.LocationPickerActivity"
|
||||||
|
android:label="Location Picker" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<service
|
||||||
|
android:name=".auth.WikiAccountAuthenticatorService"
|
||||||
|
android:exported="true"
|
||||||
|
android:process=":auth">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.accounts.AccountAuthenticator" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<data android:mimeType="image/*" />
|
<meta-data
|
||||||
<data android:mimeType="audio/ogg" />
|
android:name="android.accounts.AccountAuthenticator"
|
||||||
</intent-filter>
|
android:resource="@xml/authenticator" />
|
||||||
<intent-filter android:label="@string/intent_share_upload_label">
|
</service>
|
||||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
<service
|
||||||
|
android:name="org.acra.sender.SenderService"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":acra" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<provider
|
||||||
|
android:name=".filepicker.ExtendedFileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
|
<provider
|
||||||
|
android:name=".category.CategoryContentProvider"
|
||||||
|
android:authorities="${applicationId}.categories.contentprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/provider_categories"
|
||||||
|
android:syncable="false" />
|
||||||
|
<provider
|
||||||
|
android:name=".explore.recentsearches.RecentSearchesContentProvider"
|
||||||
|
android:authorities="${applicationId}.explore.recentsearches.contentprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/provider_searches"
|
||||||
|
android:syncable="false" />
|
||||||
|
<provider
|
||||||
|
android:name=".recentlanguages.RecentLanguagesContentProvider"
|
||||||
|
android:authorities="${applicationId}.recentlanguages.contentprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/provider_recent_languages"
|
||||||
|
android:syncable="false" />
|
||||||
|
<provider
|
||||||
|
android:name=".bookmarks.pictures.BookmarkPicturesContentProvider"
|
||||||
|
android:authorities="${applicationId}.bookmarks.contentprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/provider_bookmarks"
|
||||||
|
android:syncable="false" />
|
||||||
|
<provider
|
||||||
|
android:name=".bookmarks.items.BookmarkItemsContentProvider"
|
||||||
|
android:authorities="${applicationId}.bookmarks.items.contentprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/provider_bookmarks_location"
|
||||||
|
android:syncable="false" />
|
||||||
|
|
||||||
<data android:mimeType="image/*" />
|
<receiver
|
||||||
<data android:mimeType="audio/ogg" />
|
android:name=".widget.PicOfDayAppWidget"
|
||||||
</intent-filter>
|
android:exported="true">
|
||||||
</activity>
|
<intent-filter>
|
||||||
<activity
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
android:name=".contributions.MainActivity"
|
</intent-filter>
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:configChanges="screenSize|keyboard|orientation" />
|
|
||||||
<activity
|
|
||||||
android:name=".settings.SettingsActivity"
|
|
||||||
android:label="@string/title_activity_settings" />
|
|
||||||
<activity
|
|
||||||
android:name=".AboutActivity"
|
|
||||||
android:label="@string/title_activity_about"
|
|
||||||
android:parentActivityName=".contributions.MainActivity" />
|
|
||||||
|
|
||||||
<activity
|
<meta-data
|
||||||
android:name=".auth.SignupActivity"
|
android:name="android.appwidget.provider"
|
||||||
android:configChanges="orientation|screenLayout|screenSize"
|
android:resource="@xml/pic_of_day_app_widget_info" />
|
||||||
android:label="@string/title_activity_signup" />
|
</receiver>
|
||||||
|
|
||||||
<activity
|
<uses-library
|
||||||
android:name=".notification.NotificationActivity"
|
android:name="org.apache.http.legacy"
|
||||||
android:label="@string/navigation_item_notification" />
|
android:required="false" />
|
||||||
|
</application>
|
||||||
<activity android:name=".quiz.QuizActivity"
|
|
||||||
android:label="@string/quiz"/>
|
|
||||||
|
|
||||||
<activity android:name=".quiz.QuizResultActivity"
|
|
||||||
android:label="@string/result"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".customselector.ui.selector.CustomSelectorActivity"
|
|
||||||
android:label="@string/title_activity_custom_selector"
|
|
||||||
android:configChanges="screenSize|keyboard|orientation"
|
|
||||||
android:parentActivityName=".contributions.MainActivity" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".category.CategoryDetailsActivity"
|
|
||||||
android:label="@string/title_activity_featured_images"
|
|
||||||
android:configChanges="screenSize|keyboard|orientation"
|
|
||||||
android:parentActivityName=".contributions.MainActivity" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".explore.depictions.WikidataItemDetailsActivity"
|
|
||||||
android:label="@string/title_activity_featured_images"
|
|
||||||
android:configChanges="screenSize|keyboard|orientation"
|
|
||||||
android:parentActivityName=".contributions.MainActivity" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".explore.SearchActivity"
|
|
||||||
android:label="@string/title_activity_search"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
|
||||||
android:parentActivityName=".contributions.MainActivity"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".profile.ProfileActivity"
|
|
||||||
android:configChanges="orientation|screenSize|keyboard"
|
|
||||||
android:label="@string/Profile" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".review.ReviewActivity"
|
|
||||||
android:label="@string/title_activity_review" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".LocationPicker.LocationPickerActivity"
|
|
||||||
android:label="Location Picker" />
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".auth.WikiAccountAuthenticatorService"
|
|
||||||
android:exported="true"
|
|
||||||
android:process=":auth">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.accounts.AccountAuthenticator" />
|
|
||||||
</intent-filter>
|
|
||||||
<meta-data
|
|
||||||
android:name="android.accounts.AccountAuthenticator"
|
|
||||||
android:resource="@xml/authenticator" />
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="org.acra.sender.SenderService"
|
|
||||||
android:exported="false"
|
|
||||||
android:process=":acra" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name=".filepicker.ExtendedFileProvider"
|
|
||||||
android:authorities="${applicationId}.provider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/provider_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name=".category.CategoryContentProvider"
|
|
||||||
android:authorities="${applicationId}.categories.contentprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/provider_categories"
|
|
||||||
android:syncable="false" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name=".explore.recentsearches.RecentSearchesContentProvider"
|
|
||||||
android:authorities="${applicationId}.explore.recentsearches.contentprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/provider_searches"
|
|
||||||
android:syncable="false" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name=".recentlanguages.RecentLanguagesContentProvider"
|
|
||||||
android:authorities="${applicationId}.recentlanguages.contentprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/provider_recent_languages"
|
|
||||||
android:syncable="false" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name=".bookmarks.pictures.BookmarkPicturesContentProvider"
|
|
||||||
android:authorities="${applicationId}.bookmarks.contentprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/provider_bookmarks"
|
|
||||||
android:syncable="false" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name=".bookmarks.locations.BookmarkLocationsContentProvider"
|
|
||||||
android:authorities="${applicationId}.bookmarks.locations.contentprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/provider_bookmarks_location"
|
|
||||||
android:syncable="false" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name=".bookmarks.items.BookmarkItemsContentProvider"
|
|
||||||
android:authorities="${applicationId}.bookmarks.items.contentprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="@string/provider_bookmarks_location"
|
|
||||||
android:syncable="false" />
|
|
||||||
|
|
||||||
<receiver android:name=".widget.PicOfDayAppWidget"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.appwidget.provider"
|
|
||||||
android:resource="@xml/pic_of_day_app_widget_info" />
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<uses-library android:name="org.apache.http.legacy" android:required="false" />
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import fr.free.nrw.commons.databinding.ActivityAboutBinding;
|
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
|
||||||
import fr.free.nrw.commons.utils.DialogUtil;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents about screen of this app
|
|
||||||
*/
|
|
||||||
public class AboutActivity extends BaseActivity {
|
|
||||||
|
|
||||||
/*
|
|
||||||
This View Binding class is auto-generated for each xml file. The format is usually the name
|
|
||||||
of the file with PascalCasing (The underscore characters will be ignored).
|
|
||||||
More information is available at https://developer.android.com/topic/libraries/view-binding
|
|
||||||
*/
|
|
||||||
private ActivityAboutBinding binding;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method helps in the creation About screen
|
|
||||||
*
|
|
||||||
* @param savedInstanceState Data bundle
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@SuppressLint("StringFormatInvalid")
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
/*
|
|
||||||
Instead of just setting the view with the xml file. We need to use View Binding class.
|
|
||||||
*/
|
|
||||||
binding = ActivityAboutBinding.inflate(getLayoutInflater());
|
|
||||||
final View view = binding.getRoot();
|
|
||||||
setContentView(view);
|
|
||||||
|
|
||||||
setSupportActionBar(binding.toolbarBinding.toolbar);
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
final String aboutText = getString(R.string.about_license);
|
|
||||||
/*
|
|
||||||
We can then access all the views by just using the id names like this.
|
|
||||||
camelCasing is used with underscore characters being ignored.
|
|
||||||
*/
|
|
||||||
binding.aboutLicense.setHtmlText(aboutText);
|
|
||||||
|
|
||||||
@SuppressLint("StringFormatMatches")
|
|
||||||
String improveText = String.format(getString(R.string.about_improve), Urls.NEW_ISSUE_URL);
|
|
||||||
binding.aboutImprove.setHtmlText(improveText);
|
|
||||||
binding.aboutVersion.setText(ConfigUtils.getVersionNameWithSha(getApplicationContext()));
|
|
||||||
|
|
||||||
Utils.setUnderlinedText(binding.aboutFaq, R.string.about_faq, getApplicationContext());
|
|
||||||
Utils.setUnderlinedText(binding.aboutRateUs, R.string.about_rate_us, getApplicationContext());
|
|
||||||
Utils.setUnderlinedText(binding.aboutUserGuide, R.string.user_guide, getApplicationContext());
|
|
||||||
Utils.setUnderlinedText(binding.aboutPrivacyPolicy, R.string.about_privacy_policy, getApplicationContext());
|
|
||||||
Utils.setUnderlinedText(binding.aboutTranslate, R.string.about_translate, getApplicationContext());
|
|
||||||
Utils.setUnderlinedText(binding.aboutCredits, R.string.about_credits, getApplicationContext());
|
|
||||||
|
|
||||||
/*
|
|
||||||
To set listeners, we can create a separate method and use lambda syntax.
|
|
||||||
*/
|
|
||||||
binding.facebookLaunchIcon.setOnClickListener(this::launchFacebook);
|
|
||||||
binding.githubLaunchIcon.setOnClickListener(this::launchGithub);
|
|
||||||
binding.websiteLaunchIcon.setOnClickListener(this::launchWebsite);
|
|
||||||
binding.aboutRateUs.setOnClickListener(this::launchRatings);
|
|
||||||
binding.aboutCredits.setOnClickListener(this::launchCredits);
|
|
||||||
binding.aboutPrivacyPolicy.setOnClickListener(this::launchPrivacyPolicy);
|
|
||||||
binding.aboutUserGuide.setOnClickListener(this::launchUserGuide);
|
|
||||||
binding.aboutFaq.setOnClickListener(this::launchFrequentlyAskedQuesions);
|
|
||||||
binding.aboutTranslate.setOnClickListener(this::launchTranslate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onSupportNavigateUp() {
|
|
||||||
onBackPressed();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchFacebook(View view) {
|
|
||||||
Intent intent;
|
|
||||||
try {
|
|
||||||
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.FACEBOOK_APP_URL));
|
|
||||||
intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME);
|
|
||||||
startActivity(intent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.FACEBOOK_WEB_URL));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchGithub(View view) {
|
|
||||||
Intent intent;
|
|
||||||
try {
|
|
||||||
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.GITHUB_REPO_URL));
|
|
||||||
intent.setPackage(Urls.GITHUB_PACKAGE_NAME);
|
|
||||||
startActivity(intent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.GITHUB_REPO_URL));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchWebsite(View view) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.WEBSITE_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchRatings(View view){
|
|
||||||
Utils.rateApp(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchCredits(View view) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.CREDITS_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchUserGuide(View view) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.USER_GUIDE_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchPrivacyPolicy(View view) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchFrequentlyAskedQuesions(View view) {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(Urls.FAQ_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
MenuInflater inflater = getMenuInflater();
|
|
||||||
inflater.inflate(R.menu.menu_about, menu);
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.share_app_icon:
|
|
||||||
String shareText = String.format(getString(R.string.share_text), Urls.PLAY_STORE_URL_PREFIX + this.getPackageName());
|
|
||||||
Intent sendIntent = new Intent();
|
|
||||||
sendIntent.setAction(Intent.ACTION_SEND);
|
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);
|
|
||||||
sendIntent.setType("text/plain");
|
|
||||||
startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via)));
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launchTranslate(View view) {
|
|
||||||
@NonNull List<String> sortedLocalizedNamesRef = CommonsApplication.getInstance().getLanguageLookUpTable().getCanonicalNames();
|
|
||||||
Collections.sort(sortedLocalizedNamesRef);
|
|
||||||
final ArrayAdapter<String> languageAdapter = new ArrayAdapter<>(AboutActivity.this,
|
|
||||||
android.R.layout.simple_spinner_dropdown_item, sortedLocalizedNamesRef);
|
|
||||||
final Spinner spinner = new Spinner(AboutActivity.this);
|
|
||||||
spinner.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
|
||||||
spinner.setAdapter(languageAdapter);
|
|
||||||
spinner.setGravity(17);
|
|
||||||
spinner.setPadding(50,0,0,0);
|
|
||||||
|
|
||||||
Runnable positiveButtonRunnable = () -> {
|
|
||||||
String langCode = CommonsApplication.getInstance().getLanguageLookUpTable().getCodes().get(spinner.getSelectedItemPosition());
|
|
||||||
Utils.handleWebUrl(AboutActivity.this, Uri.parse(Urls.TRANSLATE_WIKI_URL + langCode));
|
|
||||||
};
|
|
||||||
DialogUtil.showAlertDialog(this,
|
|
||||||
getString(R.string.about_translate_title),
|
|
||||||
getString(R.string.about_translate_message),
|
|
||||||
getString(R.string.about_translate_proceed),
|
|
||||||
getString(R.string.about_translate_cancel),
|
|
||||||
positiveButtonRunnable,
|
|
||||||
() -> {},
|
|
||||||
spinner,
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
207
app/src/main/java/fr/free/nrw/commons/AboutActivity.kt
Normal file
207
app/src/main/java/fr/free/nrw/commons/AboutActivity.kt
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.ACTION_VIEW
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Spinner
|
||||||
|
import fr.free.nrw.commons.CommonsApplication.Companion.instance
|
||||||
|
import fr.free.nrw.commons.databinding.ActivityAboutBinding
|
||||||
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
|
import java.util.Collections
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets
|
||||||
|
import fr.free.nrw.commons.utils.handleWebUrl
|
||||||
|
import fr.free.nrw.commons.utils.setUnderlinedText
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents about screen of this app
|
||||||
|
*/
|
||||||
|
class AboutActivity : BaseActivity() {
|
||||||
|
/*
|
||||||
|
This View Binding class is auto-generated for each xml file. The format is usually the name
|
||||||
|
of the file with PascalCasing (The underscore characters will be ignored).
|
||||||
|
More information is available at https://developer.android.com/topic/libraries/view-binding
|
||||||
|
*/
|
||||||
|
private var binding: ActivityAboutBinding? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method helps in the creation About screen
|
||||||
|
*
|
||||||
|
* @param savedInstanceState Data bundle
|
||||||
|
*/
|
||||||
|
@SuppressLint("StringFormatInvalid") //TODO:
|
||||||
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Instead of just setting the view with the xml file. We need to use View Binding class.
|
||||||
|
*/
|
||||||
|
binding = ActivityAboutBinding.inflate(layoutInflater)
|
||||||
|
val view: View = binding!!.root
|
||||||
|
applyEdgeToEdgeTopInsets(binding!!.toolbarLayout)
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
|
setSupportActionBar(binding!!.toolbarBinding.toolbar)
|
||||||
|
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||||
|
val aboutText = getString(R.string.about_license)
|
||||||
|
/*
|
||||||
|
We can then access all the views by just using the id names like this.
|
||||||
|
camelCasing is used with underscore characters being ignored.
|
||||||
|
*/
|
||||||
|
binding!!.aboutLicense.setHtmlText(aboutText)
|
||||||
|
|
||||||
|
@SuppressLint("StringFormatMatches") // TODO:
|
||||||
|
val improveText =
|
||||||
|
String.format(getString(R.string.about_improve), Urls.NEW_ISSUE_URL)
|
||||||
|
binding!!.aboutImprove.setHtmlText(improveText)
|
||||||
|
binding!!.aboutVersion.text = applicationContext.getVersionNameWithSha()
|
||||||
|
|
||||||
|
binding!!.aboutFaq.setUnderlinedText(R.string.about_faq)
|
||||||
|
binding!!.aboutRateUs.setUnderlinedText(R.string.about_rate_us)
|
||||||
|
binding!!.aboutUserGuide.setUnderlinedText(R.string.user_guide)
|
||||||
|
binding!!.aboutPrivacyPolicy.setUnderlinedText(R.string.about_privacy_policy)
|
||||||
|
binding!!.aboutTranslate.setUnderlinedText(R.string.about_translate)
|
||||||
|
binding!!.aboutCredits.setUnderlinedText(R.string.about_credits)
|
||||||
|
|
||||||
|
/*
|
||||||
|
To set listeners, we can create a separate method and use lambda syntax.
|
||||||
|
*/
|
||||||
|
binding!!.facebookLaunchIcon.setOnClickListener(::launchFacebook)
|
||||||
|
binding!!.githubLaunchIcon.setOnClickListener(::launchGithub)
|
||||||
|
binding!!.websiteLaunchIcon.setOnClickListener(::launchWebsite)
|
||||||
|
binding!!.aboutRateUs.setOnClickListener(::launchRatings)
|
||||||
|
binding!!.aboutCredits.setOnClickListener(::launchCredits)
|
||||||
|
binding!!.aboutPrivacyPolicy.setOnClickListener(::launchPrivacyPolicy)
|
||||||
|
binding!!.aboutUserGuide.setOnClickListener(::launchUserGuide)
|
||||||
|
binding!!.aboutFaq.setOnClickListener(::launchFrequentlyAskedQuesions)
|
||||||
|
binding!!.aboutTranslate.setOnClickListener(::launchTranslate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
onBackPressed()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchFacebook(view: View?) {
|
||||||
|
val intent: Intent
|
||||||
|
try {
|
||||||
|
intent = Intent(ACTION_VIEW, Urls.FACEBOOK_APP_URL.toUri())
|
||||||
|
intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME)
|
||||||
|
startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handleWebUrl(this, Urls.FACEBOOK_WEB_URL.toUri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchGithub(view: View?) {
|
||||||
|
val intent: Intent
|
||||||
|
try {
|
||||||
|
intent = Intent(ACTION_VIEW, Urls.GITHUB_REPO_URL.toUri())
|
||||||
|
intent.setPackage(Urls.GITHUB_PACKAGE_NAME)
|
||||||
|
startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handleWebUrl(this, Urls.GITHUB_REPO_URL.toUri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchWebsite(view: View?) {
|
||||||
|
handleWebUrl(this, Urls.WEBSITE_URL.toUri())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchRatings(view: View?) {
|
||||||
|
try {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
ACTION_VIEW,
|
||||||
|
(Urls.PLAY_STORE_PREFIX + packageName).toUri()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
handleWebUrl(this, (Urls.PLAY_STORE_URL_PREFIX + packageName).toUri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchCredits(view: View?) {
|
||||||
|
handleWebUrl(this, Urls.CREDITS_URL.toUri())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchUserGuide(view: View?) {
|
||||||
|
handleWebUrl(this, Urls.USER_GUIDE_URL.toUri())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchPrivacyPolicy(view: View?) {
|
||||||
|
handleWebUrl(this, BuildConfig.PRIVACY_POLICY_URL.toUri())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchFrequentlyAskedQuesions(view: View?) {
|
||||||
|
handleWebUrl(this, Urls.FAQ_URL.toUri())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
val inflater = menuInflater
|
||||||
|
inflater.inflate(R.menu.menu_about, menu)
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.share_app_icon -> {
|
||||||
|
val shareText = String.format(
|
||||||
|
getString(R.string.share_text),
|
||||||
|
Urls.PLAY_STORE_URL_PREFIX + this.packageName
|
||||||
|
)
|
||||||
|
val sendIntent = Intent()
|
||||||
|
sendIntent.setAction(Intent.ACTION_SEND)
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_TEXT, shareText)
|
||||||
|
sendIntent.setType("text/plain")
|
||||||
|
startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via)))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchTranslate(view: View?) {
|
||||||
|
val sortedLocalizedNamesRef = instance.languageLookUpTable!!.getCanonicalNames()
|
||||||
|
Collections.sort(sortedLocalizedNamesRef)
|
||||||
|
val languageAdapter = ArrayAdapter(
|
||||||
|
this@AboutActivity,
|
||||||
|
android.R.layout.simple_spinner_dropdown_item, sortedLocalizedNamesRef
|
||||||
|
)
|
||||||
|
val spinner = Spinner(this@AboutActivity)
|
||||||
|
spinner.layoutParams =
|
||||||
|
LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
spinner.adapter = languageAdapter
|
||||||
|
spinner.gravity = 17
|
||||||
|
spinner.setPadding(50, 0, 0, 0)
|
||||||
|
|
||||||
|
val positiveButtonRunnable = Runnable {
|
||||||
|
val langCode = instance.languageLookUpTable!!.getCodes()[spinner.selectedItemPosition]
|
||||||
|
handleWebUrl(this@AboutActivity, (Urls.TRANSLATE_WIKI_URL + langCode).toUri())
|
||||||
|
}
|
||||||
|
showAlertDialog(
|
||||||
|
this,
|
||||||
|
getString(R.string.about_translate_title),
|
||||||
|
getString(R.string.about_translate_message),
|
||||||
|
getString(R.string.about_translate_proceed),
|
||||||
|
getString(R.string.about_translate_cancel),
|
||||||
|
positiveButtonRunnable,
|
||||||
|
{},
|
||||||
|
spinner
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
63
app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
Normal file
63
app/src/main/java/fr/free/nrw/commons/BaseMarker.kt
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import fr.free.nrw.commons.location.LatLng
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
|
||||||
|
class BaseMarker {
|
||||||
|
private var _position: LatLng = LatLng(0.0, 0.0, 0f)
|
||||||
|
private var _title: String = ""
|
||||||
|
private var _place: Place = Place()
|
||||||
|
private var _icon: Bitmap? = null
|
||||||
|
|
||||||
|
var position: LatLng
|
||||||
|
get() = _position
|
||||||
|
set(value) {
|
||||||
|
_position = value
|
||||||
|
}
|
||||||
|
var title: String
|
||||||
|
get() = _title
|
||||||
|
set(value) {
|
||||||
|
_title = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var place: Place
|
||||||
|
get() = _place
|
||||||
|
set(value) {
|
||||||
|
_place = value
|
||||||
|
}
|
||||||
|
var icon: Bitmap?
|
||||||
|
get() = _icon
|
||||||
|
set(value) {
|
||||||
|
_icon = value
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromResource(
|
||||||
|
context: Context,
|
||||||
|
drawableResId: Int,
|
||||||
|
) {
|
||||||
|
val drawable: Drawable = context.resources.getDrawable(drawableResId)
|
||||||
|
icon =
|
||||||
|
if (drawable is BitmapDrawable) {
|
||||||
|
drawable.bitmap
|
||||||
|
} else {
|
||||||
|
val bitmap =
|
||||||
|
Bitmap.createBitmap(
|
||||||
|
drawable.intrinsicWidth,
|
||||||
|
drawable.intrinsicHeight,
|
||||||
|
Bitmap.Config.ARGB_8888,
|
||||||
|
)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||||
|
drawable.draw(canvas)
|
||||||
|
bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base presenter, enforcing contracts to atach and detach view
|
|
||||||
*/
|
|
||||||
public interface BasePresenter<T> {
|
|
||||||
/**
|
|
||||||
* Until a view is attached, it is open to listen events from the presenter
|
|
||||||
*/
|
|
||||||
void onAttachView(@NonNull T view);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detaching a view makes sure that the view no more receives events from the presenter
|
|
||||||
*/
|
|
||||||
void onDetachView();
|
|
||||||
}
|
|
||||||
10
app/src/main/java/fr/free/nrw/commons/BasePresenter.kt
Normal file
10
app/src/main/java/fr/free/nrw/commons/BasePresenter.kt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base presenter, enforcing contracts to attach and detach view
|
||||||
|
*/
|
||||||
|
interface BasePresenter<T> {
|
||||||
|
fun onAttachView(view: T)
|
||||||
|
|
||||||
|
fun onDetachView()
|
||||||
|
}
|
||||||
|
|
@ -10,9 +10,10 @@ object BetaConstants {
|
||||||
* production server where beta server does not work
|
* production server where beta server does not work
|
||||||
*/
|
*/
|
||||||
const val COMMONS_URL = "https://commons.wikimedia.org/"
|
const val COMMONS_URL = "https://commons.wikimedia.org/"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Commons production's depicts property which is used in beta for some specific GET calls on
|
* Commons production's depicts property which is used in beta for some specific GET calls on
|
||||||
* production server where beta server does not work
|
* production server where beta server does not work
|
||||||
*/
|
*/
|
||||||
const val DEPICTS_PROPERTY = "P180"
|
const val DEPICTS_PROPERTY = "P180"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
app/src/main/java/fr/free/nrw/commons/CameraPosition.kt
Normal file
33
app/src/main/java/fr/free/nrw/commons/CameraPosition.kt
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
class CameraPosition(
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val zoom: Double,
|
||||||
|
) : Parcelable {
|
||||||
|
constructor(parcel: Parcel) : this(
|
||||||
|
parcel.readDouble(),
|
||||||
|
parcel.readDouble(),
|
||||||
|
parcel.readDouble(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun writeToParcel(
|
||||||
|
parcel: Parcel,
|
||||||
|
flags: Int,
|
||||||
|
) {
|
||||||
|
parcel.writeDouble(latitude)
|
||||||
|
parcel.writeDouble(longitude)
|
||||||
|
parcel.writeDouble(zoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int = 0
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<CameraPosition> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): CameraPosition = CameraPosition(parcel)
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<CameraPosition?> = arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.wikipedia.AppAdapter;
|
|
||||||
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
import org.wikipedia.json.GsonMarshaller;
|
|
||||||
import org.wikipedia.json.GsonUnmarshaller;
|
|
||||||
import org.wikipedia.login.LoginResult;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
|
|
||||||
public class CommonsAppAdapter extends AppAdapter {
|
|
||||||
private final int DEFAULT_THUMB_SIZE = 640;
|
|
||||||
private final String COOKIE_STORE_NAME = "cookie_store";
|
|
||||||
|
|
||||||
private final SessionManager sessionManager;
|
|
||||||
private final JsonKvStore preferences;
|
|
||||||
|
|
||||||
CommonsAppAdapter(@NonNull SessionManager sessionManager, @NonNull JsonKvStore preferences) {
|
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
this.preferences = preferences;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getMediaWikiBaseUrl() {
|
|
||||||
return BuildConfig.COMMONS_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getRestbaseUriFormat() {
|
|
||||||
return BuildConfig.COMMONS_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OkHttpClient getOkHttpClient(@NonNull WikiSite wikiSite) {
|
|
||||||
return OkHttpConnectionFactory.getClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDesiredLeadImageDp() {
|
|
||||||
return DEFAULT_THUMB_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLoggedIn() {
|
|
||||||
return sessionManager.isUserLoggedIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUserName() {
|
|
||||||
return sessionManager.getUserName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getPassword() {
|
|
||||||
return sessionManager.getPassword();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateAccount(@NonNull LoginResult result) {
|
|
||||||
sessionManager.updateAccount(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SharedPreferenceCookieManager getCookies() {
|
|
||||||
if (!preferences.contains(COOKIE_STORE_NAME)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class,
|
|
||||||
preferences.getString(COOKIE_STORE_NAME, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setCookies(@NonNull SharedPreferenceCookieManager cookies) {
|
|
||||||
preferences.putString(COOKIE_STORE_NAME, GsonMarshaller.marshal(cookies));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean logErrorsInsteadOfCrashing() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,340 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.data.DBOpenHelper.CONTRIBUTIONS_TABLE;
|
|
||||||
import static org.acra.ReportField.ANDROID_VERSION;
|
|
||||||
import static org.acra.ReportField.APP_VERSION_CODE;
|
|
||||||
import static org.acra.ReportField.APP_VERSION_NAME;
|
|
||||||
import static org.acra.ReportField.PHONE_MODEL;
|
|
||||||
import static org.acra.ReportField.STACK_TRACE;
|
|
||||||
import static org.acra.ReportField.USER_COMMENT;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteException;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Process;
|
|
||||||
import android.util.Log;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.multidex.BuildConfig;
|
|
||||||
import androidx.multidex.MultiDexApplication;
|
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
|
||||||
import com.facebook.imagepipeline.core.ImagePipeline;
|
|
||||||
import com.facebook.imagepipeline.core.ImagePipelineConfig;
|
|
||||||
import com.mapbox.mapboxsdk.Mapbox;
|
|
||||||
import com.mapbox.mapboxsdk.WellKnownTileServer;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table;
|
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
|
||||||
import fr.free.nrw.commons.category.CategoryDao;
|
|
||||||
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler;
|
|
||||||
import fr.free.nrw.commons.concurrency.ThreadPoolService;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.logging.FileLoggingTree;
|
|
||||||
import fr.free.nrw.commons.logging.LogUtils;
|
|
||||||
import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher;
|
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
|
||||||
import fr.free.nrw.commons.upload.FileUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
|
||||||
import io.reactivex.Completable;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.internal.functions.Functions;
|
|
||||||
import io.reactivex.plugins.RxJavaPlugins;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import org.acra.ACRA;
|
|
||||||
import org.acra.annotation.AcraCore;
|
|
||||||
import org.acra.annotation.AcraDialog;
|
|
||||||
import org.acra.annotation.AcraMailSender;
|
|
||||||
import org.acra.data.StringFormat;
|
|
||||||
import org.wikipedia.AppAdapter;
|
|
||||||
import org.wikipedia.language.AppLanguageLookUpTable;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
@AcraCore(
|
|
||||||
buildConfigClass = BuildConfig.class,
|
|
||||||
resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
|
|
||||||
reportFormat = StringFormat.KEY_VALUE_LIST,
|
|
||||||
reportContent = {USER_COMMENT, APP_VERSION_CODE, APP_VERSION_NAME, ANDROID_VERSION, PHONE_MODEL,
|
|
||||||
STACK_TRACE}
|
|
||||||
)
|
|
||||||
|
|
||||||
@AcraMailSender(
|
|
||||||
mailTo = "commons-app-android-private@googlegroups.com",
|
|
||||||
reportAsFile = false
|
|
||||||
)
|
|
||||||
|
|
||||||
@AcraDialog(
|
|
||||||
resTheme = R.style.Theme_AppCompat_Dialog,
|
|
||||||
resText = R.string.crash_dialog_text,
|
|
||||||
resTitle = R.string.crash_dialog_title,
|
|
||||||
resCommentPrompt = R.string.crash_dialog_comment_prompt
|
|
||||||
)
|
|
||||||
|
|
||||||
public class CommonsApplication extends MultiDexApplication {
|
|
||||||
|
|
||||||
public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled";
|
|
||||||
@Inject
|
|
||||||
SessionManager sessionManager;
|
|
||||||
@Inject
|
|
||||||
DBOpenHelper dbOpenHelper;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@Named("default_preferences")
|
|
||||||
JsonKvStore defaultPrefs;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constants begin
|
|
||||||
*/
|
|
||||||
public static final int OPEN_APPLICATION_DETAIL_SETTINGS = 1001;
|
|
||||||
|
|
||||||
public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]";
|
|
||||||
|
|
||||||
public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com";
|
|
||||||
|
|
||||||
public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App Feedback";
|
|
||||||
|
|
||||||
public static final String REPORT_EMAIL = "commons-app-android-private@googlegroups.com";
|
|
||||||
|
|
||||||
public static final String REPORT_EMAIL_SUBJECT = "Report a violation";
|
|
||||||
|
|
||||||
public static final String NOTIFICATION_CHANNEL_ID_ALL = "CommonsNotificationAll";
|
|
||||||
|
|
||||||
public static final String FEEDBACK_EMAIL_TEMPLATE_HEADER = "-- Technical information --";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constants End
|
|
||||||
*/
|
|
||||||
|
|
||||||
private static CommonsApplication INSTANCE;
|
|
||||||
|
|
||||||
public static CommonsApplication getInstance() {
|
|
||||||
return INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AppLanguageLookUpTable languageLookUpTable;
|
|
||||||
|
|
||||||
public AppLanguageLookUpTable getLanguageLookUpTable() {
|
|
||||||
return languageLookUpTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ContributionDao contributionDao;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In-memory list of contributions whose uploads have been paused by the user
|
|
||||||
*/
|
|
||||||
public static Map<String, Boolean> pauseUploads = new HashMap<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to declare and initialize various components and dependencies
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
INSTANCE = this;
|
|
||||||
ACRA.init(this);
|
|
||||||
Mapbox.getInstance(this, getString(R.string.mapbox_commons_app_token), WellKnownTileServer.Mapbox);
|
|
||||||
|
|
||||||
ApplicationlessInjection
|
|
||||||
.getInstance(this)
|
|
||||||
.getCommonsApplicationComponent()
|
|
||||||
.inject(this);
|
|
||||||
|
|
||||||
AppAdapter.set(new CommonsAppAdapter(sessionManager, defaultPrefs));
|
|
||||||
|
|
||||||
initTimber();
|
|
||||||
|
|
||||||
if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) {
|
|
||||||
Set<String> defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS);
|
|
||||||
if (null == defaultExifTagsSet) {
|
|
||||||
defaultExifTagsSet = new HashSet<>();
|
|
||||||
}
|
|
||||||
defaultExifTagsSet.add(getString(R.string.exif_tag_location));
|
|
||||||
defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set DownsampleEnabled to True to downsample the image in case it's heavy
|
|
||||||
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
|
|
||||||
.setNetworkFetcher(customOkHttpNetworkFetcher)
|
|
||||||
.setDownsampleEnabled(true)
|
|
||||||
.build();
|
|
||||||
try {
|
|
||||||
Fresco.initialize(this, config);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e);
|
|
||||||
// TODO: Remove when we're able to initialize Fresco in test builds.
|
|
||||||
}
|
|
||||||
|
|
||||||
createNotificationChannel(this);
|
|
||||||
|
|
||||||
languageLookUpTable = new AppLanguageLookUpTable(this);
|
|
||||||
|
|
||||||
// This handler will catch exceptions thrown from Observables after they are disposed,
|
|
||||||
// or from Observables that are (deliberately or not) missing an onError handler.
|
|
||||||
RxJavaPlugins.setErrorHandler(Functions.emptyConsumer());
|
|
||||||
|
|
||||||
// Fire progress callbacks for every 3% of uploaded content
|
|
||||||
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plants debug and file logging tree. Timber lets you plant your own logging trees.
|
|
||||||
*/
|
|
||||||
private void initTimber() {
|
|
||||||
boolean isBeta = ConfigUtils.isBetaFlavour();
|
|
||||||
String logFileName =
|
|
||||||
isBeta ? "CommonsBetaAppLogs" : "CommonsAppLogs";
|
|
||||||
String logDirectory = LogUtils.getLogDirectory();
|
|
||||||
//Delete stale logs if they have exceeded the specified size
|
|
||||||
deleteStaleLogs(logFileName, logDirectory);
|
|
||||||
|
|
||||||
FileLoggingTree tree = new FileLoggingTree(
|
|
||||||
Log.VERBOSE,
|
|
||||||
logFileName,
|
|
||||||
logDirectory,
|
|
||||||
1000,
|
|
||||||
getFileLoggingThreadPool());
|
|
||||||
|
|
||||||
Timber.plant(tree);
|
|
||||||
Timber.plant(new Timber.DebugTree());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the logs zip file at the specified directory and file locations specified in the
|
|
||||||
* params
|
|
||||||
*
|
|
||||||
* @param logFileName
|
|
||||||
* @param logDirectory
|
|
||||||
*/
|
|
||||||
private void deleteStaleLogs(String logFileName, String logDirectory) {
|
|
||||||
try {
|
|
||||||
File file = new File(logDirectory + "/zip/" + logFileName + ".zip");
|
|
||||||
if (file.exists() && file.getTotalSpace() > 1000000) {// In Kbs
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isRoboUnitTest() {
|
|
||||||
return "robolectric".equals(Build.FINGERPRINT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ThreadPoolService getFileLoggingThreadPool() {
|
|
||||||
return new ThreadPoolService.Builder("file-logging-thread")
|
|
||||||
.setPriority(Process.THREAD_PRIORITY_LOWEST)
|
|
||||||
.setPoolSize(1)
|
|
||||||
.setExceptionHandler(new BackgroundPoolExceptionHandler())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void createNotificationChannel(@NonNull Context context) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
NotificationManager manager = (NotificationManager) context
|
|
||||||
.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
NotificationChannel channel = manager
|
|
||||||
.getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL);
|
|
||||||
if (channel == null) {
|
|
||||||
channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID_ALL,
|
|
||||||
context.getString(R.string.notifications_channel_name_all),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT);
|
|
||||||
manager.createNotificationChannel(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUserAgent() {
|
|
||||||
return "Commons/" + ConfigUtils.getVersionNameWithSha(this)
|
|
||||||
+ " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* clears data of current application
|
|
||||||
*
|
|
||||||
* @param context Application context
|
|
||||||
* @param logoutListener Implementation of interface LogoutListener
|
|
||||||
*/
|
|
||||||
@SuppressLint("CheckResult")
|
|
||||||
public void clearApplicationData(Context context, LogoutListener logoutListener) {
|
|
||||||
File cacheDirectory = context.getCacheDir();
|
|
||||||
File applicationDirectory = new File(cacheDirectory.getParent());
|
|
||||||
if (applicationDirectory.exists()) {
|
|
||||||
String[] fileNames = applicationDirectory.list();
|
|
||||||
for (String fileName : fileNames) {
|
|
||||||
if (!fileName.equals("lib")) {
|
|
||||||
FileUtils.deleteFile(new File(applicationDirectory, fileName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionManager.logout()
|
|
||||||
.andThen(Completable.fromAction(() -> {
|
|
||||||
Timber.d("All accounts have been removed");
|
|
||||||
clearImageCache();
|
|
||||||
//TODO: fix preference manager
|
|
||||||
defaultPrefs.clearAll();
|
|
||||||
defaultPrefs.putBoolean("firstrun", false);
|
|
||||||
updateAllDatabases();
|
|
||||||
}
|
|
||||||
))
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(logoutListener::onLogoutComplete, Timber::e);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all images cache held by Fresco
|
|
||||||
*/
|
|
||||||
private void clearImageCache() {
|
|
||||||
ImagePipeline imagePipeline = Fresco.getImagePipeline();
|
|
||||||
imagePipeline.clearCaches();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes all tables and re-creates them.
|
|
||||||
*/
|
|
||||||
private void updateAllDatabases() {
|
|
||||||
dbOpenHelper.getReadableDatabase().close();
|
|
||||||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
|
||||||
|
|
||||||
CategoryDao.Table.onDelete(db);
|
|
||||||
dbOpenHelper.deleteTable(db,
|
|
||||||
CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions
|
|
||||||
|
|
||||||
try {
|
|
||||||
contributionDao.deleteAll();
|
|
||||||
} catch (SQLiteException e) {
|
|
||||||
Timber.e(e);
|
|
||||||
}
|
|
||||||
BookmarkPicturesDao.Table.onDelete(db);
|
|
||||||
BookmarkLocationsDao.Table.onDelete(db);
|
|
||||||
Table.onDelete(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface used to get log-out events
|
|
||||||
*/
|
|
||||||
public interface LogoutListener {
|
|
||||||
|
|
||||||
void onLogoutComplete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
417
app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt
Normal file
417
app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.database.sqlite.SQLiteException
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Process
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.multidex.MultiDexApplication
|
||||||
|
import com.facebook.drawee.backends.pipeline.Fresco
|
||||||
|
import com.facebook.imagepipeline.core.ImagePipelineConfig
|
||||||
|
import fr.free.nrw.commons.auth.LoginActivity
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
|
||||||
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
|
||||||
|
import fr.free.nrw.commons.category.CategoryDao
|
||||||
|
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler
|
||||||
|
import fr.free.nrw.commons.concurrency.ThreadPoolService
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionDao
|
||||||
|
import fr.free.nrw.commons.data.DBOpenHelper
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.language.AppLanguageLookUpTable
|
||||||
|
import fr.free.nrw.commons.logging.FileLoggingTree
|
||||||
|
import fr.free.nrw.commons.logging.LogUtils
|
||||||
|
import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher
|
||||||
|
import fr.free.nrw.commons.settings.Prefs
|
||||||
|
import fr.free.nrw.commons.upload.FileUtils
|
||||||
|
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
|
||||||
|
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
||||||
|
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.internal.functions.Functions
|
||||||
|
import io.reactivex.plugins.RxJavaPlugins
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import org.acra.ACRA.init
|
||||||
|
import org.acra.ReportField
|
||||||
|
import org.acra.annotation.AcraCore
|
||||||
|
import org.acra.annotation.AcraDialog
|
||||||
|
import org.acra.annotation.AcraMailSender
|
||||||
|
import org.acra.data.StringFormat
|
||||||
|
import timber.log.Timber
|
||||||
|
import timber.log.Timber.DebugTree
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
@AcraCore(
|
||||||
|
buildConfigClass = BuildConfig::class,
|
||||||
|
resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
|
||||||
|
reportFormat = StringFormat.KEY_VALUE_LIST,
|
||||||
|
reportContent = [ReportField.USER_COMMENT, ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME, ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL, ReportField.STACK_TRACE]
|
||||||
|
)
|
||||||
|
|
||||||
|
@AcraMailSender(mailTo = "commons-app-android-private@googlegroups.com", reportAsFile = false)
|
||||||
|
|
||||||
|
@AcraDialog(
|
||||||
|
resTheme = R.style.Theme_AppCompat_Dialog,
|
||||||
|
resText = R.string.crash_dialog_text,
|
||||||
|
resTitle = R.string.crash_dialog_title,
|
||||||
|
resCommentPrompt = R.string.crash_dialog_comment_prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
class CommonsApplication : MultiDexApplication() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dbOpenHelper: DBOpenHelper
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@field:Named("default_preferences")
|
||||||
|
lateinit var defaultPrefs: JsonKvStore
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var cookieJar: CommonsCookieJar
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var customOkHttpNetworkFetcher: CustomOkHttpNetworkFetcher
|
||||||
|
|
||||||
|
var languageLookUpTable: AppLanguageLookUpTable? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var contributionDao: ContributionDao
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to declare and initialize various components and dependencies
|
||||||
|
*/
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
instance = this
|
||||||
|
init(this)
|
||||||
|
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(this)
|
||||||
|
.commonsApplicationComponent
|
||||||
|
.inject(this)
|
||||||
|
|
||||||
|
initTimber()
|
||||||
|
|
||||||
|
if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) {
|
||||||
|
var defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS)
|
||||||
|
if (null == defaultExifTagsSet) {
|
||||||
|
defaultExifTagsSet = HashSet()
|
||||||
|
}
|
||||||
|
defaultExifTagsSet.add(getString(R.string.exif_tag_location))
|
||||||
|
defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set DownsampleEnabled to True to downsample the image in case it's heavy
|
||||||
|
val config = ImagePipelineConfig.newBuilder(this)
|
||||||
|
.setNetworkFetcher(customOkHttpNetworkFetcher)
|
||||||
|
.setDownsampleEnabled(true)
|
||||||
|
.build()
|
||||||
|
try {
|
||||||
|
Fresco.initialize(this, config)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
// TODO: Remove when we're able to initialize Fresco in test builds.
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotificationChannel(this)
|
||||||
|
|
||||||
|
languageLookUpTable = AppLanguageLookUpTable(this)
|
||||||
|
|
||||||
|
// This handler will catch exceptions thrown from Observables after they are disposed,
|
||||||
|
// or from Observables that are (deliberately or not) missing an onError handler.
|
||||||
|
RxJavaPlugins.setErrorHandler(Functions.emptyConsumer())
|
||||||
|
|
||||||
|
// Fire progress callbacks for every 3% of uploaded content
|
||||||
|
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plants debug and file logging tree. Timber lets you plant your own logging trees.
|
||||||
|
*/
|
||||||
|
private fun initTimber() {
|
||||||
|
val isBeta = isBetaFlavour
|
||||||
|
val logFileName =
|
||||||
|
if (isBeta) "CommonsBetaAppLogs" else "CommonsAppLogs"
|
||||||
|
val logDirectory = LogUtils.getLogDirectory()
|
||||||
|
//Delete stale logs if they have exceeded the specified size
|
||||||
|
deleteStaleLogs(logFileName, logDirectory)
|
||||||
|
|
||||||
|
val tree = FileLoggingTree(
|
||||||
|
Log.VERBOSE,
|
||||||
|
logFileName,
|
||||||
|
logDirectory,
|
||||||
|
1000,
|
||||||
|
fileLoggingThreadPool
|
||||||
|
)
|
||||||
|
|
||||||
|
Timber.plant(tree)
|
||||||
|
Timber.plant(DebugTree())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the logs zip file at the specified directory and file locations specified in the
|
||||||
|
* params
|
||||||
|
*
|
||||||
|
* @param logFileName
|
||||||
|
* @param logDirectory
|
||||||
|
*/
|
||||||
|
private fun deleteStaleLogs(logFileName: String, logDirectory: String) {
|
||||||
|
try {
|
||||||
|
val file = File("$logDirectory/zip/$logFileName.zip")
|
||||||
|
if (file.exists() && file.totalSpace > 1000000) { // In Kbs
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val fileLoggingThreadPool: ThreadPoolService
|
||||||
|
get() = ThreadPoolService.Builder("file-logging-thread")
|
||||||
|
.setPriority(Process.THREAD_PRIORITY_LOWEST)
|
||||||
|
.setPoolSize(1)
|
||||||
|
.setExceptionHandler(BackgroundPoolExceptionHandler())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val userAgent: String
|
||||||
|
get() = ("Commons/" + this.getVersionNameWithSha()
|
||||||
|
+ " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clears data of current application
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
* @param logoutListener Implementation of interface LogoutListener
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
fun clearApplicationData(context: Context, logoutListener: LogoutListener) {
|
||||||
|
val cacheDirectory = context.cacheDir
|
||||||
|
val applicationDirectory = File(cacheDirectory.parent)
|
||||||
|
if (applicationDirectory.exists()) {
|
||||||
|
val fileNames = applicationDirectory.list()
|
||||||
|
for (fileName in fileNames) {
|
||||||
|
if (fileName != "lib") {
|
||||||
|
FileUtils.deleteFile(File(applicationDirectory, fileName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionManager.logout()
|
||||||
|
.andThen(Completable.fromAction { cookieJar.clear() })
|
||||||
|
.andThen(Completable.fromAction {
|
||||||
|
Timber.d("All accounts have been removed")
|
||||||
|
clearImageCache()
|
||||||
|
//TODO: fix preference manager
|
||||||
|
defaultPrefs.clearAll()
|
||||||
|
defaultPrefs.putBoolean("firstrun", false)
|
||||||
|
updateAllDatabases()
|
||||||
|
})
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ logoutListener.onLogoutComplete() }, { t: Throwable? -> Timber.e(t) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all images cache held by Fresco
|
||||||
|
*/
|
||||||
|
private fun clearImageCache() {
|
||||||
|
val imagePipeline = Fresco.getImagePipeline()
|
||||||
|
imagePipeline.clearCaches()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all tables and re-creates them.
|
||||||
|
*/
|
||||||
|
private fun updateAllDatabases() {
|
||||||
|
dbOpenHelper.readableDatabase.close()
|
||||||
|
val db = dbOpenHelper.writableDatabase
|
||||||
|
|
||||||
|
CategoryDao.Table.onDelete(db)
|
||||||
|
dbOpenHelper.deleteTable(
|
||||||
|
db,
|
||||||
|
DBOpenHelper.CONTRIBUTIONS_TABLE
|
||||||
|
) //Delete the contributions table in the existing db on older versions
|
||||||
|
|
||||||
|
dbOpenHelper.deleteTable(
|
||||||
|
db,
|
||||||
|
DBOpenHelper.BOOKMARKS_LOCATIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
contributionDao.deleteAll()
|
||||||
|
} catch (e: SQLiteException) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
BookmarksTable.onDelete(db)
|
||||||
|
BookmarkItemsTable.onDelete(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface used to get log-out events
|
||||||
|
*/
|
||||||
|
interface LogoutListener {
|
||||||
|
fun onLogoutComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity
|
||||||
|
* with relevant intent parameters. It does not perform the actual logout operation.
|
||||||
|
*/
|
||||||
|
open class BaseLogoutListener : LogoutListener {
|
||||||
|
var ctx: Context
|
||||||
|
var loginMessage: String? = null
|
||||||
|
var userName: String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for BaseLogoutListener.
|
||||||
|
*
|
||||||
|
* @param ctx Application context
|
||||||
|
*/
|
||||||
|
constructor(ctx: Context) {
|
||||||
|
this.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for BaseLogoutListener
|
||||||
|
*
|
||||||
|
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
|
||||||
|
* @param loginMessage Message to be displayed on the login page
|
||||||
|
* @param loginUsername Username to be pre-filled on the login page
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
ctx: Context, loginMessage: String?,
|
||||||
|
loginUsername: String?
|
||||||
|
) {
|
||||||
|
this.ctx = ctx
|
||||||
|
this.loginMessage = loginMessage
|
||||||
|
this.userName = loginUsername
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLogoutComplete() {
|
||||||
|
Timber.d("Logout complete callback received.")
|
||||||
|
val loginIntent = Intent(ctx, LoginActivity::class.java)
|
||||||
|
loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
|
if (loginMessage != null) {
|
||||||
|
loginIntent.putExtra(LOGIN_MESSAGE_INTENT_KEY, loginMessage)
|
||||||
|
}
|
||||||
|
if (userName != null) {
|
||||||
|
loginIntent.putExtra(LOGIN_USERNAME_INTENT_KEY, userName)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.startActivity(loginIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is an extension of BaseLogoutListener, providing additional functionality or customization
|
||||||
|
* for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen.
|
||||||
|
*/
|
||||||
|
class ActivityLogoutListener : BaseLogoutListener {
|
||||||
|
var activity: Activity
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for ActivityLogoutListener.
|
||||||
|
*
|
||||||
|
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
|
||||||
|
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
|
||||||
|
*/
|
||||||
|
constructor(activity: Activity, ctx: Context) : super(ctx) {
|
||||||
|
this.activity = activity
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for ActivityLogoutListener with additional parameters for the login screen.
|
||||||
|
*
|
||||||
|
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
|
||||||
|
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
|
||||||
|
* @param loginMessage Message to be displayed on the login page after logout.
|
||||||
|
* @param loginUsername Username to be pre-filled on the login page after logout.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
activity: Activity, ctx: Context?,
|
||||||
|
loginMessage: String?, loginUsername: String?
|
||||||
|
) : super(activity, loginMessage, loginUsername) {
|
||||||
|
this.activity = activity
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLogoutComplete() {
|
||||||
|
super.onLogoutComplete()
|
||||||
|
activity.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val LOGIN_MESSAGE_INTENT_KEY: String = "loginMessage"
|
||||||
|
const val LOGIN_USERNAME_INTENT_KEY: String = "loginUsername"
|
||||||
|
|
||||||
|
const val IS_LIMITED_CONNECTION_MODE_ENABLED: String = "is_limited_connection_mode_enabled"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants begin
|
||||||
|
*/
|
||||||
|
const val OPEN_APPLICATION_DETAIL_SETTINGS: Int = 1001
|
||||||
|
|
||||||
|
const val DEFAULT_EDIT_SUMMARY: String = "Uploaded using [[COM:MOA|Commons Mobile App]]"
|
||||||
|
|
||||||
|
const val FEEDBACK_EMAIL: String = "commons-app-android@googlegroups.com"
|
||||||
|
|
||||||
|
const val FEEDBACK_EMAIL_SUBJECT: String = "Commons Android App Feedback"
|
||||||
|
|
||||||
|
const val REPORT_EMAIL: String = "commons-app-android-private@googlegroups.com"
|
||||||
|
|
||||||
|
const val REPORT_EMAIL_SUBJECT: String = "Report a violation"
|
||||||
|
|
||||||
|
const val NOTIFICATION_CHANNEL_ID_ALL: String = "CommonsNotificationAll"
|
||||||
|
|
||||||
|
const val FEEDBACK_EMAIL_TEMPLATE_HEADER: String = "-- Technical information --"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants End
|
||||||
|
*/
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
lateinit var instance: CommonsApplication
|
||||||
|
private set
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var isPaused: Boolean = false
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun createNotificationChannel(context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val manager = context
|
||||||
|
.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
var channel = manager
|
||||||
|
.getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL)
|
||||||
|
if (channel == null) {
|
||||||
|
channel = NotificationChannel(
|
||||||
|
NOTIFICATION_CHANNEL_ID_ALL,
|
||||||
|
context.getString(R.string.notifications_channel_name_all),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* represents Licence object
|
|
||||||
*/
|
|
||||||
public class License {
|
|
||||||
private String key;
|
|
||||||
private String template;
|
|
||||||
private String url;
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new instance of License.
|
|
||||||
*
|
|
||||||
* @param key license key
|
|
||||||
* @param template license template
|
|
||||||
* @param url license URL
|
|
||||||
* @param name licence name
|
|
||||||
*
|
|
||||||
* @throws RuntimeException if License.key or Licence.template is null
|
|
||||||
*/
|
|
||||||
public License(String key, String template, String url, String name) {
|
|
||||||
if (key == null) {
|
|
||||||
throw new RuntimeException("License.key must not be null");
|
|
||||||
}
|
|
||||||
if (template == null) {
|
|
||||||
throw new RuntimeException("License.template must not be null");
|
|
||||||
}
|
|
||||||
this.key = key;
|
|
||||||
this.template = template;
|
|
||||||
this.url = url;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the license key.
|
|
||||||
* @return license key as a String.
|
|
||||||
*/
|
|
||||||
public String getKey() {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the license template.
|
|
||||||
* @return license template as a String.
|
|
||||||
*/
|
|
||||||
public String getTemplate() {
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the license name. If name is null, return license key.
|
|
||||||
* @return license name as string. if name null, license key as String
|
|
||||||
*/
|
|
||||||
public String getName() {
|
|
||||||
if (name == null) {
|
|
||||||
// hack
|
|
||||||
return getKey();
|
|
||||||
} else {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the license URL
|
|
||||||
*
|
|
||||||
* @param language license language
|
|
||||||
* @return URL
|
|
||||||
*/
|
|
||||||
public @Nullable String getUrl(String language) {
|
|
||||||
if (url == null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return url.replace("$lang", language);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
package fr.free.nrw.commons.LocationPicker;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class for starting the activity
|
|
||||||
*/
|
|
||||||
public final class LocationPicker {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Getting camera position from the intent using constants
|
|
||||||
*
|
|
||||||
* @param data intent
|
|
||||||
* @return CameraPosition
|
|
||||||
*/
|
|
||||||
public static CameraPosition getCameraPosition(final Intent data) {
|
|
||||||
return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class IntentBuilder {
|
|
||||||
|
|
||||||
private final Intent intent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new builder that creates an intent to launch the place picker activity.
|
|
||||||
*/
|
|
||||||
public IntentBuilder() {
|
|
||||||
intent = new Intent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets and puts location in intent
|
|
||||||
* @param position CameraPosition
|
|
||||||
* @return LocationPicker.IntentBuilder
|
|
||||||
*/
|
|
||||||
public LocationPicker.IntentBuilder defaultLocation(
|
|
||||||
final CameraPosition position) {
|
|
||||||
intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets and puts activity name in intent
|
|
||||||
* @param activity activity key
|
|
||||||
* @return LocationPicker.IntentBuilder
|
|
||||||
*/
|
|
||||||
public LocationPicker.IntentBuilder activityKey(
|
|
||||||
final String activity) {
|
|
||||||
intent.putExtra(LocationPickerConstants.ACTIVITY_KEY, activity);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets and sets the activity
|
|
||||||
* @param activity Activity
|
|
||||||
* @return Intent
|
|
||||||
*/
|
|
||||||
public Intent build(final Activity activity) {
|
|
||||||
intent.setClass(activity, LocationPickerActivity.class);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,554 +0,0 @@
|
||||||
package fr.free.nrw.commons.LocationPicker;
|
|
||||||
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.Property.NONE;
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE;
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement;
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
|
|
||||||
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
|
|
||||||
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION;
|
|
||||||
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.location.Location;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.Html;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.animation.OvershootInterpolator;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.widget.AppCompatTextView;
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
|
||||||
import androidx.lifecycle.Observer;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import com.mapbox.geojson.Point;
|
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition.Builder;
|
|
||||||
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
|
|
||||||
import com.mapbox.mapboxsdk.geometry.LatLng;
|
|
||||||
import com.mapbox.mapboxsdk.location.LocationComponent;
|
|
||||||
import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions;
|
|
||||||
import com.mapbox.mapboxsdk.location.engine.LocationEngineCallback;
|
|
||||||
import com.mapbox.mapboxsdk.location.engine.LocationEngineResult;
|
|
||||||
import com.mapbox.mapboxsdk.location.modes.CameraMode;
|
|
||||||
import com.mapbox.mapboxsdk.location.modes.RenderMode;
|
|
||||||
import com.mapbox.mapboxsdk.location.permissions.PermissionsManager;
|
|
||||||
import com.mapbox.mapboxsdk.maps.MapView;
|
|
||||||
import com.mapbox.mapboxsdk.maps.MapboxMap;
|
|
||||||
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraIdleListener;
|
|
||||||
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener;
|
|
||||||
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
|
|
||||||
import com.mapbox.mapboxsdk.maps.Style;
|
|
||||||
import com.mapbox.mapboxsdk.maps.UiSettings;
|
|
||||||
import com.mapbox.mapboxsdk.style.layers.Layer;
|
|
||||||
import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
|
|
||||||
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
|
|
||||||
import fr.free.nrw.commons.MapStyle;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
import fr.free.nrw.commons.filepicker.Constants;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.location.LocationPermissionsHelper;
|
|
||||||
import fr.free.nrw.commons.location.LocationPermissionsHelper.Dialog;
|
|
||||||
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
|
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
|
||||||
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helps to pick location and return the result with an intent
|
|
||||||
*/
|
|
||||||
public class LocationPickerActivity extends BaseActivity implements OnMapReadyCallback,
|
|
||||||
OnCameraMoveStartedListener, OnCameraIdleListener, Observer<CameraPosition>, LocationPermissionCallback {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DROPPED_MARKER_LAYER_ID : id for layer
|
|
||||||
*/
|
|
||||||
private static final String DROPPED_MARKER_LAYER_ID = "DROPPED_MARKER_LAYER_ID";
|
|
||||||
/**
|
|
||||||
* cameraPosition : position of picker
|
|
||||||
*/
|
|
||||||
private CameraPosition cameraPosition;
|
|
||||||
/**
|
|
||||||
* markerImage : picker image
|
|
||||||
*/
|
|
||||||
private ImageView markerImage;
|
|
||||||
/**
|
|
||||||
* mapboxMap : map
|
|
||||||
*/
|
|
||||||
private MapboxMap mapboxMap;
|
|
||||||
/**
|
|
||||||
* mapView : view of the map
|
|
||||||
*/
|
|
||||||
private MapView mapView;
|
|
||||||
/**
|
|
||||||
* tvAttribution : credit
|
|
||||||
*/
|
|
||||||
private AppCompatTextView tvAttribution;
|
|
||||||
/**
|
|
||||||
* activity : activity key
|
|
||||||
*/
|
|
||||||
private String activity;
|
|
||||||
/**
|
|
||||||
* location : location
|
|
||||||
*/
|
|
||||||
private Location location;
|
|
||||||
/**
|
|
||||||
* modifyLocationButton : button for start editing location
|
|
||||||
*/
|
|
||||||
Button modifyLocationButton;
|
|
||||||
/**
|
|
||||||
* showInMapButton : button for showing in map
|
|
||||||
*/
|
|
||||||
TextView showInMapButton;
|
|
||||||
/**
|
|
||||||
* placeSelectedButton : fab for selecting location
|
|
||||||
*/
|
|
||||||
FloatingActionButton placeSelectedButton;
|
|
||||||
/**
|
|
||||||
* fabCenterOnLocation: button for center on location;
|
|
||||||
*/
|
|
||||||
FloatingActionButton fabCenterOnLocation;
|
|
||||||
/**
|
|
||||||
* droppedMarkerLayer : Layer for static screen
|
|
||||||
*/
|
|
||||||
private Layer droppedMarkerLayer;
|
|
||||||
/**
|
|
||||||
* shadow : imageview of shadow
|
|
||||||
*/
|
|
||||||
private ImageView shadow;
|
|
||||||
/**
|
|
||||||
* largeToolbarText : textView of shadow
|
|
||||||
*/
|
|
||||||
private TextView largeToolbarText;
|
|
||||||
/**
|
|
||||||
* smallToolbarText : textView of shadow
|
|
||||||
*/
|
|
||||||
private TextView smallToolbarText;
|
|
||||||
/**
|
|
||||||
* applicationKvStore : for storing values
|
|
||||||
*/
|
|
||||||
@Inject
|
|
||||||
@Named("default_preferences")
|
|
||||||
public
|
|
||||||
JsonKvStore applicationKvStore;
|
|
||||||
/**
|
|
||||||
* isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly
|
|
||||||
*/
|
|
||||||
@Inject
|
|
||||||
SystemThemeUtils systemThemeUtils;
|
|
||||||
private boolean isDarkTheme;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
LocationServiceManager locationManager;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
|
||||||
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
isDarkTheme = systemThemeUtils.isDeviceInNightMode();
|
|
||||||
|
|
||||||
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
|
|
||||||
final ActionBar actionBar = getSupportActionBar();
|
|
||||||
if (actionBar != null) {
|
|
||||||
actionBar.hide();
|
|
||||||
}
|
|
||||||
setContentView(R.layout.activity_location_picker);
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
cameraPosition = getIntent()
|
|
||||||
.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
|
|
||||||
activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
final LocationPickerViewModel viewModel = new ViewModelProvider(this)
|
|
||||||
.get(LocationPickerViewModel.class);
|
|
||||||
viewModel.getResult().observe(this, this);
|
|
||||||
|
|
||||||
bindViews();
|
|
||||||
addBackButtonListener();
|
|
||||||
addPlaceSelectedButton();
|
|
||||||
addCredits();
|
|
||||||
getToolbarUI();
|
|
||||||
addCenterOnGPSButton();
|
|
||||||
|
|
||||||
if ("UploadActivity".equals(activity)) {
|
|
||||||
placeSelectedButton.setVisibility(View.GONE);
|
|
||||||
modifyLocationButton.setVisibility(View.VISIBLE);
|
|
||||||
showInMapButton.setVisibility(View.VISIBLE);
|
|
||||||
largeToolbarText.setText(getResources().getString(R.string.image_location));
|
|
||||||
smallToolbarText.setText(getResources().
|
|
||||||
getString(R.string.check_whether_location_is_correct));
|
|
||||||
fabCenterOnLocation.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
mapView.onCreate(savedInstanceState);
|
|
||||||
mapView.getMapAsync(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For showing credits
|
|
||||||
*/
|
|
||||||
private void addCredits() {
|
|
||||||
tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
|
|
||||||
tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clicking back button destroy locationPickerActivity
|
|
||||||
*/
|
|
||||||
private void addBackButtonListener() {
|
|
||||||
final ImageView backButton = findViewById(R.id.maplibre_place_picker_toolbar_back_button);
|
|
||||||
backButton.setOnClickListener(view -> finish());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds mapView and location picker icon
|
|
||||||
*/
|
|
||||||
private void bindViews() {
|
|
||||||
mapView = findViewById(R.id.map_view);
|
|
||||||
markerImage = findViewById(R.id.location_picker_image_view_marker);
|
|
||||||
tvAttribution = findViewById(R.id.tv_attribution);
|
|
||||||
modifyLocationButton = findViewById(R.id.modify_location);
|
|
||||||
showInMapButton = findViewById(R.id.show_in_map);
|
|
||||||
showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase());
|
|
||||||
shadow = findViewById(R.id.location_picker_image_view_shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds the listeners
|
|
||||||
*/
|
|
||||||
private void bindListeners() {
|
|
||||||
mapboxMap.addOnCameraMoveStartedListener(
|
|
||||||
this);
|
|
||||||
mapboxMap.addOnCameraIdleListener(
|
|
||||||
this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets toolbar color
|
|
||||||
*/
|
|
||||||
private void getToolbarUI() {
|
|
||||||
final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar);
|
|
||||||
largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view);
|
|
||||||
smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view);
|
|
||||||
toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes action when map is ready to show
|
|
||||||
* @param mapboxMap map
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onMapReady(final MapboxMap mapboxMap) {
|
|
||||||
this.mapboxMap = mapboxMap;
|
|
||||||
mapboxMap.setStyle(isDarkTheme ? MapStyle.DARK : MapStyle.STREETS, this::onStyleLoaded);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes dropped marker and layer
|
|
||||||
* Handles camera position based on options
|
|
||||||
* Enables location components
|
|
||||||
*
|
|
||||||
* @param style style
|
|
||||||
*/
|
|
||||||
private void onStyleLoaded(final Style style) {
|
|
||||||
if (modifyLocationButton.getVisibility() == View.VISIBLE) {
|
|
||||||
initDroppedMarker(style);
|
|
||||||
adjustCameraBasedOnOptions();
|
|
||||||
enableLocationComponent(style);
|
|
||||||
if (style.getLayer(DROPPED_MARKER_LAYER_ID) != null) {
|
|
||||||
final GeoJsonSource source = style.getSourceAs("dropped-marker-source-id");
|
|
||||||
if (source != null) {
|
|
||||||
source.setGeoJson(Point.fromLngLat(cameraPosition.target.getLongitude(),
|
|
||||||
cameraPosition.target.getLatitude()));
|
|
||||||
}
|
|
||||||
droppedMarkerLayer = style.getLayer(DROPPED_MARKER_LAYER_ID);
|
|
||||||
if (droppedMarkerLayer != null) {
|
|
||||||
droppedMarkerLayer.setProperties(visibility(VISIBLE));
|
|
||||||
markerImage.setVisibility(View.GONE);
|
|
||||||
shadow.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
adjustCameraBasedOnOptions();
|
|
||||||
enableLocationComponent(style);
|
|
||||||
bindListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyLocationButton.setOnClickListener(v -> onClickModifyLocation());
|
|
||||||
showInMapButton.setOnClickListener(v -> showInMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles onclick event of modifyLocationButton
|
|
||||||
*/
|
|
||||||
private void onClickModifyLocation() {
|
|
||||||
placeSelectedButton.setVisibility(View.VISIBLE);
|
|
||||||
modifyLocationButton.setVisibility(View.GONE);
|
|
||||||
showInMapButton.setVisibility(View.GONE);
|
|
||||||
droppedMarkerLayer.setProperties(visibility(NONE));
|
|
||||||
markerImage.setVisibility(View.VISIBLE);
|
|
||||||
shadow.setVisibility(View.VISIBLE);
|
|
||||||
largeToolbarText.setText(getResources().getString(R.string.choose_a_location));
|
|
||||||
smallToolbarText.setText(getResources().getString(R.string.pan_and_zoom_to_adjust));
|
|
||||||
bindListeners();
|
|
||||||
fabCenterOnLocation.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the location in map app
|
|
||||||
*/
|
|
||||||
public void showInMap(){
|
|
||||||
Utils.handleGeoCoordinates(this,
|
|
||||||
new fr.free.nrw.commons.location.LatLng(cameraPosition.target.getLatitude(),
|
|
||||||
cameraPosition.target.getLongitude(), 0.0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize Dropped Marker and layer without showing
|
|
||||||
* @param loadedMapStyle style
|
|
||||||
*/
|
|
||||||
private void initDroppedMarker(@NonNull final Style loadedMapStyle) {
|
|
||||||
// Add the marker image to map
|
|
||||||
loadedMapStyle.addImage("dropped-icon-image", BitmapFactory.decodeResource(
|
|
||||||
getResources(), R.drawable.map_default_map_marker));
|
|
||||||
loadedMapStyle.addSource(new GeoJsonSource("dropped-marker-source-id"));
|
|
||||||
loadedMapStyle.addLayer(new SymbolLayer(DROPPED_MARKER_LAYER_ID,
|
|
||||||
"dropped-marker-source-id").withProperties(
|
|
||||||
iconImage("dropped-icon-image"),
|
|
||||||
visibility(NONE),
|
|
||||||
iconAllowOverlap(true),
|
|
||||||
iconIgnorePlacement(true)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* move the location to the current media coordinates
|
|
||||||
*/
|
|
||||||
private void adjustCameraBasedOnOptions() {
|
|
||||||
mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables location components
|
|
||||||
* @param loadedMapStyle Style
|
|
||||||
*/
|
|
||||||
@SuppressWarnings( {"MissingPermission"})
|
|
||||||
private void enableLocationComponent(@NonNull final Style loadedMapStyle) {
|
|
||||||
final UiSettings uiSettings = mapboxMap.getUiSettings();
|
|
||||||
uiSettings.setAttributionEnabled(false);
|
|
||||||
|
|
||||||
// Check if permissions are enabled and if not request
|
|
||||||
if (PermissionsManager.areLocationPermissionsGranted(this)) {
|
|
||||||
|
|
||||||
// Get an instance of the component
|
|
||||||
final LocationComponent locationComponent = mapboxMap.getLocationComponent();
|
|
||||||
|
|
||||||
// Activate with options
|
|
||||||
locationComponent.activateLocationComponent(
|
|
||||||
LocationComponentActivationOptions.builder(this, loadedMapStyle).build());
|
|
||||||
|
|
||||||
// Enable to make component visible
|
|
||||||
locationComponent.setLocationComponentEnabled(true);
|
|
||||||
|
|
||||||
// Set the component's camera mode
|
|
||||||
locationComponent.setCameraMode(CameraMode.NONE);
|
|
||||||
|
|
||||||
// Set the component's render mode
|
|
||||||
locationComponent.setRenderMode(RenderMode.NORMAL);
|
|
||||||
|
|
||||||
// Get the component's location engine to receive user's last location
|
|
||||||
locationComponent.getLocationEngine().getLastLocation(
|
|
||||||
new LocationEngineCallback<LocationEngineResult>() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(LocationEngineResult result) {
|
|
||||||
location = result.getLastLocation();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Exception exception) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acts on camera moving
|
|
||||||
* @param reason int
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onCameraMoveStarted(final int reason) {
|
|
||||||
Timber.v("Map camera has begun moving.");
|
|
||||||
if (markerImage.getTranslationY() == 0) {
|
|
||||||
markerImage.animate().translationY(-75)
|
|
||||||
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acts on camera idle
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onCameraIdle() {
|
|
||||||
Timber.v("Map camera is now idling.");
|
|
||||||
markerImage.animate().translationY(0)
|
|
||||||
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes action on camera position
|
|
||||||
* @param position position of picker
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onChanged(@Nullable CameraPosition position) {
|
|
||||||
if (position == null) {
|
|
||||||
position = new Builder()
|
|
||||||
.target(new LatLng(mapboxMap.getCameraPosition().target.getLatitude(),
|
|
||||||
mapboxMap.getCameraPosition().target.getLongitude()))
|
|
||||||
.zoom(16).build();
|
|
||||||
}
|
|
||||||
cameraPosition = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select the preferable location
|
|
||||||
*/
|
|
||||||
private void addPlaceSelectedButton() {
|
|
||||||
placeSelectedButton = findViewById(R.id.location_chosen_button);
|
|
||||||
placeSelectedButton.setOnClickListener(view -> placeSelected());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the intent with required data
|
|
||||||
*/
|
|
||||||
void placeSelected() {
|
|
||||||
if (activity.equals("NoLocationUploadActivity")) {
|
|
||||||
applicationKvStore.putString(LAST_LOCATION,
|
|
||||||
mapboxMap.getCameraPosition().target.getLatitude()
|
|
||||||
+ ","
|
|
||||||
+ mapboxMap.getCameraPosition().target.getLongitude());
|
|
||||||
applicationKvStore.putString(LAST_ZOOM, mapboxMap.getCameraPosition().zoom + "");
|
|
||||||
}
|
|
||||||
final Intent returningIntent = new Intent();
|
|
||||||
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION,
|
|
||||||
mapboxMap.getCameraPosition());
|
|
||||||
setResult(AppCompatActivity.RESULT_OK, returningIntent);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Center the camera on the last saved location
|
|
||||||
*/
|
|
||||||
private void addCenterOnGPSButton(){
|
|
||||||
fabCenterOnLocation = findViewById(R.id.center_on_gps);
|
|
||||||
fabCenterOnLocation.setOnClickListener(view -> getCenter());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Center the map at user's current location
|
|
||||||
*/
|
|
||||||
private void getCenter() {
|
|
||||||
LocationPermissionsHelper.Dialog locationAccessDialog = new Dialog(
|
|
||||||
R.string.location_permission_title,
|
|
||||||
R.string.upload_map_location_access
|
|
||||||
);
|
|
||||||
|
|
||||||
LocationPermissionsHelper.Dialog locationOffDialog = new Dialog(
|
|
||||||
R.string.ask_to_turn_location_on,
|
|
||||||
R.string.upload_map_location_access
|
|
||||||
);
|
|
||||||
LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper(
|
|
||||||
this, locationManager, this);
|
|
||||||
locationPermissionsHelper.handleLocationPermissions(locationAccessDialog, locationOffDialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions,
|
|
||||||
@NonNull final int[] grantResults) {
|
|
||||||
if (requestCode == Constants.RequestCodes.LOCATION && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
onLocationPermissionGranted();
|
|
||||||
} else {
|
|
||||||
onLocationPermissionDenied("");
|
|
||||||
}
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
mapView.onStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
mapView.onResume();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
mapView.onPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
mapView.onStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(final @NotNull Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
mapView.onSaveInstanceState(outState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
mapView.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLowMemory() {
|
|
||||||
super.onLowMemory();
|
|
||||||
mapView.onLowMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationPermissionDenied(String toastMessage) {
|
|
||||||
//do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLocationPermissionGranted() {
|
|
||||||
fr.free.nrw.commons.location.LatLng currLocation = locationManager.getLastLocation();
|
|
||||||
if (currLocation != null) {
|
|
||||||
final CameraPosition position;
|
|
||||||
position = new CameraPosition.Builder()
|
|
||||||
.target(new com.mapbox.mapboxsdk.geometry.LatLng(currLocation.getLatitude(),
|
|
||||||
currLocation.getLongitude(), 0)) // Sets the new camera position
|
|
||||||
.zoom(mapboxMap.getCameraPosition().zoom) // Same zoom level
|
|
||||||
.build();
|
|
||||||
|
|
||||||
mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package fr.free.nrw.commons.LocationPicker;
|
|
||||||
|
|
||||||
import com.mapbox.mapboxsdk.maps.Style;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constants need for location picking
|
|
||||||
*/
|
|
||||||
public final class LocationPickerConstants {
|
|
||||||
|
|
||||||
public static final String ACTIVITY_KEY
|
|
||||||
= "location.picker.activity";
|
|
||||||
|
|
||||||
public static final String MAP_CAMERA_POSITION
|
|
||||||
= "location.picker.cameraPosition";
|
|
||||||
|
|
||||||
|
|
||||||
private LocationPickerConstants() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
package fr.free.nrw.commons.LocationPicker;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import retrofit2.Call;
|
|
||||||
import retrofit2.Callback;
|
|
||||||
import retrofit2.Response;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observes live camera position data
|
|
||||||
*/
|
|
||||||
public class LocationPickerViewModel extends AndroidViewModel implements Callback<CameraPosition> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapping CameraPosition with MutableLiveData
|
|
||||||
*/
|
|
||||||
private final MutableLiveData<CameraPosition> result = new MutableLiveData<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for this class
|
|
||||||
*
|
|
||||||
* @param application Application
|
|
||||||
*/
|
|
||||||
public LocationPickerViewModel(@NonNull final Application application) {
|
|
||||||
super(application);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Responses on camera position changing
|
|
||||||
*
|
|
||||||
* @param call Call<CameraPosition>
|
|
||||||
* @param response Response<CameraPosition>
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onResponse(final @NotNull Call<CameraPosition> call,
|
|
||||||
final Response<CameraPosition> response) {
|
|
||||||
if (response.body() == null) {
|
|
||||||
result.setValue(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
result.setValue(response.body());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(final @NotNull Call<CameraPosition> call, final @NotNull Throwable t) {
|
|
||||||
Timber.e(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets live CameraPosition
|
|
||||||
*
|
|
||||||
* @return MutableLiveData<CameraPosition>
|
|
||||||
*/
|
|
||||||
public MutableLiveData<CameraPosition> getResult() {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
|
||||||
import fr.free.nrw.commons.nearby.Place;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public abstract class MapController {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We pass this variable as a group of placeList and boundaryCoordinates
|
|
||||||
*/
|
|
||||||
public class NearbyPlacesInfo {
|
|
||||||
public List<Place> placeList; // List of nearby places
|
|
||||||
public LatLng[] boundaryCoordinates; // Corners of nearby area
|
|
||||||
public LatLng curLatLng; // Current location when this places are populated
|
|
||||||
public LatLng searchLatLng; // Search location for finding this places
|
|
||||||
public List<Media> mediaList; // Search location for finding this places
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We pass this variable as a group of placeList and boundaryCoordinates
|
|
||||||
*/
|
|
||||||
public class ExplorePlacesInfo {
|
|
||||||
public List<Place> explorePlaceList; // List of nearby places
|
|
||||||
public LatLng[] boundaryCoordinates; // Corners of nearby area
|
|
||||||
public LatLng curLatLng; // Current location when this places are populated
|
|
||||||
public LatLng searchLatLng; // Search location for finding this places
|
|
||||||
public List<Media> mediaList; // Search location for finding this places
|
|
||||||
}
|
|
||||||
}
|
|
||||||
46
app/src/main/java/fr/free/nrw/commons/MapController.kt
Normal file
46
app/src/main/java/fr/free/nrw/commons/MapController.kt
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.location.LatLng
|
||||||
|
import fr.free.nrw.commons.nearby.Place
|
||||||
|
|
||||||
|
abstract class MapController {
|
||||||
|
/**
|
||||||
|
* We pass this variable as a group of placeList and boundaryCoordinates
|
||||||
|
*/
|
||||||
|
inner class NearbyPlacesInfo {
|
||||||
|
@JvmField
|
||||||
|
var placeList: List<Place> = emptyList() // List of nearby places
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var boundaryCoordinates: Array<LatLng> = emptyArray() // Corners of nearby area
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var currentLatLng: LatLng? = null // Current location when this places are populated
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var searchLatLng: LatLng? = null // Search location for finding this places
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var mediaList: List<Media>? = null // Search location for finding this places
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We pass this variable as a group of placeList and boundaryCoordinates
|
||||||
|
*/
|
||||||
|
inner class ExplorePlacesInfo {
|
||||||
|
@JvmField
|
||||||
|
var explorePlaceList: List<Place> = emptyList() // List of nearby places
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var boundaryCoordinates: Array<LatLng> = emptyArray() // Corners of nearby area
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var currentLatLng: LatLng? = null // Current location when this places are populated
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var searchLatLng: LatLng? = null // Search location for finding this places
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var mediaList: List<Media> = emptyList() // Search location for finding this places
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import com.mapbox.mapboxsdk.maps.Style;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constants for various map styles
|
|
||||||
*/
|
|
||||||
public final class MapStyle {
|
|
||||||
public static final String DARK = Style.getPredefinedStyle("Dark");
|
|
||||||
public static final String OUTDOORS = Style.getPredefinedStyle("Outdoors");
|
|
||||||
public static final String STREETS = Style.getPredefinedStyle("Streets");
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
package fr.free.nrw.commons
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import fr.free.nrw.commons.BuildConfig.COMMONS_URL
|
||||||
import fr.free.nrw.commons.location.LatLng
|
import fr.free.nrw.commons.location.LatLng
|
||||||
import kotlinx.android.parcel.Parcelize
|
import fr.free.nrw.commons.wikidata.model.WikiSite
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryPage
|
import fr.free.nrw.commons.wikidata.model.page.PageTitle
|
||||||
import org.wikipedia.page.PageTitle
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import java.util.*
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Media constructor(
|
class Media constructor(
|
||||||
|
|
@ -15,7 +19,6 @@ class Media constructor(
|
||||||
*/
|
*/
|
||||||
var pageId: String = UUID.randomUUID().toString(),
|
var pageId: String = UUID.randomUUID().toString(),
|
||||||
var thumbUrl: String? = null,
|
var thumbUrl: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets image URL
|
* Gets image URL
|
||||||
* @return Image URL
|
* @return Image URL
|
||||||
|
|
@ -27,16 +30,9 @@ class Media constructor(
|
||||||
*/
|
*/
|
||||||
var filename: String? = null,
|
var filename: String? = null,
|
||||||
/**
|
/**
|
||||||
* Gets the file description.
|
* The fallback description of the file, used if no other description is provided.
|
||||||
* @return file description as a string
|
|
||||||
*/
|
|
||||||
// monolingual description on input...
|
|
||||||
/**
|
|
||||||
* Sets the file description.
|
|
||||||
* @param fallbackDescription the new description of the file
|
|
||||||
*/
|
*/
|
||||||
var fallbackDescription: String? = null,
|
var fallbackDescription: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the upload date of the file.
|
* Gets the upload date of the file.
|
||||||
* Can be null.
|
* Can be null.
|
||||||
|
|
@ -44,28 +40,25 @@ class Media constructor(
|
||||||
*/
|
*/
|
||||||
var dateUploaded: Date? = null,
|
var dateUploaded: Date? = null,
|
||||||
/**
|
/**
|
||||||
* Gets the license name of the file.
|
* The license name of the file.
|
||||||
* @return license as a String
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Sets the license name of the file.
|
|
||||||
*
|
|
||||||
* @param license license name as a String
|
|
||||||
*/
|
*/
|
||||||
var license: String? = null,
|
var license: String? = null,
|
||||||
|
/**
|
||||||
|
* The URL corresponding to the license.
|
||||||
|
*/
|
||||||
var licenseUrl: String? = null,
|
var licenseUrl: String? = null,
|
||||||
/**
|
/**
|
||||||
* Gets the name of the creator of the file.
|
* The name of the creator of the file.
|
||||||
* @return author name as a String
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Sets the author name of the file.
|
|
||||||
* @param author creator name as a string
|
|
||||||
*/
|
*/
|
||||||
var author: String? = null,
|
var author: String? = null,
|
||||||
|
/**
|
||||||
var user:String?=null,
|
* The username of the uploader.
|
||||||
|
*/
|
||||||
|
var user: String? = null,
|
||||||
|
/**
|
||||||
|
* The full name of the file's creator, if different from username.
|
||||||
|
*/
|
||||||
|
var creatorName: String? = null,
|
||||||
/**
|
/**
|
||||||
* Gets the categories the file falls under.
|
* Gets the categories the file falls under.
|
||||||
* @return file categories as an ArrayList of Strings
|
* @return file categories as an ArrayList of Strings
|
||||||
|
|
@ -79,46 +72,111 @@ class Media constructor(
|
||||||
var captions: Map<String, String> = emptyMap(),
|
var captions: Map<String, String> = emptyMap(),
|
||||||
var descriptions: Map<String, String> = emptyMap(),
|
var descriptions: Map<String, String> = emptyMap(),
|
||||||
var depictionIds: List<String> = emptyList(),
|
var depictionIds: List<String> = emptyList(),
|
||||||
|
var creatorIds: List<String> = emptyList(),
|
||||||
/**
|
/**
|
||||||
* This field was added to find non-hidden categories
|
* This field was added to find non-hidden categories
|
||||||
* Stores the mapping of category title to hidden attribute
|
* Stores the mapping of category title to hidden attribute
|
||||||
* Example: "Mountains" => false, "CC-BY-SA-2.0" => true
|
* Example: "Mountains" => false, "CC-BY-SA-2.0" => true
|
||||||
*/
|
*/
|
||||||
var categoriesHiddenStatus: Map<String, Boolean> = emptyMap()
|
var categoriesHiddenStatus: Map<String, Boolean> = emptyMap(),
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
constructor(
|
||||||
|
captions: Map<String, String>,
|
||||||
|
categories: List<String>?,
|
||||||
|
filename: String?,
|
||||||
|
fallbackDescription: String?,
|
||||||
|
author: String?,
|
||||||
|
user: String?,
|
||||||
|
) : this(
|
||||||
|
filename = filename,
|
||||||
|
fallbackDescription = fallbackDescription,
|
||||||
|
dateUploaded = Date(),
|
||||||
|
author = author,
|
||||||
|
user = user,
|
||||||
|
categories = categories,
|
||||||
|
captions = captions,
|
||||||
|
)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
captions: Map<String, String>,
|
captions: Map<String, String>,
|
||||||
categories: List<String>?,
|
categories: List<String>?,
|
||||||
filename: String?,
|
filename: String?,
|
||||||
fallbackDescription: String?,
|
fallbackDescription: String?,
|
||||||
author: String?, user:String?
|
author: String?,
|
||||||
|
user: String?,
|
||||||
|
dateUploaded: Date? = Date(),
|
||||||
|
license: String? = null,
|
||||||
|
licenseUrl: String? = null,
|
||||||
|
imageUrl: String? = null,
|
||||||
|
thumbUrl: String? = null,
|
||||||
|
coordinates: LatLng? = null,
|
||||||
|
descriptions: Map<String, String> = emptyMap(),
|
||||||
|
depictionIds: List<String> = emptyList(),
|
||||||
|
categoriesHiddenStatus: Map<String, Boolean> = emptyMap()
|
||||||
) : this(
|
) : this(
|
||||||
|
pageId = UUID.randomUUID().toString(),
|
||||||
filename = filename,
|
filename = filename,
|
||||||
fallbackDescription = fallbackDescription,
|
fallbackDescription = fallbackDescription,
|
||||||
dateUploaded = Date(),
|
dateUploaded = dateUploaded,
|
||||||
author = author,
|
author = author,
|
||||||
user=user,
|
user = user,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
captions = captions
|
captions = captions,
|
||||||
|
license = license,
|
||||||
|
licenseUrl = licenseUrl,
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
thumbUrl = thumbUrl,
|
||||||
|
coordinates = coordinates,
|
||||||
|
descriptions = descriptions,
|
||||||
|
depictionIds = depictionIds,
|
||||||
|
categoriesHiddenStatus = categoriesHiddenStatus
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Author if it's not null or empty, otherwise
|
||||||
|
* returns user
|
||||||
|
* @return Author or User
|
||||||
|
*/
|
||||||
|
@Deprecated("Use user for uploader username. Use attributedAuthor() for attribution. Note that the uploader may not be the creator/author.")
|
||||||
|
fun getAuthorOrUser(): String? {
|
||||||
|
return if (!author.isNullOrEmpty()) {
|
||||||
|
author
|
||||||
|
} else{
|
||||||
|
user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns author if it's not null or empty, otherwise
|
||||||
|
* returns creator name
|
||||||
|
* @return name of author or creator
|
||||||
|
*/
|
||||||
|
fun getAttributedAuthor(): String? {
|
||||||
|
return if (!author.isNullOrEmpty()) {
|
||||||
|
author
|
||||||
|
} else{
|
||||||
|
creatorName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets media display title
|
* Gets media display title
|
||||||
* @return Media title
|
* @return Media title
|
||||||
*/
|
*/
|
||||||
val displayTitle: String
|
val displayTitle: String
|
||||||
get() =
|
get() =
|
||||||
if (filename != null)
|
if (filename != null) {
|
||||||
pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "")
|
pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "")
|
||||||
else
|
} else {
|
||||||
""
|
""
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets file page title
|
* Gets file page title
|
||||||
* @return New media page title
|
* @return New media page title
|
||||||
*/
|
*/
|
||||||
val pageTitle: PageTitle get() = Utils.getPageTitle(filename!!)
|
val pageTitle: PageTitle
|
||||||
|
get() = PageTitle(filename!!, WikiSite(COMMONS_URL))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns wikicode to use the media file on a MediaWiki site
|
* Returns wikicode to use the media file on a MediaWiki site
|
||||||
|
|
@ -128,17 +186,21 @@ class Media constructor(
|
||||||
get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption)
|
get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption)
|
||||||
|
|
||||||
val mostRelevantCaption: String
|
val mostRelevantCaption: String
|
||||||
get() = captions[Locale.getDefault().language]
|
get() =
|
||||||
?: captions.values.firstOrNull()
|
captions[Locale.getDefault().language]
|
||||||
?: displayTitle
|
?: captions.values.firstOrNull()
|
||||||
|
?: displayTitle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the categories the file falls under.
|
* Gets the categories the file falls under.
|
||||||
* @return file categories as an ArrayList of Strings
|
* @return file categories as an ArrayList of Strings
|
||||||
*/
|
*/
|
||||||
|
@IgnoredOnParcel
|
||||||
var addedCategories: List<String>? = null
|
var addedCategories: List<String>? = null
|
||||||
// TODO added categories should be removed. It is added for a short fix. On category update,
|
// TODO added categories should be removed. It is added for a short fix. On category update,
|
||||||
// categories should be re-fetched instead
|
// categories should be re-fetched instead
|
||||||
get() = field // getter
|
get() = field // getter
|
||||||
set(value) { field = value } // setter
|
set(value) {
|
||||||
|
field = value
|
||||||
|
} // setter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package fr.free.nrw.commons
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
|
import fr.free.nrw.commons.media.IdAndLabels
|
||||||
import fr.free.nrw.commons.media.IdAndCaptions
|
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
|
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -17,42 +17,56 @@ import javax.inject.Singleton
|
||||||
* to the media and may change due to editing.
|
* to the media and may change due to editing.
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClient) {
|
class MediaDataExtractor
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val mediaClient: MediaClient,
|
||||||
|
) {
|
||||||
|
fun fetchDepictionIdsAndLabels(media: Media) =
|
||||||
|
mediaClient
|
||||||
|
.getEntities(media.depictionIds)
|
||||||
|
.map {
|
||||||
|
it
|
||||||
|
.entities()
|
||||||
|
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
|
||||||
|
}.map { it.map { (key, value) -> IdAndLabels(key, value) } }
|
||||||
|
.onErrorReturn { emptyList() }
|
||||||
|
|
||||||
fun fetchDepictionIdsAndLabels(media: Media) =
|
fun fetchCreatorIdsAndLabels(media: Media) =
|
||||||
mediaClient.getEntities(media.depictionIds)
|
mediaClient
|
||||||
.map {
|
.getEntities(media.creatorIds)
|
||||||
it.entities()
|
.map {
|
||||||
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
|
it
|
||||||
}
|
.entities()
|
||||||
.map { it.map { (key, value) -> IdAndCaptions(key, value) } }
|
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
|
||||||
.onErrorReturn { emptyList() }
|
}.map { it.map { (key, value) -> IdAndLabels(key, value) } }
|
||||||
|
.onErrorReturn { emptyList() }
|
||||||
|
|
||||||
fun checkDeletionRequestExists(media: Media) =
|
fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
|
||||||
mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
|
|
||||||
|
|
||||||
fun fetchDiscussion(media: Media) =
|
fun fetchDiscussion(media: Media) =
|
||||||
mediaClient.getPageHtml(media.filename!!.replace("File", "File talk"))
|
mediaClient
|
||||||
.map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
|
.getPageHtml(media.filename!!.replace("File", "File talk"))
|
||||||
.onErrorReturn {
|
.map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
|
||||||
Timber.d("Error occurred while fetching discussion")
|
.onErrorReturn {
|
||||||
""
|
Timber.d("Error occurred while fetching discussion")
|
||||||
}
|
""
|
||||||
|
}
|
||||||
|
|
||||||
fun refresh(media: Media): Single<Media> {
|
fun refresh(media: Media): Single<Media> =
|
||||||
return Single.ambArray(
|
Single.ambArray(
|
||||||
mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId)
|
mediaClient
|
||||||
.onErrorResumeNext { Single.never() },
|
.getMediaById(PAGE_ID_PREFIX + media.pageId)
|
||||||
mediaClient.getMedia(media.filename)
|
.onErrorResumeNext { Single.never() },
|
||||||
.onErrorResumeNext { Single.never() }
|
mediaClient
|
||||||
)
|
.getMediaSuppressingErrors(media.filename)
|
||||||
|
.onErrorResumeNext { Single.never() },
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches wikitext from mediaClient
|
||||||
|
*/
|
||||||
|
fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches wikitext from mediaClient
|
|
||||||
*/
|
|
||||||
fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base interface for all the views
|
|
||||||
*/
|
|
||||||
public interface MvpView {
|
|
||||||
void showMessage(String message);
|
|
||||||
}
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import okhttp3.Cache;
|
|
||||||
import okhttp3.Interceptor;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
import okhttp3.ResponseBody;
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor;
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor.Level;
|
|
||||||
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
|
|
||||||
import org.wikipedia.dataclient.okhttp.HttpStatusException;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public final class OkHttpConnectionFactory {
|
|
||||||
private static final String CACHE_DIR_NAME = "okhttp-cache";
|
|
||||||
private static final long NET_CACHE_SIZE = 64 * 1024 * 1024;
|
|
||||||
@NonNull private static final Cache NET_CACHE = new Cache(new File(CommonsApplication.getInstance().getCacheDir(),
|
|
||||||
CACHE_DIR_NAME), NET_CACHE_SIZE);
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static final OkHttpClient CLIENT = createClient();
|
|
||||||
|
|
||||||
@NonNull public static OkHttpClient getClient() {
|
|
||||||
return CLIENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static OkHttpClient createClient() {
|
|
||||||
return new OkHttpClient.Builder()
|
|
||||||
.cookieJar(SharedPreferenceCookieManager.getInstance())
|
|
||||||
.cache(NET_CACHE)
|
|
||||||
.connectTimeout(120, TimeUnit.SECONDS)
|
|
||||||
.writeTimeout(120, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(120, TimeUnit.SECONDS)
|
|
||||||
.addInterceptor(getLoggingInterceptor())
|
|
||||||
.addInterceptor(new UnsuccessfulResponseInterceptor())
|
|
||||||
.addInterceptor(new CommonHeaderRequestInterceptor())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HttpLoggingInterceptor getLoggingInterceptor() {
|
|
||||||
final HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor()
|
|
||||||
.setLevel(Level.BASIC);
|
|
||||||
|
|
||||||
httpLoggingInterceptor.redactHeader("Authorization");
|
|
||||||
httpLoggingInterceptor.redactHeader("Cookie");
|
|
||||||
|
|
||||||
return httpLoggingInterceptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class CommonHeaderRequestInterceptor implements Interceptor {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public Response intercept(@NonNull final Chain chain) throws IOException {
|
|
||||||
final Request request = chain.request().newBuilder()
|
|
||||||
.header("User-Agent", CommonsApplication.getInstance().getUserAgent())
|
|
||||||
.build();
|
|
||||||
return chain.proceed(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UnsuccessfulResponseInterceptor implements Interceptor {
|
|
||||||
private static final List<String> DO_NOT_INTERCEPT = Collections.singletonList(
|
|
||||||
"api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1");
|
|
||||||
|
|
||||||
private static final String ERRORS_PREFIX = "{\"error";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public Response intercept(@NonNull final Chain chain) throws IOException {
|
|
||||||
final Response rsp = chain.proceed(chain.request());
|
|
||||||
|
|
||||||
// Do not intercept certain requests and let the caller handle the errors
|
|
||||||
if(isExcludedUrl(chain.request())) {
|
|
||||||
return rsp;
|
|
||||||
}
|
|
||||||
if (rsp.isSuccessful()) {
|
|
||||||
try (final ResponseBody responseBody = rsp.peekBody(ERRORS_PREFIX.length())) {
|
|
||||||
if (ERRORS_PREFIX.equals(responseBody.string())) {
|
|
||||||
try (final ResponseBody body = rsp.body()) {
|
|
||||||
throw new IOException(body.string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (final IOException e) {
|
|
||||||
Timber.e(e);
|
|
||||||
}
|
|
||||||
return rsp;
|
|
||||||
}
|
|
||||||
throw new HttpStatusException(rsp);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isExcludedUrl(final Request request) {
|
|
||||||
final String requestUrl = request.url().toString();
|
|
||||||
for(final String url: DO_NOT_INTERCEPT) {
|
|
||||||
if(requestUrl.contains(url)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private OkHttpConnectionFactory() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
135
app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.kt
Normal file
135
app/src/main/java/fr/free/nrw/commons/OkHttpConnectionFactory.kt
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import fr.free.nrw.commons.wikidata.GsonUtil
|
||||||
|
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwErrorResponse
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwIOException
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwLegacyServiceError
|
||||||
|
import okhttp3.Cache
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
object OkHttpConnectionFactory {
|
||||||
|
private const val CACHE_DIR_NAME = "okhttp-cache"
|
||||||
|
private const val NET_CACHE_SIZE = (64 * 1024 * 1024).toLong()
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
var CLIENT: OkHttpClient? = null
|
||||||
|
|
||||||
|
fun getClient(cookieJar: CommonsCookieJar): OkHttpClient {
|
||||||
|
if (CLIENT == null) {
|
||||||
|
CLIENT = createClient(cookieJar)
|
||||||
|
}
|
||||||
|
return CLIENT!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createClient(cookieJar: CommonsCookieJar): OkHttpClient {
|
||||||
|
return OkHttpClient.Builder()
|
||||||
|
.cookieJar(cookieJar)
|
||||||
|
.cache(
|
||||||
|
if (CommonsApplication.instance != null) Cache(
|
||||||
|
File(CommonsApplication.instance.cacheDir, CACHE_DIR_NAME),
|
||||||
|
NET_CACHE_SIZE
|
||||||
|
) else null
|
||||||
|
)
|
||||||
|
.connectTimeout(120, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(120, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(120, TimeUnit.SECONDS)
|
||||||
|
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||||
|
setLevel(HttpLoggingInterceptor.Level.BASIC)
|
||||||
|
redactHeader("Authorization")
|
||||||
|
redactHeader("Cookie")
|
||||||
|
})
|
||||||
|
.addInterceptor(UnsuccessfulResponseInterceptor())
|
||||||
|
.addInterceptor(CommonHeaderRequestInterceptor())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommonHeaderRequestInterceptor : Interceptor {
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request().newBuilder()
|
||||||
|
.header("User-Agent", CommonsApplication.instance.userAgent)
|
||||||
|
.build()
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SUPPRESS_ERROR_LOG = "x-commons-suppress-error-log"
|
||||||
|
const val SUPPRESS_ERROR_LOG_HEADER: String = "$SUPPRESS_ERROR_LOG: true"
|
||||||
|
|
||||||
|
private class UnsuccessfulResponseInterceptor : Interceptor {
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val rq = chain.request()
|
||||||
|
|
||||||
|
// If the request contains our special "suppress errors" header, make note of it
|
||||||
|
// but don't pass that on to the server.
|
||||||
|
val suppressErrors = rq.headers.names().contains(SUPPRESS_ERROR_LOG)
|
||||||
|
val request = rq.newBuilder()
|
||||||
|
.removeHeader(SUPPRESS_ERROR_LOG)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val rsp = chain.proceed(request)
|
||||||
|
|
||||||
|
// Do not intercept certain requests and let the caller handle the errors
|
||||||
|
if (isExcludedUrl(chain.request())) {
|
||||||
|
return rsp
|
||||||
|
}
|
||||||
|
if (rsp.isSuccessful) {
|
||||||
|
try {
|
||||||
|
rsp.peekBody(ERRORS_PREFIX.length.toLong()).use { responseBody ->
|
||||||
|
if (ERRORS_PREFIX == responseBody.string()) {
|
||||||
|
rsp.body.use { body ->
|
||||||
|
val bodyString = body!!.string()
|
||||||
|
|
||||||
|
throw MwIOException(
|
||||||
|
"MediaWiki API returned error: $bodyString",
|
||||||
|
GsonUtil.defaultGson.fromJson(
|
||||||
|
bodyString,
|
||||||
|
MwErrorResponse::class.java
|
||||||
|
).error!!,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: MwIOException) {
|
||||||
|
// Log the error as debug (and therefore, "expected") or at error level
|
||||||
|
if (suppressErrors) {
|
||||||
|
Timber.d(e, "Suppressed (known / expected) error")
|
||||||
|
} else {
|
||||||
|
Timber.e(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rsp
|
||||||
|
}
|
||||||
|
throw IOException("Unsuccessful response")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isExcludedUrl(request: Request): Boolean {
|
||||||
|
val requestUrl = request.url.toString()
|
||||||
|
for (url in DO_NOT_INTERCEPT) {
|
||||||
|
if (requestUrl.contains(url)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DO_NOT_INTERCEPT = listOf(
|
||||||
|
"api.php?format=json&formatversion=2&errorformat=plaintext&action=upload&ignorewarnings=1"
|
||||||
|
)
|
||||||
|
const val ERRORS_PREFIX = "{\"error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,9 @@ internal object Urls {
|
||||||
const val FAQ_URL = "https://github.com/commons-app/commons-app-documentation/blob/master/android/Frequently-Asked-Questions.md"
|
const val FAQ_URL = "https://github.com/commons-app/commons-app-documentation/blob/master/android/Frequently-Asked-Questions.md"
|
||||||
const val PLAY_STORE_PREFIX = "market://details?id="
|
const val PLAY_STORE_PREFIX = "market://details?id="
|
||||||
const val PLAY_STORE_URL_PREFIX = "https://play.google.com/store/apps/details?id="
|
const val PLAY_STORE_URL_PREFIX = "https://play.google.com/store/apps/details?id="
|
||||||
const val TRANSLATE_WIKI_URL = "https://translatewiki.net/w/i.php?title=Special:Translate&group=commons-android-strings&filter=%21translated&action=translate&language="
|
const val TRANSLATE_WIKI_URL =
|
||||||
|
"https://translatewiki.net/w/i.php?title=Special:Translate" +
|
||||||
|
"&group=commons-android-strings&filter=%21translated&action=translate&language="
|
||||||
const val FACEBOOK_WEB_URL = "https://www.facebook.com/1921335171459985"
|
const val FACEBOOK_WEB_URL = "https://www.facebook.com/1921335171459985"
|
||||||
const val FACEBOOK_APP_URL = "fb://page/1921335171459985"
|
const val FACEBOOK_APP_URL = "fb://page/1921335171459985"
|
||||||
const val FACEBOOK_PACKAGE_NAME = "com.facebook.katana"
|
const val FACEBOOK_PACKAGE_NAME = "com.facebook.katana"
|
||||||
|
|
|
||||||
|
|
@ -1,261 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.style.UnderlineSpan;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.browser.customtabs.CustomTabColorSchemeParams;
|
|
||||||
import androidx.browser.customtabs.CustomTabsIntent;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
import org.wikipedia.page.PageTitle;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.location.LatLng;
|
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static android.widget.Toast.LENGTH_SHORT;
|
|
||||||
import static fr.free.nrw.commons.campaigns.CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE;
|
|
||||||
|
|
||||||
public class Utils {
|
|
||||||
|
|
||||||
public static PageTitle getPageTitle(@NonNull String title) {
|
|
||||||
return new PageTitle(title, new WikiSite(BuildConfig.COMMONS_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates licence name with given ID
|
|
||||||
* @param license License ID
|
|
||||||
* @return Name of license
|
|
||||||
*/
|
|
||||||
public static int licenseNameFor(String license) {
|
|
||||||
switch (license) {
|
|
||||||
case Prefs.Licenses.CC_BY_3:
|
|
||||||
return R.string.license_name_cc_by;
|
|
||||||
case Prefs.Licenses.CC_BY_4:
|
|
||||||
return R.string.license_name_cc_by_four;
|
|
||||||
case Prefs.Licenses.CC_BY_SA_3:
|
|
||||||
return R.string.license_name_cc_by_sa;
|
|
||||||
case Prefs.Licenses.CC_BY_SA_4:
|
|
||||||
return R.string.license_name_cc_by_sa_four;
|
|
||||||
case Prefs.Licenses.CC0:
|
|
||||||
return R.string.license_name_cc0;
|
|
||||||
}
|
|
||||||
throw new IllegalStateException("Unrecognized license value: " + license);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates license url with given ID
|
|
||||||
* @param license License ID
|
|
||||||
* @return Url of license
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static String licenseUrlFor(String license) {
|
|
||||||
switch (license) {
|
|
||||||
case Prefs.Licenses.CC_BY_3:
|
|
||||||
return "https://creativecommons.org/licenses/by/3.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_4:
|
|
||||||
return "https://creativecommons.org/licenses/by/4.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_3:
|
|
||||||
return "https://creativecommons.org/licenses/by-sa/3.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_4:
|
|
||||||
return "https://creativecommons.org/licenses/by-sa/4.0/";
|
|
||||||
case Prefs.Licenses.CC0:
|
|
||||||
return "https://creativecommons.org/publicdomain/zero/1.0/";
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("Unrecognized license value: " + license);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds extension to filename. Converts to .jpg if system provides .jpeg, adds .jpg if no extension detected
|
|
||||||
* @param title File name
|
|
||||||
* @param extension Correct extension
|
|
||||||
* @return File with correct extension
|
|
||||||
*/
|
|
||||||
public static String fixExtension(String title, String extension) {
|
|
||||||
Pattern jpegPattern = Pattern.compile("\\.jpeg$", Pattern.CASE_INSENSITIVE);
|
|
||||||
|
|
||||||
// People are used to ".jpg" more than ".jpeg" which the system gives us.
|
|
||||||
if (extension != null && extension.toLowerCase(Locale.ENGLISH).equals("jpeg")) {
|
|
||||||
extension = "jpg";
|
|
||||||
}
|
|
||||||
title = jpegPattern.matcher(title).replaceFirst(".jpg");
|
|
||||||
if (extension != null && !title.toLowerCase(Locale.getDefault())
|
|
||||||
.endsWith("." + extension.toLowerCase(Locale.ENGLISH))) {
|
|
||||||
title += "." + extension;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If extension is still null, make it jpg. (Hotfix for https://github.com/commons-app/apps-android-commons/issues/228)
|
|
||||||
// If title has an extension in it, if won't be true
|
|
||||||
if (extension == null && title.lastIndexOf(".")<=0) {
|
|
||||||
extension = "jpg";
|
|
||||||
title += "." + extension;
|
|
||||||
}
|
|
||||||
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launches intent to rate app
|
|
||||||
* @param context
|
|
||||||
*/
|
|
||||||
public static void rateApp(Context context) {
|
|
||||||
final String appPackageName = context.getPackageName();
|
|
||||||
try {
|
|
||||||
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(Urls.PLAY_STORE_PREFIX + appPackageName)));
|
|
||||||
}
|
|
||||||
catch (android.content.ActivityNotFoundException anfe) {
|
|
||||||
handleWebUrl(context, Uri.parse(Urls.PLAY_STORE_URL_PREFIX + appPackageName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens Custom Tab Activity with in-app browser for the specified URL.
|
|
||||||
* Launches intent for web URL
|
|
||||||
* @param context
|
|
||||||
* @param url
|
|
||||||
*/
|
|
||||||
public static void handleWebUrl(Context context, Uri url) {
|
|
||||||
Timber.d("Launching web url %s", url.toString());
|
|
||||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, url);
|
|
||||||
if (browserIntent.resolveActivity(context.getPackageManager()) == null) {
|
|
||||||
Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT);
|
|
||||||
toast.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final CustomTabColorSchemeParams color = new CustomTabColorSchemeParams.Builder()
|
|
||||||
.setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor))
|
|
||||||
.setSecondaryToolbarColor(ContextCompat.getColor(context, R.color.primaryDarkColor))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
|
||||||
builder.setDefaultColorSchemeParams(color);
|
|
||||||
builder.setExitAnimations(context, android.R.anim.slide_in_left, android.R.anim.slide_out_right);
|
|
||||||
CustomTabsIntent customTabsIntent = builder.build();
|
|
||||||
// Clear previous browser tasks, so that back/exit buttons work as intended.
|
|
||||||
customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
||||||
customTabsIntent.launchUrl(context, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Util function to handle geo coordinates
|
|
||||||
* It no longer depends on google maps and any app capable of handling the map intent can handle it
|
|
||||||
* @param context
|
|
||||||
* @param latLng
|
|
||||||
*/
|
|
||||||
public static void handleGeoCoordinates(Context context, LatLng latLng) {
|
|
||||||
Intent mapIntent = new Intent(Intent.ACTION_VIEW, latLng.getGmmIntentUri());
|
|
||||||
if (mapIntent.resolveActivity(context.getPackageManager()) != null) {
|
|
||||||
context.startActivity(mapIntent);
|
|
||||||
} else {
|
|
||||||
ViewUtil.showShortToast(context, context.getString(R.string.map_application_missing));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To take screenshot of the screen and return it in Bitmap format
|
|
||||||
*
|
|
||||||
* @param view
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static Bitmap getScreenShot(View view) {
|
|
||||||
View screenView = view.getRootView();
|
|
||||||
screenView.setDrawingCacheEnabled(true);
|
|
||||||
Bitmap drawingCache = screenView.getDrawingCache();
|
|
||||||
if (drawingCache != null) {
|
|
||||||
Bitmap bitmap = Bitmap.createBitmap(drawingCache);
|
|
||||||
screenView.setDrawingCacheEnabled(false);
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
*Copies the content to the clipboard
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public static void copy(String label,String text, Context context){
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
||||||
ClipData clip = ClipData.newPlainText(label, text);
|
|
||||||
clipboard.setPrimaryClip(clip);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method sets underlined string text to a TextView
|
|
||||||
*
|
|
||||||
* @param textView TextView associated with string resource
|
|
||||||
* @param stringResourceName string resource name
|
|
||||||
* @param context
|
|
||||||
*/
|
|
||||||
public static void setUnderlinedText(TextView textView, int stringResourceName, Context context) {
|
|
||||||
SpannableString content = new SpannableString(context.getString(stringResourceName));
|
|
||||||
content.setSpan(new UnderlineSpan(), 0, content.length(), 0);
|
|
||||||
textView.setText(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For now we are enabling the monuments only when the date lies between 1 Sept & 31 OCt
|
|
||||||
* @param date
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static boolean isMonumentsEnabled(final Date date) {
|
|
||||||
if (date.getMonth() == 8) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Util function to get the start date of wlm monument
|
|
||||||
* For this release we are hardcoding it to be 1st September
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getWLMStartDate() {
|
|
||||||
return "1 Sep";
|
|
||||||
}
|
|
||||||
|
|
||||||
/***
|
|
||||||
* Util function to get the end date of wlm monument
|
|
||||||
* For this release we are hardcoding it to be 31st October
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getWLMEndDate() {
|
|
||||||
return "30 Sep";
|
|
||||||
}
|
|
||||||
|
|
||||||
/***
|
|
||||||
* Function to get the current WLM year
|
|
||||||
* It increments at the start of September in line with the other WLM functions
|
|
||||||
* (No consideration of locales for now)
|
|
||||||
* @param calendar
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static int getWikiLovesMonumentsYear(Calendar calendar) {
|
|
||||||
int year = calendar.get(Calendar.YEAR);
|
|
||||||
if (calendar.get(Calendar.MONTH) < Calendar.SEPTEMBER) {
|
|
||||||
year -= 1;
|
|
||||||
}
|
|
||||||
return year;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public interface ViewHolder<T> {
|
|
||||||
void bindModel(Context context, T model);
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This adapter will be used to display fragments in a ViewPager
|
|
||||||
*/
|
|
||||||
public class ViewPagerAdapter extends FragmentPagerAdapter {
|
|
||||||
private List<Fragment> fragmentList = new ArrayList<>();
|
|
||||||
private List<String> fragmentTitleList = new ArrayList<>();
|
|
||||||
|
|
||||||
public ViewPagerAdapter(FragmentManager manager) {
|
|
||||||
super(manager);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method returns the fragment of the viewpager at a particular position
|
|
||||||
* @param position
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Fragment getItem(int position) {
|
|
||||||
return fragmentList.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method returns the total number of fragments in the viewpager.
|
|
||||||
* @return size
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return fragmentList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method sets the fragment and title list in the viewpager
|
|
||||||
* @param fragmentList List of all fragments to be displayed in the viewpager
|
|
||||||
* @param fragmentTitleList List of all titles of the fragments
|
|
||||||
*/
|
|
||||||
public void setTabData(List<Fragment> fragmentList, List<String> fragmentTitleList) {
|
|
||||||
this.fragmentList = fragmentList;
|
|
||||||
this.fragmentTitleList = fragmentTitleList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method returns the title of the page at a particular position
|
|
||||||
* @param position
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public CharSequence getPageTitle(int position) {
|
|
||||||
return fragmentTitleList.get(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
44
app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.kt
Normal file
44
app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.kt
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.FragmentPagerAdapter
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This adapter will be used to display fragments in a ViewPager
|
||||||
|
*/
|
||||||
|
class ViewPagerAdapter : FragmentPagerAdapter {
|
||||||
|
private val context: Context
|
||||||
|
private var fragmentList: List<Fragment> = emptyList()
|
||||||
|
private var fragmentTitleList: List<String> = emptyList()
|
||||||
|
|
||||||
|
constructor(context: Context, manager: FragmentManager) : super(manager) {
|
||||||
|
this.context = context
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context, manager: FragmentManager, behavior: Int) : super(manager, behavior) {
|
||||||
|
this.context = context
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItem(position: Int): Fragment = fragmentList[position]
|
||||||
|
|
||||||
|
override fun getPageTitle(position: Int): CharSequence = fragmentTitleList[position]
|
||||||
|
|
||||||
|
override fun getCount(): Int = fragmentList.size
|
||||||
|
|
||||||
|
fun setTabs(vararg titlesToFragments: Pair<Int, Fragment>) {
|
||||||
|
// Enforce that every title must come from strings.xml and all will consistently be uppercase
|
||||||
|
fragmentTitleList = titlesToFragments.map {
|
||||||
|
context.getString(it.first).uppercase(Locale.ROOT)
|
||||||
|
}
|
||||||
|
fragmentList = titlesToFragments.map { it.second }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Convenience method for Java callers, can be removed when everything is migrated
|
||||||
|
@JvmStatic
|
||||||
|
fun pairOf(first: Int, second: Fragment) = first to second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
import fr.free.nrw.commons.databinding.ActivityWelcomeBinding;
|
|
||||||
import fr.free.nrw.commons.databinding.PopupForCopyrightBinding;
|
|
||||||
import fr.free.nrw.commons.quiz.QuizActivity;
|
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
|
||||||
|
|
||||||
public class WelcomeActivity extends BaseActivity {
|
|
||||||
|
|
||||||
private ActivityWelcomeBinding binding;
|
|
||||||
private PopupForCopyrightBinding copyrightBinding;
|
|
||||||
|
|
||||||
private final WelcomePagerAdapter adapter = new WelcomePagerAdapter();
|
|
||||||
private boolean isQuiz;
|
|
||||||
private AlertDialog.Builder dialogBuilder;
|
|
||||||
private AlertDialog dialog;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialises exiting fields and dependencies
|
|
||||||
*
|
|
||||||
* @param savedInstanceState WelcomeActivity bundled data
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onCreate(final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
binding = ActivityWelcomeBinding.inflate(getLayoutInflater());
|
|
||||||
final View view = binding.getRoot();
|
|
||||||
setContentView(view);
|
|
||||||
|
|
||||||
if (getIntent() != null) {
|
|
||||||
final Bundle bundle = getIntent().getExtras();
|
|
||||||
if (bundle != null) {
|
|
||||||
isQuiz = bundle.getBoolean("isQuiz");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isQuiz = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable skip button if beta flavor
|
|
||||||
if (ConfigUtils.isBetaFlavour()) {
|
|
||||||
binding.finishTutorialButton.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
dialogBuilder = new AlertDialog.Builder(this);
|
|
||||||
copyrightBinding = PopupForCopyrightBinding.inflate(getLayoutInflater());
|
|
||||||
final View contactPopupView = copyrightBinding.getRoot();
|
|
||||||
dialogBuilder.setView(contactPopupView);
|
|
||||||
dialog = dialogBuilder.create();
|
|
||||||
dialog.show();
|
|
||||||
|
|
||||||
copyrightBinding.buttonOk.setOnClickListener(v -> dialog.dismiss());
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.welcomePager.setAdapter(adapter);
|
|
||||||
binding.welcomePagerIndicator.setViewPager(binding.welcomePager);
|
|
||||||
|
|
||||||
binding.finishTutorialButton.setOnClickListener(v -> finishTutorial());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* References WelcomePageAdapter to null before the activity is destroyed
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
if (isQuiz) {
|
|
||||||
final Intent i = new Intent(this, QuizActivity.class);
|
|
||||||
startActivity(i);
|
|
||||||
}
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a way to change current activity to WelcomeActivity
|
|
||||||
*
|
|
||||||
* @param context Activity context
|
|
||||||
*/
|
|
||||||
public static void startYourself(final Context context) {
|
|
||||||
final Intent welcomeIntent = new Intent(context, WelcomeActivity.class);
|
|
||||||
context.startActivity(welcomeIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override onBackPressed() to go to previous tutorial 'pages' if not on first page
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
if (binding.welcomePager.getCurrentItem() != 0) {
|
|
||||||
binding.welcomePager.setCurrentItem(binding.welcomePager.getCurrentItem() - 1, true);
|
|
||||||
} else {
|
|
||||||
if (defaultKvStore.getBoolean("firstrun", true)) {
|
|
||||||
finishAffinity();
|
|
||||||
} else {
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void finishTutorial() {
|
|
||||||
defaultKvStore.putBoolean("firstrun", false);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
80
app/src/main/java/fr/free/nrw/commons/WelcomeActivity.kt
Normal file
80
app/src/main/java/fr/free/nrw/commons/WelcomeActivity.kt
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import fr.free.nrw.commons.databinding.ActivityWelcomeBinding
|
||||||
|
import fr.free.nrw.commons.databinding.PopupForCopyrightBinding
|
||||||
|
import fr.free.nrw.commons.quiz.QuizActivity
|
||||||
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
|
||||||
|
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
||||||
|
|
||||||
|
class WelcomeActivity : BaseActivity() {
|
||||||
|
private var binding: ActivityWelcomeBinding? = null
|
||||||
|
private var isQuiz = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialises exiting fields and dependencies
|
||||||
|
*
|
||||||
|
* @param savedInstanceState WelcomeActivity bundled data
|
||||||
|
*/
|
||||||
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityWelcomeBinding.inflate(layoutInflater)
|
||||||
|
applyEdgeToEdgeAllInsets(binding!!.welcomePager.rootView)
|
||||||
|
setContentView(binding!!.root)
|
||||||
|
|
||||||
|
isQuiz = intent?.extras?.getBoolean("isQuiz", false) ?: false
|
||||||
|
|
||||||
|
// Enable skip button if beta flavor
|
||||||
|
if (isBetaFlavour) {
|
||||||
|
binding!!.finishTutorialButton.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val copyrightBinding = PopupForCopyrightBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setView(copyrightBinding.root)
|
||||||
|
.setCancelable(false)
|
||||||
|
.create()
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
copyrightBinding.buttonOk.setOnClickListener { v: View? -> dialog.dismiss() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val adapter = WelcomePagerAdapter()
|
||||||
|
binding!!.welcomePager.adapter = adapter
|
||||||
|
binding!!.welcomePagerIndicator.setViewPager(binding!!.welcomePager)
|
||||||
|
binding!!.finishTutorialButton.setOnClickListener { v: View? -> finishTutorial() }
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onDestroy() {
|
||||||
|
if (isQuiz) {
|
||||||
|
startActivity(Intent(this, QuizActivity::class.java))
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (binding!!.welcomePager.currentItem != 0) {
|
||||||
|
binding!!.welcomePager.setCurrentItem(binding!!.welcomePager.currentItem - 1, true)
|
||||||
|
} else {
|
||||||
|
if (defaultKvStore.getBoolean("firstrun", true)) {
|
||||||
|
finishAffinity()
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finishTutorial() {
|
||||||
|
defaultKvStore.putBoolean("firstrun", false)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.startWelcome() {
|
||||||
|
startActivity(Intent(this, WelcomeActivity::class.java))
|
||||||
|
}
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
package fr.free.nrw.commons;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.Html;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
|
||||||
|
|
||||||
public class WelcomePagerAdapter extends PagerAdapter {
|
|
||||||
private static final int[] PAGE_LAYOUTS = new int[]{
|
|
||||||
R.layout.welcome_wikipedia,
|
|
||||||
R.layout.welcome_do_upload,
|
|
||||||
R.layout.welcome_dont_upload,
|
|
||||||
R.layout.welcome_image_example,
|
|
||||||
R.layout.welcome_final
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets total number of layouts
|
|
||||||
* @return Number of layouts
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return PAGE_LAYOUTS.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares given view with provided object
|
|
||||||
* @param view Adapter view
|
|
||||||
* @param object Adapter object
|
|
||||||
* @return Equality between view and object
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean isViewFromObject(View view, Object object) {
|
|
||||||
return (view == object);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object instantiateItem(ViewGroup container, int position) {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(container.getContext());
|
|
||||||
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false);
|
|
||||||
|
|
||||||
// If final page
|
|
||||||
if (position == PAGE_LAYOUTS.length - 1) {
|
|
||||||
// Add link to more information
|
|
||||||
TextView moreInfo = layout.findViewById(R.id.welcomeInfo);
|
|
||||||
Utils.setUnderlinedText(moreInfo, R.string.welcome_help_button_text, container.getContext());
|
|
||||||
moreInfo.setOnClickListener(view -> Utils.handleWebUrl(
|
|
||||||
container.getContext(),
|
|
||||||
Uri.parse("https://commons.wikimedia.org/wiki/Help:Contents")
|
|
||||||
));
|
|
||||||
|
|
||||||
// Handle click of finishTutorialButton ("YES!" button) inside layout
|
|
||||||
layout.findViewById(R.id.finishTutorialButton)
|
|
||||||
.setOnClickListener(view -> ((WelcomeActivity) container.getContext()).finishTutorial());
|
|
||||||
}
|
|
||||||
|
|
||||||
container.addView(layout);
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a way to remove an item from container
|
|
||||||
* @param container Adapter view group container
|
|
||||||
* @param position Index of item
|
|
||||||
* @param obj Adapter object
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void destroyItem(ViewGroup container, int position, Object obj) {
|
|
||||||
container.removeView((View) obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
70
app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.kt
Normal file
70
app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.kt
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
package fr.free.nrw.commons
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.viewpager.widget.PagerAdapter
|
||||||
|
import fr.free.nrw.commons.utils.UnderlineUtils.setUnderlinedText
|
||||||
|
import fr.free.nrw.commons.utils.handleWebUrl
|
||||||
|
|
||||||
|
class WelcomePagerAdapter : PagerAdapter() {
|
||||||
|
/**
|
||||||
|
* Gets total number of layouts
|
||||||
|
* @return Number of layouts
|
||||||
|
*/
|
||||||
|
override fun getCount(): Int = PAGE_LAYOUTS.size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares given view with provided object
|
||||||
|
* @param view Adapter view
|
||||||
|
* @param obj Adapter object
|
||||||
|
* @return Equality between view and object
|
||||||
|
*/
|
||||||
|
override fun isViewFromObject(view: View, obj: Any): Boolean = (view === obj)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a way to remove an item from container
|
||||||
|
* @param container Adapter view group container
|
||||||
|
* @param position Index of item
|
||||||
|
* @param obj Adapter object
|
||||||
|
*/
|
||||||
|
override fun destroyItem(container: ViewGroup, position: Int, obj: Any) =
|
||||||
|
container.removeView(obj as View)
|
||||||
|
|
||||||
|
override fun instantiateItem(container: ViewGroup, position: Int): Any {
|
||||||
|
val inflater = LayoutInflater.from(container.context)
|
||||||
|
val layout = inflater.inflate(PAGE_LAYOUTS[position], container, false) as ViewGroup
|
||||||
|
|
||||||
|
// If final page
|
||||||
|
if (position == PAGE_LAYOUTS.size - 1) {
|
||||||
|
// Add link to more information
|
||||||
|
val moreInfo = layout.findViewById<TextView>(R.id.welcomeInfo)
|
||||||
|
setUnderlinedText(moreInfo, R.string.welcome_help_button_text)
|
||||||
|
moreInfo.setOnClickListener {
|
||||||
|
handleWebUrl(
|
||||||
|
container.context,
|
||||||
|
"https://commons.wikimedia.org/wiki/Help:Contents".toUri()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle click of finishTutorialButton ("YES!" button) inside layout
|
||||||
|
layout.findViewById<View>(R.id.finishTutorialButton)
|
||||||
|
.setOnClickListener { view: View? -> (container.context as WelcomeActivity).finishTutorial() }
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addView(layout)
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val PAGE_LAYOUTS = intArrayOf(
|
||||||
|
R.layout.welcome_wikipedia,
|
||||||
|
R.layout.welcome_do_upload,
|
||||||
|
R.layout.welcome_dont_upload,
|
||||||
|
R.layout.welcome_image_example,
|
||||||
|
R.layout.welcome_final
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package fr.free.nrw.commons.actions
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response of the Thanks API.
|
||||||
|
* Context:
|
||||||
|
* The Commons Android app lets you thank other contributors who have uploaded a great picture.
|
||||||
|
* See https://www.mediawiki.org/wiki/Extension:Thanks
|
||||||
|
*/
|
||||||
|
class MwThankPostResponse : MwResponse() {
|
||||||
|
var result: Result? = null
|
||||||
|
|
||||||
|
inner class Result {
|
||||||
|
var success: Int? = null
|
||||||
|
var recipient: String? = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package fr.free.nrw.commons.actions
|
package fr.free.nrw.commons.actions
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||||
|
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import org.wikipedia.csrf.CsrfTokenClient
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class acts as a Client to facilitate wiki page editing
|
* This class acts as a Client to facilitate wiki page editing
|
||||||
|
|
@ -13,9 +14,8 @@ import org.wikipedia.csrf.CsrfTokenClient
|
||||||
*/
|
*/
|
||||||
class PageEditClient(
|
class PageEditClient(
|
||||||
private val csrfTokenClient: CsrfTokenClient,
|
private val csrfTokenClient: CsrfTokenClient,
|
||||||
private val pageEditInterface: PageEditInterface
|
private val pageEditInterface: PageEditInterface,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the content of a wiki page
|
* Replace the content of a wiki page
|
||||||
* @param pageTitle Title of the page to edit
|
* @param pageTitle Title of the page to edit
|
||||||
|
|
@ -23,14 +23,60 @@ class PageEditClient(
|
||||||
* @param summary Edit summary
|
* @param summary Edit summary
|
||||||
* @return whether the edit was successful
|
* @return whether the edit was successful
|
||||||
*/
|
*/
|
||||||
fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> {
|
fun edit(
|
||||||
return try {
|
pageTitle: String,
|
||||||
pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.tokenBlocking)
|
text: String,
|
||||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
summary: String,
|
||||||
|
): Observable<Boolean> =
|
||||||
|
try {
|
||||||
|
pageEditInterface
|
||||||
|
.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
|
||||||
|
.map { editResponse ->
|
||||||
|
editResponse.edit()!!.editSucceeded()
|
||||||
|
}
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
Observable.just(false)
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
throw throwable
|
||||||
|
} else {
|
||||||
|
Observable.just(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new page with the given title, text, and summary.
|
||||||
|
*
|
||||||
|
* @param pageTitle The title of the page to be created.
|
||||||
|
* @param text The content of the page in wikitext format.
|
||||||
|
* @param summary The edit summary for the page creation.
|
||||||
|
* @return An observable that emits true if the page creation succeeded, false otherwise.
|
||||||
|
* @throws InvalidLoginTokenException If an invalid login token is encountered during the process.
|
||||||
|
*/
|
||||||
|
fun postCreate(
|
||||||
|
pageTitle: String,
|
||||||
|
text: String,
|
||||||
|
summary: String,
|
||||||
|
): Observable<Boolean> =
|
||||||
|
try {
|
||||||
|
pageEditInterface
|
||||||
|
.postCreate(
|
||||||
|
pageTitle,
|
||||||
|
summary,
|
||||||
|
text,
|
||||||
|
"text/x-wiki",
|
||||||
|
"wikitext",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
csrfTokenClient.getTokenBlocking(),
|
||||||
|
).map { editResponse ->
|
||||||
|
editResponse.edit()!!.editSucceeded()
|
||||||
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
throw throwable
|
||||||
|
} else {
|
||||||
|
Observable.just(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Append text to the end of a wiki page
|
* Append text to the end of a wiki page
|
||||||
|
|
@ -39,14 +85,22 @@ class PageEditClient(
|
||||||
* @param summary Edit summary
|
* @param summary Edit summary
|
||||||
* @return whether the edit was successful
|
* @return whether the edit was successful
|
||||||
*/
|
*/
|
||||||
fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> {
|
fun appendEdit(
|
||||||
return try {
|
pageTitle: String,
|
||||||
pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.tokenBlocking)
|
appendText: String,
|
||||||
|
summary: String,
|
||||||
|
): Observable<Boolean> =
|
||||||
|
try {
|
||||||
|
pageEditInterface
|
||||||
|
.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
|
||||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
Observable.just(false)
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
throw throwable
|
||||||
|
} else {
|
||||||
|
Observable.just(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepend text to the beginning of a wiki page
|
* Prepend text to the beginning of a wiki page
|
||||||
|
|
@ -55,14 +109,48 @@ class PageEditClient(
|
||||||
* @param summary Edit summary
|
* @param summary Edit summary
|
||||||
* @return whether the edit was successful
|
* @return whether the edit was successful
|
||||||
*/
|
*/
|
||||||
fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> {
|
fun prependEdit(
|
||||||
return try {
|
pageTitle: String,
|
||||||
pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.tokenBlocking)
|
prependText: String,
|
||||||
|
summary: String,
|
||||||
|
): Observable<Boolean> =
|
||||||
|
try {
|
||||||
|
pageEditInterface
|
||||||
|
.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
|
||||||
|
.map { editResponse -> editResponse.edit()?.editSucceeded() ?: false }
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
throw throwable
|
||||||
|
} else {
|
||||||
|
Observable.just(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a new section to the wiki page
|
||||||
|
* @param pageTitle Title of the page to edit
|
||||||
|
* @param sectionTitle Title of the new section that needs to be created
|
||||||
|
* @param sectionText The page content that is to be added to the section
|
||||||
|
* @param summary Edit summary
|
||||||
|
* @return whether the edit was successful
|
||||||
|
*/
|
||||||
|
fun createNewSection(
|
||||||
|
pageTitle: String,
|
||||||
|
sectionTitle: String,
|
||||||
|
sectionText: String,
|
||||||
|
summary: String,
|
||||||
|
): Observable<Boolean> =
|
||||||
|
try {
|
||||||
|
pageEditInterface
|
||||||
|
.postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking())
|
||||||
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
.map { editResponse -> editResponse.edit()!!.editSucceeded() }
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
Observable.just(false)
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
throw throwable
|
||||||
|
} else {
|
||||||
|
Observable.just(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set new labels to Wikibase server of commons
|
* Set new labels to Wikibase server of commons
|
||||||
|
|
@ -72,24 +160,42 @@ class PageEditClient(
|
||||||
* @param value label
|
* @param value label
|
||||||
* @return 1 when the edit was successful
|
* @return 1 when the edit was successful
|
||||||
*/
|
*/
|
||||||
fun setCaptions(summary: String, title: String,
|
fun setCaptions(
|
||||||
language: String, value: String) : Observable<Int>{
|
summary: String,
|
||||||
return try {
|
title: String,
|
||||||
pageEditInterface.postCaptions(summary, title, language,
|
language: String,
|
||||||
value, csrfTokenClient.tokenBlocking).map { it.success }
|
value: String,
|
||||||
|
): Observable<Int> =
|
||||||
|
try {
|
||||||
|
pageEditInterface
|
||||||
|
.postCaptions(
|
||||||
|
summary,
|
||||||
|
title,
|
||||||
|
language,
|
||||||
|
value,
|
||||||
|
csrfTokenClient.getTokenBlocking(),
|
||||||
|
).map { it.success }
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
Observable.just(0)
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
throw throwable
|
||||||
|
} else {
|
||||||
|
Observable.just(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get whole WikiText of required file
|
* Get whole WikiText of required file
|
||||||
* @param title : Name of the file
|
* @param title : Name of the file
|
||||||
* @return Observable<MwQueryResult>
|
* @return Observable<MwQueryResult>
|
||||||
*/
|
*/
|
||||||
fun getCurrentWikiText(title: String): Single<String?> {
|
fun getCurrentWikiText(title: String): Single<String?> =
|
||||||
return pageEditInterface.getWikiText(title).map {
|
pageEditInterface.getWikiText(title).map {
|
||||||
it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content()
|
it
|
||||||
|
.query()
|
||||||
|
?.pages()
|
||||||
|
?.get(0)
|
||||||
|
?.revisions()
|
||||||
|
?.get(0)
|
||||||
|
?.content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
package fr.free.nrw.commons.actions
|
package fr.free.nrw.commons.actions
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||||
|
import fr.free.nrw.commons.wikidata.model.Entities
|
||||||
|
import fr.free.nrw.commons.wikidata.model.edit.Edit
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import org.wikipedia.dataclient.Service
|
import retrofit2.http.Field
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
import retrofit2.http.FormUrlEncoded
|
||||||
import org.wikipedia.edit.Edit
|
import retrofit2.http.GET
|
||||||
import org.wikipedia.wikidata.Entities
|
import retrofit2.http.Headers
|
||||||
import retrofit2.http.*
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface facilitates wiki commons page editing services to the Networking module
|
* This interface facilitates wiki commons page editing services to the Networking module
|
||||||
|
|
@ -27,13 +32,40 @@ interface PageEditInterface {
|
||||||
*/
|
*/
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@Headers("Cache-Control: no-cache")
|
@Headers("Cache-Control: no-cache")
|
||||||
@POST(Service.MW_API_PREFIX + "action=edit")
|
@POST(MW_API_PREFIX + "action=edit")
|
||||||
fun postEdit(
|
fun postEdit(
|
||||||
@Field("title") title: String,
|
@Field("title") title: String,
|
||||||
@Field("summary") summary: String,
|
@Field("summary") summary: String,
|
||||||
@Field("text") text: String,
|
@Field("text") text: String,
|
||||||
// NOTE: This csrf shold always be sent as the last field of form data
|
// NOTE: This csrf shold always be sent as the last field of form data
|
||||||
@Field("token") token: String
|
@Field("token") token: String,
|
||||||
|
): Observable<Edit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method creates or edits a page for nearby items.
|
||||||
|
*
|
||||||
|
* @param title Title of the page to edit. Cannot be used together with pageid.
|
||||||
|
* @param summary Edit summary. Also used as the section title when section=new and sectiontitle is not set.
|
||||||
|
* @param text Text of the page.
|
||||||
|
* @param contentformat Format of the content (e.g., "text/x-wiki").
|
||||||
|
* @param contentmodel Model of the content (e.g., "wikitext").
|
||||||
|
* @param minor Whether the edit is a minor edit.
|
||||||
|
* @param recreate Whether to recreate the page if it does not exist.
|
||||||
|
* @param token A "csrf" token. This should always be sent as the last field of form data.
|
||||||
|
*/
|
||||||
|
@FormUrlEncoded
|
||||||
|
@Headers("Cache-Control: no-cache")
|
||||||
|
@POST(MW_API_PREFIX + "action=edit")
|
||||||
|
fun postCreate(
|
||||||
|
@Field("title") title: String,
|
||||||
|
@Field("summary") summary: String,
|
||||||
|
@Field("text") text: String,
|
||||||
|
@Field("contentformat") contentformat: String,
|
||||||
|
@Field("contentmodel") contentmodel: String,
|
||||||
|
@Field("minor") minor: Boolean,
|
||||||
|
@Field("recreate") recreate: Boolean,
|
||||||
|
// NOTE: This csrf shold always be sent as the last field of form data
|
||||||
|
@Field("token") token: String,
|
||||||
): Observable<Edit>
|
): Observable<Edit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -47,12 +79,12 @@ interface PageEditInterface {
|
||||||
*/
|
*/
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@Headers("Cache-Control: no-cache")
|
@Headers("Cache-Control: no-cache")
|
||||||
@POST(Service.MW_API_PREFIX + "action=edit")
|
@POST(MW_API_PREFIX + "action=edit")
|
||||||
fun postAppendEdit(
|
fun postAppendEdit(
|
||||||
@Field("title") title: String,
|
@Field("title") title: String,
|
||||||
@Field("summary") summary: String,
|
@Field("summary") summary: String,
|
||||||
@Field("appendtext") appendText: String,
|
@Field("appendtext") appendText: String,
|
||||||
@Field("token") token: String
|
@Field("token") token: String,
|
||||||
): Observable<Edit>
|
): Observable<Edit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -66,36 +98,44 @@ interface PageEditInterface {
|
||||||
*/
|
*/
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@Headers("Cache-Control: no-cache")
|
@Headers("Cache-Control: no-cache")
|
||||||
@POST(Service.MW_API_PREFIX + "action=edit")
|
@POST(MW_API_PREFIX + "action=edit")
|
||||||
fun postPrependEdit(
|
fun postPrependEdit(
|
||||||
@Field("title") title: String,
|
@Field("title") title: String,
|
||||||
@Field("summary") summary: String,
|
@Field("summary") summary: String,
|
||||||
@Field("prependtext") prependText: String,
|
@Field("prependtext") prependText: String,
|
||||||
@Field("token") token: String
|
@Field("token") token: String,
|
||||||
): Observable<Edit>
|
): Observable<Edit>
|
||||||
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@Headers("Cache-Control: no-cache")
|
@Headers("Cache-Control: no-cache")
|
||||||
@POST(Service.MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2")
|
@POST(MW_API_PREFIX + "action=edit§ion=new")
|
||||||
|
fun postNewSection(
|
||||||
|
@Field("title") title: String,
|
||||||
|
@Field("summary") summary: String,
|
||||||
|
@Field("sectiontitle") sectionTitle: String,
|
||||||
|
@Field("text") sectionText: String,
|
||||||
|
@Field("token") token: String,
|
||||||
|
): Observable<Edit>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@Headers("Cache-Control: no-cache")
|
||||||
|
@POST(MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2")
|
||||||
fun postCaptions(
|
fun postCaptions(
|
||||||
@Field("summary") summary: String,
|
@Field("summary") summary: String,
|
||||||
@Field("title") title: String,
|
@Field("title") title: String,
|
||||||
@Field("language") language: String,
|
@Field("language") language: String,
|
||||||
@Field("value") value: String,
|
@Field("value") value: String,
|
||||||
@Field("token") token: String
|
@Field("token") token: String,
|
||||||
): Observable<Entities>
|
): Observable<Entities>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get wiki text for provided file names
|
* Gets the wiki text for the provided file name.
|
||||||
* @param titles : Name of the file
|
*
|
||||||
* @return Single<MwQueryResult>
|
* @param title The title (name) of the file to fetch wiki text for.
|
||||||
|
* @return A Single emitting the wiki query response.
|
||||||
*/
|
*/
|
||||||
@GET(
|
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
|
||||||
Service.MW_API_PREFIX +
|
|
||||||
"action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles="
|
|
||||||
)
|
|
||||||
fun getWikiText(
|
fun getWikiText(
|
||||||
@Query("titles") title: String
|
@Query("titles") title: String,
|
||||||
): Single<MwQueryResponse?>
|
): Single<MwQueryResponse?>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package fr.free.nrw.commons.actions
|
package fr.free.nrw.commons.actions
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication
|
import fr.free.nrw.commons.CommonsApplication
|
||||||
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
|
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||||
|
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||||
|
import fr.free.nrw.commons.di.NetworkingModule.Companion.NAMED_COMMONS_CSRF
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import org.wikipedia.csrf.CsrfTokenClient
|
|
||||||
import org.wikipedia.dataclient.Service
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwPostResponse
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
@ -15,22 +14,33 @@ import javax.inject.Singleton
|
||||||
* Thanks are used by a user to show gratitude to another user for their contributions
|
* Thanks are used by a user to show gratitude to another user for their contributions
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class ThanksClient @Inject constructor(
|
class ThanksClient
|
||||||
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
|
@Inject
|
||||||
@param:Named("commons-service") private val service: Service
|
constructor(
|
||||||
) {
|
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
|
||||||
/**
|
private val service: ThanksInterface,
|
||||||
* Thanks a user for a particular revision
|
) {
|
||||||
* @param revisionId The revision ID the user would like to thank someone for
|
/**
|
||||||
* @return if thanks was successfully sent to intended recipient
|
* Thanks a user for a particular revision
|
||||||
*/
|
* @param revisionId The revision ID the user would like to thank someone for
|
||||||
fun thank(revisionId: Long): Observable<Boolean> {
|
* @return if thanks was successfully sent to intended recipient
|
||||||
return try {
|
*/
|
||||||
service.thank(revisionId.toString(), null, csrfTokenClient.tokenBlocking, CommonsApplication.getInstance().userAgent)
|
fun thank(revisionId: Long): Observable<Boolean> =
|
||||||
.map { mwThankPostResponse -> mwThankPostResponse.result.success== 1 }
|
try {
|
||||||
} catch (throwable: Throwable) {
|
service
|
||||||
Observable.just(false)
|
.thank(
|
||||||
}
|
revisionId.toString(), // Rev
|
||||||
|
null, // Log
|
||||||
|
csrfTokenClient.getTokenBlocking(), // Token
|
||||||
|
CommonsApplication.instance.userAgent, // Source
|
||||||
|
).map { mwThankPostResponse ->
|
||||||
|
mwThankPostResponse.result?.success == 1
|
||||||
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
if (throwable is InvalidLoginTokenException) {
|
||||||
|
Observable.error(throwable)
|
||||||
|
} else {
|
||||||
|
Observable.just(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package fr.free.nrw.commons.actions
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import retrofit2.http.Field
|
||||||
|
import retrofit2.http.FormUrlEncoded
|
||||||
|
import retrofit2.http.POST
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thanks API.
|
||||||
|
* Context:
|
||||||
|
* The Commons Android app lets you thank another contributor who has uploaded a great picture.
|
||||||
|
* See https://www.mediawiki.org/wiki/Extension:Thanks
|
||||||
|
*/
|
||||||
|
interface ThanksInterface {
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MW_API_PREFIX + "action=thank")
|
||||||
|
fun thank(
|
||||||
|
@Field("rev") rev: String?,
|
||||||
|
@Field("log") log: String?,
|
||||||
|
@Field("token") token: String,
|
||||||
|
@Field("source") source: String?,
|
||||||
|
): Observable<MwThankPostResponse?>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
package fr.free.nrw.commons.activity
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.webkit.ConsoleMessage
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import fr.free.nrw.commons.CommonsApplication
|
||||||
|
import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection
|
||||||
|
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SingleWebViewActivity is a reusable activity webView based on a given url(initial url) and
|
||||||
|
* closes itself when a specified success URL is reached to success url.
|
||||||
|
*/
|
||||||
|
class SingleWebViewActivity : ComponentActivity() {
|
||||||
|
@Inject
|
||||||
|
lateinit var cookieJar: CommonsCookieJar
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val url = intent.getStringExtra(VANISH_ACCOUNT_URL)
|
||||||
|
val successUrl = intent.getStringExtra(VANISH_ACCOUNT_SUCCESS_URL)
|
||||||
|
if (url == null || successUrl == null) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(applicationContext)
|
||||||
|
.commonsApplicationComponent
|
||||||
|
.inject(this)
|
||||||
|
setCookies(url)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
modifier = Modifier,
|
||||||
|
title = { Text(getString(R.string.vanish_account)) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
// Close the WebView Activity if the user taps the back button
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
// TODO("Add contentDescription)
|
||||||
|
contentDescription = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
WebViewComponent(
|
||||||
|
url = url,
|
||||||
|
successUrl = successUrl,
|
||||||
|
onSuccess = {
|
||||||
|
//Redirect the user to login screen like we do when the user logout's
|
||||||
|
val app = applicationContext as CommonsApplication
|
||||||
|
app.clearApplicationData(
|
||||||
|
applicationContext,
|
||||||
|
ActivityLogoutListener(activity = this, ctx = applicationContext)
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(it)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param url The initial URL which we are loading in the WebView.
|
||||||
|
* @param successUrl The URL that, when reached, triggers the `onSuccess` callback.
|
||||||
|
* @param onSuccess A callback that is invoked when the current url of webView is successUrl.
|
||||||
|
* This is used when we want to close when the webView once a success url is hit.
|
||||||
|
* @param modifier An optional [Modifier] to customize the layout or appearance of the WebView.
|
||||||
|
*/
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
@Composable
|
||||||
|
private fun WebViewComponent(
|
||||||
|
url: String,
|
||||||
|
successUrl: String,
|
||||||
|
onSuccess: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val webView = remember { mutableStateOf<WebView?>(null) }
|
||||||
|
AndroidView(
|
||||||
|
modifier = modifier,
|
||||||
|
factory = {
|
||||||
|
WebView(it).apply {
|
||||||
|
settings.apply {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
domStorageEnabled = true
|
||||||
|
javaScriptCanOpenWindowsAutomatically = true
|
||||||
|
|
||||||
|
}
|
||||||
|
webViewClient = object : WebViewClient() {
|
||||||
|
override fun shouldOverrideUrlLoading(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
request?.url?.let { url ->
|
||||||
|
Timber.d("URL Loading: $url")
|
||||||
|
if (url.toString() == successUrl) {
|
||||||
|
Timber.d("Success URL detected. Closing WebView.")
|
||||||
|
onSuccess() // Close the activity
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
setCookies(url.orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
webChromeClient = object : WebChromeClient() {
|
||||||
|
override fun onConsoleMessage(message: ConsoleMessage): Boolean {
|
||||||
|
Timber.d("%s%s",
|
||||||
|
"Console: ${message.message()} -- From line ",
|
||||||
|
"${message.lineNumber()} of ${message.sourceId()}")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUrl(url)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = {
|
||||||
|
webView.value = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets cookies for the given URL using the cookies stored in the `CommonsCookieJar`.
|
||||||
|
*
|
||||||
|
* @param url The URL for which cookies need to be set.
|
||||||
|
*/
|
||||||
|
private fun setCookies(url: String) {
|
||||||
|
CookieManager.getInstance().let {
|
||||||
|
val cookies = cookieJar.loadForRequest(url.toHttpUrl())
|
||||||
|
for (cookie in cookies) {
|
||||||
|
it.setCookie(url, cookie.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val VANISH_ACCOUNT_URL = "VanishAccountUrl"
|
||||||
|
private const val VANISH_ACCOUNT_SUCCESS_URL = "vanishAccountSuccessUrl"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the WebViewActivity with the specified URL and success URL.
|
||||||
|
* @param context The context from which the activity is launched.
|
||||||
|
* @param url The initial URL to load in the WebView.
|
||||||
|
* @param successUrl The URL that triggers the WebView to close when matched.
|
||||||
|
*/
|
||||||
|
fun showWebView(
|
||||||
|
context: Context,
|
||||||
|
url: String,
|
||||||
|
successUrl: String
|
||||||
|
) {
|
||||||
|
val intent = Intent(
|
||||||
|
context,
|
||||||
|
SingleWebViewActivity::class.java
|
||||||
|
).apply {
|
||||||
|
putExtra(VANISH_ACCOUNT_URL, url)
|
||||||
|
putExtra(VANISH_ACCOUNT_SUCCESS_URL, successUrl)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class AccountUtil {
|
|
||||||
|
|
||||||
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
|
|
||||||
|
|
||||||
public AccountUtil() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Account|null
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public static Account account(Context context) {
|
|
||||||
try {
|
|
||||||
Account[] accounts = accountManager(context).getAccountsByType(BuildConfig.ACCOUNT_TYPE);
|
|
||||||
if (accounts.length > 0) {
|
|
||||||
return accounts[0];
|
|
||||||
}
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
Timber.e(e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static String getUserName(Context context) {
|
|
||||||
Account account = account(context);
|
|
||||||
return account == null ? null : account.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AccountManager accountManager(Context context) {
|
|
||||||
return AccountManager.get(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt
Normal file
24
app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
package fr.free.nrw.commons.auth
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
const val AUTH_TOKEN_TYPE: String = "CommonsAndroid"
|
||||||
|
|
||||||
|
fun getUserName(context: Context): String? {
|
||||||
|
return account(context)?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun account(context: Context): Account? = try {
|
||||||
|
val accountManager = AccountManager.get(context)
|
||||||
|
val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE)
|
||||||
|
if (accounts.isNotEmpty()) accounts[0] else null
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
@ -1,500 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth;
|
|
||||||
|
|
||||||
import android.accounts.AccountAuthenticatorActivity;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorRes;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
|
||||||
import androidx.core.app.NavUtils;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.utils.ActivityUtils;
|
|
||||||
import java.util.Locale;
|
|
||||||
import org.wikipedia.AppAdapter;
|
|
||||||
import org.wikipedia.dataclient.ServiceFactory;
|
|
||||||
import org.wikipedia.dataclient.WikiSite;
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
|
||||||
import org.wikipedia.login.LoginClient;
|
|
||||||
import org.wikipedia.login.LoginClient.LoginCallback;
|
|
||||||
import org.wikipedia.login.LoginResult;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import butterknife.OnClick;
|
|
||||||
import butterknife.OnEditorAction;
|
|
||||||
import butterknife.OnFocusChange;
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.Utils;
|
|
||||||
import fr.free.nrw.commons.WelcomeActivity;
|
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
|
||||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
|
||||||
import fr.free.nrw.commons.utils.SystemThemeUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
|
||||||
import retrofit2.Call;
|
|
||||||
import retrofit2.Callback;
|
|
||||||
import retrofit2.Response;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static android.view.KeyEvent.KEYCODE_ENTER;
|
|
||||||
import static android.view.View.VISIBLE;
|
|
||||||
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
|
|
||||||
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_WIKI_SITE;
|
|
||||||
|
|
||||||
public class LoginActivity extends AccountAuthenticatorActivity {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
SessionManager sessionManager;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@Named(NAMED_COMMONS_WIKI_SITE)
|
|
||||||
WikiSite commonsWikiSite;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@Named("default_preferences")
|
|
||||||
JsonKvStore applicationKvStore;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
LoginClient loginClient;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
SystemThemeUtils systemThemeUtils;
|
|
||||||
|
|
||||||
@BindView(R.id.login_button)
|
|
||||||
Button loginButton;
|
|
||||||
|
|
||||||
@BindView(R.id.login_username)
|
|
||||||
EditText usernameEdit;
|
|
||||||
|
|
||||||
@BindView(R.id.login_password)
|
|
||||||
EditText passwordEdit;
|
|
||||||
|
|
||||||
@BindView(R.id.login_two_factor)
|
|
||||||
EditText twoFactorEdit;
|
|
||||||
|
|
||||||
@BindView(R.id.error_message_container)
|
|
||||||
ViewGroup errorMessageContainer;
|
|
||||||
|
|
||||||
@BindView(R.id.error_message)
|
|
||||||
TextView errorMessage;
|
|
||||||
|
|
||||||
@BindView(R.id.login_credentials)
|
|
||||||
TextView loginCredentials;
|
|
||||||
|
|
||||||
@BindView(R.id.two_factor_container)
|
|
||||||
TextInputLayout twoFactorContainer;
|
|
||||||
|
|
||||||
ProgressDialog progressDialog;
|
|
||||||
private AppCompatDelegate delegate;
|
|
||||||
private LoginTextWatcher textWatcher = new LoginTextWatcher();
|
|
||||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
|
||||||
private Call<MwQueryResponse> loginToken;
|
|
||||||
final String saveProgressDailog="ProgressDailog_state";
|
|
||||||
final String saveErrorMessage ="errorMessage";
|
|
||||||
final String saveUsername="username";
|
|
||||||
final String savePassword="password";
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
ApplicationlessInjection
|
|
||||||
.getInstance(this.getApplicationContext())
|
|
||||||
.getCommonsApplicationComponent()
|
|
||||||
.inject(this);
|
|
||||||
|
|
||||||
boolean isDarkTheme = systemThemeUtils.isDeviceInNightMode();
|
|
||||||
setTheme(isDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme);
|
|
||||||
getDelegate().installViewFactory();
|
|
||||||
getDelegate().onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_login);
|
|
||||||
|
|
||||||
ButterKnife.bind(this);
|
|
||||||
|
|
||||||
usernameEdit.addTextChangedListener(textWatcher);
|
|
||||||
passwordEdit.addTextChangedListener(textWatcher);
|
|
||||||
twoFactorEdit.addTextChangedListener(textWatcher);
|
|
||||||
|
|
||||||
if (ConfigUtils.isBetaFlavour()) {
|
|
||||||
loginCredentials.setText(getString(R.string.login_credential));
|
|
||||||
} else {
|
|
||||||
loginCredentials.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Hides the keyboard if the user's focus is not on the password (hasFocus is false).
|
|
||||||
* @param view The keyboard
|
|
||||||
* @param hasFocus Set to true if the keyboard has focus
|
|
||||||
*/
|
|
||||||
@OnFocusChange(R.id.login_password)
|
|
||||||
void onPasswordFocusChanged(View view, boolean hasFocus) {
|
|
||||||
if (!hasFocus) {
|
|
||||||
ViewUtil.hideKeyboard(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnEditorAction(R.id.login_password)
|
|
||||||
boolean onEditorAction(int actionId, KeyEvent keyEvent) {
|
|
||||||
if (loginButton.isEnabled()) {
|
|
||||||
if (actionId == IME_ACTION_DONE) {
|
|
||||||
performLogin();
|
|
||||||
return true;
|
|
||||||
} else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) {
|
|
||||||
performLogin();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@OnClick(R.id.skip_login)
|
|
||||||
void skipLogin() {
|
|
||||||
new AlertDialog.Builder(this).setTitle(R.string.skip_login_title)
|
|
||||||
.setMessage(R.string.skip_login_message)
|
|
||||||
.setCancelable(false)
|
|
||||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
|
||||||
dialog.cancel();
|
|
||||||
performSkipLogin();
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.forgot_password)
|
|
||||||
void forgotPassword() {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.about_privacy_policy)
|
|
||||||
void onPrivacyPolicyClicked() {
|
|
||||||
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL));
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.sign_up_button)
|
|
||||||
void signUp() {
|
|
||||||
Intent intent = new Intent(this, SignupActivity.class);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostCreate(Bundle savedInstanceState) {
|
|
||||||
super.onPostCreate(savedInstanceState);
|
|
||||||
getDelegate().onPostCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
if (sessionManager.getCurrentAccount() != null
|
|
||||||
&& sessionManager.isUserLoggedIn()) {
|
|
||||||
applicationKvStore.putBoolean("login_skipped", false);
|
|
||||||
startMainActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (applicationKvStore.getBoolean("login_skipped", false)) {
|
|
||||||
performSkipLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
compositeDisposable.clear();
|
|
||||||
try {
|
|
||||||
// To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method
|
|
||||||
if (progressDialog != null && progressDialog.isShowing()) {
|
|
||||||
progressDialog.dismiss();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
usernameEdit.removeTextChangedListener(textWatcher);
|
|
||||||
passwordEdit.removeTextChangedListener(textWatcher);
|
|
||||||
twoFactorEdit.removeTextChangedListener(textWatcher);
|
|
||||||
delegate.onDestroy();
|
|
||||||
if(null!=loginClient) {
|
|
||||||
loginClient.cancel();
|
|
||||||
}
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.login_button)
|
|
||||||
public void performLogin() {
|
|
||||||
Timber.d("Login to start!");
|
|
||||||
final String username = usernameEdit.getText().toString();
|
|
||||||
final String rawUsername = usernameEdit.getText().toString().trim();
|
|
||||||
final String password = passwordEdit.getText().toString();
|
|
||||||
String twoFactorCode = twoFactorEdit.getText().toString();
|
|
||||||
|
|
||||||
showLoggingProgressBar();
|
|
||||||
doLogin(username, password, twoFactorCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doLogin(String username, String password, String twoFactorCode) {
|
|
||||||
progressDialog.show();
|
|
||||||
loginToken = ServiceFactory.get(commonsWikiSite).getLoginToken();
|
|
||||||
loginToken.enqueue(
|
|
||||||
new Callback<MwQueryResponse>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(Call<MwQueryResponse> call,
|
|
||||||
Response<MwQueryResponse> response) {
|
|
||||||
loginClient.login(commonsWikiSite, username, password, null, twoFactorCode,
|
|
||||||
response.body().query().loginToken(), Locale.getDefault().getLanguage(), new LoginCallback() {
|
|
||||||
@Override
|
|
||||||
public void success(@NonNull LoginResult result) {
|
|
||||||
Timber.d("Login Success");
|
|
||||||
onLoginSuccess(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void twoFactorPrompt(@NonNull Throwable caught,
|
|
||||||
@Nullable String token) {
|
|
||||||
Timber.d("Requesting 2FA prompt");
|
|
||||||
hideProgress();
|
|
||||||
askUserForTwoFactorAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void passwordResetPrompt(@Nullable String token) {
|
|
||||||
Timber.d("Showing password reset prompt");
|
|
||||||
hideProgress();
|
|
||||||
showPasswordResetPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void error(@NonNull Throwable caught) {
|
|
||||||
Timber.e(caught);
|
|
||||||
hideProgress();
|
|
||||||
showMessageAndCancelDialog(caught.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(Call<MwQueryResponse> call, Throwable t) {
|
|
||||||
Timber.e(t);
|
|
||||||
showMessageAndCancelDialog(t.getLocalizedMessage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hideProgress() {
|
|
||||||
progressDialog.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showPasswordResetPrompt() {
|
|
||||||
showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is called when user skips the login.
|
|
||||||
* It redirects the user to Explore Activity.
|
|
||||||
*/
|
|
||||||
private void performSkipLogin() {
|
|
||||||
applicationKvStore.putBoolean("login_skipped", true);
|
|
||||||
MainActivity.startYourself(this);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showLoggingProgressBar() {
|
|
||||||
progressDialog = new ProgressDialog(this);
|
|
||||||
progressDialog.setIndeterminate(true);
|
|
||||||
progressDialog.setTitle(getString(R.string.logging_in_title));
|
|
||||||
progressDialog.setMessage(getString(R.string.logging_in_message));
|
|
||||||
progressDialog.setCanceledOnTouchOutside(false);
|
|
||||||
progressDialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onLoginSuccess(LoginResult loginResult) {
|
|
||||||
if (!progressDialog.isShowing()) {
|
|
||||||
// no longer attached to activity!
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
compositeDisposable.clear();
|
|
||||||
sessionManager.setUserLoggedIn(true);
|
|
||||||
AppAdapter.get().updateAccount(loginResult);
|
|
||||||
progressDialog.dismiss();
|
|
||||||
showSuccessAndDismissDialog();
|
|
||||||
startMainActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
delegate.onStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
delegate.onStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostResume() {
|
|
||||||
super.onPostResume();
|
|
||||||
getDelegate().onPostResume();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setContentView(View view, ViewGroup.LayoutParams params) {
|
|
||||||
getDelegate().setContentView(view, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case android.R.id.home:
|
|
||||||
NavUtils.navigateUpFromSameTask(this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public MenuInflater getMenuInflater() {
|
|
||||||
return getDelegate().getMenuInflater();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void askUserForTwoFactorAuth() {
|
|
||||||
progressDialog.dismiss();
|
|
||||||
twoFactorContainer.setVisibility(VISIBLE);
|
|
||||||
twoFactorEdit.setVisibility(VISIBLE);
|
|
||||||
twoFactorEdit.requestFocus();
|
|
||||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
|
|
||||||
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showMessageAndCancelDialog(@StringRes int resId) {
|
|
||||||
showMessage(resId, R.color.secondaryDarkColor);
|
|
||||||
if (progressDialog != null) {
|
|
||||||
progressDialog.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showMessageAndCancelDialog(String error) {
|
|
||||||
showMessage(error, R.color.secondaryDarkColor);
|
|
||||||
if (progressDialog != null) {
|
|
||||||
progressDialog.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showSuccessAndDismissDialog() {
|
|
||||||
showMessage(R.string.login_success, R.color.primaryDarkColor);
|
|
||||||
progressDialog.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startMainActivity() {
|
|
||||||
ActivityUtils.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
|
|
||||||
errorMessage.setText(getString(resId));
|
|
||||||
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
|
||||||
errorMessageContainer.setVisibility(VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showMessage(String message, @ColorRes int colorResId) {
|
|
||||||
errorMessage.setText(message);
|
|
||||||
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
|
|
||||||
errorMessageContainer.setVisibility(VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private AppCompatDelegate getDelegate() {
|
|
||||||
if (delegate == null) {
|
|
||||||
delegate = AppCompatDelegate.create(this, null);
|
|
||||||
}
|
|
||||||
return delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LoginTextWatcher implements TextWatcher {
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable editable) {
|
|
||||||
boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0
|
|
||||||
&& (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != VISIBLE);
|
|
||||||
loginButton.setEnabled(enabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void startYourself(Context context) {
|
|
||||||
Intent intent = new Intent(context, LoginActivity.class);
|
|
||||||
context.startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
// if progressDialog is visible during the configuration change then store state as true else false so that
|
|
||||||
// we maintain visibility of progressDailog after configuration change
|
|
||||||
if(progressDialog!=null&&progressDialog.isShowing()) {
|
|
||||||
outState.putBoolean(saveProgressDailog,true);
|
|
||||||
} else {
|
|
||||||
outState.putBoolean(saveProgressDailog,false);
|
|
||||||
}
|
|
||||||
outState.putString(saveErrorMessage,errorMessage.getText().toString()); //Save the errorMessage
|
|
||||||
outState.putString(saveUsername,getUsername()); // Save the username
|
|
||||||
outState.putString(savePassword,getPassword()); // Save the password
|
|
||||||
}
|
|
||||||
private String getUsername() {
|
|
||||||
return usernameEdit.getText().toString();
|
|
||||||
}
|
|
||||||
private String getPassword(){
|
|
||||||
return passwordEdit.getText().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onRestoreInstanceState(final Bundle savedInstanceState) {
|
|
||||||
super.onRestoreInstanceState(savedInstanceState);
|
|
||||||
usernameEdit.setText(savedInstanceState.getString(saveUsername));
|
|
||||||
passwordEdit.setText(savedInstanceState.getString(savePassword));
|
|
||||||
if(savedInstanceState.getBoolean(saveProgressDailog)) {
|
|
||||||
performLogin();
|
|
||||||
}
|
|
||||||
String errorMessage=savedInstanceState.getString(saveErrorMessage);
|
|
||||||
if(sessionManager.isUserLoggedIn()) {
|
|
||||||
showMessage(R.string.login_success, R.color.primaryDarkColor);
|
|
||||||
} else {
|
|
||||||
showMessage(errorMessage, R.color.secondaryDarkColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
489
app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt
Normal file
489
app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt
Normal file
|
|
@ -0,0 +1,489 @@
|
||||||
|
package fr.free.nrw.commons.auth
|
||||||
|
|
||||||
|
import android.accounts.AccountAuthenticatorActivity
|
||||||
|
import android.app.ProgressDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.app.NavUtils
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import fr.free.nrw.commons.BuildConfig
|
||||||
|
import fr.free.nrw.commons.CommonsApplication
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginCallback
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginClient
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult
|
||||||
|
import fr.free.nrw.commons.contributions.MainActivity
|
||||||
|
import fr.free.nrw.commons.databinding.ActivityLoginBinding
|
||||||
|
import fr.free.nrw.commons.di.ApplicationlessInjection
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
|
||||||
|
import fr.free.nrw.commons.utils.AbstractTextWatcher
|
||||||
|
import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags
|
||||||
|
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
||||||
|
import fr.free.nrw.commons.utils.SystemThemeUtils
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard
|
||||||
|
import fr.free.nrw.commons.utils.handleKeyboardInsets
|
||||||
|
import fr.free.nrw.commons.utils.handleWebUrl
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
class LoginActivity : AccountAuthenticatorActivity() {
|
||||||
|
@Inject
|
||||||
|
lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@field:Named("default_preferences")
|
||||||
|
lateinit var applicationKvStore: JsonKvStore
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var loginClient: LoginClient
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var systemThemeUtils: SystemThemeUtils
|
||||||
|
|
||||||
|
private var binding: ActivityLoginBinding? = null
|
||||||
|
private var progressDialog: ProgressDialog? = null
|
||||||
|
private val textWatcher = AbstractTextWatcher(::onTextChanged)
|
||||||
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
private val delegate: AppCompatDelegate by lazy {
|
||||||
|
AppCompatDelegate.create(this, null)
|
||||||
|
}
|
||||||
|
private var lastLoginResult: LoginResult? = null
|
||||||
|
|
||||||
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
ApplicationlessInjection
|
||||||
|
.getInstance(this.applicationContext)
|
||||||
|
.commonsApplicationComponent
|
||||||
|
.inject(this)
|
||||||
|
|
||||||
|
val isDarkTheme = systemThemeUtils.isDeviceInNightMode()
|
||||||
|
setTheme(if (isDarkTheme) R.style.DarkAppTheme else R.style.LightAppTheme)
|
||||||
|
delegate.installViewFactory()
|
||||||
|
delegate.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView)
|
||||||
|
.isAppearanceLightStatusBars = !isDarkTheme
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
|
applyEdgeToEdgeAllInsets(binding!!.root)
|
||||||
|
binding!!.root.handleKeyboardInsets()
|
||||||
|
with(binding!!) {
|
||||||
|
setContentView(root)
|
||||||
|
|
||||||
|
loginUsername.addTextChangedListener(textWatcher)
|
||||||
|
loginPassword.addTextChangedListener(textWatcher)
|
||||||
|
loginTwoFactor.addTextChangedListener(textWatcher)
|
||||||
|
|
||||||
|
skipLogin.setOnClickListener { skipLogin() }
|
||||||
|
forgotPassword.setOnClickListener { forgotPassword() }
|
||||||
|
aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() }
|
||||||
|
signUpButton.setOnClickListener { signUp() }
|
||||||
|
loginButton.setOnClickListener { performLogin() }
|
||||||
|
loginPassword.setOnEditorActionListener { textView, actionId, keyEvent ->
|
||||||
|
if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) {
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_NEXT && lastLoginResult != null) {
|
||||||
|
askUserForTwoFactorAuthWithKeyboard()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
performLogin()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loginPassword.onFocusChangeListener =
|
||||||
|
View.OnFocusChangeListener(::onPasswordFocusChanged)
|
||||||
|
|
||||||
|
if (isBetaFlavour) {
|
||||||
|
loginCredentials.text = getString(R.string.login_credential)
|
||||||
|
} else {
|
||||||
|
loginCredentials.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
intent.getStringExtra(CommonsApplication.LOGIN_MESSAGE_INTENT_KEY)?.let {
|
||||||
|
showMessage(it, R.color.secondaryDarkColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
intent.getStringExtra(CommonsApplication.LOGIN_USERNAME_INTENT_KEY)?.let {
|
||||||
|
loginUsername.setText(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun askUserForTwoFactorAuthWithKeyboard() {
|
||||||
|
if (binding == null) {
|
||||||
|
Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuthWithKeyboard")
|
||||||
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding!!.root)
|
||||||
|
}
|
||||||
|
progressDialog!!.dismiss()
|
||||||
|
if (binding != null) {
|
||||||
|
with(binding!!) {
|
||||||
|
twoFactorContainer.visibility = View.VISIBLE
|
||||||
|
twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code)
|
||||||
|
loginTwoFactor.visibility = View.VISIBLE
|
||||||
|
loginTwoFactor.requestFocus()
|
||||||
|
|
||||||
|
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imm.showSoftInput(loginTwoFactor, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
|
||||||
|
loginTwoFactor.setOnEditorActionListener { _, actionId, event ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE ||
|
||||||
|
(event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) {
|
||||||
|
performLogin()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.e("Binding is null in askUserForTwoFactorAuthWithKeyboard after reinitialization attempt")
|
||||||
|
}
|
||||||
|
showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed))
|
||||||
|
}
|
||||||
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onPostCreate(savedInstanceState)
|
||||||
|
delegate.onPostCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
if (sessionManager.currentAccount != null && sessionManager.isUserLoggedIn) {
|
||||||
|
applicationKvStore.putBoolean("login_skipped", false)
|
||||||
|
startMainActivity()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applicationKvStore.getBoolean("login_skipped", false)) {
|
||||||
|
performSkipLogin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
compositeDisposable.clear()
|
||||||
|
try {
|
||||||
|
// To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method
|
||||||
|
if (progressDialog?.isShowing == true) {
|
||||||
|
progressDialog!!.dismiss()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
with(binding!!) {
|
||||||
|
loginUsername.removeTextChangedListener(textWatcher)
|
||||||
|
loginPassword.removeTextChangedListener(textWatcher)
|
||||||
|
loginTwoFactor.removeTextChangedListener(textWatcher)
|
||||||
|
}
|
||||||
|
delegate.onDestroy()
|
||||||
|
loginClient.cancel()
|
||||||
|
binding = null
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
delegate.onStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
delegate.onStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostResume() {
|
||||||
|
super.onPostResume()
|
||||||
|
delegate.onPostResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setContentView(view: View, params: ViewGroup.LayoutParams) {
|
||||||
|
delegate.setContentView(view, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
NavUtils.navigateUpFromSameTask(this)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
// if progressDialog is visible during the configuration change then store state as true else false so that
|
||||||
|
// we maintain visibility of progressDialog after configuration change
|
||||||
|
if (progressDialog != null && progressDialog!!.isShowing) {
|
||||||
|
outState.putBoolean(SAVE_PROGRESS_DIALOG, true)
|
||||||
|
} else {
|
||||||
|
outState.putBoolean(SAVE_PROGRESS_DIALOG, false)
|
||||||
|
}
|
||||||
|
outState.putString(
|
||||||
|
SAVE_ERROR_MESSAGE,
|
||||||
|
binding!!.errorMessage.text.toString()
|
||||||
|
) //Save the errorMessage
|
||||||
|
outState.putString(
|
||||||
|
SAVE_USERNAME,
|
||||||
|
binding!!.loginUsername.text.toString()
|
||||||
|
) // Save the username
|
||||||
|
outState.putString(
|
||||||
|
SAVE_PASSWORD,
|
||||||
|
binding!!.loginPassword.text.toString()
|
||||||
|
) // Save the password
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
|
binding!!.loginUsername.setText(savedInstanceState.getString(SAVE_USERNAME))
|
||||||
|
binding!!.loginPassword.setText(savedInstanceState.getString(SAVE_PASSWORD))
|
||||||
|
if (savedInstanceState.getBoolean(SAVE_PROGRESS_DIALOG)) {
|
||||||
|
performLogin()
|
||||||
|
}
|
||||||
|
val errorMessage = savedInstanceState.getString(SAVE_ERROR_MESSAGE)
|
||||||
|
if (sessionManager.isUserLoggedIn) {
|
||||||
|
showMessage(R.string.login_success, R.color.primaryDarkColor)
|
||||||
|
} else {
|
||||||
|
showMessage(errorMessage, R.color.secondaryDarkColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the keyboard if the user's focus is not on the password (hasFocus is false).
|
||||||
|
* @param view The keyboard
|
||||||
|
* @param hasFocus Set to true if the keyboard has focus
|
||||||
|
*/
|
||||||
|
private fun onPasswordFocusChanged(view: View, hasFocus: Boolean) {
|
||||||
|
if (!hasFocus) {
|
||||||
|
hideKeyboard(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEditorAction(textView: TextView, actionId: Int, keyEvent: KeyEvent?) =
|
||||||
|
if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) {
|
||||||
|
performLogin()
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
|
||||||
|
private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) =
|
||||||
|
actionId == EditorInfo.IME_ACTION_NEXT || actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
|
||||||
|
|
||||||
|
private fun skipLogin() {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.skip_login_title)
|
||||||
|
.setMessage(R.string.skip_login_message)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setPositiveButton(R.string.yes) { dialog: DialogInterface, which: Int ->
|
||||||
|
dialog.cancel()
|
||||||
|
performSkipLogin()
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.no) { dialog: DialogInterface, which: Int ->
|
||||||
|
dialog.cancel()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun forgotPassword() =
|
||||||
|
handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL))
|
||||||
|
|
||||||
|
private fun onPrivacyPolicyClicked() =
|
||||||
|
handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL))
|
||||||
|
|
||||||
|
private fun signUp() =
|
||||||
|
startActivity(Intent(this, SignupActivity::class.java))
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun performLogin() {
|
||||||
|
Timber.d("Login to start!")
|
||||||
|
val username = binding!!.loginUsername.text.toString()
|
||||||
|
val password = binding!!.loginPassword.text.toString()
|
||||||
|
val twoFactorCode = binding!!.loginTwoFactor.text.toString()
|
||||||
|
|
||||||
|
showLoggingProgressBar()
|
||||||
|
loginClient.doLogin(username,
|
||||||
|
password,
|
||||||
|
lastLoginResult,
|
||||||
|
twoFactorCode,
|
||||||
|
Locale.getDefault().language,
|
||||||
|
object : LoginCallback {
|
||||||
|
override fun success(loginResult: LoginResult) = runOnUiThread {
|
||||||
|
Timber.d("Login Success")
|
||||||
|
progressDialog!!.dismiss()
|
||||||
|
onLoginSuccess(loginResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun twoFactorPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread {
|
||||||
|
Timber.d("Requesting 2FA prompt")
|
||||||
|
progressDialog!!.dismiss()
|
||||||
|
lastLoginResult = loginResult
|
||||||
|
askUserForTwoFactorAuthWithKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread {
|
||||||
|
Timber.d("Requesting email auth prompt")
|
||||||
|
progressDialog!!.dismiss()
|
||||||
|
lastLoginResult = loginResult
|
||||||
|
askUserForTwoFactorAuthWithKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun passwordResetPrompt(token: String?) = runOnUiThread {
|
||||||
|
Timber.d("Showing password reset prompt")
|
||||||
|
progressDialog!!.dismiss()
|
||||||
|
showPasswordResetPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun error(caught: Throwable) = runOnUiThread {
|
||||||
|
Timber.e(caught)
|
||||||
|
progressDialog!!.dismiss()
|
||||||
|
showMessageAndCancelDialog(caught.localizedMessage ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPasswordResetPrompt() =
|
||||||
|
showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called when user skips the login.
|
||||||
|
* It redirects the user to Explore Activity.
|
||||||
|
*/
|
||||||
|
private fun performSkipLogin() {
|
||||||
|
applicationKvStore.putBoolean("login_skipped", true)
|
||||||
|
MainActivity.startYourself(this)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoggingProgressBar() {
|
||||||
|
progressDialog = ProgressDialog(this).apply {
|
||||||
|
isIndeterminate = true
|
||||||
|
setTitle(getString(R.string.logging_in_title))
|
||||||
|
setMessage(getString(R.string.logging_in_message))
|
||||||
|
setCancelable(false)
|
||||||
|
}
|
||||||
|
progressDialog!!.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLoginSuccess(loginResult: LoginResult) {
|
||||||
|
compositeDisposable.clear()
|
||||||
|
sessionManager.setUserLoggedIn(true)
|
||||||
|
sessionManager.updateAccount(loginResult)
|
||||||
|
progressDialog!!.dismiss()
|
||||||
|
showSuccessAndDismissDialog()
|
||||||
|
startMainActivity()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMenuInflater(): MenuInflater =
|
||||||
|
delegate.menuInflater
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun askUserForTwoFactorAuth() {
|
||||||
|
if (binding == null) {
|
||||||
|
Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuth")
|
||||||
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding!!.root)
|
||||||
|
}
|
||||||
|
progressDialog!!.dismiss()
|
||||||
|
if (binding != null) {
|
||||||
|
with(binding!!) {
|
||||||
|
twoFactorContainer.visibility = View.VISIBLE
|
||||||
|
twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code)
|
||||||
|
loginTwoFactor.visibility = View.VISIBLE
|
||||||
|
loginTwoFactor.requestFocus()
|
||||||
|
|
||||||
|
loginTwoFactor.setOnEditorActionListener { _, actionId, event ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE ||
|
||||||
|
(event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) {
|
||||||
|
performLogin()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.e("Binding is null in askUserForTwoFactorAuth after reinitialization attempt")
|
||||||
|
}
|
||||||
|
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
|
||||||
|
showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed))
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun showMessageAndCancelDialog(@StringRes resId: Int) {
|
||||||
|
showMessage(resId, R.color.secondaryDarkColor)
|
||||||
|
progressDialog?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun showMessageAndCancelDialog(error: String) {
|
||||||
|
showMessage(error, R.color.secondaryDarkColor)
|
||||||
|
progressDialog?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun showSuccessAndDismissDialog() {
|
||||||
|
showMessage(R.string.login_success, R.color.primaryDarkColor)
|
||||||
|
progressDialog!!.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun startMainActivity() {
|
||||||
|
startActivityWithFlags(this, MainActivity::class.java, Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showMessage(@StringRes resId: Int, @ColorRes colorResId: Int) = with(binding!!) {
|
||||||
|
errorMessage.text = getString(resId)
|
||||||
|
errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId))
|
||||||
|
errorMessageContainer.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showMessage(message: String?, @ColorRes colorResId: Int) = with(binding!!) {
|
||||||
|
errorMessage.text = message
|
||||||
|
errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId))
|
||||||
|
errorMessageContainer.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onTextChanged(text: String) {
|
||||||
|
val enabled =
|
||||||
|
binding!!.loginUsername.text!!.length != 0 && binding!!.loginPassword.text!!.length != 0 &&
|
||||||
|
(BuildConfig.DEBUG || binding!!.loginTwoFactor.text!!.length != 0 || binding!!.loginTwoFactor.visibility != View.VISIBLE)
|
||||||
|
binding!!.loginButton.isEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun startYourself(context: Context) =
|
||||||
|
context.startActivity(Intent(context, LoginActivity::class.java))
|
||||||
|
|
||||||
|
const val SAVE_PROGRESS_DIALOG: String = "ProgressDialog_state"
|
||||||
|
const val SAVE_ERROR_MESSAGE: String = "errorMessage"
|
||||||
|
const val SAVE_USERNAME: String = "username"
|
||||||
|
const val SAVE_PASSWORD: String = "password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth;
|
|
||||||
|
|
||||||
|
|
||||||
import org.wikipedia.dataclient.Service;
|
|
||||||
import org.wikipedia.dataclient.mwapi.MwPostResponse;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import io.reactivex.Observable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for logout
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
public class LogoutClient {
|
|
||||||
|
|
||||||
private final Service service;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public LogoutClient(@Named("commons-service") Service service) {
|
|
||||||
this.service = service;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the CSRF token and uses that to post the logout api call
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Observable<MwPostResponse> postLogout() {
|
|
||||||
return service.getCsrfToken().concatMap(tokenResponse -> service.postLogout(
|
|
||||||
Objects.requireNonNull(Objects.requireNonNull(tokenResponse.query()).csrfToken())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.wikipedia.login.LoginResult;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import javax.inject.Singleton;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import io.reactivex.Completable;
|
|
||||||
import io.reactivex.Observable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manage the current logged in user session.
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
public class SessionManager {
|
|
||||||
private final Context context;
|
|
||||||
private Account currentAccount; // Unlike a savings account... ;-)
|
|
||||||
private JsonKvStore defaultKvStore;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public SessionManager(Context context,
|
|
||||||
@Named("default_preferences") JsonKvStore defaultKvStore) {
|
|
||||||
this.context = context;
|
|
||||||
this.currentAccount = null;
|
|
||||||
this.defaultKvStore = defaultKvStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean createAccount(@NonNull String userName, @NonNull String password) {
|
|
||||||
Account account = getCurrentAccount();
|
|
||||||
if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) {
|
|
||||||
removeAccount();
|
|
||||||
account = new Account(userName, BuildConfig.ACCOUNT_TYPE);
|
|
||||||
return accountManager().addAccountExplicitly(account, password, null);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeAccount() {
|
|
||||||
Account account = getCurrentAccount();
|
|
||||||
if (account != null) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
||||||
accountManager().removeAccountExplicitly(account);
|
|
||||||
} else {
|
|
||||||
//noinspection deprecation
|
|
||||||
accountManager().removeAccount(account, null, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateAccount(LoginResult result) {
|
|
||||||
boolean accountCreated = createAccount(result.getUserName(), result.getPassword());
|
|
||||||
if (accountCreated) {
|
|
||||||
setPassword(result.getPassword());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setPassword(@NonNull String password) {
|
|
||||||
Account account = getCurrentAccount();
|
|
||||||
if (account != null) {
|
|
||||||
accountManager().setPassword(account, password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Account|null
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public Account getCurrentAccount() {
|
|
||||||
if (currentAccount == null) {
|
|
||||||
AccountManager accountManager = AccountManager.get(context);
|
|
||||||
Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE);
|
|
||||||
if (allAccounts.length != 0) {
|
|
||||||
currentAccount = allAccounts[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return currentAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean doesAccountExist() {
|
|
||||||
return getCurrentAccount() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getUserName() {
|
|
||||||
Account account = getCurrentAccount();
|
|
||||||
return account == null ? null : account.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getPassword() {
|
|
||||||
Account account = getCurrentAccount();
|
|
||||||
return account == null ? null : accountManager().getPassword(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
private AccountManager accountManager() {
|
|
||||||
return AccountManager.get(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isUserLoggedIn() {
|
|
||||||
return defaultKvStore.getBoolean("isUserLoggedIn", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setUserLoggedIn(boolean isLoggedIn) {
|
|
||||||
defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void forceLogin(Context context) {
|
|
||||||
if (context != null) {
|
|
||||||
LoginActivity.startYourself(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Clears existing accounts from account manager
|
|
||||||
* 2. Calls MediaWikiApi's logout function to clear cookies
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Completable logout() {
|
|
||||||
AccountManager accountManager = AccountManager.get(context);
|
|
||||||
Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE);
|
|
||||||
return Completable.fromObservable(Observable.fromArray(allAccounts)
|
|
||||||
.map(a -> accountManager.removeAccount(a, null, null).getResult()))
|
|
||||||
.doOnComplete(() -> {
|
|
||||||
currentAccount = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a corresponding boolean preference
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public boolean getPreference(String key) {
|
|
||||||
return defaultKvStore.getBoolean(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
95
app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt
Normal file
95
app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
package fr.free.nrw.commons.auth
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.text.TextUtils
|
||||||
|
import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage the current logged in user session.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class SessionManager @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
|
@param:Named("default_preferences") private val defaultKvStore: JsonKvStore
|
||||||
|
) {
|
||||||
|
private val accountManager: AccountManager get() = AccountManager.get(context)
|
||||||
|
|
||||||
|
private var _currentAccount: Account? = null // Unlike a savings account... ;-)
|
||||||
|
val currentAccount: Account? get() {
|
||||||
|
if (_currentAccount == null) {
|
||||||
|
val allAccounts = AccountManager.get(context).getAccountsByType(ACCOUNT_TYPE)
|
||||||
|
if (allAccounts.isNotEmpty()) {
|
||||||
|
_currentAccount = allAccounts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _currentAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
val userName: String?
|
||||||
|
get() = currentAccount?.name
|
||||||
|
|
||||||
|
var password: String?
|
||||||
|
get() = currentAccount?.let { accountManager.getPassword(it) }
|
||||||
|
private set(value) {
|
||||||
|
currentAccount?.let { accountManager.setPassword(it, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val isUserLoggedIn: Boolean
|
||||||
|
get() = defaultKvStore.getBoolean("isUserLoggedIn", false)
|
||||||
|
|
||||||
|
fun updateAccount(result: LoginResult) {
|
||||||
|
if (createAccount(result.userName!!, result.password!!)) {
|
||||||
|
password = result.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doesAccountExist(): Boolean =
|
||||||
|
currentAccount != null
|
||||||
|
|
||||||
|
fun setUserLoggedIn(isLoggedIn: Boolean) =
|
||||||
|
defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn)
|
||||||
|
|
||||||
|
fun forceLogin(context: Context?) =
|
||||||
|
context?.let { LoginActivity.startYourself(it) }
|
||||||
|
|
||||||
|
fun getPreference(key: String): Boolean =
|
||||||
|
defaultKvStore.getBoolean(key)
|
||||||
|
|
||||||
|
fun logout(): Completable = Completable.fromObservable(
|
||||||
|
Observable.empty<Any>()
|
||||||
|
.doOnComplete {
|
||||||
|
removeAccount()
|
||||||
|
_currentAccount = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createAccount(userName: String, password: String): Boolean {
|
||||||
|
var account = currentAccount
|
||||||
|
if (account == null || TextUtils.isEmpty(account.name) || account.name != userName) {
|
||||||
|
removeAccount()
|
||||||
|
account = Account(userName, ACCOUNT_TYPE)
|
||||||
|
return accountManager.addAccountExplicitly(account, password, null)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeAccount() {
|
||||||
|
currentAccount?.let {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
accountManager.removeAccountExplicitly(it)
|
||||||
|
} else {
|
||||||
|
accountManager.removeAccount(it, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth;
|
|
||||||
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.webkit.WebSettings;
|
|
||||||
import android.webkit.WebView;
|
|
||||||
import android.webkit.WebViewClient;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
public class SignupActivity extends BaseActivity {
|
|
||||||
|
|
||||||
private WebView webView;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
Timber.d("Signup Activity started");
|
|
||||||
|
|
||||||
webView = new WebView(this);
|
|
||||||
setContentView(webView);
|
|
||||||
|
|
||||||
webView.setWebViewClient(new MyWebViewClient());
|
|
||||||
WebSettings webSettings = webView.getSettings();
|
|
||||||
/*Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can
|
|
||||||
trust Wikimedia's site... right?*/
|
|
||||||
webSettings.setJavaScriptEnabled(true);
|
|
||||||
|
|
||||||
webView.loadUrl(BuildConfig.SIGNUP_LANDING_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MyWebViewClient extends WebViewClient {
|
|
||||||
@Override
|
|
||||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
|
||||||
if (url.equals(BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL)) {
|
|
||||||
//Signup success, so clear cookies, notify user, and load LoginActivity again
|
|
||||||
Timber.d("Overriding URL %s", url);
|
|
||||||
|
|
||||||
Toast toast = Toast.makeText(SignupActivity.this,
|
|
||||||
R.string.account_created, Toast.LENGTH_LONG);
|
|
||||||
toast.show();
|
|
||||||
// terminate on task completion.
|
|
||||||
finish();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
//If user clicks any other links in the webview
|
|
||||||
Timber.d("Not overriding URL, URL is: %s", url);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
if (webView.canGoBack()) {
|
|
||||||
webView.goBack();
|
|
||||||
} else {
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Known bug in androidx.appcompat library version 1.1.0 being tracked here
|
|
||||||
* https://issuetracker.google.com/issues/141132133
|
|
||||||
* App tries to put light/dark theme to webview and crashes in the process
|
|
||||||
* This code tries to prevent applying the theme when sdk is between api 21 to 25
|
|
||||||
* @param overrideConfiguration
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void applyOverrideConfiguration(final Configuration overrideConfiguration) {
|
|
||||||
if (Build.VERSION.SDK_INT <= 25 &&
|
|
||||||
(getResources().getConfiguration().uiMode == getApplicationContext().getResources().getConfiguration().uiMode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
super.applyOverrideConfiguration(overrideConfiguration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
77
app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt
Normal file
77
app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
package fr.free.nrw.commons.auth
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.Toast
|
||||||
|
import fr.free.nrw.commons.BuildConfig
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.utils.applyEdgeToEdgeAllInsets
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class SignupActivity : BaseActivity() {
|
||||||
|
private var webView: WebView? = null
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
Timber.d("Signup Activity started")
|
||||||
|
|
||||||
|
webView = WebView(this)
|
||||||
|
applyEdgeToEdgeAllInsets(webView!!)
|
||||||
|
with(webView!!) {
|
||||||
|
setContentView(this)
|
||||||
|
webViewClient = MyWebViewClient()
|
||||||
|
// Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can
|
||||||
|
// trust Wikimedia's site... right?
|
||||||
|
settings.javaScriptEnabled = true
|
||||||
|
loadUrl(BuildConfig.SIGNUP_LANDING_URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (webView!!.canGoBack()) {
|
||||||
|
webView!!.goBack()
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known bug in androidx.appcompat library version 1.1.0 being tracked here
|
||||||
|
* https://issuetracker.google.com/issues/141132133
|
||||||
|
* App tries to put light/dark theme to webview and crashes in the process
|
||||||
|
* This code tries to prevent applying the theme when sdk is between api 21 to 25
|
||||||
|
*/
|
||||||
|
override fun applyOverrideConfiguration(overrideConfiguration: Configuration) {
|
||||||
|
if (Build.VERSION.SDK_INT <= 25 &&
|
||||||
|
(resources.configuration.uiMode == applicationContext.resources.configuration.uiMode)
|
||||||
|
) return
|
||||||
|
super.applyOverrideConfiguration(overrideConfiguration)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class MyWebViewClient : WebViewClient() {
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean =
|
||||||
|
if (url == BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL) {
|
||||||
|
//Signup success, so clear cookies, notify user, and load LoginActivity again
|
||||||
|
Timber.d("Overriding URL %s", url)
|
||||||
|
|
||||||
|
Toast.makeText(
|
||||||
|
this@SignupActivity, R.string.account_created, Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
|
// terminate on task completion.
|
||||||
|
finish()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
//If user clicks any other links in the webview
|
||||||
|
Timber.d("Not overriding URL, URL is: %s", url)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth;
|
|
||||||
|
|
||||||
import android.accounts.AbstractAccountAuthenticator;
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountAuthenticatorResponse;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.accounts.NetworkErrorException;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles WikiMedia commons account Authentication
|
|
||||||
*/
|
|
||||||
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
|
|
||||||
private static final String[] SYNC_AUTHORITIES = {BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY};
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
public WikiAccountAuthenticator(@NonNull Context context) {
|
|
||||||
super(context);
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides Bundle with edited Account Properties
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putString("test", "editProperties");
|
|
||||||
return bundle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
|
|
||||||
@NonNull String accountType, @Nullable String authTokenType,
|
|
||||||
@Nullable String[] requiredFeatures, @Nullable Bundle options)
|
|
||||||
throws NetworkErrorException {
|
|
||||||
// account type not supported returns bundle without loginActivity Intent, it just contains "test" key
|
|
||||||
if (!supportedAccountType(accountType)) {
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putString("test", "addAccount");
|
|
||||||
return bundle;
|
|
||||||
}
|
|
||||||
|
|
||||||
return addAccount(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
|
|
||||||
@NonNull Account account, @Nullable Bundle options)
|
|
||||||
throws NetworkErrorException {
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putString("test", "confirmCredentials");
|
|
||||||
return bundle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response,
|
|
||||||
@NonNull Account account, @NonNull String authTokenType,
|
|
||||||
@Nullable Bundle options)
|
|
||||||
throws NetworkErrorException {
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putString("test", "getAuthToken");
|
|
||||||
return bundle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public String getAuthTokenLabel(@NonNull String authTokenType) {
|
|
||||||
return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
|
|
||||||
@NonNull Account account, @Nullable String authTokenType,
|
|
||||||
@Nullable Bundle options)
|
|
||||||
throws NetworkErrorException {
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putString("test", "updateCredentials");
|
|
||||||
return bundle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response,
|
|
||||||
@NonNull Account account, @NonNull String[] features)
|
|
||||||
throws NetworkErrorException {
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
|
|
||||||
return bundle;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean supportedAccountType(@Nullable String type) {
|
|
||||||
return BuildConfig.ACCOUNT_TYPE.equals(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a bundle containing a Parcel
|
|
||||||
* the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type)
|
|
||||||
*/
|
|
||||||
private Bundle addAccount(AccountAuthenticatorResponse response) {
|
|
||||||
Intent intent = new Intent(context, LoginActivity.class);
|
|
||||||
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
|
||||||
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
|
|
||||||
|
|
||||||
return bundle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
|
|
||||||
Account account) throws NetworkErrorException {
|
|
||||||
Bundle result = super.getAccountRemovalAllowed(response, account);
|
|
||||||
|
|
||||||
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
|
|
||||||
&& !result.containsKey(AccountManager.KEY_INTENT)) {
|
|
||||||
boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
|
|
||||||
|
|
||||||
if (allowed) {
|
|
||||||
for (String auth : SYNC_AUTHORITIES) {
|
|
||||||
ContentResolver.cancelSync(account, auth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package fr.free.nrw.commons.auth
|
||||||
|
|
||||||
|
import android.accounts.AbstractAccountAuthenticator
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.accounts.AccountAuthenticatorResponse
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.accounts.NetworkErrorException
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import fr.free.nrw.commons.BuildConfig
|
||||||
|
|
||||||
|
private val SYNC_AUTHORITIES = arrayOf(
|
||||||
|
BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles WikiMedia commons account Authentication
|
||||||
|
*/
|
||||||
|
class WikiAccountAuthenticator(
|
||||||
|
private val context: Context
|
||||||
|
) : AbstractAccountAuthenticator(context) {
|
||||||
|
/**
|
||||||
|
* Provides Bundle with edited Account Properties
|
||||||
|
*/
|
||||||
|
override fun editProperties(
|
||||||
|
response: AccountAuthenticatorResponse,
|
||||||
|
accountType: String
|
||||||
|
) = bundleOf("test" to "editProperties")
|
||||||
|
|
||||||
|
// account type not supported returns bundle without loginActivity Intent, it just contains "test" key
|
||||||
|
@Throws(NetworkErrorException::class)
|
||||||
|
override fun addAccount(
|
||||||
|
response: AccountAuthenticatorResponse,
|
||||||
|
accountType: String,
|
||||||
|
authTokenType: String?,
|
||||||
|
requiredFeatures: Array<String>?,
|
||||||
|
options: Bundle?
|
||||||
|
) = if (BuildConfig.ACCOUNT_TYPE == accountType) {
|
||||||
|
addAccount(response)
|
||||||
|
} else {
|
||||||
|
bundleOf("test" to "addAccount")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(NetworkErrorException::class)
|
||||||
|
override fun confirmCredentials(
|
||||||
|
response: AccountAuthenticatorResponse, account: Account, options: Bundle?
|
||||||
|
) = bundleOf("test" to "confirmCredentials")
|
||||||
|
|
||||||
|
@Throws(NetworkErrorException::class)
|
||||||
|
override fun getAuthToken(
|
||||||
|
response: AccountAuthenticatorResponse,
|
||||||
|
account: Account,
|
||||||
|
authTokenType: String,
|
||||||
|
options: Bundle?
|
||||||
|
) = bundleOf("test" to "getAuthToken")
|
||||||
|
|
||||||
|
override fun getAuthTokenLabel(authTokenType: String) =
|
||||||
|
if (BuildConfig.ACCOUNT_TYPE == authTokenType) AUTH_TOKEN_TYPE else null
|
||||||
|
|
||||||
|
@Throws(NetworkErrorException::class)
|
||||||
|
override fun updateCredentials(
|
||||||
|
response: AccountAuthenticatorResponse,
|
||||||
|
account: Account,
|
||||||
|
authTokenType: String?,
|
||||||
|
options: Bundle?
|
||||||
|
) = bundleOf("test" to "updateCredentials")
|
||||||
|
|
||||||
|
@Throws(NetworkErrorException::class)
|
||||||
|
override fun hasFeatures(
|
||||||
|
response: AccountAuthenticatorResponse,
|
||||||
|
account: Account, features: Array<String>
|
||||||
|
) = bundleOf(AccountManager.KEY_BOOLEAN_RESULT to false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a bundle containing a Parcel
|
||||||
|
* the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type)
|
||||||
|
*/
|
||||||
|
private fun addAccount(response: AccountAuthenticatorResponse): Bundle {
|
||||||
|
val intent = Intent(context, LoginActivity::class.java)
|
||||||
|
.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
|
||||||
|
return bundleOf(AccountManager.KEY_INTENT to intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(NetworkErrorException::class)
|
||||||
|
override fun getAccountRemovalAllowed(
|
||||||
|
response: AccountAuthenticatorResponse?,
|
||||||
|
account: Account?
|
||||||
|
): Bundle {
|
||||||
|
val result = super.getAccountRemovalAllowed(response, account)
|
||||||
|
|
||||||
|
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
|
||||||
|
&& !result.containsKey(AccountManager.KEY_INTENT)
|
||||||
|
) {
|
||||||
|
val allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)
|
||||||
|
|
||||||
|
if (allowed) {
|
||||||
|
for (auth in SYNC_AUTHORITIES) {
|
||||||
|
ContentResolver.cancelSync(account, auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package fr.free.nrw.commons.auth;
|
|
||||||
|
|
||||||
import android.accounts.AbstractAccountAuthenticator;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.IBinder;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the Auth service of the App, see AndroidManifests for details
|
|
||||||
* (Uses Dagger 2 as injector)
|
|
||||||
*/
|
|
||||||
public class WikiAccountAuthenticatorService extends CommonsDaggerService {
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private AbstractAccountAuthenticator authenticator;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
authenticator = new WikiAccountAuthenticator(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return authenticator == null ? null : authenticator.getIBinder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package fr.free.nrw.commons.auth
|
||||||
|
|
||||||
|
import android.accounts.AbstractAccountAuthenticator
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the Auth service of the App, see AndroidManifests for details
|
||||||
|
* (Uses Dagger 2 as injector)
|
||||||
|
*/
|
||||||
|
class WikiAccountAuthenticatorService : CommonsDaggerService() {
|
||||||
|
private var authenticator: AbstractAccountAuthenticator? = null
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
authenticator = WikiAccountAuthenticator(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder? =
|
||||||
|
authenticator?.iBinder
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
package fr.free.nrw.commons.auth.csrf
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginCallback
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginClient
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginFailedException
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Response
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.Callable
|
||||||
|
import java.util.concurrent.Executors.newSingleThreadExecutor
|
||||||
|
|
||||||
|
class CsrfTokenClient(
|
||||||
|
private val sessionManager: SessionManager,
|
||||||
|
private val csrfTokenInterface: CsrfTokenInterface,
|
||||||
|
private val loginClient: LoginClient,
|
||||||
|
private val logoutClient: LogoutClient,
|
||||||
|
) {
|
||||||
|
private var retries = 0
|
||||||
|
private var csrfTokenCall: Call<MwQueryResponse?>? = null
|
||||||
|
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun getTokenBlocking(): String {
|
||||||
|
var token = ""
|
||||||
|
val userName = sessionManager.userName ?: ""
|
||||||
|
val password = sessionManager.password ?: ""
|
||||||
|
|
||||||
|
for (retry in 0 until MAX_RETRIES_OF_LOGIN_BLOCKING) {
|
||||||
|
try {
|
||||||
|
if (retry > 0) {
|
||||||
|
// Log in explicitly
|
||||||
|
loginClient.loginBlocking(userName, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get CSRFToken response off the main thread.
|
||||||
|
val response =
|
||||||
|
newSingleThreadExecutor()
|
||||||
|
.submit(
|
||||||
|
Callable {
|
||||||
|
csrfTokenInterface.getCsrfTokenCall().execute()
|
||||||
|
},
|
||||||
|
).get()
|
||||||
|
|
||||||
|
if (response
|
||||||
|
.body()
|
||||||
|
?.query()
|
||||||
|
?.csrfToken()
|
||||||
|
.isNullOrEmpty()
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
token = response.body()!!.query()!!.csrfToken()!!
|
||||||
|
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
|
||||||
|
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} catch (e: LoginFailedException) {
|
||||||
|
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Timber.w(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.isEmpty() || token == ANON_TOKEN) {
|
||||||
|
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun request(
|
||||||
|
service: CsrfTokenInterface,
|
||||||
|
cb: Callback,
|
||||||
|
): Call<MwQueryResponse?> =
|
||||||
|
requestToken(
|
||||||
|
service,
|
||||||
|
object : Callback {
|
||||||
|
override fun success(token: String?) {
|
||||||
|
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
|
||||||
|
retryWithLogin(cb) {
|
||||||
|
InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb.success(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
|
||||||
|
|
||||||
|
override fun twoFactorPrompt() = cb.twoFactorPrompt()
|
||||||
|
|
||||||
|
override fun emailAuthPrompt() = cb.emailAuthPrompt()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun requestToken(
|
||||||
|
service: CsrfTokenInterface,
|
||||||
|
cb: Callback,
|
||||||
|
): Call<MwQueryResponse?> {
|
||||||
|
val call = service.getCsrfTokenCall()
|
||||||
|
call.enqueue(
|
||||||
|
object : retrofit2.Callback<MwQueryResponse?> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<MwQueryResponse?>,
|
||||||
|
response: Response<MwQueryResponse?>,
|
||||||
|
) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.success(response.body()!!.query()!!.csrfToken())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<MwQueryResponse?>,
|
||||||
|
t: Throwable,
|
||||||
|
) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.failure(t)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return call
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retryWithLogin(
|
||||||
|
callback: Callback,
|
||||||
|
caught: () -> Throwable?,
|
||||||
|
) {
|
||||||
|
val userName = sessionManager.userName
|
||||||
|
val password = sessionManager.password
|
||||||
|
if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) {
|
||||||
|
retries++
|
||||||
|
logoutClient.logout()
|
||||||
|
login(userName, password, callback) {
|
||||||
|
Timber.i("retrying...")
|
||||||
|
cancel()
|
||||||
|
csrfTokenCall = request(csrfTokenInterface, callback)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback.failure(caught())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun login(
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
callback: Callback,
|
||||||
|
retryCallback: () -> Unit,
|
||||||
|
) = loginClient.request(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
object : LoginCallback {
|
||||||
|
override fun success(loginResult: LoginResult) {
|
||||||
|
if (loginResult.pass) {
|
||||||
|
sessionManager.updateAccount(loginResult)
|
||||||
|
retryCallback()
|
||||||
|
} else {
|
||||||
|
callback.failure(LoginFailedException(loginResult.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun twoFactorPrompt(
|
||||||
|
loginResult: LoginResult,
|
||||||
|
caught: Throwable,
|
||||||
|
token: String?,
|
||||||
|
) = callback.twoFactorPrompt()
|
||||||
|
|
||||||
|
override fun emailAuthPrompt(
|
||||||
|
loginResult: LoginResult,
|
||||||
|
caught: Throwable,
|
||||||
|
token: String?,
|
||||||
|
) = callback.emailAuthPrompt()
|
||||||
|
|
||||||
|
// Should not happen here, but call the callback just in case.
|
||||||
|
override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password."))
|
||||||
|
|
||||||
|
override fun error(caught: Throwable) = callback.failure(caught)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun cancel() {
|
||||||
|
loginClient.cancel()
|
||||||
|
if (csrfTokenCall != null) {
|
||||||
|
csrfTokenCall!!.cancel()
|
||||||
|
csrfTokenCall = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun success(token: String?)
|
||||||
|
|
||||||
|
fun failure(caught: Throwable?)
|
||||||
|
|
||||||
|
fun twoFactorPrompt()
|
||||||
|
|
||||||
|
fun emailAuthPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ANON_TOKEN = "+\\"
|
||||||
|
private const val MAX_RETRIES = 1
|
||||||
|
private const val MAX_RETRIES_OF_LOGIN_BLOCKING = 2
|
||||||
|
const val INVALID_TOKEN_ERROR_MESSAGE = "Invalid token, or login failure."
|
||||||
|
const val ANONYMOUS_TOKEN_MESSAGE = "App believes we're logged in, but got anonymous token."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidLoginTokenException(
|
||||||
|
message: String,
|
||||||
|
) : Exception(message)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package fr.free.nrw.commons.auth.csrf
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Headers
|
||||||
|
|
||||||
|
interface CsrfTokenInterface {
|
||||||
|
@Headers("Cache-Control: no-cache")
|
||||||
|
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
|
||||||
|
fun getCsrfTokenCall(): Call<MwQueryResponse?>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package fr.free.nrw.commons.auth.csrf
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LogoutClient
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val store: CommonsCookieStorage,
|
||||||
|
) {
|
||||||
|
fun logout() = store.clear()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
interface LoginCallback {
|
||||||
|
fun success(loginResult: LoginResult)
|
||||||
|
|
||||||
|
fun twoFactorPrompt(
|
||||||
|
loginResult: LoginResult,
|
||||||
|
caught: Throwable,
|
||||||
|
token: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun emailAuthPrompt(
|
||||||
|
loginResult: LoginResult,
|
||||||
|
caught: Throwable,
|
||||||
|
token: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun passwordResetPrompt(token: String?)
|
||||||
|
|
||||||
|
fun error(caught: Throwable)
|
||||||
|
}
|
||||||
276
app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
Normal file
276
app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt
Normal file
|
|
@ -0,0 +1,276 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for making login related requests to the server.
|
||||||
|
*/
|
||||||
|
class LoginClient(
|
||||||
|
private val loginInterface: LoginInterface,
|
||||||
|
) {
|
||||||
|
private var tokenCall: Call<MwQueryResponse?>? = null
|
||||||
|
private var loginCall: Call<LoginResponse?>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* userLanguage
|
||||||
|
* It holds the value of the user's device language code.
|
||||||
|
* For example, if user's device language is English it will hold En
|
||||||
|
* The value will be fetched when the user clicks Login Button in the LoginActivity
|
||||||
|
*/
|
||||||
|
private var userLanguage = ""
|
||||||
|
|
||||||
|
private fun getLoginToken() = loginInterface.getLoginToken()
|
||||||
|
|
||||||
|
fun request(
|
||||||
|
userName: String,
|
||||||
|
password: String,
|
||||||
|
cb: LoginCallback,
|
||||||
|
) {
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
tokenCall = getLoginToken()
|
||||||
|
tokenCall!!.enqueue(
|
||||||
|
object : Callback<MwQueryResponse?> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<MwQueryResponse?>,
|
||||||
|
response: Response<MwQueryResponse?>,
|
||||||
|
) {
|
||||||
|
login(
|
||||||
|
userName,
|
||||||
|
password,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
response.body()!!.query()!!.loginToken(),
|
||||||
|
userLanguage,
|
||||||
|
cb,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<MwQueryResponse?>,
|
||||||
|
caught: Throwable,
|
||||||
|
) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.error(caught)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun login(
|
||||||
|
userName: String,
|
||||||
|
password: String,
|
||||||
|
retypedPassword: String?,
|
||||||
|
twoFactorCode: String?,
|
||||||
|
emailAuthCode: String?,
|
||||||
|
loginToken: String?,
|
||||||
|
userLanguage: String,
|
||||||
|
cb: LoginCallback,
|
||||||
|
) {
|
||||||
|
this.userLanguage = userLanguage
|
||||||
|
|
||||||
|
loginCall =
|
||||||
|
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
|
||||||
|
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||||
|
} else {
|
||||||
|
loginInterface.postLogIn(
|
||||||
|
userName,
|
||||||
|
password,
|
||||||
|
retypedPassword,
|
||||||
|
twoFactorCode,
|
||||||
|
emailAuthCode,
|
||||||
|
loginToken,
|
||||||
|
userLanguage,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
loginCall!!.enqueue(
|
||||||
|
object : Callback<LoginResponse?> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<LoginResponse?>,
|
||||||
|
response: Response<LoginResponse?>,
|
||||||
|
) {
|
||||||
|
val loginResult = response.body()?.toLoginResult(password)
|
||||||
|
if (loginResult != null) {
|
||||||
|
if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) {
|
||||||
|
// The server could do some transformations on user names, e.g. on some
|
||||||
|
// wikis is uppercases the first letter.
|
||||||
|
getExtendedInfo(loginResult.userName, loginResult, cb)
|
||||||
|
} else if ("UI" == loginResult.status) {
|
||||||
|
when (loginResult) {
|
||||||
|
is OAuthResult ->
|
||||||
|
cb.twoFactorPrompt(
|
||||||
|
loginResult,
|
||||||
|
LoginFailedException(loginResult.message),
|
||||||
|
loginToken,
|
||||||
|
)
|
||||||
|
|
||||||
|
is EmailAuthResult ->
|
||||||
|
cb.emailAuthPrompt(
|
||||||
|
loginResult,
|
||||||
|
LoginFailedException(loginResult.message),
|
||||||
|
loginToken
|
||||||
|
)
|
||||||
|
|
||||||
|
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
|
||||||
|
|
||||||
|
is LoginResult.Result ->
|
||||||
|
cb.error(
|
||||||
|
LoginFailedException(loginResult.message),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb.error(LoginFailedException(loginResult.message))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb.error(IOException("Login failed. Unexpected response."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<LoginResponse?>,
|
||||||
|
t: Throwable,
|
||||||
|
) {
|
||||||
|
if (call.isCanceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cb.error(t)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doLogin(
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
lastLoginResult: LoginResult?,
|
||||||
|
twoFactorCode: String,
|
||||||
|
userLanguage: String,
|
||||||
|
loginCallback: LoginCallback,
|
||||||
|
) {
|
||||||
|
getLoginToken().enqueue(
|
||||||
|
object : Callback<MwQueryResponse?> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<MwQueryResponse?>,
|
||||||
|
response: Response<MwQueryResponse?>,
|
||||||
|
) = if (response.isSuccessful) {
|
||||||
|
val loginToken = response.body()?.query()?.loginToken()
|
||||||
|
loginToken?.let {
|
||||||
|
login(username, password, null,
|
||||||
|
if (lastLoginResult is OAuthResult) twoFactorCode else null,
|
||||||
|
if (lastLoginResult is EmailAuthResult) twoFactorCode else null,
|
||||||
|
it, userLanguage, loginCallback)
|
||||||
|
} ?: run {
|
||||||
|
loginCallback.error(IOException("Failed to retrieve login token"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loginCallback.error(IOException("Failed to retrieve login token"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
call: Call<MwQueryResponse?>,
|
||||||
|
t: Throwable,
|
||||||
|
) {
|
||||||
|
loginCallback.error(t)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun loginBlocking(
|
||||||
|
userName: String,
|
||||||
|
password: String,
|
||||||
|
twoFactorCode: String? = null,
|
||||||
|
emailAuthCode: String? = null
|
||||||
|
) {
|
||||||
|
val tokenResponse = getLoginToken().execute()
|
||||||
|
if (tokenResponse
|
||||||
|
.body()
|
||||||
|
?.query()
|
||||||
|
?.loginToken()
|
||||||
|
.isNullOrEmpty()
|
||||||
|
) {
|
||||||
|
throw IOException("Unexpected response when getting login token.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val loginToken = tokenResponse.body()?.query()?.loginToken()
|
||||||
|
val tempLoginCall =
|
||||||
|
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty()) {
|
||||||
|
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
|
||||||
|
} else {
|
||||||
|
loginInterface.postLogIn(
|
||||||
|
userName,
|
||||||
|
password,
|
||||||
|
null,
|
||||||
|
twoFactorCode,
|
||||||
|
emailAuthCode,
|
||||||
|
loginToken,
|
||||||
|
userLanguage,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = tempLoginCall.execute()
|
||||||
|
val loginResponse = response.body() ?: throw IOException("Unexpected response when logging in.")
|
||||||
|
val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.")
|
||||||
|
|
||||||
|
if ("UI" == loginResult.status) {
|
||||||
|
if (loginResult is OAuthResult || loginResult is EmailAuthResult) {
|
||||||
|
// TODO: Find a better way to boil up the warning about 2FA
|
||||||
|
throw LoginFailedException(loginResult.message)
|
||||||
|
}
|
||||||
|
throw LoginFailedException(loginResult.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loginResult.pass || TextUtils.isEmpty(loginResult.userName)) {
|
||||||
|
throw LoginFailedException(loginResult.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExtendedInfo(
|
||||||
|
userName: String,
|
||||||
|
loginResult: LoginResult,
|
||||||
|
cb: LoginCallback,
|
||||||
|
) = loginInterface
|
||||||
|
.getUserInfo(userName)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ response: MwQueryResponse? ->
|
||||||
|
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
|
||||||
|
loginResult.groups =
|
||||||
|
response?.query()?.getUserResponse(userName)?.getGroups() ?: emptySet()
|
||||||
|
cb.success(loginResult)
|
||||||
|
}, { caught: Throwable ->
|
||||||
|
Timber.e(caught, "Login succeeded but getting group information failed. ")
|
||||||
|
cb.error(caught)
|
||||||
|
})
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
tokenCall?.let {
|
||||||
|
it.cancel()
|
||||||
|
tokenCall = null
|
||||||
|
}
|
||||||
|
|
||||||
|
loginCall?.let {
|
||||||
|
it.cancel()
|
||||||
|
loginCall = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
class LoginFailedException(
|
||||||
|
message: String?,
|
||||||
|
) : Throwable(message)
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.Field
|
||||||
|
import retrofit2.http.FormUrlEncoded
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Headers
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
interface LoginInterface {
|
||||||
|
@Headers("Cache-Control: no-cache")
|
||||||
|
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=login")
|
||||||
|
fun getLoginToken(): Call<MwQueryResponse?>
|
||||||
|
|
||||||
|
@Headers("Cache-Control: no-cache")
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MW_API_PREFIX + "action=clientlogin&rememberMe=")
|
||||||
|
fun postLogIn(
|
||||||
|
@Field("username") user: String?,
|
||||||
|
@Field("password") pass: String?,
|
||||||
|
@Field("logintoken") token: String?,
|
||||||
|
@Field("uselang") userLanguage: String?,
|
||||||
|
@Field("loginreturnurl") url: String?,
|
||||||
|
): Call<LoginResponse?>
|
||||||
|
|
||||||
|
@Headers("Cache-Control: no-cache")
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MW_API_PREFIX + "action=clientlogin&rememberMe=")
|
||||||
|
fun postLogIn(
|
||||||
|
@Field("username") user: String?,
|
||||||
|
@Field("password") pass: String?,
|
||||||
|
@Field("retype") retypedPass: String?,
|
||||||
|
@Field("OATHToken") twoFactorCode: String?,
|
||||||
|
@Field("token") emailAuthToken: String?,
|
||||||
|
@Field("logintoken") loginToken: String?,
|
||||||
|
@Field("uselang") userLanguage: String?,
|
||||||
|
@Field("logincontinue") loginContinue: Boolean,
|
||||||
|
): Call<LoginResponse?>
|
||||||
|
|
||||||
|
@GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
|
||||||
|
fun getUserInfo(
|
||||||
|
@Query("ususers") userName: String,
|
||||||
|
): Observable<MwQueryResponse?>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
|
||||||
|
import fr.free.nrw.commons.auth.login.LoginResult.Result
|
||||||
|
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
|
||||||
|
|
||||||
|
class LoginResponse {
|
||||||
|
@SerializedName("error")
|
||||||
|
val error: MwServiceError? = null
|
||||||
|
|
||||||
|
@SerializedName("clientlogin")
|
||||||
|
private val clientLogin: ClientLogin? = null
|
||||||
|
|
||||||
|
fun toLoginResult(password: String): LoginResult? = clientLogin?.toLoginResult(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ClientLogin {
|
||||||
|
private val status: String? = null
|
||||||
|
private val requests: List<Request>? = null
|
||||||
|
private val message: String? = null
|
||||||
|
|
||||||
|
@SerializedName("username")
|
||||||
|
private val userName: String? = null
|
||||||
|
|
||||||
|
fun toLoginResult(password: String): LoginResult {
|
||||||
|
var userMessage = message
|
||||||
|
if ("UI" == status) {
|
||||||
|
requests?.forEach { request ->
|
||||||
|
request.id()?.let {
|
||||||
|
if (it.endsWith("TOTPAuthenticationRequest")) {
|
||||||
|
return OAuthResult(status, userName, password, message)
|
||||||
|
} else if (it.endsWith("EmailAuthAuthenticationRequest")) {
|
||||||
|
return EmailAuthResult(status, userName, password, message)
|
||||||
|
} else if (it.endsWith("PasswordAuthenticationRequest")) {
|
||||||
|
return ResetPasswordResult(status, userName, password, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("PASS" != status && "FAIL" != status) {
|
||||||
|
// TODO: String resource -- Looks like needed for others in this class too
|
||||||
|
userMessage = "An unknown error occurred."
|
||||||
|
}
|
||||||
|
return Result(status ?: "", userName, password, userMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Request {
|
||||||
|
private val id: String? = null
|
||||||
|
private val required: String? = null
|
||||||
|
private val provider: String? = null
|
||||||
|
private val account: String? = null
|
||||||
|
internal val fields: Map<String, RequestField>? = null
|
||||||
|
|
||||||
|
fun id(): String? = id
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class RequestField {
|
||||||
|
private val type: String? = null
|
||||||
|
private val label: String? = null
|
||||||
|
internal val help: String? = null
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package fr.free.nrw.commons.auth.login
|
||||||
|
|
||||||
|
sealed class LoginResult(
|
||||||
|
val status: String,
|
||||||
|
val userName: String?,
|
||||||
|
val password: String?,
|
||||||
|
val message: String?,
|
||||||
|
) {
|
||||||
|
var userId = 0
|
||||||
|
var groups = emptySet<String>()
|
||||||
|
val pass: Boolean get() = "PASS" == status
|
||||||
|
|
||||||
|
class Result(
|
||||||
|
status: String,
|
||||||
|
userName: String?,
|
||||||
|
password: String?,
|
||||||
|
message: String?,
|
||||||
|
) : LoginResult(status, userName, password, message)
|
||||||
|
|
||||||
|
class OAuthResult(
|
||||||
|
status: String,
|
||||||
|
userName: String?,
|
||||||
|
password: String?,
|
||||||
|
message: String?,
|
||||||
|
) : LoginResult(status, userName, password, message)
|
||||||
|
|
||||||
|
class EmailAuthResult(
|
||||||
|
status: String,
|
||||||
|
userName: String?,
|
||||||
|
password: String?,
|
||||||
|
message: String?,
|
||||||
|
) : LoginResult(status, userName, password, message)
|
||||||
|
|
||||||
|
class ResetPasswordResult(
|
||||||
|
status: String,
|
||||||
|
userName: String?,
|
||||||
|
password: String?,
|
||||||
|
message: String?,
|
||||||
|
) : LoginResult(status, userName, password, message)
|
||||||
|
}
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
|
||||||
import fr.free.nrw.commons.explore.ParentViewPager;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.theme.BaseActivity;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.contributions.ContributionController;
|
|
||||||
import javax.inject.Named;
|
|
||||||
|
|
||||||
public class BookmarkFragment extends CommonsDaggerSupportFragment {
|
|
||||||
|
|
||||||
private FragmentManager supportFragmentManager;
|
|
||||||
private BookmarksPagerAdapter adapter;
|
|
||||||
@BindView(R.id.viewPagerBookmarks)
|
|
||||||
ParentViewPager viewPager;
|
|
||||||
@BindView(R.id.tab_layout)
|
|
||||||
TabLayout tabLayout;
|
|
||||||
@BindView(R.id.fragmentContainer)
|
|
||||||
FrameLayout fragmentContainer;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
ContributionController controller;
|
|
||||||
/**
|
|
||||||
* To check if the user is loggedIn or not.
|
|
||||||
*/
|
|
||||||
@Inject
|
|
||||||
@Named("default_preferences")
|
|
||||||
public
|
|
||||||
JsonKvStore applicationKvStore;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static BookmarkFragment newInstance() {
|
|
||||||
BookmarkFragment fragment = new BookmarkFragment();
|
|
||||||
fragment.setRetainInstance(true);
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setScroll(boolean canScroll) {
|
|
||||||
viewPager.setCanScroll(canScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
|
||||||
@Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
super.onCreateView(inflater, container, savedInstanceState);
|
|
||||||
View view = inflater.inflate(R.layout.fragment_bookmarks, container, false);
|
|
||||||
ButterKnife.bind(this, view);
|
|
||||||
|
|
||||||
// Activity can call methods in the fragment by acquiring a
|
|
||||||
// reference to the Fragment from FragmentManager, using findFragmentById()
|
|
||||||
supportFragmentManager = getChildFragmentManager();
|
|
||||||
|
|
||||||
adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(),
|
|
||||||
applicationKvStore.getBoolean("login_skipped"));
|
|
||||||
viewPager.setAdapter(adapter);
|
|
||||||
tabLayout.setupWithViewPager(viewPager);
|
|
||||||
|
|
||||||
((MainActivity) getActivity()).showTabs();
|
|
||||||
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
|
||||||
|
|
||||||
setupTabLayout();
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method sets up the tab layout. If the adapter has only one element it sets the
|
|
||||||
* visibility of tabLayout to gone.
|
|
||||||
*/
|
|
||||||
public void setupTabLayout() {
|
|
||||||
tabLayout.setVisibility(View.VISIBLE);
|
|
||||||
if (adapter.getCount() == 1) {
|
|
||||||
tabLayout.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void onBackPressed() {
|
|
||||||
if (((BookmarkListRootFragment) (adapter.getItem(tabLayout.getSelectedTabPosition())))
|
|
||||||
.backPressed()) {
|
|
||||||
// The event is handled internally by the adapter , no further action required.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Event is not handled by the adapter ( performed back action ) change action bar.
|
|
||||||
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import fr.free.nrw.commons.contributions.ContributionController
|
||||||
|
import fr.free.nrw.commons.contributions.MainActivity
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentBookmarksBinding
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
|
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||||
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
class BookmarkFragment : CommonsDaggerSupportFragment() {
|
||||||
|
private var adapter: BookmarksPagerAdapter? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
var binding: FragmentBookmarksBinding? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
var controller: ContributionController? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To check if the user is loggedIn or not.
|
||||||
|
*/
|
||||||
|
@JvmField
|
||||||
|
@Inject
|
||||||
|
@Named("default_preferences")
|
||||||
|
var applicationKvStore: JsonKvStore? = null
|
||||||
|
|
||||||
|
fun setScroll(canScroll: Boolean) {
|
||||||
|
binding?.let {
|
||||||
|
it.viewPagerBookmarks.canScroll = canScroll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
binding = FragmentBookmarksBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
// Activity can call methods in the fragment by acquiring a
|
||||||
|
// reference to the Fragment from FragmentManager, using findFragmentById()
|
||||||
|
val supportFragmentManager = childFragmentManager
|
||||||
|
|
||||||
|
adapter = BookmarksPagerAdapter(
|
||||||
|
supportFragmentManager, requireContext(),
|
||||||
|
applicationKvStore!!.getBoolean("login_skipped")
|
||||||
|
)
|
||||||
|
binding!!.viewPagerBookmarks.adapter = adapter
|
||||||
|
binding!!.tabLayout.setupWithViewPager(binding!!.viewPagerBookmarks)
|
||||||
|
|
||||||
|
(requireActivity() as MainActivity).showTabs()
|
||||||
|
(requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
|
||||||
|
|
||||||
|
setupTabLayout()
|
||||||
|
return binding!!.root
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method sets up the tab layout. If the adapter has only one element it sets the
|
||||||
|
* visibility of tabLayout to gone.
|
||||||
|
*/
|
||||||
|
fun setupTabLayout() {
|
||||||
|
binding!!.tabLayout.visibility = View.VISIBLE
|
||||||
|
if (adapter!!.count == 1) {
|
||||||
|
binding!!.tabLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun onBackPressed() {
|
||||||
|
if (((adapter!!.getItem(binding!!.tabLayout.selectedTabPosition)) as BookmarkListRootFragment).backPressed()) {
|
||||||
|
// The event is handled internally by the adapter , no further action required.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event is not handled by the adapter ( performed back action ) change action bar.
|
||||||
|
(requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance(): BookmarkFragment = BookmarkFragment().apply {
|
||||||
|
retainInstance = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,256 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.Media;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment;
|
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
|
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
|
|
||||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
|
||||||
import fr.free.nrw.commons.category.GridViewAdapter;
|
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
|
||||||
import fr.free.nrw.commons.navtab.NavTab;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements
|
|
||||||
FragmentManager.OnBackStackChangedListener,
|
|
||||||
MediaDetailPagerFragment.MediaDetailProvider,
|
|
||||||
AdapterView.OnItemClickListener, CategoryImagesCallback {
|
|
||||||
|
|
||||||
private MediaDetailPagerFragment mediaDetails;
|
|
||||||
//private BookmarkPicturesFragment bookmarkPicturesFragment;
|
|
||||||
private BookmarkLocationsFragment bookmarkLocationsFragment;
|
|
||||||
public Fragment listFragment;
|
|
||||||
private BookmarksPagerAdapter bookmarksPagerAdapter;
|
|
||||||
|
|
||||||
@BindView(R.id.explore_container)
|
|
||||||
FrameLayout container;
|
|
||||||
|
|
||||||
public BookmarkListRootFragment() {
|
|
||||||
//empty constructor necessary otherwise crashes on recreate
|
|
||||||
}
|
|
||||||
|
|
||||||
public BookmarkListRootFragment(Bundle bundle, BookmarksPagerAdapter bookmarksPagerAdapter) {
|
|
||||||
String title = bundle.getString("categoryName");
|
|
||||||
int order = bundle.getInt("order");
|
|
||||||
final int orderItem = bundle.getInt("orderItem");
|
|
||||||
if (order == 0) {
|
|
||||||
listFragment = new BookmarkPicturesFragment();
|
|
||||||
} else {
|
|
||||||
listFragment = new BookmarkLocationsFragment();
|
|
||||||
if(orderItem == 2) {
|
|
||||||
listFragment = new BookmarkItemsFragment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Bundle featuredArguments = new Bundle();
|
|
||||||
featuredArguments.putString("categoryName", title);
|
|
||||||
listFragment.setArguments(featuredArguments);
|
|
||||||
this.bookmarksPagerAdapter = bookmarksPagerAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
|
||||||
@Nullable final ViewGroup container,
|
|
||||||
@Nullable final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
View view = inflater.inflate(R.layout.fragment_featured_root, container, false);
|
|
||||||
ButterKnife.bind(this, view);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
setFragment(listFragment, mediaDetails);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFragment(Fragment fragment, Fragment otherFragment) {
|
|
||||||
if (fragment.isAdded() && otherFragment != null) {
|
|
||||||
getChildFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.hide(otherFragment)
|
|
||||||
.show(fragment)
|
|
||||||
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
|
|
||||||
.commit();
|
|
||||||
getChildFragmentManager().executePendingTransactions();
|
|
||||||
} else if (fragment.isAdded() && otherFragment == null) {
|
|
||||||
getChildFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.show(fragment)
|
|
||||||
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
|
|
||||||
.commit();
|
|
||||||
getChildFragmentManager().executePendingTransactions();
|
|
||||||
} else if (!fragment.isAdded() && otherFragment != null) {
|
|
||||||
getChildFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.hide(otherFragment)
|
|
||||||
.add(R.id.explore_container, fragment)
|
|
||||||
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
|
|
||||||
.commit();
|
|
||||||
getChildFragmentManager().executePendingTransactions();
|
|
||||||
} else if (!fragment.isAdded()) {
|
|
||||||
getChildFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.replace(R.id.explore_container, fragment)
|
|
||||||
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
|
|
||||||
.commit();
|
|
||||||
getChildFragmentManager().executePendingTransactions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeFragment(Fragment fragment) {
|
|
||||||
getChildFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.remove(fragment)
|
|
||||||
.commit();
|
|
||||||
getChildFragmentManager().executePendingTransactions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(final Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMediaClicked(int position) {
|
|
||||||
Log.d("deneme8", "on media clicked");
|
|
||||||
/*container.setVisibility(View.VISIBLE);
|
|
||||||
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
|
|
||||||
mediaDetails = new MediaDetailPagerFragment(false, true, position);
|
|
||||||
setFragment(mediaDetails, bookmarkPicturesFragment);*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
|
|
||||||
*
|
|
||||||
* @param i It is the index of which media object is to be returned which is same as current
|
|
||||||
* index of viewPager.
|
|
||||||
* @return Media Object
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Media getMediaAtPosition(int i) {
|
|
||||||
if (bookmarksPagerAdapter.getMediaAdapter() == null) {
|
|
||||||
// not yet ready to return data
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return (Media) bookmarksPagerAdapter.getMediaAdapter().getItem(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
|
|
||||||
* same number of media items as that of media elements in adapter.
|
|
||||||
*
|
|
||||||
* @return Total Media count in the adapter
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int getTotalMediaCount() {
|
|
||||||
if (bookmarksPagerAdapter.getMediaAdapter() == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return bookmarksPagerAdapter.getMediaAdapter().getCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer getContributionStateAt(int position) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload media detail fragment once media is nominated
|
|
||||||
*
|
|
||||||
* @param index item position that has been nominated
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void refreshNominatedMedia(int index) {
|
|
||||||
if (mediaDetails != null && !listFragment.isVisible()) {
|
|
||||||
removeFragment(mediaDetails);
|
|
||||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
|
||||||
((BookmarkFragment) getParentFragment()).setScroll(false);
|
|
||||||
setFragment(mediaDetails, listFragment);
|
|
||||||
mediaDetails.showImage(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called on success of API call for featured images or mobile uploads. The
|
|
||||||
* viewpager will notified that number of items have changed.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void viewPagerNotifyDataSetChanged() {
|
|
||||||
if (mediaDetails != null) {
|
|
||||||
mediaDetails.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean backPressed() {
|
|
||||||
//check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException
|
|
||||||
if (mediaDetails != null) {
|
|
||||||
if (mediaDetails.isVisible()) {
|
|
||||||
// todo add get list fragment
|
|
||||||
((BookmarkFragment) getParentFragment()).setupTabLayout();
|
|
||||||
ArrayList<Integer> removed = mediaDetails.getRemovedItems();
|
|
||||||
removeFragment(mediaDetails);
|
|
||||||
((BookmarkFragment) getParentFragment()).setScroll(true);
|
|
||||||
setFragment(listFragment, mediaDetails);
|
|
||||||
((MainActivity) getActivity()).showTabs();
|
|
||||||
if (listFragment instanceof BookmarkPicturesFragment) {
|
|
||||||
GridViewAdapter adapter = ((GridViewAdapter) ((BookmarkPicturesFragment) listFragment)
|
|
||||||
.getAdapter());
|
|
||||||
Iterator i = removed.iterator();
|
|
||||||
while (i.hasNext()) {
|
|
||||||
adapter.remove(adapter.getItem((int) i.next()));
|
|
||||||
}
|
|
||||||
mediaDetails.clearRemoved();
|
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
moveToContributionsFragment();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
moveToContributionsFragment();
|
|
||||||
}
|
|
||||||
// notify mediaDetails did not handled the backPressed further actions required.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void moveToContributionsFragment() {
|
|
||||||
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
|
|
||||||
((MainActivity) getActivity()).showTabs();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
|
||||||
Log.d("deneme8", "on media clicked");
|
|
||||||
container.setVisibility(View.VISIBLE);
|
|
||||||
((BookmarkFragment) getParentFragment()).tabLayout.setVisibility(View.GONE);
|
|
||||||
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
|
|
||||||
((BookmarkFragment) getParentFragment()).setScroll(false);
|
|
||||||
setFragment(mediaDetails, listFragment);
|
|
||||||
mediaDetails.showImage(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackStackChanged() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.AdapterView.OnItemClickListener
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import fr.free.nrw.commons.Media
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment
|
||||||
|
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment
|
||||||
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment
|
||||||
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment
|
||||||
|
import fr.free.nrw.commons.category.CategoryImagesCallback
|
||||||
|
import fr.free.nrw.commons.category.GridViewAdapter
|
||||||
|
import fr.free.nrw.commons.contributions.MainActivity
|
||||||
|
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
||||||
|
import fr.free.nrw.commons.media.MediaDetailPagerFragment.Companion.newInstance
|
||||||
|
import fr.free.nrw.commons.media.MediaDetailProvider
|
||||||
|
import fr.free.nrw.commons.navtab.NavTab
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class BookmarkListRootFragment : CommonsDaggerSupportFragment,
|
||||||
|
FragmentManager.OnBackStackChangedListener, MediaDetailProvider, OnItemClickListener,
|
||||||
|
CategoryImagesCallback {
|
||||||
|
private var mediaDetails: MediaDetailPagerFragment? = null
|
||||||
|
private val bookmarkLocationsFragment: BookmarkLocationsFragment? = null
|
||||||
|
var listFragment: Fragment? = null
|
||||||
|
private var bookmarksPagerAdapter: BookmarksPagerAdapter? = null
|
||||||
|
|
||||||
|
var binding: FragmentFeaturedRootBinding? = null
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
|
||||||
|
constructor(bundle: Bundle, bookmarksPagerAdapter: BookmarksPagerAdapter) {
|
||||||
|
val title = bundle.getString("categoryName")
|
||||||
|
val order = bundle.getInt("order")
|
||||||
|
val orderItem = bundle.getInt("orderItem")
|
||||||
|
|
||||||
|
when (order) {
|
||||||
|
0 -> listFragment = BookmarkPicturesFragment()
|
||||||
|
1 -> listFragment = BookmarkLocationsFragment()
|
||||||
|
3 -> listFragment = BookmarkCategoriesFragment()
|
||||||
|
}
|
||||||
|
if (orderItem == 2) {
|
||||||
|
listFragment = BookmarkItemsFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
val featuredArguments = Bundle()
|
||||||
|
featuredArguments.putString("categoryName", title)
|
||||||
|
listFragment!!.setArguments(featuredArguments)
|
||||||
|
this.bookmarksPagerAdapter = bookmarksPagerAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false)
|
||||||
|
return binding!!.getRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
setFragment(listFragment!!, mediaDetails)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFragment(fragment: Fragment, otherFragment: Fragment?) {
|
||||||
|
if (fragment.isAdded() && otherFragment != null) {
|
||||||
|
getChildFragmentManager()
|
||||||
|
.beginTransaction()
|
||||||
|
.hide(otherFragment)
|
||||||
|
.show(fragment)
|
||||||
|
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
|
||||||
|
.commit()
|
||||||
|
getChildFragmentManager().executePendingTransactions()
|
||||||
|
} else if (fragment.isAdded() && otherFragment == null) {
|
||||||
|
getChildFragmentManager()
|
||||||
|
.beginTransaction()
|
||||||
|
.show(fragment)
|
||||||
|
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
|
||||||
|
.commit()
|
||||||
|
getChildFragmentManager().executePendingTransactions()
|
||||||
|
} else if (!fragment.isAdded() && otherFragment != null) {
|
||||||
|
getChildFragmentManager()
|
||||||
|
.beginTransaction()
|
||||||
|
.hide(otherFragment)
|
||||||
|
.add(R.id.explore_container, fragment)
|
||||||
|
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
|
||||||
|
.commit()
|
||||||
|
getChildFragmentManager().executePendingTransactions()
|
||||||
|
} else if (!fragment.isAdded()) {
|
||||||
|
getChildFragmentManager()
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.explore_container, fragment)
|
||||||
|
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
|
||||||
|
.commit()
|
||||||
|
getChildFragmentManager().executePendingTransactions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeFragment(fragment: Fragment) {
|
||||||
|
getChildFragmentManager()
|
||||||
|
.beginTransaction()
|
||||||
|
.remove(fragment)
|
||||||
|
.commit()
|
||||||
|
getChildFragmentManager().executePendingTransactions()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMediaClicked(position: Int) {
|
||||||
|
Timber.d("on media clicked")
|
||||||
|
/*container.setVisibility(View.VISIBLE);
|
||||||
|
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
|
||||||
|
mediaDetails = new MediaDetailPagerFragment(false, true, position);
|
||||||
|
setFragment(mediaDetails, bookmarkPicturesFragment);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
|
||||||
|
*
|
||||||
|
* @param i It is the index of which media object is to be returned which is same as current
|
||||||
|
* index of viewPager.
|
||||||
|
* @return Media Object
|
||||||
|
*/
|
||||||
|
override fun getMediaAtPosition(i: Int): Media? =
|
||||||
|
bookmarksPagerAdapter!!.mediaAdapter?.getItem(i) as Media?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
|
||||||
|
* same number of media items as that of media elements in adapter.
|
||||||
|
*
|
||||||
|
* @return Total Media count in the adapter
|
||||||
|
*/
|
||||||
|
override fun getTotalMediaCount(): Int =
|
||||||
|
bookmarksPagerAdapter!!.mediaAdapter?.count ?: 0
|
||||||
|
|
||||||
|
override fun getContributionStateAt(position: Int): Int? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload media detail fragment once media is nominated
|
||||||
|
*
|
||||||
|
* @param index item position that has been nominated
|
||||||
|
*/
|
||||||
|
override fun refreshNominatedMedia(index: Int) {
|
||||||
|
if (mediaDetails != null && !listFragment!!.isVisible()) {
|
||||||
|
removeFragment(mediaDetails!!)
|
||||||
|
mediaDetails = newInstance(false, true)
|
||||||
|
(parentFragment as BookmarkFragment).setScroll(false)
|
||||||
|
setFragment(mediaDetails!!, listFragment)
|
||||||
|
mediaDetails!!.showImage(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called on success of API call for featured images or mobile uploads. The
|
||||||
|
* viewpager will notified that number of items have changed.
|
||||||
|
*/
|
||||||
|
override fun viewPagerNotifyDataSetChanged() {
|
||||||
|
if (mediaDetails != null) {
|
||||||
|
mediaDetails!!.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun backPressed(): Boolean {
|
||||||
|
//check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException
|
||||||
|
if (mediaDetails != null) {
|
||||||
|
if (mediaDetails!!.isVisible()) {
|
||||||
|
// todo add get list fragment
|
||||||
|
(parentFragment as BookmarkFragment).setupTabLayout()
|
||||||
|
val removed: ArrayList<Int> = mediaDetails!!.removedItems
|
||||||
|
removeFragment(mediaDetails!!)
|
||||||
|
(parentFragment as BookmarkFragment).setScroll(true)
|
||||||
|
setFragment(listFragment!!, mediaDetails)
|
||||||
|
(requireActivity() as MainActivity).showTabs()
|
||||||
|
if (listFragment is BookmarkPicturesFragment) {
|
||||||
|
val adapter = ((listFragment as BookmarkPicturesFragment)
|
||||||
|
.getAdapter() as GridViewAdapter?)
|
||||||
|
val i: MutableIterator<*> = removed.iterator()
|
||||||
|
while (i.hasNext()) {
|
||||||
|
adapter!!.remove(adapter.getItem(i.next() as Int))
|
||||||
|
}
|
||||||
|
mediaDetails!!.clearRemoved()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
moveToContributionsFragment()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
moveToContributionsFragment()
|
||||||
|
}
|
||||||
|
// notify mediaDetails did not handled the backPressed further actions required.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveToContributionsFragment() {
|
||||||
|
(requireActivity() as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code())
|
||||||
|
(requireActivity() as MainActivity).showTabs()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
Timber.d("on media clicked")
|
||||||
|
binding!!.exploreContainer.visibility = View.VISIBLE
|
||||||
|
(parentFragment as BookmarkFragment).binding!!.tabLayout.setVisibility(View.GONE)
|
||||||
|
mediaDetails = newInstance(false, true)
|
||||||
|
(parentFragment as BookmarkFragment).setScroll(false)
|
||||||
|
setFragment(mediaDetails!!, listFragment)
|
||||||
|
mediaDetails!!.showImage(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackStackChanged() = Unit
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks;
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data class for handling a bookmark fragment and it title
|
|
||||||
*/
|
|
||||||
public class BookmarkPages {
|
|
||||||
private Fragment page;
|
|
||||||
private String title;
|
|
||||||
|
|
||||||
BookmarkPages(Fragment fragment, String title) {
|
|
||||||
this.title = title;
|
|
||||||
this.page = fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the fragment
|
|
||||||
* @return fragment object
|
|
||||||
*/
|
|
||||||
public Fragment getPage() {
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the fragment title
|
|
||||||
* @return title
|
|
||||||
*/
|
|
||||||
public String getTitle() {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
|
data class BookmarkPages (
|
||||||
|
val page: Fragment? = null,
|
||||||
|
val title: String? = null
|
||||||
|
)
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
package fr.free.nrw.commons.bookmarks;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.widget.ListAdapter;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
|
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
|
|
||||||
|
|
||||||
public class BookmarksPagerAdapter extends FragmentPagerAdapter {
|
|
||||||
|
|
||||||
private ArrayList<BookmarkPages> pages;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default Constructor
|
|
||||||
* @param fm
|
|
||||||
* @param context
|
|
||||||
* @param onlyPictures is true if the fragment requires only BookmarkPictureFragment
|
|
||||||
* (i.e. when no user is logged in).
|
|
||||||
*/
|
|
||||||
BookmarksPagerAdapter(FragmentManager fm, Context context,boolean onlyPictures) {
|
|
||||||
super(fm);
|
|
||||||
pages = new ArrayList<>();
|
|
||||||
Bundle picturesBundle = new Bundle();
|
|
||||||
picturesBundle.putString("categoryName", context.getString(R.string.title_page_bookmarks_pictures));
|
|
||||||
picturesBundle.putInt("order", 0);
|
|
||||||
pages.add(new BookmarkPages(
|
|
||||||
new BookmarkListRootFragment(picturesBundle, this),
|
|
||||||
context.getString(R.string.title_page_bookmarks_pictures)));
|
|
||||||
if (!onlyPictures) {
|
|
||||||
// if onlyPictures is false we also add the location fragment.
|
|
||||||
Bundle locationBundle = new Bundle();
|
|
||||||
locationBundle.putString("categoryName",
|
|
||||||
context.getString(R.string.title_page_bookmarks_locations));
|
|
||||||
locationBundle.putInt("order", 1);
|
|
||||||
pages.add(new BookmarkPages(
|
|
||||||
new BookmarkListRootFragment(locationBundle, this),
|
|
||||||
context.getString(R.string.title_page_bookmarks_locations)));
|
|
||||||
|
|
||||||
locationBundle.putInt("orderItem", 2);
|
|
||||||
pages.add(new BookmarkPages(
|
|
||||||
new BookmarkListRootFragment(locationBundle, this),
|
|
||||||
context.getString(R.string.title_page_bookmarks_items)));
|
|
||||||
}
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Fragment getItem(int position) {
|
|
||||||
return pages.get(position).getPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return pages.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public CharSequence getPageTitle(int position) {
|
|
||||||
return pages.get(position).getTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the Adapter used to display the picture gridview
|
|
||||||
* @return adapter
|
|
||||||
*/
|
|
||||||
public ListAdapter getMediaAdapter() {
|
|
||||||
BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment);
|
|
||||||
return fragment.getAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the pictures list for the bookmark fragment
|
|
||||||
*/
|
|
||||||
public void requestPictureListUpdate() {
|
|
||||||
BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment);
|
|
||||||
fragment.onResume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package fr.free.nrw.commons.bookmarks
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.ListAdapter
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.FragmentPagerAdapter
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment
|
||||||
|
|
||||||
|
class BookmarksPagerAdapter internal constructor(
|
||||||
|
fm: FragmentManager, context: Context, onlyPictures: Boolean
|
||||||
|
) : FragmentPagerAdapter(fm) {
|
||||||
|
private val pages = mutableListOf<BookmarkPages>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Constructor
|
||||||
|
* @param fm
|
||||||
|
* @param context
|
||||||
|
* @param onlyPictures is true if the fragment requires only BookmarkPictureFragment
|
||||||
|
* (i.e. when no user is logged in).
|
||||||
|
*/
|
||||||
|
init {
|
||||||
|
pages.add(
|
||||||
|
BookmarkPages(
|
||||||
|
BookmarkListRootFragment(
|
||||||
|
bundleOf(
|
||||||
|
"categoryName" to context.getString(R.string.title_page_bookmarks_pictures),
|
||||||
|
"order" to 0
|
||||||
|
), this
|
||||||
|
), context.getString(R.string.title_page_bookmarks_pictures)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (!onlyPictures) {
|
||||||
|
// if onlyPictures is false we also add the location fragment.
|
||||||
|
val locationBundle = bundleOf(
|
||||||
|
"categoryName" to context.getString(R.string.title_page_bookmarks_locations),
|
||||||
|
"order" to 1
|
||||||
|
)
|
||||||
|
|
||||||
|
pages.add(
|
||||||
|
BookmarkPages(
|
||||||
|
BookmarkListRootFragment(locationBundle, this),
|
||||||
|
context.getString(R.string.title_page_bookmarks_locations)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
locationBundle.putInt("orderItem", 2)
|
||||||
|
pages.add(
|
||||||
|
BookmarkPages(
|
||||||
|
BookmarkListRootFragment(locationBundle, this),
|
||||||
|
context.getString(R.string.title_page_bookmarks_items)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pages.add(
|
||||||
|
BookmarkPages(
|
||||||
|
BookmarkListRootFragment(
|
||||||
|
bundleOf(
|
||||||
|
"categoryName" to context.getString(R.string.title_page_bookmarks_categories),
|
||||||
|
"order" to 3
|
||||||
|
), this),
|
||||||
|
context.getString(R.string.title_page_bookmarks_categories)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItem(position: Int): Fragment = pages[position].page!!
|
||||||
|
|
||||||
|
override fun getCount(): Int = pages.size
|
||||||
|
|
||||||
|
override fun getPageTitle(position: Int): CharSequence? = pages[position].title
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Adapter used to display the picture gridview
|
||||||
|
* @return adapter
|
||||||
|
*/
|
||||||
|
val mediaAdapter: ListAdapter?
|
||||||
|
get() = (((pages[0].page as BookmarkListRootFragment).listFragment) as BookmarkPicturesFragment).getAdapter()
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue