Compare commits
1692 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b25628f7a | ||
|
|
e06dd1e465 | ||
|
|
a42ab10aed | ||
|
|
cd2ea90006 | ||
|
|
f0295edd83 | ||
|
|
eee0a40224 | ||
|
|
bd189ae322 | ||
|
|
714aa7a20b | ||
|
|
76c6c65018 | ||
|
|
c511b54de2 | ||
|
|
16b4a1f1a3 | ||
|
|
afb5743d31 | ||
|
|
7e91c311d4 | ||
|
|
eef9f61e20 | ||
|
|
0cc55891c1 | ||
|
|
e892eaa3d5 | ||
|
|
bcdce2a880 | ||
|
|
9f38809730 | ||
|
|
77b416a52d | ||
|
|
0684b0ec79 | ||
|
|
20057a55f0 | ||
|
|
a668dee567 | ||
|
|
11bda2fe07 | ||
|
|
a7b83bc058 | ||
|
|
19950e5115 | ||
|
|
11be65c6fe | ||
|
|
b30ce84468 | ||
|
|
c242c7ec82 | ||
|
|
f128556a49 | ||
|
|
3cebc78443 | ||
|
|
c640501430 | ||
|
|
48100a19e1 | ||
|
|
102ca8034a | ||
|
|
c1d8f5904b | ||
|
|
361086fc48 | ||
|
|
10ebacf9cf | ||
|
|
02bfb34665 | ||
|
|
9f9ba7c367 | ||
|
|
4430822768 | ||
|
|
b4904024c6 | ||
|
|
0d047e5cf9 | ||
|
|
58259d9fb0 | ||
|
|
e9dde114b7 | ||
|
|
03c6f4130b | ||
|
|
bb94c7fff4 | ||
|
|
217884aaec | ||
|
|
17508e1f36 | ||
|
|
95b33b35df | ||
|
|
4c0ae74f2d | ||
|
|
9ef9110297 | ||
|
|
71e0115cd6 | ||
|
|
ec045abf53 | ||
|
|
b8ae79faa1 | ||
|
|
b51cccf1d7 | ||
|
|
87b78acd39 | ||
|
|
a965862c1b | ||
|
|
ce427d2a0b | ||
|
|
8ea4c84a29 | ||
|
|
0bb66708c6 | ||
|
|
bf4b04ef5f | ||
|
|
eea2f9c0cd | ||
|
|
3cdc27eb94 | ||
|
|
75237ab7dc | ||
|
|
7601a7de16 | ||
|
|
3745240eea | ||
|
|
963b80cce9 | ||
|
|
8812500734 | ||
|
|
bd41807f3b | ||
|
|
90ee102eaa | ||
|
|
0bac414151 | ||
|
|
36bd1db67e | ||
|
|
d15f3e5daf | ||
|
|
4c9c444dfc | ||
|
|
0dc23789f5 | ||
|
|
3f0ebd4aed | ||
|
|
1e501c707c | ||
|
|
d57f97b492 | ||
|
|
29f9214869 | ||
|
|
c6c1fe3595 | ||
|
|
2ba4c6f443 | ||
|
|
a9ff948818 | ||
|
|
7dd9cfa7f0 | ||
|
|
15a415558b | ||
|
|
b3e53bc48d | ||
|
|
d0e33c8a12 | ||
|
|
060db42e47 | ||
|
|
6bcf259de9 | ||
|
|
4d0a673209 | ||
|
|
be9be6dc35 | ||
|
|
3f47497c12 | ||
|
|
268e0af7cb | ||
|
|
c611f723b7 | ||
|
|
4c9f500122 | ||
|
|
15147dddf5 | ||
|
|
747c81c269 | ||
|
|
63ad076046 | ||
|
|
f109033ed2 | ||
|
|
9445d3383a | ||
|
|
5db5537685 | ||
|
|
1149d1c37f | ||
|
|
e4b77551f7 | ||
|
|
401fd298f5 | ||
|
|
6a1f7f7238 | ||
|
|
7c1c0894a8 | ||
|
|
cb96ae6cbc | ||
|
|
c15d972359 | ||
|
|
c010e2371c | ||
|
|
f88a244594 | ||
|
|
e2dccda205 | ||
|
|
bc6c6bc9a5 | ||
|
|
3281eabbe1 | ||
|
|
92de091228 | ||
|
|
94ab8a4e8b | ||
|
|
799987205e | ||
|
|
f5dfe70ac6 | ||
|
|
69b03e2e59 | ||
|
|
588ebe11f4 | ||
|
|
4fe0d6e893 | ||
|
|
787c20794f | ||
|
|
c462e72279 | ||
|
|
be7e6afce2 | ||
|
|
feb9920829 | ||
|
|
4d6a1a705d | ||
|
|
59d54469dc | ||
|
|
9cccd28447 | ||
|
|
da13b4602c | ||
|
|
2084a33192 | ||
|
|
542ce40c1a | ||
|
|
ddd4ea329c | ||
|
|
b5c3875ce9 | ||
|
|
ec8387d9db | ||
|
|
cf464e8eea | ||
|
|
7b66f1c398 | ||
|
|
814e11c1f4 | ||
|
|
647277444b | ||
|
|
bd4ff9c7ec | ||
|
|
7ed6195cca | ||
|
|
43397f069a | ||
|
|
8e34e31b35 | ||
|
|
7ef1bdfc2a | ||
|
|
726f91ec75 | ||
|
|
55a5268a3c | ||
|
|
4777e3d0b7 | ||
|
|
bfc970abe5 | ||
|
|
d2d35f4f39 | ||
|
|
49cef5ea0a | ||
|
|
f2fbf55c53 | ||
|
|
eacfd2fa4f | ||
|
|
51f87848f4 | ||
|
|
c3f4637ddc | ||
|
|
2361394391 | ||
|
|
38fdf8d53e | ||
|
|
0cd3cab65d | ||
|
|
142c8b55a1 | ||
|
|
3274acaba8 | ||
|
|
99b7d46ddf | ||
|
|
4f7e45211f | ||
|
|
221773bf71 | ||
|
|
305ce2ca42 | ||
|
|
7dc2480400 | ||
|
|
9634ec6616 | ||
|
|
6f29540170 | ||
|
|
18485c1cf8 | ||
|
|
7c36c32e4b | ||
|
|
1d0af51813 | ||
|
|
d50060d602 | ||
|
|
9d455b2331 | ||
|
|
5f3b537f7d | ||
|
|
1d43ed4c8a | ||
|
|
e839323575 | ||
|
|
7ee657012e | ||
|
|
847fbed956 | ||
|
|
a89d5df313 | ||
|
|
5f58789008 | ||
|
|
7b64cad668 | ||
|
|
70f60f094d | ||
|
|
a5eff14552 | ||
|
|
36fb929387 | ||
|
|
66aabfb386 | ||
|
|
f503973db1 | ||
|
|
7cec835509 | ||
|
|
e300942455 | ||
|
|
79476eff85 | ||
|
|
b4ec470691 | ||
|
|
4f72a0c74e | ||
|
|
bcbb41aa43 | ||
|
|
5cc38d845a | ||
|
|
36acc1588a | ||
|
|
349c5200ed | ||
|
|
ff71f6a092 | ||
|
|
8863284970 | ||
|
|
d5feb4e9f9 | ||
|
|
4c284226e5 | ||
|
|
fef9cf5e64 | ||
|
|
93f3c9a9eb | ||
|
|
5827c77b0c | ||
|
|
1b36866fba | ||
|
|
ebc10d5052 | ||
|
|
8953fa48c7 | ||
|
|
59e7a296ca | ||
|
|
86432228a3 | ||
|
|
4518566c37 | ||
|
|
b392a89350 | ||
|
|
225682f35d | ||
|
|
b41ff2e18f | ||
|
|
6ef76fb5bb | ||
|
|
a36679b032 | ||
|
|
dde91778a2 | ||
|
|
1cdc6f4fcf | ||
|
|
b8a5346631 | ||
|
|
5cf222379a | ||
|
|
8f4ff49b32 | ||
|
|
3a68ca3cc0 | ||
|
|
116dc68a38 | ||
|
|
df84b0ac34 | ||
|
|
5ef737766c | ||
|
|
be17ba870b | ||
|
|
747b2d5801 | ||
|
|
b84c9bf948 | ||
|
|
b1e999cc9c | ||
|
|
89951a8547 | ||
|
|
1bb0ac1110 | ||
|
|
5bb51901f7 | ||
|
|
18562cd3ee | ||
|
|
9e01270b1e | ||
|
|
0b1ff9730c | ||
|
|
845cfde58e | ||
|
|
f81f264b37 | ||
|
|
5e14270c47 | ||
|
|
6d2427b336 | ||
|
|
d89562a4c0 | ||
|
|
08389b023d | ||
|
|
797c4d6baa | ||
|
|
9458ddd490 | ||
|
|
8f3a8af35e | ||
|
|
d0f927c8d2 | ||
|
|
2de44c8d7f | ||
|
|
9a3ab2f4d2 | ||
|
|
7cecd689bb | ||
|
|
b79cf4e087 | ||
|
|
63a9ce6eb6 | ||
|
|
1f960e8631 | ||
|
|
6dc059646e | ||
|
|
5b37db0f8e | ||
|
|
2789dd9fd1 | ||
|
|
a2a72a4aee | ||
|
|
b79fc8132a | ||
|
|
91b8735a4c | ||
|
|
313d81ffe1 | ||
|
|
192b32c1c6 | ||
|
|
2ef8be7c59 | ||
|
|
94e8d5e6d9 | ||
|
|
445600653e | ||
|
|
4349c7a9e7 | ||
|
|
c2b2c39c8a | ||
|
|
9dee6eea24 | ||
|
|
53355b31ea | ||
|
|
c2f55675a8 | ||
|
|
42f3c58d02 | ||
|
|
dac2c413d6 | ||
|
|
8dffbff97c | ||
|
|
a539eb3768 | ||
|
|
481610cd10 | ||
|
|
efb8cd565b | ||
|
|
fa78a0f6ca | ||
|
|
bcc96ff329 | ||
|
|
bb3028fff6 | ||
|
|
3e66ce8949 | ||
|
|
1f5bdb975b | ||
|
|
22dfc33974 | ||
|
|
5f89fb1e49 | ||
|
|
b5b3c2671a | ||
|
|
6a8c09c113 | ||
|
|
9660a2a019 | ||
|
|
f667b657f6 | ||
|
|
9db309634e | ||
|
|
8a96762bcc | ||
|
|
6915d19fb4 | ||
|
|
d58b24722e | ||
|
|
fe8904b7a5 | ||
|
|
6f3404aac9 | ||
|
|
9a81f720c2 | ||
|
|
3605ad4616 | ||
|
|
490ecfcb43 | ||
|
|
02b8ac55d5 | ||
|
|
fd71f04ca5 | ||
|
|
091953ada8 | ||
|
|
8707db891a | ||
|
|
1c67cb5edb | ||
|
|
a96567c329 | ||
|
|
691372119a | ||
|
|
25734af54e | ||
|
|
b37f9abeae | ||
|
|
f502374533 | ||
|
|
f10da18272 | ||
|
|
940f2ca73f | ||
|
|
db3192e75a | ||
|
|
1841568e7e | ||
|
|
f4ce0e67ac | ||
|
|
92b34f085e | ||
|
|
4e4eb05526 | ||
|
|
4dc707871d | ||
|
|
e9562378b4 | ||
|
|
c1b9aa7826 | ||
|
|
e979a348be | ||
|
|
89c5787ad6 | ||
|
|
f0b2329656 | ||
|
|
6cf9969220 | ||
|
|
1f37e7605e | ||
|
|
cf6af6f912 | ||
|
|
2c91adb03e | ||
|
|
911da90854 | ||
|
|
27f0235055 | ||
|
|
f3764222d8 | ||
|
|
64cb8c4a9a | ||
|
|
de78356f5a | ||
|
|
8a9d39397c | ||
|
|
2d89fd0cf0 | ||
|
|
402620dbe4 | ||
|
|
32776db395 | ||
|
|
4523ab8a67 | ||
|
|
760106bf5b | ||
|
|
ad2ef39ace | ||
|
|
f705afcafc | ||
|
|
3cff655e6f | ||
|
|
ed86a5a3e8 | ||
|
|
f329435f51 | ||
|
|
6a6a80bcd7 | ||
|
|
62e4983f02 | ||
|
|
6dfd991e87 | ||
|
|
e205462bf4 | ||
|
|
03f341f6f8 | ||
|
|
b9b08c5ea7 | ||
|
|
2b5498ff5d | ||
|
|
84b058873d | ||
|
|
fcf5c0822e | ||
|
|
53c3da6a3d | ||
|
|
68371c9a0f | ||
|
|
e7295aac07 | ||
|
|
ae7f65954a | ||
|
|
350a73c3eb | ||
|
|
66d8ba9b5d | ||
|
|
f944b12f45 | ||
|
|
61928a1cf0 | ||
|
|
f06196802e | ||
|
|
e162833ad7 | ||
|
|
936ffdc793 | ||
|
|
0bbf6abc0c | ||
|
|
5552dc2ac6 | ||
|
|
a65d6fbeb3 | ||
|
|
43612ffbc1 | ||
|
|
971881bbd3 | ||
|
|
390cc6b65d | ||
|
|
ee31288769 | ||
|
|
401986af29 | ||
|
|
e41e89c5cd | ||
|
|
53de0cfc63 | ||
|
|
e68481395f | ||
|
|
9a361e0688 | ||
|
|
b8cce74824 | ||
|
|
f1ad6fc511 | ||
|
|
2aa4cc1a88 | ||
|
|
fb17ba4777 | ||
|
|
6e3c464c97 | ||
|
|
640e5163a8 | ||
|
|
fdd3f2f398 | ||
|
|
dfc55a13b8 | ||
|
|
5a83b79ac2 | ||
|
|
7d954ab3c2 | ||
|
|
ec0b830f4f | ||
|
|
26256b67d3 | ||
|
|
2f9d60b9c0 | ||
|
|
499a325bc8 | ||
|
|
97ca2634a0 | ||
|
|
6630f0f8da | ||
|
|
129b253176 | ||
|
|
c2382d065e | ||
|
|
085264755a | ||
|
|
baac955e52 | ||
|
|
4a0501209a | ||
|
|
e471b36d39 | ||
|
|
5c2961cf7c | ||
|
|
6e980f17c6 | ||
|
|
2860ce8755 | ||
|
|
ca25a868a0 | ||
|
|
74f3bd5905 | ||
|
|
e0a53b4296 | ||
|
|
c20f043f38 | ||
|
|
daf3005178 | ||
|
|
d17e24faae | ||
|
|
0cd17accf9 | ||
|
|
65f7b97e60 | ||
|
|
c7324285f3 | ||
|
|
6bc795ebea | ||
|
|
f2616cdd58 | ||
|
|
d50f65ffd8 | ||
|
|
b39b2d0544 | ||
|
|
cdaaa91bcc | ||
|
|
109dca0b8a | ||
|
|
ee87da564b | ||
|
|
b143559a0f | ||
|
|
9b89727c80 | ||
|
|
68a252c85c | ||
|
|
d99cb91e89 | ||
|
|
38879cd2fe | ||
|
|
af4d98a48b | ||
|
|
39bb93d650 | ||
|
|
0a3568f424 | ||
|
|
e0b45720f0 | ||
|
|
f5b7024bb5 | ||
|
|
f1bfa1f598 | ||
|
|
653304f9a4 | ||
|
|
3d416a038a | ||
|
|
e0eeb87182 | ||
|
|
2570a86da9 | ||
|
|
7b110f16b3 | ||
|
|
d170e87325 | ||
|
|
4a60f0c576 | ||
|
|
4b5e9d604c | ||
|
|
f9562d5087 | ||
|
|
786091c0a4 | ||
|
|
436b8240ef | ||
|
|
e7253dcf97 | ||
|
|
48f9aabaf7 | ||
|
|
14d353ae27 | ||
|
|
9a82846b84 | ||
|
|
a4c9bbadc4 | ||
|
|
fa70c55084 | ||
|
|
8d0a89fb06 | ||
|
|
3caf6cb94c | ||
|
|
f4854061ea | ||
|
|
bf7607674e | ||
|
|
137a8ca27b | ||
|
|
b9ed4e0ee2 | ||
|
|
bc04672d32 | ||
|
|
70c668ecf1 | ||
|
|
64bbe2c438 | ||
|
|
32209e766e | ||
|
|
99349cff0a | ||
|
|
74ca1961e0 | ||
|
|
ef6ba7fe0c | ||
|
|
9ace2b71cc | ||
|
|
0c54654b8b | ||
|
|
bf686309fb | ||
|
|
ce4f46537b | ||
|
|
4c43207f17 | ||
|
|
afe5bcd1f3 | ||
|
|
3bda81bd43 | ||
|
|
7339b2325f | ||
|
|
ee84a9ee7e | ||
|
|
fef594150a | ||
|
|
10371f69cb | ||
|
|
75cf3d76fb | ||
|
|
51a7d00c47 | ||
|
|
9ac8261cc4 | ||
|
|
1f4ad80b7d | ||
|
|
4b090f0d68 | ||
|
|
4002bcde26 | ||
|
|
ded3777b40 | ||
|
|
7236066003 | ||
|
|
033f07ea09 | ||
|
|
283c0cba4b | ||
|
|
e3a1fc2fbb | ||
|
|
95de9e2917 | ||
|
|
a82ebeed11 | ||
|
|
3a3aa0be1c | ||
|
|
e72491c2d1 | ||
|
|
36dede1f93 | ||
|
|
ed15daf9e9 | ||
|
|
c6052c841d | ||
|
|
ce39c7ca8f | ||
|
|
b7723dcb98 | ||
|
|
ad0774f8a5 | ||
|
|
9172feb72b | ||
|
|
a297bd3281 | ||
|
|
e713a9cfc3 | ||
|
|
195395a22d | ||
|
|
7b6a62b047 | ||
|
|
ada1c9ff6d | ||
|
|
5a0a14ed56 | ||
|
|
cad3879646 | ||
|
|
5d961991d4 | ||
|
|
e27536743f | ||
|
|
9f8d4a0f34 | ||
|
|
67b6a89fd9 | ||
|
|
dabc4058ba | ||
|
|
6c468602c6 | ||
|
|
9c5d29a860 | ||
|
|
da5e2a6b50 | ||
|
|
a194569fd4 | ||
|
|
78a4ace9b2 | ||
|
|
9a664088cd | ||
|
|
1d2e6f880b | ||
|
|
2cd2918d53 | ||
|
|
9b49db6677 | ||
|
|
9f6c61e5c0 | ||
|
|
b6a2bb7881 | ||
|
|
62262010b9 | ||
|
|
72fe9a04a6 | ||
|
|
d8cf55ae21 | ||
|
|
dfb393b934 | ||
|
|
cd27716f6a | ||
|
|
469553b34e | ||
|
|
5d7c37262e | ||
|
|
3f3867473f | ||
|
|
b08cd1eb4b | ||
|
|
1f9ff8d341 | ||
|
|
528b362f64 | ||
|
|
1db10c5047 | ||
|
|
f295f5f4e7 | ||
|
|
08924bd9b0 | ||
|
|
5d432435a1 | ||
|
|
8bd76aa833 | ||
|
|
2147cb87ac | ||
|
|
00ed0f5402 | ||
|
|
870f79f6cd | ||
|
|
da879213fc | ||
|
|
db66974bd6 | ||
|
|
e3d5ae1d65 | ||
|
|
b06bc5b3b7 | ||
|
|
a4c988012d | ||
|
|
a200701e4c | ||
|
|
e8f604792c | ||
|
|
c8b0666ef9 | ||
|
|
13aa72b150 | ||
|
|
6694074b18 | ||
|
|
63aa32c636 | ||
|
|
5fbab870c3 | ||
|
|
4a34e248e0 | ||
|
|
2c45165e53 | ||
|
|
3f029ac45b | ||
|
|
a4cf76d5ba | ||
|
|
3044000cf8 | ||
|
|
ab1ef5cfd8 | ||
|
|
16b91a283a | ||
|
|
e9fbdc21fa | ||
|
|
b429e662aa | ||
|
|
834ad1736e | ||
|
|
91021699d2 | ||
|
|
0f86aa12ab | ||
|
|
fb7bf6f308 | ||
|
|
5aa67aaa78 | ||
|
|
2e892e7305 | ||
|
|
6486a1689f | ||
|
|
5966535111 | ||
|
|
a2cf4bda99 | ||
|
|
7a93c8615d | ||
|
|
cf29f11cea | ||
|
|
23188a26d7 | ||
|
|
0480dc0140 | ||
|
|
cb14b29c78 | ||
|
|
bf68272de3 | ||
|
|
730f5f8cc9 | ||
|
|
4b6d328e3d | ||
|
|
cfde38be2d | ||
|
|
a2ea8e76fb | ||
|
|
e797d8a1c2 | ||
|
|
b58c157c87 | ||
|
|
58f746a285 | ||
|
|
a6bba42a49 | ||
|
|
519d6868b2 | ||
|
|
5322120097 | ||
|
|
2c88c86480 | ||
|
|
55f32fd45b | ||
|
|
f39f0b03d1 | ||
|
|
ff2f1a4955 | ||
|
|
b283e216a7 | ||
|
|
4328d568b3 | ||
|
|
8edc47703f | ||
|
|
92ce906163 | ||
|
|
6e141e360e | ||
|
|
d1aba87e13 | ||
|
|
723853079e | ||
|
|
cd0742c093 | ||
|
|
52d5de5aec | ||
|
|
4f8a5ae5db | ||
|
|
616f2463c7 | ||
|
|
d3b711a966 | ||
|
|
827fe34709 | ||
|
|
4b6c0242d5 | ||
|
|
c3cbc16084 | ||
|
|
36493bfc88 | ||
|
|
66ce93a3ff | ||
|
|
957bc76dbb | ||
|
|
1f5a28fb33 | ||
|
|
045c58ce66 | ||
|
|
e2dde7239f | ||
|
|
512ad93eea | ||
|
|
19759023a4 | ||
|
|
714d3399ce | ||
|
|
e1850e5282 | ||
|
|
a05c917b2c | ||
|
|
8f3a9c265c | ||
|
|
6f1a33b76e | ||
|
|
8f0451175f | ||
|
|
37a3a4f1c0 | ||
|
|
bd85746726 | ||
|
|
96265010bf | ||
|
|
4209951ce3 | ||
|
|
f1cbd95439 | ||
|
|
d63382c6d9 | ||
|
|
20697fb334 | ||
|
|
1090d1ca42 | ||
|
|
bec4acdf51 | ||
|
|
800b78bfd8 | ||
|
|
52b01b7bbe | ||
|
|
8b71764207 | ||
|
|
a5d7a75f32 | ||
|
|
8839bcb7aa | ||
|
|
bcaf71760d | ||
|
|
1d95204648 | ||
|
|
83bf2a808f | ||
|
|
7ffb0a01c6 | ||
|
|
0710113148 | ||
|
|
7c43e9a1af | ||
|
|
f9e768c378 | ||
|
|
2063dbd0b0 | ||
|
|
057683c72f | ||
|
|
4963a0e722 | ||
|
|
1d66d288bd | ||
|
|
b8f49157c3 | ||
|
|
d77a62ef6f | ||
|
|
c531150483 | ||
|
|
991f41c531 | ||
|
|
b5fb7dd2ec | ||
|
|
4fe8532971 | ||
|
|
4ae1e7d33e | ||
|
|
43fa4526a4 | ||
|
|
fc4b1da323 | ||
|
|
843755f4e4 | ||
|
|
80e02f7520 | ||
|
|
af8f52e589 | ||
|
|
bc3f48dec9 | ||
|
|
74ee832507 | ||
|
|
da1b2d09b1 | ||
|
|
99f8607211 | ||
|
|
ef293088e1 | ||
|
|
e08e72ccb0 | ||
|
|
b692440bab | ||
|
|
7061abc64b | ||
|
|
0dd5064abb | ||
|
|
a1aafff6ce | ||
|
|
1f88f154af | ||
|
|
3d1e0364c6 | ||
|
|
0f1b5431bb | ||
|
|
0369d3fa62 | ||
|
|
154e3a732a | ||
|
|
9c979db043 | ||
|
|
0af089db89 | ||
|
|
1335613860 | ||
|
|
cb86bfd8dc | ||
|
|
a0d32ae493 | ||
|
|
f7e56a6c40 | ||
|
|
56613c75f7 | ||
|
|
fb3c35c0a0 | ||
|
|
4b3dc0a59f | ||
|
|
7855615a7b | ||
|
|
ff6576f4da | ||
|
|
931fa9a9b0 | ||
|
|
77a70967f2 | ||
|
|
e5506d952c | ||
|
|
2c2dbd0761 | ||
|
|
e6f5ecd496 | ||
|
|
73cea2d83c | ||
|
|
835a576f44 | ||
|
|
0a090341cc | ||
|
|
453671abfb | ||
|
|
cfa7daa984 | ||
|
|
88f913f586 | ||
|
|
5b4aeb4923 | ||
|
|
19133a2913 | ||
|
|
293035b7c8 | ||
|
|
d06723de5c | ||
|
|
bc45d0c499 | ||
|
|
c320cccf6f | ||
|
|
e3aebbd145 | ||
|
|
e15d378e46 | ||
|
|
b6720d10fb | ||
|
|
83a2dbe8a1 | ||
|
|
5b8592a99d | ||
|
|
18a094c06c | ||
|
|
a319ff3dc0 | ||
|
|
0cb46eca1a | ||
|
|
d85c814cba | ||
|
|
f49e660f29 | ||
|
|
afa407e7d1 | ||
|
|
37e0f5ecea | ||
|
|
5000fdcfea | ||
|
|
2ec7489dbf | ||
|
|
05965cea6e | ||
|
|
279e22ccb3 | ||
|
|
6a6fc1ca8b | ||
|
|
b6b5426297 | ||
|
|
e332ddda74 | ||
|
|
2cd4cfb883 | ||
|
|
ef12d09d35 | ||
|
|
1e365a8a7c | ||
|
|
e9363b41fd | ||
|
|
5e99df137a | ||
|
|
c0d0b45e24 | ||
|
|
3340b4cdfa | ||
|
|
d4afcc3383 | ||
|
|
dad423eb04 | ||
|
|
7b275d7e3d | ||
|
|
5274ecb721 | ||
|
|
e45367a482 | ||
|
|
83532edaab | ||
|
|
793d28da6a | ||
|
|
a8e575f680 | ||
|
|
98b0b3f9dd | ||
|
|
2e6d9c296a | ||
|
|
a07dc96ef9 | ||
|
|
8ba097a68a | ||
|
|
b1dd990fea | ||
|
|
ba7864b910 | ||
|
|
5d8fa343cd | ||
|
|
3fc49c431b | ||
|
|
79b6e65ce3 | ||
|
|
9f457d0d76 | ||
|
|
aa2ff62db4 | ||
|
|
73fffca569 | ||
|
|
45589fc033 | ||
|
|
79b81ed932 | ||
|
|
d1242870df | ||
|
|
e0dbbc4bc0 | ||
|
|
bf89791817 | ||
|
|
e3197f6dc1 | ||
|
|
e6317aa898 | ||
|
|
c73dc326fd | ||
|
|
287e250357 | ||
|
|
9673a14420 | ||
|
|
3333fdc8d7 | ||
|
|
9fb4b8bb6e | ||
|
|
7b10ed13f4 | ||
|
|
c528bd797d | ||
|
|
264529705c | ||
|
|
4669e3dfc7 | ||
|
|
eff3798964 | ||
|
|
78c526c25b | ||
|
|
ec13415d1f | ||
|
|
96622184ae | ||
|
|
3742c1c862 | ||
|
|
a0c7757428 | ||
|
|
15f9f4906a | ||
|
|
d577cd9b21 | ||
|
|
cf610cbb87 | ||
|
|
1e1095204d | ||
|
|
3fb6a13a3a | ||
|
|
2826655fe2 | ||
|
|
0ccf450b28 | ||
|
|
d28b9460af | ||
|
|
3e1bdf98c2 | ||
|
|
3f87764230 | ||
|
|
25e8e2e9e1 | ||
|
|
3faf2ce9b9 | ||
|
|
cbe243fc9e | ||
|
|
a438f633be | ||
|
|
37ef67d7ac | ||
|
|
67d631b0f0 | ||
|
|
fc302ffa5f | ||
|
|
8c28556a94 | ||
|
|
45cc531eec | ||
|
|
5c9ad9286d | ||
|
|
ad1c9486d7 | ||
|
|
ad6a03b712 | ||
|
|
36bb8010bc | ||
|
|
2200da7a16 | ||
|
|
688c0e2e85 | ||
|
|
714345a65d | ||
|
|
34a1c7e408 | ||
|
|
6255221d6a | ||
|
|
58364de72a | ||
|
|
6d64df4ee4 | ||
|
|
7bac2f206b | ||
|
|
75e1a17a2c | ||
|
|
47b13384a8 | ||
|
|
77b9efa7d1 | ||
|
|
be5f3b18af | ||
|
|
d5d12a7ce5 | ||
|
|
d7726d7755 | ||
|
|
0cd0d37eff | ||
|
|
4521def103 | ||
|
|
5c70f0a758 | ||
|
|
c12c2c0416 | ||
|
|
db45c422e7 | ||
|
|
affd9a95c5 | ||
|
|
7baf25869a | ||
|
|
12096fb427 | ||
|
|
ef7136cb81 | ||
|
|
3c4baf0126 | ||
|
|
f0b87c62a5 | ||
|
|
a319435e91 | ||
|
|
5bd0e988e3 | ||
|
|
b2be669b9e | ||
|
|
51952b0485 | ||
|
|
2b0c5e7fac | ||
|
|
3e6cea1a6a | ||
|
|
1aec7c0999 | ||
|
|
5da98809a5 | ||
|
|
49695614b7 | ||
|
|
3fbbc104b7 | ||
|
|
2fe7c0b85e | ||
|
|
09d0e82216 | ||
|
|
d208fcea7d | ||
|
|
cc0674db34 | ||
|
|
1d5b84943d | ||
|
|
14fe992ca5 | ||
|
|
15232bddaf | ||
|
|
160ef25621 | ||
|
|
2afb8688a3 | ||
|
|
9d1af035ea | ||
|
|
fb7574d814 | ||
|
|
201a3cb9e3 | ||
|
|
cc735ee6a1 | ||
|
|
0165e14ea0 | ||
|
|
97d19605d5 | ||
|
|
bc490218f9 | ||
|
|
6dac05a21d | ||
|
|
fd3fff6322 | ||
|
|
edb64fff2e | ||
|
|
fe0e854e72 | ||
|
|
06c85fb203 | ||
|
|
69926c4ae1 | ||
|
|
ef44b0a412 | ||
|
|
8577ac1027 | ||
|
|
32da050106 | ||
|
|
526b74b3ef | ||
|
|
97ab328a9c | ||
|
|
21603eedcf | ||
|
|
7fbef273a1 | ||
|
|
9e19716504 | ||
|
|
b473642ab4 | ||
|
|
fba55f01a0 | ||
|
|
015e63ba66 | ||
|
|
d92e2407f3 | ||
|
|
a4f84fb8cd | ||
|
|
bfe88745ca | ||
|
|
0d334237ba | ||
|
|
fd5cff3fea | ||
|
|
af5b82e9fd | ||
|
|
d3561748c8 | ||
|
|
791a1d804b | ||
|
|
2442424e3b | ||
|
|
0ecedd2820 | ||
|
|
958d62ec0c | ||
|
|
400cfb2141 | ||
|
|
52b860dd8f | ||
|
|
4d57d8d576 | ||
|
|
9a098accd8 | ||
|
|
62f3b2522c | ||
|
|
9b48cd2037 | ||
|
|
69776d45d1 | ||
|
|
b8fb2660a4 | ||
|
|
941281298d | ||
|
|
8afc4511a6 | ||
|
|
f43ef325ae | ||
|
|
bbdc323204 | ||
|
|
8ed9fb6276 | ||
|
|
27cbb70352 | ||
|
|
f5b10b516c | ||
|
|
5580308968 | ||
|
|
901c70efc3 | ||
|
|
3d44e5d2cc | ||
|
|
33ea3da84d | ||
|
|
572901ec9d | ||
|
|
965239d215 | ||
|
|
ac1e5e991e | ||
|
|
e97203a6e3 | ||
|
|
66b7b127f9 | ||
|
|
b3f2987b14 | ||
|
|
c7426453a5 | ||
|
|
664d5cc4c3 | ||
|
|
a44e0e036a | ||
|
|
5d54c1bae4 | ||
|
|
2ef17ba051 | ||
|
|
f63daf3a4e | ||
|
|
52b079be2a | ||
|
|
efeca17106 | ||
|
|
6827166c1d | ||
|
|
04483e61e8 | ||
|
|
0ed858b99c | ||
|
|
9b3e153a4d | ||
|
|
e525aef3d9 | ||
|
|
22fe174922 | ||
|
|
f143da3913 | ||
|
|
7e9f41c74b | ||
|
|
a1474d0d29 | ||
|
|
0dce936ad3 | ||
|
|
6ebbbb4c6c | ||
|
|
dc6ddbd0ee | ||
|
|
86c81d6b53 | ||
|
|
451a92aa36 | ||
|
|
5c42e67e73 | ||
|
|
d20d36d964 | ||
|
|
1a8d46c71e | ||
|
|
91da10eca3 | ||
|
|
3bb0dcee53 | ||
|
|
d3fd4b200f | ||
|
|
fed96864e1 | ||
|
|
3b351bea27 | ||
|
|
254e01dca1 | ||
|
|
19158e1d48 | ||
|
|
bffb78fccf | ||
|
|
a3800592a2 | ||
|
|
22a498dfc9 | ||
|
|
9ea96e32bd | ||
|
|
68f51a123e | ||
|
|
43b1b63581 | ||
|
|
43b4a2c515 | ||
|
|
5b9cfdb689 | ||
|
|
b43cd7103a | ||
|
|
30e4f6e0f5 | ||
|
|
1d0ebf889b | ||
|
|
7c4f1da485 | ||
|
|
8163921014 | ||
|
|
993393dd96 | ||
|
|
82aed43934 | ||
|
|
fa65134c26 | ||
|
|
af4266c739 | ||
|
|
f72ea2e763 | ||
|
|
c5540270a3 | ||
|
|
201b72c9c8 | ||
|
|
26b99f5f68 | ||
|
|
d3dc774492 | ||
|
|
1f7155a932 | ||
|
|
02729fe02b | ||
|
|
498078b6e0 | ||
|
|
526f5e319b | ||
|
|
d419dba44a | ||
|
|
fd98159fce | ||
|
|
f5b98009dd | ||
|
|
cf0b66d852 | ||
|
|
197d0caf44 | ||
|
|
a4a082f76a | ||
|
|
84026afb92 | ||
|
|
4dea7d2a52 | ||
|
|
2df1b7dd61 | ||
|
|
89042113a5 | ||
|
|
48665ebcce | ||
|
|
2528d48010 | ||
|
|
5456d71979 | ||
|
|
e36aae3cf3 | ||
|
|
6d12e2dd72 | ||
|
|
f117249bb5 | ||
|
|
cf1d537367 | ||
|
|
517d13b400 | ||
|
|
103aaafff1 | ||
|
|
fae870c93a | ||
|
|
f8e00dcc80 | ||
|
|
5fdbb597bb | ||
|
|
d74b286a9d | ||
|
|
ecb3c521ff | ||
|
|
1d093ce928 | ||
|
|
46b711af2e | ||
|
|
772e6ddb5d | ||
|
|
f84e8443d2 | ||
|
|
250c18ebf1 | ||
|
|
e3d0f38b79 | ||
|
|
c512f97783 | ||
|
|
0594680775 | ||
|
|
f999881f59 | ||
|
|
4fe9192ac6 | ||
|
|
d936702fa9 | ||
|
|
74e284b0de | ||
|
|
4c42b72ed8 | ||
|
|
0e0046df65 | ||
|
|
c80d1d10c2 | ||
|
|
da97971011 | ||
|
|
700447dbe7 | ||
|
|
37e7b5ee93 | ||
|
|
1265afa93f | ||
|
|
1e09481b02 | ||
|
|
9996a5a05e | ||
|
|
f20aac7c81 | ||
|
|
98f7b0bacd | ||
|
|
3f6d3fb3a2 | ||
|
|
663b49c76b | ||
|
|
16e38f2541 | ||
|
|
842cc55e47 | ||
|
|
72db099e6f | ||
|
|
be130bc3a7 | ||
|
|
42253336e1 | ||
|
|
572631e1d7 | ||
|
|
723777a800 | ||
|
|
b825d534c1 | ||
|
|
b9749620a8 | ||
|
|
70ea9989aa | ||
|
|
b3ec9c981c | ||
|
|
bf72085abb | ||
|
|
64dd416b59 | ||
|
|
ab2a920455 | ||
|
|
7580446d60 | ||
|
|
ade18ac6fc | ||
|
|
005c851d72 | ||
|
|
0f1d46c765 | ||
|
|
21fbb07b1d | ||
|
|
dff2217e80 | ||
|
|
22aac3d943 | ||
|
|
53afc120f3 | ||
|
|
a75ce70615 | ||
|
|
4a3b948760 | ||
|
|
f81283c892 | ||
|
|
7eae879037 | ||
|
|
1b0ce5d893 | ||
|
|
c17745368d | ||
|
|
e78b518654 | ||
|
|
55a8634be2 | ||
|
|
ac891eea53 | ||
|
|
74fa2a3081 | ||
|
|
6c1c5b7759 | ||
|
|
1f4152b588 | ||
|
|
70386ea1b2 | ||
|
|
cbce90c461 | ||
|
|
74ae3bf706 | ||
|
|
1feccdc26d | ||
|
|
c38c2a425b | ||
|
|
f43352b790 | ||
|
|
c5b52b2781 | ||
|
|
b91840fb95 | ||
|
|
fc10fbffb0 | ||
|
|
e40841c128 | ||
|
|
98a02e874b | ||
|
|
b06df8c3d0 | ||
|
|
a00afd5d7f | ||
|
|
9a41a2d6fb | ||
|
|
2cd98a6620 | ||
|
|
283b56be5b | ||
|
|
6d56771aba | ||
|
|
1724d8a532 | ||
|
|
b4cdf35d36 | ||
|
|
cad0ad7a59 | ||
|
|
ca60003c39 | ||
|
|
0f030e0bac | ||
|
|
6d4f212a18 | ||
|
|
183b39bc24 | ||
|
|
27ad0c6fcf | ||
|
|
b5f661f1af | ||
|
|
0015f3f0bf | ||
|
|
c5d0fdd645 | ||
|
|
2d09ad44fb | ||
|
|
667fffd124 | ||
|
|
699233d8c7 | ||
|
|
56aabdc4a6 | ||
|
|
443e2c7a6f | ||
|
|
985b0f6e63 | ||
|
|
cc86edf276 | ||
|
|
4071b9342d | ||
|
|
f71d1bc5d3 | ||
|
|
6bcdbaba34 | ||
|
|
a2beead3a5 | ||
|
|
e7a25e353d | ||
|
|
af04a01130 | ||
|
|
fe1cfa1d7b | ||
|
|
b248797bb0 | ||
|
|
f24eba08d3 | ||
|
|
0e89559a47 | ||
|
|
d02a72e079 | ||
|
|
3be57d1b0b | ||
|
|
bed550e97c | ||
|
|
7e2619ea75 | ||
|
|
4b22f1d3a7 | ||
|
|
9dcc7e293f | ||
|
|
6a68cf5e41 | ||
|
|
29297be4a3 | ||
|
|
90b87529e0 | ||
|
|
39af05524d | ||
|
|
e3fb2cd03c | ||
|
|
90f84d628a | ||
|
|
b89e0b5c5a | ||
|
|
aac89c354c | ||
|
|
a032f9af10 | ||
|
|
642aaec6da | ||
|
|
ff667d6aed | ||
|
|
5e98496ea6 | ||
|
|
972fe1d15b | ||
|
|
26eaa36faa | ||
|
|
c517f41595 | ||
|
|
56a6d7243f | ||
|
|
18e43dfc22 | ||
|
|
816f6370ef | ||
|
|
ebc2b2e59d | ||
|
|
c9a796dbfe | ||
|
|
1ba185ea9c | ||
|
|
a78be8bc1d | ||
|
|
abfb497577 | ||
|
|
a10b184508 | ||
|
|
f0ea6660e6 | ||
|
|
a829f25d56 | ||
|
|
deff3dd8e0 | ||
|
|
6c5fb5ea09 | ||
|
|
afe0c9e0db | ||
|
|
1f2213042f | ||
|
|
5edd2466f9 | ||
|
|
f3b3a1a577 | ||
|
|
068619b815 | ||
|
|
f121e94979 | ||
|
|
b5b52529d4 | ||
|
|
876bf73454 | ||
|
|
522dbf6e4a | ||
|
|
ae685095ba | ||
|
|
30d5fe2f12 | ||
|
|
2bf27c561c | ||
|
|
bbdc72323d | ||
|
|
6e335930f3 | ||
|
|
9b309939da | ||
|
|
faf2e5115d | ||
|
|
dc5d9412c8 | ||
|
|
fc0680d66f | ||
|
|
56c9a5433f | ||
|
|
60e473ee55 | ||
|
|
ae34ecd5c3 | ||
|
|
fd1caa8729 | ||
|
|
1182e5c60c | ||
|
|
d99d515dfa | ||
|
|
70a15e7d9c | ||
|
|
1691382369 | ||
|
|
b7da9c6d51 | ||
|
|
3426538dca | ||
|
|
63de2b200b | ||
|
|
ff1ee766dc | ||
|
|
f033411adf | ||
|
|
a738eaf8c0 | ||
|
|
5074aadd6e | ||
|
|
0854961470 | ||
|
|
227b077935 | ||
|
|
1e4358290a | ||
|
|
925169eb31 | ||
|
|
e1abeb9252 | ||
|
|
cbe0add211 | ||
|
|
299b524d62 | ||
|
|
31c094e696 | ||
|
|
a8038a2863 | ||
|
|
29933bb916 | ||
|
|
5ec0c078d8 | ||
|
|
e6287f1ff2 | ||
|
|
be9caf8905 | ||
|
|
f375142084 | ||
|
|
fd3668d520 | ||
|
|
d5e03e9d9e | ||
|
|
d62f094919 | ||
|
|
6d84f28600 | ||
|
|
209e603f2c | ||
|
|
1b4dc01c74 | ||
|
|
645af12c3f | ||
|
|
fadc42d72b | ||
|
|
fc831e7d42 | ||
|
|
2998ee9145 | ||
|
|
971c4e5879 | ||
|
|
b396ee7987 | ||
|
|
0f803cd4fa | ||
|
|
167a14b8db | ||
|
|
81cbc2d10c | ||
|
|
9bd8aff99b | ||
|
|
a770828165 | ||
|
|
ab457035ff | ||
|
|
f886e4c1d2 | ||
|
|
be73c9e81c | ||
|
|
1c2183bf1a | ||
|
|
1789d90dc3 | ||
|
|
57306ff7fe | ||
|
|
f0eb6573f4 | ||
|
|
e7f5dd3357 | ||
|
|
8101bb9ea1 | ||
|
|
228fdc8ffe | ||
|
|
e9df125cde | ||
|
|
16ef577a7a | ||
|
|
734b3bced6 | ||
|
|
5f6f3c94c9 | ||
|
|
09ba42a974 | ||
|
|
d76e823489 | ||
|
|
900b204bb0 | ||
|
|
17d679901a | ||
|
|
d8036779f8 | ||
|
|
3f6bda28b3 | ||
|
|
c0b4f4dd79 | ||
|
|
f36aee44c6 | ||
|
|
cd24526a9d | ||
|
|
a345ac1390 | ||
|
|
3d987b8e1d | ||
|
|
57043912e0 | ||
|
|
00aef5ea6b | ||
|
|
369b69668c | ||
|
|
65245f4560 | ||
|
|
4d4fdc97d4 | ||
|
|
c96577891c | ||
|
|
f48b2fc9cb | ||
|
|
2fca2580ed | ||
|
|
7adc1da361 | ||
|
|
6dc24dde43 | ||
|
|
4929e0e6ec | ||
|
|
16a8b8ed71 | ||
|
|
ad1412817e | ||
|
|
d9e6bb3bea | ||
|
|
8970404638 | ||
|
|
2a2241d7f9 | ||
|
|
db1a47e8eb | ||
|
|
3e57061cef | ||
|
|
cd200f8450 | ||
|
|
782013079f | ||
|
|
e5db8acd66 | ||
|
|
1a6a8019c8 | ||
|
|
e935eef29f | ||
|
|
381defda51 | ||
|
|
02ae80c204 | ||
|
|
82214b30e8 | ||
|
|
33a1f48602 | ||
|
|
aee845e5cc | ||
|
|
cd780f6006 | ||
|
|
d4741fefa0 | ||
|
|
7e1e8a2616 | ||
|
|
d73c05cdfc | ||
|
|
78323023cb | ||
|
|
2cf084c98f | ||
|
|
e5bdeba1d7 | ||
|
|
8d7db7774f | ||
|
|
78d22c670c | ||
|
|
0a679109f5 | ||
|
|
e843142b7e | ||
|
|
72e728f655 | ||
|
|
ef56792f56 | ||
|
|
504a6959e8 | ||
|
|
b8e3060887 | ||
|
|
1aa1ede421 | ||
|
|
480dba7629 | ||
|
|
9b9c66a149 | ||
|
|
0f5eb923ee | ||
|
|
90ed28e7a0 | ||
|
|
d2b45c1c84 | ||
|
|
a119ba5f80 | ||
|
|
8c1191a08f | ||
|
|
4275d596e6 | ||
|
|
cc83f2baf3 | ||
|
|
728496b831 | ||
|
|
bbc99162c6 | ||
|
|
eed3af9e3e | ||
|
|
50187ff376 | ||
|
|
5f30919fb4 | ||
|
|
14c3cfac85 | ||
|
|
e978f02765 | ||
|
|
8d877c480f | ||
|
|
c53efee9a1 | ||
|
|
148c461e86 | ||
|
|
fcadb9883d | ||
|
|
bb6491e10a | ||
|
|
6248ccf376 | ||
|
|
c9e08f36fa | ||
|
|
10b95d753b | ||
|
|
c3989083cf | ||
|
|
01db585094 | ||
|
|
cc67cb330c | ||
|
|
52ed3c5a04 | ||
|
|
5976f6230a | ||
|
|
3553f03a95 | ||
|
|
d6e2d889c3 | ||
|
|
a777b3b450 | ||
|
|
9957efbea0 | ||
|
|
22e7b9730f | ||
|
|
91470b8509 | ||
|
|
c9d5327328 | ||
|
|
1aa61b72e5 | ||
|
|
3ca5edc3fc | ||
|
|
a092ebaeb3 | ||
|
|
5b9e84c255 | ||
|
|
9c058b926f | ||
|
|
4f2d2ae6e8 | ||
|
|
75aa26a018 | ||
|
|
0f795254e5 | ||
|
|
33592f0a83 | ||
|
|
d6fd01eaca | ||
|
|
ee6e0ff26c | ||
|
|
4d9574bf38 | ||
|
|
813be9a2be | ||
|
|
cc76ebfafb | ||
|
|
7989ee0243 | ||
|
|
3aa1997cfd | ||
|
|
c3da15552e | ||
|
|
a014fe9443 | ||
|
|
92551d4ca3 | ||
|
|
8010858e85 | ||
|
|
4efb4875b0 | ||
|
|
c5d041e46d | ||
|
|
53c2223aae | ||
|
|
25034ac0ae | ||
|
|
ac9de72b75 | ||
|
|
1f48ad93f2 | ||
|
|
38f7f7aa00 | ||
|
|
fe8175c63a | ||
|
|
2d9e01bbc1 | ||
|
|
022a227b08 | ||
|
|
a2228259f1 | ||
|
|
a61af7c56f | ||
|
|
5d6a646976 | ||
|
|
628d0d7492 | ||
|
|
b76c8745ec | ||
|
|
51e67bc441 | ||
|
|
8887f75b70 | ||
|
|
9436a838c0 | ||
|
|
ef120fa36f | ||
|
|
8c6385e2c5 | ||
|
|
0bd85d9905 | ||
|
|
ce0dab7b28 | ||
|
|
ddcc5670ce | ||
|
|
86afa184e2 | ||
|
|
77f341f139 | ||
|
|
918b5d99c2 | ||
|
|
7a098d6eff | ||
|
|
71f81283f5 | ||
|
|
058c7c3c33 | ||
|
|
870e33879b | ||
|
|
3ca82bdfc5 | ||
|
|
4721bad286 | ||
|
|
f040cf2f07 | ||
|
|
8d50717c90 | ||
|
|
2512ad3c95 | ||
|
|
bc7e007634 | ||
|
|
1f3c87e0c7 | ||
|
|
4bb255e0bb | ||
|
|
73e08faee9 | ||
|
|
02dc7711e4 | ||
|
|
67b4d80e5b | ||
|
|
5168d2bb39 | ||
|
|
57190a75bf | ||
|
|
f10e865895 | ||
|
|
91b4dc412b | ||
|
|
c896fd8df8 | ||
|
|
72cd987284 | ||
|
|
d868d05080 | ||
|
|
9dfb039a69 | ||
|
|
f2920e877b | ||
|
|
ecb0b3f9d7 | ||
|
|
67c128be69 | ||
|
|
1ecbbc2d4b | ||
|
|
82c481b014 | ||
|
|
e7da6d7897 | ||
|
|
e9f1e3038b | ||
|
|
a591096819 | ||
|
|
f1ef60475f | ||
|
|
ce75bb3984 | ||
|
|
d59235e04c | ||
|
|
42f8f7e58f | ||
|
|
f9cbc9ae27 | ||
|
|
a50d6599bf | ||
|
|
cc578b496e | ||
|
|
662944d246 | ||
|
|
42810df4a5 | ||
|
|
c3a058d2e1 | ||
|
|
ce5d835ae5 | ||
|
|
60dd561729 | ||
|
|
08c9f9ad7d | ||
|
|
d47907906d | ||
|
|
935f0f6e05 | ||
|
|
ac0b21d574 | ||
|
|
9b5d05369f | ||
|
|
489f9f5e59 | ||
|
|
e62f7c23c9 | ||
|
|
91ed7d49b5 | ||
|
|
c9e9abd811 | ||
|
|
b1e5023f62 | ||
|
|
604690b3f5 | ||
|
|
13ada6ecc6 | ||
|
|
b897eb913e | ||
|
|
c25602a650 | ||
|
|
2defc9af3f | ||
|
|
446525389b | ||
|
|
756b30d04f | ||
|
|
51ec842815 | ||
|
|
c38822849e | ||
|
|
3c69201f67 | ||
|
|
ed9d701406 | ||
|
|
e70c5aa2e9 | ||
|
|
0c4589b257 | ||
|
|
84d08392fb | ||
|
|
8ff117308d | ||
|
|
b6c703adbc | ||
|
|
22e6934de5 | ||
|
|
1b8a1d69ac | ||
|
|
b6ae83937b | ||
|
|
7115556663 | ||
|
|
cb3296661e | ||
|
|
6dd20a6df9 | ||
|
|
71c1d0e59a | ||
|
|
2b275e1ff7 | ||
|
|
a7fcae1033 | ||
|
|
19bd189b33 | ||
|
|
2d5089c047 | ||
|
|
be7469bd54 | ||
|
|
146d8daa6e | ||
|
|
f3928d9e09 | ||
|
|
d4090d459d | ||
|
|
7dd7554c08 | ||
|
|
9de9a1d97d | ||
|
|
04ee366fbe | ||
|
|
c8784150fc | ||
|
|
7b7bccb37a | ||
|
|
84e2636bca | ||
|
|
dc73613b56 | ||
|
|
fd8868ef4d | ||
|
|
127df0b8e0 | ||
|
|
c2989df902 | ||
|
|
3f9ee99b69 | ||
|
|
7204c4e804 | ||
|
|
f1131cf8e7 | ||
|
|
5b9a8beb07 | ||
|
|
9e18e35c66 | ||
|
|
9db3dfa955 | ||
|
|
4a3e56d300 | ||
|
|
3bb4125c50 | ||
|
|
ce58883618 | ||
|
|
e7d3c60bac | ||
|
|
08e90139ad | ||
|
|
0d4dc34453 | ||
|
|
fe142c4626 | ||
|
|
d8dfa6017d | ||
|
|
b7a5d4296b | ||
|
|
85d4c1fc24 | ||
|
|
66489d79be | ||
|
|
30a66a26c6 | ||
|
|
fbc3081e68 | ||
|
|
7a66c94907 | ||
|
|
4e38bc5769 | ||
|
|
0dc428dbd6 | ||
|
|
6ac7fc94ea | ||
|
|
af28ed1783 | ||
|
|
00daf084f2 | ||
|
|
5518848e28 | ||
|
|
6ded856b2f | ||
|
|
d302f5132e | ||
|
|
012e29ee3a | ||
|
|
dbe9579d7f | ||
|
|
c8d0221d9b | ||
|
|
885c663d93 | ||
|
|
3b399d5815 | ||
|
|
9f18d1bc8b | ||
|
|
9a8cf61e38 | ||
|
|
eb822282c0 | ||
|
|
273823a65f | ||
|
|
f2aa1400c5 | ||
|
|
cf8a9e1823 | ||
|
|
b3320d534b | ||
|
|
f58b4c2989 | ||
|
|
47b3b1e307 | ||
|
|
e5649c4a42 | ||
|
|
cea77eca02 | ||
|
|
4fbbcfba59 | ||
|
|
6a24f70537 | ||
|
|
4681160924 | ||
|
|
6f8c2f4a44 | ||
|
|
69de9dce38 | ||
|
|
721ae9c68d | ||
|
|
6a6ed89d29 | ||
|
|
3e4377a366 | ||
|
|
287de66e0c | ||
|
|
df2ae9d964 | ||
|
|
ef55f1f49b | ||
|
|
aebf7e9f1f | ||
|
|
a014ce6eb5 | ||
|
|
aed1efceb9 | ||
|
|
124c375b14 | ||
|
|
1d46e22a7f | ||
|
|
28c334429d | ||
|
|
4726f98d4f | ||
|
|
985f382436 | ||
|
|
f822c788b0 | ||
|
|
46f8982aa6 | ||
|
|
7b3cec9289 | ||
|
|
a3f227cb8d | ||
|
|
d71feb2cfc | ||
|
|
b69565e9e6 | ||
|
|
bf996feccf | ||
|
|
b1143b0eec | ||
|
|
ce977163c2 | ||
|
|
ded74bda83 | ||
|
|
7180113397 | ||
|
|
81ac8a3bc9 | ||
|
|
4b74da5d38 | ||
|
|
0375cfa260 | ||
|
|
52108a675a | ||
|
|
406e95d3f7 | ||
|
|
a9f355dea9 | ||
|
|
1a63c12327 | ||
|
|
7bc2f8b352 | ||
|
|
d56ef227a7 | ||
|
|
fcc371ab5a | ||
|
|
9f43a99772 | ||
|
|
9fc5a4e390 | ||
|
|
a2b3f873f6 | ||
|
|
f6c1509e48 | ||
|
|
d9bbb32a28 | ||
|
|
d2ef6e77af | ||
|
|
6133e9bac3 | ||
|
|
6ca48f35f1 | ||
|
|
cb016b4383 | ||
|
|
d0b21df28b | ||
|
|
ce48ee888a | ||
|
|
5cd8bc5a46 | ||
|
|
4109cd75d3 | ||
|
|
97a889e019 | ||
|
|
b9a1b3591d | ||
|
|
fde84e3cfb | ||
|
|
0985eb4fac | ||
|
|
5957c1a221 | ||
|
|
e7e34aa2c8 | ||
|
|
b8742591b8 | ||
|
|
50e73ac12e | ||
|
|
9ed277a9b2 | ||
|
|
c9e2984d68 | ||
|
|
00aaff10a7 | ||
|
|
6da4256adf | ||
|
|
da9c826791 | ||
|
|
bed02e248e | ||
|
|
f53531889f | ||
|
|
ef3246ae2a | ||
|
|
5504b534f7 | ||
|
|
0e553d7868 | ||
|
|
38be51367e | ||
|
|
0d4af0970c | ||
|
|
b51335ffd6 | ||
|
|
444fa07984 | ||
|
|
feb0f304fb | ||
|
|
ccd4a1aa9f | ||
|
|
b3723c2977 | ||
|
|
c5b70a9ada | ||
|
|
82789179e7 | ||
|
|
b0a309a817 | ||
|
|
32a27b6e59 | ||
|
|
f73a318dad | ||
|
|
4fff2c5f5c | ||
|
|
81abac657f | ||
|
|
74decd3ec7 | ||
|
|
11d17d1f3f | ||
|
|
ca2384ba8c | ||
|
|
ded23342db | ||
|
|
a35c14865f | ||
|
|
0952d97557 | ||
|
|
e1db5f15ca | ||
|
|
c9e467ac2f | ||
|
|
74a5e970d9 | ||
|
|
935de7d02e | ||
|
|
a04522ff72 | ||
|
|
90a93ffba6 | ||
|
|
069c55d4b9 | ||
|
|
1efaf4b605 | ||
|
|
ff74516ce2 | ||
|
|
aee4b7aaab | ||
|
|
1d94479bde | ||
|
|
85c95d899e | ||
|
|
18d4210e7d | ||
|
|
ac25bc6d42 | ||
|
|
ec05ef3b4c | ||
|
|
9827d97374 | ||
|
|
4fab92d516 | ||
|
|
44bc9c4e40 | ||
|
|
1030773ef6 | ||
|
|
1a0cb4b8c8 | ||
|
|
4295a3672c | ||
|
|
fd2a8fe230 | ||
|
|
e2d1eccfb9 | ||
|
|
05d1d3e725 | ||
|
|
37261928c2 | ||
|
|
63132110d9 | ||
|
|
ffd7e415a2 | ||
|
|
d43664d018 | ||
|
|
8e06362ff8 | ||
|
|
625ccfd31f | ||
|
|
d7b5d242ff | ||
|
|
7973c87b9a | ||
|
|
3085b1507b | ||
|
|
f17ef17492 | ||
|
|
8d0a31d0f9 | ||
|
|
6c6d3fed05 | ||
|
|
b616e2e11b | ||
|
|
b18771bb79 | ||
|
|
40ef4c179a | ||
|
|
62212dc6c9 | ||
|
|
3ed2b67037 | ||
|
|
d240750606 | ||
|
|
ed009d3e2e | ||
|
|
be2fa0f217 | ||
|
|
a8d9b4538b | ||
|
|
2b7b3de043 | ||
|
|
64362968fc | ||
|
|
aa9caefed1 | ||
|
|
14bbe1ffef | ||
|
|
b6976fb519 | ||
|
|
040237de2b | ||
|
|
fc307ff43f | ||
|
|
04304b3397 | ||
|
|
7260db6668 | ||
|
|
1dadc51ddf | ||
|
|
fe85351869 | ||
|
|
74a83c6ac4 | ||
|
|
df77ba61ad | ||
|
|
ed40f74d59 | ||
|
|
42e26bef68 | ||
|
|
af60adb55f | ||
|
|
5dfa9237ad | ||
|
|
c890195567 | ||
|
|
b50a327b17 | ||
|
|
17957b69d1 | ||
|
|
c4ac4ee173 | ||
|
|
659b4e2fcd | ||
|
|
02b1ad8d7a | ||
|
|
47eeb01b75 | ||
|
|
3d5fb2dfea | ||
|
|
ef6238b593 | ||
|
|
bc9bec3d66 | ||
|
|
a24b4363d7 | ||
|
|
02ddad22e7 | ||
|
|
a705512dc5 | ||
|
|
cfd6954755 | ||
|
|
702ac43f86 | ||
|
|
6ede2d22bb | ||
|
|
315d26ad52 | ||
|
|
a78b0687f7 | ||
|
|
148b8e9369 | ||
|
|
ae6ce0f9b0 | ||
|
|
31c8665653 | ||
|
|
d7f73e02c5 | ||
|
|
e897b3af57 | ||
|
|
fdbf331432 | ||
|
|
aed86ac6f0 | ||
|
|
3a13d4d6c0 | ||
|
|
f5336564d0 | ||
|
|
07ca5a8b77 | ||
|
|
6926a212f4 | ||
|
|
936f39161b | ||
|
|
7c6ec2e3d7 | ||
|
|
31c7116a15 | ||
|
|
47edc3180b | ||
|
|
8053e8bb05 | ||
|
|
fe84dc4823 | ||
|
|
5c480b37b3 | ||
|
|
92335d8678 | ||
|
|
285eb25706 | ||
|
|
85c3d9f65f | ||
|
|
1dcb5717ea | ||
|
|
36f1a557d7 | ||
|
|
bd7157c172 | ||
|
|
2e1795dc6f | ||
|
|
cd1be782fa | ||
|
|
67059f3d71 | ||
|
|
15f4d3326b | ||
|
|
e65404a466 | ||
|
|
3d47d1b4db | ||
|
|
d1749ab610 | ||
|
|
806c264686 | ||
|
|
34a9cb5a74 | ||
|
|
64fad2e871 | ||
|
|
34a2af8429 | ||
|
|
9c89c26097 | ||
|
|
e3b6a5d389 | ||
|
|
0fb54efde5 | ||
|
|
a4a3f32dba | ||
|
|
15883f2138 | ||
|
|
03a1e29e0c | ||
|
|
eda9ff272b | ||
|
|
89501271ce | ||
|
|
b3728e06ac | ||
|
|
968a6ea9b3 | ||
|
|
e253d8f4f3 | ||
|
|
cfabe47e10 | ||
|
|
d3fe7857b7 | ||
|
|
642e96a439 | ||
|
|
2b8451e045 | ||
|
|
62074e554a | ||
|
|
0434cda2da | ||
|
|
4b4c88d44d | ||
|
|
9e116bec97 | ||
|
|
dc25e16c00 | ||
|
|
42a7c324fa | ||
|
|
849888d128 | ||
|
|
8a7e910e7c | ||
|
|
72d72d443e | ||
|
|
7b0a3f0f96 | ||
|
|
cf4604e0d8 | ||
|
|
e873dd7d0a | ||
|
|
4492e940e5 | ||
|
|
c833c03dc3 | ||
|
|
30b0d226b5 | ||
|
|
8afad21113 | ||
|
|
6b5e5b0f25 | ||
|
|
3c0ab6822f | ||
|
|
f7215d00ca | ||
|
|
43bbe9be0f | ||
|
|
477a691c9e | ||
|
|
e5d60050a2 | ||
|
|
9a698fda18 | ||
|
|
d5701c1073 | ||
|
|
955b9a4b2b | ||
|
|
09ffda2605 | ||
|
|
039fb0c505 | ||
|
|
20799ef1a8 |
12
.github/FUNDING.yml
vendored
@@ -1,12 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
|
||||||
patreon: mastodon
|
|
||||||
open_collective: # Replace with a single Open Collective username e.g., user1
|
|
||||||
ko_fi: # Replace with a single Ko-fi username e.g., user1
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username e.g., user1
|
|
||||||
issuehunt: # Replace with a single IssueHunt username e.g., user1
|
|
||||||
otechie: # Replace with a single Otechie username e.g., user1
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
79
.github/workflows/build_and_deploy.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
name: Build and deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'ci_setup'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
java-version: 21
|
||||||
|
distribution: temurin
|
||||||
|
|
||||||
|
- name: Set up Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 2.7.2
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Set up Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Decode keystore
|
||||||
|
uses: timheuer/base64-to-file@v1
|
||||||
|
id: android_keystore
|
||||||
|
with:
|
||||||
|
fileName: "release.jks"
|
||||||
|
encodedString: ${{ secrets.KEYSTORE_FILE }}
|
||||||
|
|
||||||
|
- name: Prepare Gradle environment
|
||||||
|
run: |
|
||||||
|
echo "apply from: 'ci_signing.gradle'" >> mastodon/build.gradle
|
||||||
|
echo "sdk.dir=$ANDROID_SDK_ROOT" > local.properties
|
||||||
|
|
||||||
|
- name: Build and deploy to Google Play
|
||||||
|
run: bundle exec fastlane deploy
|
||||||
|
env:
|
||||||
|
KEYSTORE_FILE: ${{ steps.android_keystore.outputs.filePath }}
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
SUPPLY_JSON_KEY_DATA: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
|
||||||
|
SUPPLY_SKIP_UPLOAD_METADATA: true
|
||||||
|
SUPPLY_SKIP_UPLOAD_CHANGELOGS: true
|
||||||
|
|
||||||
|
- name: Build release apk
|
||||||
|
run: ./gradlew assembleRelease
|
||||||
|
env:
|
||||||
|
KEYSTORE_FILE: ${{ steps.android_keystore.outputs.filePath }}
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Upload release apk
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mastodon-release.apk
|
||||||
|
path: mastodon/build/outputs/apk/release/mastodon-release.apk
|
||||||
|
|
||||||
|
- name: Build githubRelease apk
|
||||||
|
run: ./gradlew assembleGithubRelease
|
||||||
|
env:
|
||||||
|
KEYSTORE_FILE: ${{ steps.android_keystore.outputs.filePath }}
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Upload githubRelease apk
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mastodon-githubRelease.apk
|
||||||
|
path: mastodon/build/outputs/apk/githubRelease/mastodon-githubRelease.apk
|
||||||
|
|
||||||
|
- name: Upload mappings
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mappings
|
||||||
|
path: mastodon/build/outputs/mapping/*/mapping.txt
|
||||||
1
.gitignore
vendored
@@ -9,3 +9,4 @@
|
|||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
*.jks
|
*.jks
|
||||||
|
/fastlane/report.xml
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:7.1.3"
|
classpath "com.android.tools.build:gradle:8.2.2"
|
||||||
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
|
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
2
fastlane/Appfile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
|
||||||
|
package_name("org.joinmastodon.android") # e.g. com.krausefx.app
|
||||||
36
fastlane/Fastfile
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# This file contains the fastlane.tools configuration
|
||||||
|
# You can find the documentation at https://docs.fastlane.tools
|
||||||
|
#
|
||||||
|
# For a list of all available actions, check out
|
||||||
|
#
|
||||||
|
# https://docs.fastlane.tools/actions
|
||||||
|
#
|
||||||
|
# For a list of all available plugins, check out
|
||||||
|
#
|
||||||
|
# https://docs.fastlane.tools/plugins/available-plugins
|
||||||
|
#
|
||||||
|
|
||||||
|
# Uncomment the line if you want fastlane to automatically update itself
|
||||||
|
# update_fastlane
|
||||||
|
|
||||||
|
default_platform(:android)
|
||||||
|
|
||||||
|
platform :android do
|
||||||
|
desc "Runs all the tests"
|
||||||
|
lane :test do
|
||||||
|
gradle(task: "test")
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Deploy a new version to the Google Play"
|
||||||
|
lane :deploy do
|
||||||
|
gradle(
|
||||||
|
task: "bundle",
|
||||||
|
build_type: "release",
|
||||||
|
)
|
||||||
|
upload_to_play_store(
|
||||||
|
changes_not_sent_for_review: true,
|
||||||
|
skip_upload_images: true,
|
||||||
|
skip_upload_screenshots: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
40
fastlane/README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
fastlane documentation
|
||||||
|
----
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
Make sure you have the latest version of the Xcode command line tools installed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xcode-select --install
|
||||||
|
```
|
||||||
|
|
||||||
|
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
|
||||||
|
|
||||||
|
# Available Actions
|
||||||
|
|
||||||
|
## Android
|
||||||
|
|
||||||
|
### android test
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android test
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs all the tests
|
||||||
|
|
||||||
|
### android deploy
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy a new version to the Google Play
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||||
|
|
||||||
|
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
|
||||||
|
|
||||||
|
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
ماستودون هي أكبر شبكة اجتماعية لا مركزيَّة على الإنترنت. بدلاً من كونها على موقع ويب واحد مركزي، هي عبارة عن شبكة من ملايين المستخدمين في مجتمعات مُستقلَّة يمكنهم جميعًا التفاعل مع بعضهم البعض بسلاسة. بغض النظر عن اهتماماتك، يمكنك مقابلة أشخاص متحمسين ينشرون عنها في ماستودون!
|
ماستدون هي أكبر شبكة اجتماعية لا مركزيَّة على الإنترنت. بدلاً من كونها على موقع ويب واحد مركزي، هي عبارة عن شبكة من ملايين المستخدمين في مجتمعات مُستقلَّة يمكنهم جميعًا التفاعل مع بعضهم البعض بسلاسة. بغض النظر عن اهتماماتك، يمكنك مقابلة أشخاص متحمسين ينشرون عنها في ماستودون!
|
||||||
|
|
||||||
اِنضم إلَى مُجتَمع وأنشئ مِلَفَّكَ التَّعريفِيّ. ابحث عن أشخاص رائعين، تابعهم واقرأ منشوراتهم في خطٍّ زمني خالٍ من الإعلانات. عبِّر عَن نَفسِكَ باِستخدام رُموزٍ تَعبيرِيَّةٍ مُخصَّصَة، أو صُوَر، أو صُوَرٍ مُتحَرِّكَة، أو مَقاطِعٍ مَرئِّيَة أو مَقاطِعٍ صَوتِيَّةٍ فِي مَنشوراتٍ ذَاتُ خَمسِمائَة حَرف. رُدّ على سَلاسِلِ المَنشوراتِ، وأعِد تَدوينَ مَنشُوراتِ أيِّ شَخصٍ لِمُشارَكَةِ الأُمُورِ الرَّائِعَة. اِبحَث عَن حِساباتٍ جَديدَةٍ لِمُتابَعَتِها، وَعَن وُسُومٍ شَائِعَةٍ لِتَوسيعِ شَبَكَتِك.
|
اِنضم إلَى مُجتَمع وأنشئ مِلَفَّكَ التَّعريفِيّ. ابحث عن أشخاص رائعين، تابعهم واقرأ منشوراتهم في خطٍّ زمني خالٍ من الإعلانات. عبِّر عَن نَفسِكَ باِستخدام رُموزٍ تَعبيرِيَّةٍ مُخصَّصَة، أو صُوَر، أو صُوَرٍ مُتحَرِّكَة، أو مَقاطِعٍ مَرئِّيَة أو مَقاطِعٍ صَوتِيَّةٍ فِي مَنشوراتٍ ذَاتُ خَمسِمائَة حَرف. رُدّ على سَلاسِلِ المَنشوراتِ، وأعِد تَدوينَ مَنشُوراتِ أيِّ شَخصٍ لِمُشارَكَةِ الأُمُورِ الرَّائِعَة. اِبحَث عَن حِساباتٍ جَديدَةٍ لِمُتابَعَتِها، وَعَن وُسُومٍ شَائِعَةٍ لِتَوسيعِ شَبَكَتِك.
|
||||||
|
|
||||||
ماستودون مبني بتركيز على الأمان والخصوصيَّة. حدِّد ما إذا أردتَ مُشارَكَةَ مَنشُوراتِكَ مَعَ مُتابِعيك، أو الأشخاصِ الَّذينَ أشَرتَ إليهِم فَقَط أو العالَمَ بأسرِه. تتيح لك تحذيرات المحتوى إخفاء المنشورات التي تحتوي على مواد حساسة أو محفِّزَة حتى تكون مستعد للتفاعل مع محتواها. لكل مجتمع إرشاداته الخاصة ومشرفيه الخاصين للحفاظ على أمان أعضائه، كما تُساعد أدوات الحظر والإبلاغ القوية في منع إساءة الاستخدام.
|
ماستدون مبني بتركيزٍ على الأمان والخصوصيَّة. حدِّد ما إذا أردتَ مُشارَكَةَ مَنشُوراتِكَ مَعَ مُتابِعيك، أو الأشخاصِ الَّذينَ أشَرتَ إليهِم فَقَط أو العالَمَ بأسرِه. تتيح لك تحذيرات المحتوى إخفاء المنشورات التي تحتوي على مواد حساسة أو محفِّزَة حتى تكون مستعد للتفاعل مع محتواها. لكل مجتمع إرشاداته الخاصة ومشرفيه الخاصين للحفاظ على أمان أعضائه، كما تُساعد أدوات الحظر والإبلاغ القوية في منع إساءة الاستخدام.
|
||||||
|
|
||||||
مَزيدٌ مِنَ المَزايَا:
|
مَزيدٌ مِنَ المَزايَا:
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodon er det største decentraliserede sociale netværk på internettet. I stedet for et enkelt website, er det et netværk af millioner af brugere i uafhængige fællesskaber som alle sammen kan interagere med hinanden. Uanset hvad du interesserer dig for, kan du møde engagerede mennesker som skriver om det på Mastodon!
|
Mastodon er det største decentraliserede sociale netværk på internet. Frem for ét enkelt website, er det i stedet et netværk af millioner af brugere i uafhængige fællesskaber, som alle kan interagere med hinanden. Uanset interessesfære, kan man møde engagerede personer, som skriver herom på Mastodon!
|
||||||
|
|
||||||
Find et fællesskab og opret din profil. Find og følg fascinerende mennesker og læs deres indlæg i en reklamefri, kronologisk tidslinje. Udtryk dig selv med emojis, billeder, GIFs, videoer og lyd i indlæg på op til 500 tegn. Svar på tråde og del alt det gode ved at booste indlæg fra andre. Find nye brugere at følge og aktuelle hashtags så dit netværk udvides.
|
Find et fællesskab og opret din profil. Find og følg fascinerende folk og læs deres indlæg i en reklamefri, kronologisk tidslinje. Udtryk dig selv med tilpassede emojis, billeder, GIF'er, videoer og lyd i 500-tegns indlæg. Svar på tråde og genpost indlæg fra enhver for dele alt det gode. Find nye konti at følge, og populære hashtags, for at udvide dit netværk.
|
||||||
|
|
||||||
Mastodon er bygget med fokus på privatliv og sikkerhed. Beslut om dine indlæg skal deles med dine følgere, bare dem du nævner eller hele verden. Indholdsadvarsler giver dig mulighed for at gemme indlæg med sensitivt eller triggende indhold indtil du er klar til at læse dem. Hvert fællesskab har sine egne regler og moderatorer som holder øje og sikrer medlemmerne mod spam og trolde. De har robuste blokerings- og rapporteringsredskaber til deres rådighed.
|
Mastodon er bygget med fokus på fortrolighed og sikkerhed. Afgør, hvorvidt dine indlæg skal deles med Følgere, blot dem du nævner eller hele verden. Indholdsadvarsler muliggør at skjule indlæg med sensitivt eller udløsende indhold, indtil du er klar til at læse dem. Hvert fællesskab har deres egne retningslinjer og moderatorer til at holde deres medlemmer sikre, og robuste blokerings- og anmeldelsesværktøjer hjælper med at forhindre misbrug.
|
||||||
|
|
||||||
Flere funktioner:
|
Flere funktioner:
|
||||||
|
|
||||||
• Mørk Mode: Læs indlæg i lys, mørk eller ægte sort tilstand
|
• Mørk tilstand: Læs indlæg i lys, mørk eller ægte sort tilstand
|
||||||
• Afstemninger: Spørg tilhængere om deres mening og stemme
|
• Afstemninger: Spørg Følgere om deres mening og stemme
|
||||||
• Udforsk: Populære hashtags og konti er et tryk væk
|
• Udforsk: Populære hashtags og konti er ét tryk væk
|
||||||
• Notifikationer: Få besked om nye følgere, svar og boosts
|
• Notifikationer: Få besked om nye Følgere, svar og genpostninger
|
||||||
• Deling: Send direkte til Mastodon fra en hvilken som app
|
• Deling: Post direkte til Mastodon fra enhver apps delingsfunktion
|
||||||
• Nuttethedsfaktor: Vores maskot er en yndig elefant, og du vil se dem dukke op fra tid til anden
|
• Nuttethed: Vores maskot er en yndig elefant, som du vil se dukke op fra tid til anden
|
||||||
|
|
||||||
Mastodon er en registreret nonprofit og udvikling understøttes direkte af dine donationer. Der er ingen reklame, ingen indtægtsgenerering og ingen risikovillig kapital, og sådan forbliver det.
|
Mastodon er en registreret nonprofit, hvis udvikling direkte understøttes af dine donationer. Der er ingen annoncering, ingen indtægtsgenerering og ingen risikovillig kapital, og intentionen er, at det forbliver sådan.
|
||||||
|
|||||||
3
fastlane/metadata/android/en-US/changelogs/86.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
- You can now easily share and scan QR codes to quickly find each other
|
||||||
|
- We've updated the look of the tab bar to better match current platform guidelines
|
||||||
|
- Various minor usability improvements
|
||||||
3
fastlane/metadata/android/en-US/changelogs/88.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
- QR code scanner can now be accessed from the explore page
|
||||||
|
- You can now pin posts to your profile
|
||||||
|
- Fixed featured hashtags on profiles not showing the profile's own posts exclusively
|
||||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 844 KiB After Width: | Height: | Size: 596 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 776 KiB After Width: | Height: | Size: 748 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 790 KiB After Width: | Height: | Size: 722 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
@@ -1,6 +1,6 @@
|
|||||||
Mastodon Interneteko sare sozial deszentralizatu handiena da. Webgune bakar bat izan ordez, beren artean elkarreragin dezaketen komunitate independenteetako milioika erabiltzailek osatutako sarea da. Zure interesak direnak direla ere, jende interesgarria aurkituko duzu Mastodonen!
|
Mastodon Interneteko sare sozial deszentralizatu handiena da. Webgune bakar bat izan ordez, beren artean elkarreragin dezaketen komunitate independenteetako milioika erabiltzailek osatutako sarea da. Zure interesak direnak direla ere, jende interesgarria aurkituko duzu Mastodonen!
|
||||||
|
|
||||||
Batu komunitate batera eta sortu zure profila. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Adierazi nahi duzuna 500 karaktereko bidalketetan emoji pertsonalizatuak, irudiak, GIFak, bideoak eta audioak erabiliz. Erantzun edozeinen hariak eta eman bultzada bidalketei edukiak partekatzeko. Bilatu jarraitzeko kontu berriak eta traolen joerak zure sarea zabaltzeko.
|
Batu komunitate batera eta sortu zure profila. Bilatu eta jarraitu jende zoragarria eta irakurri beren bidalketak, publizitaterik gabeko denbora-lerro kronologikoan. Adierazi nahi duzuna 500 karaktereko bidalketetan emoji pertsonalizatuak, irudiak, GIFak, bideoak eta audioak erabiliz. Erantzun edozeinen hariak eta eman bultzada bidalketei edukiak partekatzeko. Bilatu jarraitzeko kontu berriak eta traolen joerak zure sarea zabaltzeko.
|
||||||
|
|
||||||
Mastodon pribatutasunean eta segurtasunean arreta jarriz eraikia dago. Erabaki zure bidalketak norekin partekatu: zure jarraitzaileekin, aipatzen dituzunekin edo mundu osoarekin. Edukiaren abisuek aukera ematen dute eduki sentibera edo zuregan eragina izan dezaketen bidalketak zuk erabaki arte ezkutatzeko. Komunitate bakoitzak bere gidalerro eta moderatzaileak ditu, bertako kideak seguru mantentzeko. Baita blokeatzeko eta salatzeko tresna sendoak ere abusuak galarazteko.
|
Mastodon pribatutasunean eta segurtasunean arreta jarriz eraikia dago. Erabaki zure bidalketak norekin partekatu: zure jarraitzaileekin, aipatzen dituzunekin edo mundu osoarekin. Edukiaren abisuek aukera ematen dute eduki sentibera edo zuregan eragina izan dezaketen bidalketak zuk erabaki arte ezkutatzeko. Komunitate bakoitzak bere gidalerro eta moderatzaileak ditu, bertako kideak seguru mantentzeko. Baita blokeatzeko eta salatzeko tresna sendoak ere abusuak galarazteko.
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
ماستودون بزرگترین شبکه اجتماعی غیرمتمرکز در اینترنت است. به جای یک وب سایت واحد، شبکه ای متشکل از میلیون ها کاربر در جوامع مستقل است که همگی می توانند به صورت یکپارچه با یکدیگر تعامل داشته باشند. مهم نیست که به چه چیزی علاقه دارید، می توانید با افراد پرشوری که درباره آن در ماستودون فرسته ارسال میکنند صحبت کنید!
|
||||||
|
|
||||||
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
به یک اجتماع بپیوندید و نمایه خود را ایجاد کنید. افراد جذاب را پیدا کنید و پیگیری کنید و فرستههای آنها را در یک خط زمانی بدون تبلیغات و زمانی بخوانید. با ایموجیهای سفارشی، تصاویر، گیفها، ویدیوها و صدا در فرستههای 500 کاراکتری، خود را بیان کنید. برای به اشتراک گذاشتن مطالب عالی، به موضوعات پاسخ دهید و فرستههای هر کسی را مجددا ریبلاگ کنید. اکانت های جدیدی را برای پیگیری پیدا کنید و برچسبهای پرطرفدار را برای گسترش شبکه خود پیدا کنید.
|
||||||
|
|
||||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
ماستودون با تمرکز بر حریم خصوصی و ایمنی ساخته شده است. تصمیم بگیرید که آیا پستهای شما با پیگیرهایتان، فقط افرادی که نام میبرید، یا کل دنیا به اشتراک گذاشته شود. هشدارهای محتوا به شما امکان میدهند پستهای حاوی مطالب حساس یا محرک را پنهان کنید تا زمانی که آماده تعامل با آنها باشید. هر اجتماعای دستورالعملها و ناظران خود را دارد تا اعضای خود را ایمن نگه دارد و ابزارهای قوی مسدود کردن و گزارشدهی به جلوگیری از سوء استفاده کمک میکند.
|
||||||
|
|
||||||
More features:
|
قابلیت های دیگر:
|
||||||
|
|
||||||
• Dark Mode: Read posts in light, dark, or true black mode
|
• حالت تاریک: فرستهها را در حالت روشن، تاریک، یا سیاه بخوانید
|
||||||
• Polls: Ask followers for their opinion and tally the votes
|
• نظرسنجی: از پیگیران خود نظر بخواهید و رایها را جمعآوری کنید
|
||||||
• Explore: Trending hashtags and accounts are a tap away
|
• کاوش: برچسبها و حسابهای پرطرفدار یک ضربه فاصله دارند
|
||||||
• Notifications: Get notified about new follows, replies, and reblogs
|
• آگاهیها: از پیگیریها، پاسخهای جدید و ریبلاگها مطلع شوید
|
||||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
• اشتراک گذاری: از هر برگه اشتراکی در هر برنامه، مستقیماً به ماستودون ارسال کنید
|
||||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
• ناز: طلسم ما یک فیل شایان ستایش است، و شما هر از گاهی آنها را خواهید دید
|
||||||
|
|
||||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
ماستودون یک سازمان غیرانتفاعی ثبت شده است و توسعه مستقیماً توسط اعانه های مالی شما پشتیبانی می شود. نه تبلیغاتی وجود دارد،نه کسب درآمدی و نه سرمایه گذاری خطرپذیری و ما قصد داریم آن را به همین شکل حفظ کنیم.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Decentralized social network
|
شبکه اجتماعی نامتمرکز
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Mastodon on internetin suurin hajautettu sosiaalinen verkosto. Yhden verkkopalvelun sijaan, se on miljoonien itsenäisissä yhteisöissä olevien käyttäjien verkosto, jotka voivat olla vuorovaikutuksessa toistensa kanssa saumattomasti. Riippumatta siitä, mistä olet kiinnostunut, voit tavata intohimoisia ihmisiä, jotka julkaisevat aiheesta Mastodonissa!
|
Mastodon on internetin suurin hajautettu sosiaalinen verkosto. Yhden verkkopalvelun sijaan, se on miljoonien itsenäisissä yhteisöissä olevien käyttäjien verkosto, jotka voivat olla vuorovaikutuksessa toistensa kanssa saumattomasti. Riippumatta siitä, mistä olet kiinnostunut, voit tavata samanmielisiä ihmisiä, jotka julkaisevat aiheesta Mastodonissa!
|
||||||
|
|
||||||
Liity yhteisöön ja luo itsellesi tili. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Ilmaise itseäsi mukautetuilla emojeilla, kuvilla, videoilla ja audiolla 500 merkin pituisissa julkaisuissa. Vastaa viestiketjuihin ja edelleen jaa julkaisuja keneltä tahansa, jakaaksesi hienoja juttuja. Löydä uusia tilejä seurattavaksi ja trendaavia hashtageja laajentaaksesi verkostoasi.
|
Liity yhteisöön ja luo itsellesi tili. Löydä ja seuraa kiehtovia ihmisiä ja lue heidän julkaisunsa ilman mainoksia, kronologisella aikajanalla. Ilmaise itseäsi mukautetuilla emojeilla, kuvilla, videoilla ja audiolla 500 merkin pituisissa julkaisuissa. Vastaa viestiketjuihin ja edelleen jaa julkaisuja keneltä tahansa, jakaaksesi hienoja juttuja. Löydä uusia tilejä seurattavaksi ja suosittuja aihetunnisteita laajentaaksesi verkostoasi.
|
||||||
|
|
||||||
Mastodon on rakennettu keskittyen yksityisyyteen ja turvallisuuteen. Päätä, jaetaanko julkaisusi seuraajille, vain mainitsemillesi ihmisille vai koko maailmalle. Sisältövaroitusten avulla, voit piilottaa julkaisut, jotka sisältävät arkaluontoista tai laukaisevaa materiaalia, kunnes olet valmis käsittelemään niitä. Jokaisella yhteisöllä on omat ohjeistonsa ja valvojansa, jotka pitävät jäsenensä turvassa, ja tehokkaat esto- ja ilmiantotyökalut auttavat torjumaan väärinkäytöksiä.
|
Mastodon on rakennettu keskittyen yksityisyyteen ja turvallisuuteen. Päätä, jaetaanko julkaisusi seuraajille, vain mainitsemillesi ihmisille vai koko maailmalle. Sisältövaroitusten avulla, voit piilottaa julkaisut, jotka sisältävät arkaluontoista tai laukaisevaa materiaalia, kunnes olet valmis käsittelemään niitä. Jokaisella yhteisöllä on omat ohjeistonsa ja valvojansa, jotka pitävät jäsenensä turvassa, ja tehokkaat esto- ja ilmiantotyökalut auttavat torjumaan väärinkäytöksiä.
|
||||||
|
|
||||||
@@ -8,8 +8,8 @@ Lisää ominaisuuksia:
|
|||||||
|
|
||||||
• Tumma tila: Lue julkaisut vaaleassa, tummassa tai mustan tummassa tilassa
|
• Tumma tila: Lue julkaisut vaaleassa, tummassa tai mustan tummassa tilassa
|
||||||
• Kyselyt: Kysy seuraajilta heidän mielipidettään ja laske äänet
|
• Kyselyt: Kysy seuraajilta heidän mielipidettään ja laske äänet
|
||||||
• Tutustu: Trendaavat hashtagit ja tilit ovat vain napsautuksen päässä
|
• Tutustu: Suositut aihetunnisteet ja tilit ovat vain napsautuksen päässä
|
||||||
• Ilmoitukset: Saat ilmoituksen uusista seuraajista, vastauksista ja edelleen jaoista
|
• Ilmoitukset: Saat ilmoituksen uusista seuraajista, vastauksista ja tehostuksista
|
||||||
• Jakaminen: Julkaise suoraan Mastodoniin minkä tahansa sovelluksen jakovalikon kautta
|
• Jakaminen: Julkaise suoraan Mastodoniin minkä tahansa sovelluksen jakovalikon kautta
|
||||||
• Suloisuus: Maskottimme on ihastuttava mastodontti ja näet sen ajoittain
|
• Suloisuus: Maskottimme on ihastuttava mastodontti ja näet sen ajoittain
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
Mastodon इंटरनेट का सबसे बड़ा डिसेंट्रलाइज़्ड सोशल नेटवर्क है। एक सिंगल वेबसाइट के जगह, ये हज़ारों आज़ाद ग्रुपों के लाखों यूज़रों का एक नेटवर्क है जो एक दूसरे से आसानी से बात करते है। चाहे आपकी जो भी दिलचस्पी हो, आपको उसके ऊपर पोस्ट करनेवाले लोग Mastodon पे ज़रूर मिलेंगे!
|
||||||
|
|
||||||
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
कोई ग्रुप जॉइन करें और अपना प्रोफाइल बनाएं। दिलचस्प लोगों को ढूंढ़ें और फॉलो करें और उनके पोस्ट पढ़ें बिना किसी ऐड के। Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||||
|
|
||||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||||
|
|
||||||
More features:
|
एक्स्ट्रा फीचर:
|
||||||
|
|
||||||
• Dark Mode: Read posts in light, dark, or true black mode
|
• डार्क मोड: पोस्ट लाइट, डार्क, या प्योर ब्लैक मोड में पढ़ें
|
||||||
• Polls: Ask followers for their opinion and tally the votes
|
• Polls: Ask followers for their opinion and tally the votes
|
||||||
• Explore: Trending hashtags and accounts are a tap away
|
• Explore: Trending hashtags and accounts are a tap away
|
||||||
• Notifications: Get notified about new follows, replies, and reblogs
|
• Notifications: Get notified about new follows, replies, and reblogs
|
||||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
• क्यूटपन: हमारा मैस्कॉट एक प्यारा हाथी है, और आप उसे समय-समय पे देखेंगे
|
||||||
|
|
||||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Decentralized social network
|
डिसेंट्रलाइज़्ड सोशल नेटवर्क
|
||||||
@@ -1 +1 @@
|
|||||||
Decentralizált szociális hálózat
|
Decentralizált közösségi hálózat
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
Մաստոդոնը համացանցի ամենամեծ ապակենտրոնացված սոցցանցն է։ Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
Մաստոդոնը համացանցի ամենամեծ ապակենտրոնացված սոցցանցն է։ Այն մի կայք չէ, այլ իրար հետ կապակցված անկախ համայնքների միլիոնավոր օգտատերերից կազմված ցանց։ No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||||
|
|
||||||
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
Միացեք համայնքին և ստեղծեք հաշիվ։ Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||||
|
|
||||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Զգայուն կամ հրահրող թեմաներով գրառումները կարելի է թաքցնել նախազգուշացումներով։ Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||||
|
|
||||||
Ավելին՝
|
Ավելին.
|
||||||
|
|
||||||
• Մուգ տարբերակ՝ կարդացեք գրառումներ մուգ, բաց կամ իսկական սև տարբերակներում
|
• Մուգ տարբերակ. կարդացեք գրառումներ մուգ, բաց կամ իսկական սև տարբերակներում
|
||||||
• Հարցումներ՝ իմացեք ձեր հետևորդների կարծիքը և հաշվեք քվեները
|
• Հարցումներ. իմացեք ձեր հետևորդների կարծիքը և հաշվեք ձայները
|
||||||
• Explore: Trending hashtags and accounts are a tap away
|
• Explore: Trending hashtags and accounts are a tap away
|
||||||
• Notifications: Get notified about new follows, replies, and reblogs
|
• Հիշեցումներ` ստացեք հիշեցումներ նոր հետևումների, մեկնաբանությունների և կիսումների մասին
|
||||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||||
|
|
||||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
Մաստոդոնը գրանցված շահույթ չհետապնդող կազմակերպություն է, և աջակցվում է ձեր նվիրաբերություններով։ There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Decentralized social network
|
Ապակենտրոնացված սոցիալական ցանց
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Mastodon adalah jejaring sosial terdesentralisasi terbesar di internet. Daripada sebuah satu situs web, ini adalah jaringan dari jutaan pengguna dalam komunitas tersendiri yang dapat berinteraksi antar sesama, tanpa masalah. Tanpa memedulikan apa yang Anda minat, Anda dapat bertemu orang-orang yang mengirimkan apa yang mereka minat di Mastodon!
|
Mastodon dalah jaringan sosial terdesentralisasi terbesar di internet. Bukan hanya satu situs web, ini adalah jaringan dari jutaan pengguna dalam komunitas tersendiri yang dapat saling interaksi antar sesama, tanpa batasan. Apapun yang kamu minati, kamu dapat bertemu orang-orang baru yang mengirimkan apa yang mereka minati di Mastodon!
|
||||||
|
|
||||||
Bergabung sebuah komunitas dan buat profil Anda. Temukan dan ikuti orang-orang yang menarik dan baca postingan mereka dalam lini masa yang kronologis dan bebas iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
|
Bergabunglah dalam sebuah komunitas dan buat profil kalian. Temukan dan ikuti orang-orang yang menarik dan baca postingan mereka dalam linimasa yang kronologis serta bebas iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
|
||||||
|
|
||||||
Mastodon dibuat dengan fokus pada privasi dan keamanan. Tentukan apakah kiriman Anda dibagikan kepada pengikut, hanya orang yang disebut, atau seluruh dunia. Peringatan konten memungkinkan Anda untuk menyembunyikan kiriman yang berisi material sensitif atau memicu sampai Anda siap untuk terlibat dengan mereka. Setiap komunitas memiliki pedoman dan moderator sendiri-sendiri untuk menjaga anggotanya aman, dan alat pemblokiran dan pelaporan yang kokoh membantu mencegah pelecehan.
|
Mastodon dibuat dengan fokus pada privasi dan keamanan. Tentukan apakah kiriman Anda dibagikan kepada pengikut, hanya orang yang disebut, atau seluruh dunia. Peringatan konten memungkinkan Anda untuk menyembunyikan kiriman yang berisi material sensitif atau memicu sampai Anda siap untuk terlibat dengan mereka. Setiap komunitas memiliki pedoman dan moderator sendiri-sendiri untuk menjaga anggotanya aman, dan alat pemblokiran dan pelaporan yang kokoh membantu mencegah pelecehan.
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodonは、インターネット上で最大の分散型ソーシャルネットワークです。 Mastodonは単一のウェブサイトではなく、それぞれ独立したコミュニティに参加している何百万人ものユーザーによって構成されたネットワークなのです。ユーザーたちはその中で、誰もがお互いとシームレスにやり取りできます。 あなたの興味関心がどんな分野にあっても、きっとMastodonのどこかで同じ情熱を投稿している仲間がいることでしょう。
|
Mastodon は、インターネット上で最大の分散型ソーシャルネットワークです。 Mastodon は単一のウェブサイトではなく、それぞれ独立したコミュニティに参加している何百万人ものユーザーによって構成されたネットワークなのです。ユーザーたちはその中で、誰もがお互いとシームレスにやり取りできます。 あなたの興味関心がどんな分野にあっても、きっと Mastodon のどこかで同じ情熱を投稿している仲間がいることでしょう。
|
||||||
|
|
||||||
まずはコミュニティに参加して、自分のプロフィールを作成しましょう。 魅力的なユーザーを見つけたら、フォローしてタイムラインで投稿を見てみましょう。タイムラインに広告は出てきません。順番も時系列順です。 そして、500文字まで使える投稿で自分を表現してみましょう。カスタム絵文字や画像、GIF、動画、音声も使用できます。 スレッドに返事したり、他の誰かの面白い投稿をブーストして共有したりすることもできます。 興味のあるアカウントとホットなハッシュタグをどんどん見つけて、あなた自身のネットワークを広げていきましょう。
|
まずはコミュニティに参加して、自分のプロフィールを作成しましょう。 魅力的なユーザーを見つけたら、フォローしてタイムラインで投稿を見てみましょう。タイムラインに広告は出てきません。順番も時系列順です。 そして、500 文字まで使える投稿で自分を表現してみましょう。カスタム絵文字や画像、GIF、動画、音声も使用できます。 スレッドに返事したり、他の誰かの面白い投稿をブーストして共有したりすることもできます。 興味のあるアカウントとホットなハッシュタグをどんどん見つけて、あなた自身のネットワークを広げていきましょう。
|
||||||
|
|
||||||
Mastondonはプライバシーと安全性を重視しています。 自分の投稿をフォロワーだけに限定公開にするのか、メンションした特定のユーザーだけに共有するのか、全世界に放流するのか、自由に決められます。 また、入力中の投稿について「ちょっとセンシティブな内容だな」と思ったら、閲覧注意機能で内容を伏せることで、見たくない人に配慮した投稿が作成できます。 そして、各コミュニティにはそれぞれのガイドラインと管理者・モデレーターが存在し、コミュニティメンバーの安全を守っています。強力なブロック・通報機能も、不正利用の防止をお手伝いします。
|
Mastondon はプライバシーと安全性を重視しています。 自分の投稿をフォロワーだけに限定公開にするのか、メンションした特定のユーザーだけに共有するのか、全世界に放流するのか、自由に決められます。 また、入力中の投稿について「ちょっとセンシティブな内容だな」と思ったら、閲覧注意機能で内容を伏せることで、見たくない人に配慮した投稿が作成できます。 そして、各コミュニティにはそれぞれのガイドラインと管理者・モデレーターが存在し、コミュニティメンバーの安全を守っています。強力なブロック・通報機能も、不正利用の防止をお手伝いします。
|
||||||
|
|
||||||
その他の機能:
|
その他の機能:
|
||||||
|
|
||||||
• ダークモード対応:ライトモードだけでなく、ダークモードや「真っ黒」モードで投稿を閲覧
|
• ダークモード: ライトモードだけでなく、ダークモードや「真っ黒」モードで投稿を閲覧
|
||||||
• 投票機能:フォロワーたちの意見を投票形式で集計
|
• アンケート: フォロワーたちの意見を投票形式で集計
|
||||||
• 探索:話題のハッシュタグやアカウントに1タップでアクセス
|
• 探索: 話題のハッシュタグやアカウントに 1 タップでアクセス
|
||||||
• 通知設定:新しいフォローやリプライ、ブーストがあった時に通知
|
• 通知: 新しいフォローやリプライ、ブーストがあった時に通知
|
||||||
• 共有:どのアプリからでも、「共有」メニューを通じてMastodonへ直接投稿
|
• 共有: どのアプリからでも「共有」メニューを通じて Mastodon へ直接投稿
|
||||||
• 癒し:Mastodonが誇る象のマスコット(かわいい)が、画面にお邪魔したり、しなかったり
|
• 癒し: Mastodon が誇る象のマスコット(かわいい)が、画面にお邪魔したり、しなかったり
|
||||||
|
|
||||||
Mastodonは公認の非営利アプリです。開発は全てユーザーの寄付から成り立っています。 広告なし、アフィリエイトなし、第三者組織による出資なし。今でも、そしてこれからもそんなアプリであり続けるために、我々は日々努力し続けています。
|
Mastodon は公認の非営利アプリです。開発は全てユーザーの寄付から成り立っています。 広告なし、アフィリエイトなし、第三者組織による出資なし。今でも、そしてこれからもそんなアプリであり続けるために、我々は日々努力し続けています。
|
||||||
|
|||||||
16
fastlane/metadata/android/ka-GE/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||||
|
|
||||||
|
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||||
|
|
||||||
|
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||||
|
|
||||||
|
More features:
|
||||||
|
|
||||||
|
• Dark Mode: Read posts in light, dark, or true black mode
|
||||||
|
• Polls: Ask followers for their opinion and tally the votes
|
||||||
|
• Explore: Trending hashtags and accounts are a tap away
|
||||||
|
• Notifications: Get notified about new follows, replies, and reblogs
|
||||||
|
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||||
|
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||||
|
|
||||||
|
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||||
1
fastlane/metadata/android/ka-GE/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Decentralized social network
|
||||||
1
fastlane/metadata/android/ka-GE/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Mastodon
|
||||||
16
fastlane/metadata/android/lt-LT/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Mastodon – didžiausias decentralizuotas socialinis tinklas internete. Vietoj vienos svetainės tai yra milijonų naudotojų, priklausančių nepriklausomoms bendruomenėms, kurios gali sklandžiai bendrauti tarpusavyje, tinklas. Nesvarbu, kuo domiesi, Mastodon gali sutikti aistringų žmonių, skelbiančių apie tai!
|
||||||
|
|
||||||
|
Prisijunk prie bendruomenės ir susikurk savo profilį. Rask ir sek žavius žmones bei skaityk jų įrašus chronologinėje laiko skalėje be reklamų. Išreikšk save su pasirinktais jaustukais, vaizdais, GIF, vaizdo ir garso įrašais 500 simbolių įrašuose. Atsakyk į gijas ir perrašyk bet kurio asmens įrašus, kad galėtum dalytis puikiais dalykais. Ieškok naujų paskyrų sekti ir tendencingų saitažodžių, kad praplėstum savo tinklą.
|
||||||
|
|
||||||
|
Mastodon sukurtas daugiausia dėmesio skiriant privatumui ir saugumui. Nuspręsk, ar tavo įrašai bus bendrinami tavo sekėjams, tik tavo paminėtiems žmonėms, ar visam pasauliui. Turinio įspėjimai leidžia paslėpti įrašus, kuriuose yra jautrios ar dirginančios medžiagos, kol būsi pasiruošęs (-usi) su jais bendrauti. Kiekviena bendruomenė turi savo gaires ir prižiūrėtojus, kad jos nariai būtų saugūs, o patikimi blokavimo ir pranešimo įrankiai padeda užkirsti kelią piktnaudžiavimui.
|
||||||
|
|
||||||
|
Daugiau funkcijų:
|
||||||
|
|
||||||
|
• Tamsusis režimas: skaityk įrašus šviesiu, tamsiu arba tikru juodu režimu
|
||||||
|
• Apklausos: paklausk sekėjų nuomonės ir suskaičiuok balsus
|
||||||
|
• Naršyti: tendencingos saitažodžiai ir paskyros – vos nuo vieno prisilietimo
|
||||||
|
• Pranešimai: gauk pranešimus apie naujus sekėjus, atsakymus ir tinklaraščių perrašymus
|
||||||
|
• Bendrinimas: skelbk tiesiogiai į Mastodon iš bet kurio bendrinimo lapo bet kurioje programėlėje
|
||||||
|
• Mielumas: mūsų talismanas yra žavus drambliukas, kurį retkarčiais pamatysi
|
||||||
|
|
||||||
|
Mastodon yra registruota ne pelno siekianti organizacija, kurios plėtra yra tiesiogiai paremta aukomis. Nėra jokios reklamos, jokių monetizacijos ir rizikos kapitalo, ir mes planuojame, kad taip ir liks.
|
||||||
1
fastlane/metadata/android/lt-LT/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Decentralizuotas socialinis tinklas
|
||||||
1
fastlane/metadata/android/lt-LT/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Mastodon
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Mastodon is het grootste gedecentraliseerde sociale netwerk op het internet. In plaats van één enkele website is het een netwerk van miljoenen gebruikers in onafhankelijke gemeenschappen die allemaal naadloos met elkaar kunnen communiceren. Wat je ook leuk vindt, je kunt gepassioneerde mensen volgen die het met je willen delen op Mastodon!
|
Mastodon is het grootste gedecentraliseerde sociale netwerk op het internet. In plaats van één enkele website is het een netwerk van miljoenen gebruikers in onafhankelijke communities die allemaal naadloos met elkaar kunnen communiceren. Wat je ook leuk vindt, je kunt gepassioneerde mensen volgen die het met je willen delen op Mastodon!
|
||||||
|
|
||||||
Meld je aan bij een community en maak je profiel aan. Vind en volg fascinerende mensen en lees hun berichten in een advertentievrije, chronologische tijdlijn. Druk jezelf uit met aangepaste emoji, afbeeldingen, GIF’s, video’s en audio in berichten van 500 karakters. Reageer op discussies en boost berichten van anderen om geweldige dingen te delen. Vind nieuwe accounts om te volgen en hashtags om je netwerk uit te breiden.
|
Meld je aan bij een community en maak je profiel aan. Vind en volg fascinerende mensen en lees hun berichten in een advertentievrije, chronologische tijdlijn. Druk jezelf uit met aangepaste emoji, afbeeldingen, GIF’s, video’s en audio in berichten van 500 karakters. Reageer op discussies en boost berichten van anderen om geweldige dingen te delen. Vind nieuwe accounts om te volgen en hashtags om je netwerk uit te breiden.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
Mastodon เป็นเครือข่ายสังคมแบบกระจายศูนย์ที่ใหญ่ที่สุดบนอินเทอร์เน็ต ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon! ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon!
|
Mastodon เป็นเครือข่ายสังคมแบบกระจายศูนย์ที่ใหญ่ที่สุดบนอินเทอร์เน็ต ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon!
|
||||||
|
|
||||||
เข้าร่วมชุมชนและสร้างโปรไฟล์ ค้นหาและติดตามผู้คนที่น่าสนใจและอ่านโพสต์ของเขาในเส้นเวลาที่ไม่มีโฆษณาและเรียงตามลำดับเวลา แสดงความรู้สึกของตัวคุณเองด้วยอีโมจิที่กำหนดเอง รูปภาพ GIF วิดีโอ และเสียงในโพสต์ 500 ตัวอักษร ตอบกลับและดันโพสต์จากคนอื่น ๆ เพื่อแชร์สิ่งดี ๆ และค้นหาบัญชีใหม่ ๆ ที่จะติดตามและแฮชแท็กที่เป็นที่นิยมเพื่อขยายเครือข่ายของคุณ
|
เข้าร่วมชุมชนและสร้างโปรไฟล์ ค้นหาและติดตามผู้คนที่น่าสนใจและอ่านโพสต์ของเขาในเส้นเวลาที่ไม่มีโฆษณาและเรียงตามลำดับเวลา แสดงความรู้สึกของตัวคุณเองด้วยอีโมจิที่กำหนดเอง รูปภาพ GIF วิดีโอ และเสียงในโพสต์ 500 ตัวอักษร ตอบกลับและดันโพสต์จากคนอื่น ๆ เพื่อแชร์สิ่งดี ๆ และค้นหาบัญชีใหม่ ๆ ที่จะติดตามและแฮชแท็กที่เป็นที่นิยมเพื่อขยายเครือข่ายของคุณ
|
||||||
|
|
||||||
Mastodon สร้างขึ้นโดยเน้นความเป็นส่วนตัวและความปลอดภัยเป็นสำคัญ คุณสามารถตัดสินใจได้ว่าโพสต์ของคุณจะถูกแชร์กับผู้ติดตามของคุณ คนที่คุณกล่าวถึง หรือคนทั้งโลกก็ได้ ฟังก์ชั่นคำเตือนเนื้อหาช่วยให้คุณซ่อนโพสต์ที่มีเนื้อหาที่ละเอียดอ่อนจนกว่าคุณจะพร้อมเห็นเนื้อหานั้น แต่ละชุมชนจะมีหลักเกณฑ์และผู้ควบคุมเป็นของตนเองเพื่อรักษาความปลอดภัยของสมาชิก รวมถึงเครื่องมือการปิดกั้นและรายงานที่มีประสิทธิภาพเพื่อช่วยป้องกันการกระทำผิด
|
Mastodon สร้างขึ้นโดยเน้นความเป็นส่วนตัวและความปลอดภัยเป็นสำคัญ คุณสามารถตัดสินใจได้ว่าโพสต์ของคุณจะถูกแชร์กับผู้ติดตามของคุณ คนที่คุณกล่าวถึง หรือคนทั้งโลกก็ได้ ฟังก์ชั่นคำเตือนเนื้อหาช่วยให้คุณซ่อนโพสต์ที่มีเนื้อหาที่ละเอียดอ่อนจนกว่าคุณจะพร้อมเห็นเนื้อหานั้น แต่ละชุมชนจะมีหลักเกณฑ์และผู้กลั่นกรองเป็นของตนเองเพื่อรักษาความปลอดภัยของสมาชิก รวมถึงเครื่องมือการปิดกั้นและรายงานที่มีประสิทธิภาพเพื่อช่วยป้องกันการกระทำผิด
|
||||||
|
|
||||||
คุณสมบัติอื่น ๆ:
|
คุณสมบัติอื่น ๆ:
|
||||||
|
|
||||||
@@ -13,4 +13,4 @@ Mastodon สร้างขึ้นโดยเน้นความเป็
|
|||||||
• การแชร์: โพสต์ลง Mastodon ได้โดยตรงจากแอปอื่น ๆ ที่อยู่ในเครื่อง
|
• การแชร์: โพสต์ลง Mastodon ได้โดยตรงจากแอปอื่น ๆ ที่อยู่ในเครื่อง
|
||||||
• ความน่ารัก: มาสคอตของเราเป็นช้างน่ารัก และคุณจะเห็นมันโผล่ออกมาเป็นระยะ ๆ
|
• ความน่ารัก: มาสคอตของเราเป็นช้างน่ารัก และคุณจะเห็นมันโผล่ออกมาเป็นระยะ ๆ
|
||||||
|
|
||||||
Mastodon เป็นองค์กรไม่แสวงหาผลกำไรที่จดทะเบียนแล้ว และการพัฒนาได้รับการสนับสนุนจากเงินบริจาคของคุณโดยตรง ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป
|
Mastodon เป็นองค์กรไม่แสวงหาผลกำไรที่จดทะเบียนแล้ว และการพัฒนาได้รับการสนับสนุนจากเงินบริจาคของคุณโดยตรง ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodon, internetteki merkezi olmayan en büyük sosyal ağdır. Tek bir web siteye bağlı kalmaksızın, milyonlarca kullanıcının bağımsız olarak birbiri ile kolayca etkileşebileceği bir ağdır. Hangi konuyla ilgili olduğun önemli değil, Mastodon'da onunla ilgili gönderi paylaşan tutkulu insanlarla tanışabilirsin!
|
Mastodon, internetteki merkezi olmayan en büyük sosyal ağdır. Tek bir web siteye bağlı kalmaksızın, milyonlarca kullanıcının bağımsız olarak birbirleriyle kolayca etkileşebileceği bir ağdır. Hangi konuyla ilgili olduğunuz önemli değil, Mastodon'da onunla ilgili gönderi paylaşan tutkulu insanlarla tanışabilirsiniz!
|
||||||
|
|
||||||
Bir topluluğa katıl ve profilini oluştur. Olağanüstü kişileri bul ve takip et; gönderilerini reklamsız ve kronolojik bir akışta oku. Gönderilerinde 500 karakter sınırlamasıyla kendini emojiler, görseller, GIFler, videolar ve sesler ile ifade et. Harika içerikler paylaşmak için başlıklara yanıt yaz, insanların gönderilerini yeniden paylaş. Ağınızı genişletmek için takip edilecek yeni hesaplar ve hashtagler bul.
|
Bir topluluğa katılın ve profilinizi oluşturun. Olağanüstü kişileri bulun ve takip edin; gönderilerini reklamsız ve kronolojik bir zaman çizelgesinde okuyun. Gönderilerinizde şimdilik 500 karakter sınırlamasıyla kendinizi emojiler, görseller, GIFler, videolar ve sesler ile ifade edin. Harika içerikler paylaşmak için başlıklara yanıt yazın, insanların gönderilerini yineleyin. Ağınızı genişletmek için takip edilecek yeni hesaplar ve hashtagler bulun.
|
||||||
|
|
||||||
Mastodon gizlilik ve güvenlik odaklı yapılmıştır. Her postunuz için takipçilerinizle mi, bahsettiğiniz kişilerle mi ya da tüm dünyayla mı paylaşılacağına karar verin. Gönderi uyarıları, hassas ve tetikleyici olabilecek içerikleri kişi görmeyi hazır olana kadar gizler. Her topluluk, üyelerini güvende tutmak için kendi kurallarına ve moderatörlerine; istismarı önlemek için de güçlü engelleme ve bildirme araçlarına sahiptir.
|
Mastodon, mahremiyet ve güvenliğe odaklanılarak inşa edilmiştir. Gönderilerinizi takipçilerinizle mi, sadece bahsettiğiniz kişilerle mi yoksa tüm dünyayla mı paylaşılacağına karar verin. İçerik uyarıları, hassas veya tetikleyici materyal içeren gönderileri, siz onlarla etkileşim kurmaya hazır olana kadar gizlemenize olanak tanır. Her topluluk, üyelerini güvende tutmak için kendi kurallarına ve moderatörlerine; istismarı önlemek için de güçlü engelleme ve bildirme araçlarına sahiptir.
|
||||||
|
|
||||||
Diğer özellikler:
|
Diğer özellikler:
|
||||||
|
|
||||||
• Koyu Mod: Gönderileri aydınlık, karanlık ya da gerçek karanlık modunda okuyabilirsin
|
• Karanlık Mod: Gönderileri aydınlık, karanlık ya da gerçek karanlık modunda okuyabilirsin
|
||||||
• Anketler: Takipçilerine görüşlerini sor ve oylarını gör
|
• Anketler: Takipçilere fikirlerini sorun ve oylarını görün
|
||||||
• Keşfet: Trend hashtagler ve hesaplar bir tık uzağında
|
• Keşfet: Öne çıkan etiketlerler ve hesaplar bir tık uzağınızda
|
||||||
• Bildirimler: Yeni takipçilerden, yanıtlardan ve yeniden paylaşımlardan haberin olsun
|
• Bildirimler: Yeni takipçilerden, yanıtlardan ve yeniden paylaşımlardan haberiniz olsun
|
||||||
• Paylaşım: Doğrudan Mastodon'a herhangi bir tipte gönderi paylaş
|
• Paylaşım: Doğrudan Mastodon'a herhangi bir türde gönderi paylaş
|
||||||
• Sevimlilik: Maskotumuz şirin bir fil ve onu uygulamada zaman zaman göreceksin
|
• Sevimlilik: Maskotumuz şirin bir fil ve onu uygulamada zaman zaman göreceksiniz
|
||||||
|
|
||||||
Mastodon kar amacı gütmeyen bir kuruluştur ve geliştirilmesi doğrudan bağışlarınızla sağlanmaktadır. Reklam, para kazanma amacı, risk sermayesi yoktur ve bunu böyle tutmayı planlıyoruz.
|
Mastodon kâr amacı gütmeyen bir kuruluştur ve geliştirilmesi doğrudan bağışlarınızla sağlanmaktadır. Reklam yok, para kazanma güdüsü yok, risk sermayesi yok ve bu şekilde devam etmeyi planlıyoruz.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Merkezsizleştirilmiş sosyal ağ
|
Merkezi olmayan sosyal ağ
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodon — найбільша децентралізована соціальна мережа в інтернеті. Замість одного сайту це мережа мільйонів користувачів у незалежних спільнотах, які можуть взаємодіяти один з одним. Незалежно від того, чим ви займаєтеся, ви можете зустріти захоплених людей, які пишуть про це на Mastodon!
|
Mastodon — найбільша децентралізована соціальна мережа в інтернеті. Це не один сайт, а мережа з мільйонів користувачів у незалежних спільнотах, які взаємодіють одна з одною. Незалежно від того, чим ви займаєтеся, ви можете зустріти людей зі спільними інтересами, які пишуть про це на Mastodon!
|
||||||
|
|
||||||
Приєднуйтесь до спільноти і створіть свій профіль. Знайдіть і підпишіться на цікавих людей і читайте пости у вільний від реклами стрічці. Виразіть себе за допомогою користувацьких емоджі, зображень, GIF, відео й аудіо з 500-символьними постами. Відповідайте на теми й робіть репости постів від будь-кого, щоб ділитися гарними матеріалами. Знаходьте нові облікові записи, щоб підписатися і популярні хештеги для розширення вашої мережі.
|
Приєднуйтесь до спільноти та створіть свій профіль. Знайдіть і підпишіться на цікавих людей та читайте дописи у вільний від реклами стрічці. Виразіть себе за допомогою користувацьких емоджі, зображень, GIF, відео й аудіо у дописах, обмеженими 500 символами. Відповідайте на теми та поширюйте дописи від будь-кого, щоб ділитися цікавим. Знаходьте нових користувачів, щоб підписатися та слідкуйте за популярними хештегами, щоб розширити свій кругозір.
|
||||||
|
|
||||||
Mastodon будується з акцентом на конфіденційність та безпеці. Вирішіть, чи будуть ваші пости тільки для підписників, або для людей, яких ви згадали, чи для цілого світу. Попередження щодо вмісту дозволяють приховати публікації, що містять конфіденційний або провокаційний матеріал, доки ви не будете готові до нього. Кожна спільнота має свої правила і модераторів, щоб залишити учасників в безпеці, а також надійне блокування та інструменти для скарг, щоб запобігти зловживання.
|
Mastodon будується з акцентом на конфіденційність та безпеку. Вирішіть, чи будуть ваші дописи доступні лише підписникам, або для людей, яких ви згадали, чи взагалі для всіх. Попередження про вміст дозволяють приховати дописи, що містять чутливі або провокаційні матеріали, доки ви не будете готові взаємодіяти з ними. Кожна спільнота має власні правила та модераторів, які забезпечують безпеку учасників, а надійні інструменти блокування та система скарг допомагають запобігти зловживанням.
|
||||||
|
|
||||||
Більше можливостей:
|
Більше можливостей:
|
||||||
|
|
||||||
• Темна Тема: Читайте у світлій, темній, або справжній чорній темі
|
• Темна тема: Читайте дописи у світлому, темному або повністю чорному режимі
|
||||||
• Опитування: запитуйте думку підписникіна та підраховуйте голоси
|
• Опитування: Запитайте у підписників їхню думку та підраховуйте голоси
|
||||||
Досліджуйте: Популярні Хештеги й Користувачі за одним дотиком
|
• Досліджуйте: Популярні хештеги та користувачі на відстані одного дотику
|
||||||
• Сповіщення: отримуйте сповіщення про нових підписників, відповіді та репости
|
• Сповіщення: Отримуйте сповіщення про нові підписки, відповіді та поширення
|
||||||
Діліться: Публікуйте безпосередньо в Mastodon з будь-якого меню "поділитися" в будь-якому додатку
|
• Діліться: Публікуйте безпосередньо в Mastodon через будь-яке меню "поділитися" у будь-якому додатку
|
||||||
• Привабливість: Нашим талісманом є чарівний слон, і ви побачите, як він з'являється час від часу
|
• Миле: Наш талісман - чарівне слоненя, яке час від часу ви помічатимете
|
||||||
|
|
||||||
Mastodon є зареєстрованою некомерційною організацією і розробка підтримується безпосередньо вашими пожертвуваннями. Тут немає реклами, монетизації та венчурного капіталу, і плануємо так тримати.
|
Mastodon є зареєстрованою некомерційною організацією і розробка підтримується безпосередньо вашими пожертвуваннями. Тут немає реклами, монетизації та венчурного капіталу. І ми плануємо, що так буде й надалі.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Mạng xã hội phi tập trung
|
Mạng xã hội liên hợp
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你进入哪一个节点,你都可以与所有在 Mastodon 的人之间进行交流。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你进入哪一个节点,你都可以与所有在 Mastodon 的人之间进行交流。
|
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你关注什么话题,你都能在 Mastodon 上找到兴趣相投的人进行交流。
|
||||||
|
|
||||||
加入一个社区节点并创建你的账户。 Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. 用自定义表情、图像、GIF、视频和音频在 500 个字符的帖子里写下你的想法。 回复及转发其他人的帖子来共同分享美好的事物。 寻找新的账户以及热门的话题来拓展你的网络。
|
加入一个社区节点并创建你的账户。 查找、关注有趣的同好,无广告、无时间线干扰地阅读他们的帖子。 借助自定义 emoji、图像、GIF、视频和音频,在最多 500 字的帖文中表达自我。 通过回复或转发其他人的帖文来分享美好的事物。 通过准寻新账户并关注热门话题标签来扩展你的社交网络。
|
||||||
|
|
||||||
Mastodon 以隐私和安全为首要目标。 自由选择你的帖子是分享给关注者,或是你提到的人,亦或是整个世界。 内容警告允许让你隐藏可能剧透的内容,让其他人在交互之前做好充分准备。 每个社区都有自己的规则和管理员,让其用户保持安全。通过强有力的屏蔽和举报工具防止滥用。
|
Mastodon 以隐私和安全为首要目标。 你可以自主决定帖文的分享分享对象,可以是你的关注者、你提到的人或是整个世界。 在你做好充足的互动准备之前,内容警告可以隐藏包含敏感或刺激内容的帖文。 每个社区都有自己的规则和管理员来保证其成员安全,同时还有强力的屏蔽和举报工具来避免滥用。
|
||||||
|
|
||||||
更多功能:
|
更多功能:
|
||||||
|
|
||||||
• 暗色模式:以浅色、深色、纯黑模式下进行阅读
|
• 暗色模式:在浅色、深色或纯黑模式下阅读帖文
|
||||||
• 投票:询问关注者的意见并统计他们的投票
|
• 投票:询问关注者的意见并统计他们的投票
|
||||||
• 探索:热门的话题以及账号
|
• 探索:热门的话题标签及账号只有一触之遥
|
||||||
• 通知:获得新关注、回复和转发的通知
|
• 通知:获取关注、回复和转发相关的通知提醒
|
||||||
• 分享:从其他应用中的分享菜单中直接发布到 Mastodon
|
• 分享:从其他应用中的分享菜单中直接发布到 Mastodon
|
||||||
• 吉祥物:你会不时地看到我们可爱的长毛象
|
• 吉祥物:你会不时地看到我们可爱的长毛象
|
||||||
|
|
||||||
Mastodon 是一个注册的非营利开发项目,直接由您的捐赠支持。 没有广告,没有商业化,也没有风险资本,我们计划保持这种方式。 没有广告,没有商业化,也没有风险资本,我们计划保持这种方式。
|
Mastodon 是一个直接由用户捐赠支持、已注册非营利开发项目。 它没有广告、没有商业化,也没有风险资本,并且我们也计划保持这种方式。
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
Mastodon 是網際網路上最大的去中心化社群網路。 它是一個由能無縫互動的獨立社群中,數百萬使用者組成的網路,而非單一網站。 無論您對什麼事情感興趣,您都能在 Mastodon 上遇到充滿熱情的人們討論該話題。
|
Mastodon 是網際網路上最大的去中心化社群網路。 它是一個由能無縫互動的獨立社群中,數百萬使用者組成的網路,而非單一網站。 無論您對什麼事情感興趣,您都能於 Mastodon 上遇到充滿熱情的人們討論該話題。
|
||||||
|
|
||||||
加入社群並建立您的個人檔案。 尋找並跟隨迷人的朋友們,並在無廣告、按時間順序排列的時間軸上閱讀他們的嘟文。 在 500 個字元的嘟文中使用自訂表情符號、GIF、視訊與音訊來表達您自己。 回覆任何人的話題與轉發貼文以分享精彩內容。 尋找要跟隨的新帳號與熱門主題標籤來拓展您的網路。
|
加入社群並建立您的個人檔案。 尋找並跟隨迷人的朋友們,並於無廣告、按時間順序排列的時間軸上閱讀他們的嘟文。 於 500 個字元的嘟文中使用自訂表情符號、GIF、視訊與音訊表達您自己。 回覆任何人的話題與轉發貼文以分享精彩內容。 尋找要跟隨的新帳號與熱門主題標籤以拓展您的網路。
|
||||||
|
|
||||||
Mastodon 以隱私與安全為要。 決定您的嘟文要與您的跟隨者分享、只與您提及的人們分享,又或是與全世界分享。 內容警告可讓您隱藏包含敏感或可能觸發強烈情緒反應的嘟文,直到您準備好與它們進行互動。 每個社群都有它們自己的指導方針與管理原來確保其成員安全,強大的封鎖與回報工具有助於防止濫用。
|
Mastodon 以隱私與安全為要。 決定您的嘟文要與您的跟隨者分享、只與您提及的人們分享,又或是與全世界分享。 內容警告可讓您隱藏包含敏感或可能觸發強烈情緒反應的嘟文,直到您準備好與它們進行互動。 每個社群都有它們自己的指導方針與管理員以確保其成員安全,強大的封鎖與回報工具有助於防止濫用。
|
||||||
|
|
||||||
更多功能:
|
更多功能:
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ Mastodon 以隱私與安全為要。 決定您的嘟文要與您的跟隨者分
|
|||||||
• 投票:詢問跟隨者們的意見並計票
|
• 投票:詢問跟隨者們的意見並計票
|
||||||
• 探索:僅需輕點一下,即可看到熱門主題標籤與帳號
|
• 探索:僅需輕點一下,即可看到熱門主題標籤與帳號
|
||||||
• 通知:取得關於新跟隨者們、回覆與轉發的通知
|
• 通知:取得關於新跟隨者們、回覆與轉發的通知
|
||||||
• 分享:從任何應用程式中的分享表中直接發表嘟文到 Mastodon 中
|
• 分享:自任何應用程式中的分享表中直接發表嘟文到 Mastodon 中
|
||||||
• 可愛:我們的吉祥物是一隻可愛的大象,您會不時看到牠出現
|
• 可愛:我們的吉祥物是一隻可愛的大象,您會不時看到牠出現
|
||||||
|
|
||||||
Mastodon 是一家註冊的非營利組織,您的捐款會直接支援開發工作。 沒有廣告、沒有貨幣化、沒有風險投資,我們計畫維持這種狀態。 沒有廣告、沒有貨幣化、沒有風險投資,我們計畫維持這種狀態。
|
Mastodon 是一家註冊的非營利組織,您的捐款會直接支援開發工作。 沒有廣告、沒有貨幣化、沒有風險投資,我們計畫維持這種狀態。 沒有廣告、沒有貨幣化、沒有風險投資,我們計畫維持這種狀態。
|
||||||
|
|||||||
6
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
#Thu Jan 13 11:33:43 MSK 2022
|
#Sat Jun 03 23:40:27 MSK 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
279
mastodon/.editorconfig
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = tab
|
||||||
|
insert_final_newline = false
|
||||||
|
max_line_length = 300
|
||||||
|
tab_width = 4
|
||||||
|
ij_continuation_indent_size = 8
|
||||||
|
ij_formatter_off_tag = @formatter:off
|
||||||
|
ij_formatter_on_tag = @formatter:on
|
||||||
|
ij_formatter_tags_enabled = false
|
||||||
|
ij_smart_tabs = false
|
||||||
|
ij_visual_guides = none
|
||||||
|
ij_wrap_on_typing = false
|
||||||
|
|
||||||
|
[*.java]
|
||||||
|
ij_java_align_consecutive_assignments = false
|
||||||
|
ij_java_align_consecutive_variable_declarations = false
|
||||||
|
ij_java_align_group_field_declarations = false
|
||||||
|
ij_java_align_multiline_annotation_parameters = false
|
||||||
|
ij_java_align_multiline_array_initializer_expression = false
|
||||||
|
ij_java_align_multiline_assignment = false
|
||||||
|
ij_java_align_multiline_binary_operation = false
|
||||||
|
ij_java_align_multiline_chained_methods = false
|
||||||
|
ij_java_align_multiline_extends_list = false
|
||||||
|
ij_java_align_multiline_for = true
|
||||||
|
ij_java_align_multiline_method_parentheses = false
|
||||||
|
ij_java_align_multiline_parameters = true
|
||||||
|
ij_java_align_multiline_parameters_in_calls = false
|
||||||
|
ij_java_align_multiline_parenthesized_expression = false
|
||||||
|
ij_java_align_multiline_records = true
|
||||||
|
ij_java_align_multiline_resources = true
|
||||||
|
ij_java_align_multiline_ternary_operation = false
|
||||||
|
ij_java_align_multiline_text_blocks = false
|
||||||
|
ij_java_align_multiline_throws_list = false
|
||||||
|
ij_java_align_subsequent_simple_methods = false
|
||||||
|
ij_java_align_throws_keyword = false
|
||||||
|
ij_java_align_types_in_multi_catch = true
|
||||||
|
ij_java_annotation_parameter_wrap = off
|
||||||
|
ij_java_array_initializer_new_line_after_left_brace = false
|
||||||
|
ij_java_array_initializer_right_brace_on_new_line = false
|
||||||
|
ij_java_array_initializer_wrap = off
|
||||||
|
ij_java_assert_statement_colon_on_next_line = false
|
||||||
|
ij_java_assert_statement_wrap = off
|
||||||
|
ij_java_assignment_wrap = off
|
||||||
|
ij_java_binary_operation_sign_on_next_line = false
|
||||||
|
ij_java_binary_operation_wrap = off
|
||||||
|
ij_java_blank_lines_after_anonymous_class_header = 0
|
||||||
|
ij_java_blank_lines_after_class_header = 0
|
||||||
|
ij_java_blank_lines_after_imports = 1
|
||||||
|
ij_java_blank_lines_after_package = 1
|
||||||
|
ij_java_blank_lines_around_class = 1
|
||||||
|
ij_java_blank_lines_around_field = 0
|
||||||
|
ij_java_blank_lines_around_field_in_interface = 0
|
||||||
|
ij_java_blank_lines_around_initializer = 1
|
||||||
|
ij_java_blank_lines_around_method = 1
|
||||||
|
ij_java_blank_lines_around_method_in_interface = 1
|
||||||
|
ij_java_blank_lines_before_class_end = 0
|
||||||
|
ij_java_blank_lines_before_imports = 1
|
||||||
|
ij_java_blank_lines_before_method_body = 0
|
||||||
|
ij_java_blank_lines_before_package = 0
|
||||||
|
ij_java_block_brace_style = end_of_line
|
||||||
|
ij_java_block_comment_add_space = false
|
||||||
|
ij_java_block_comment_at_first_column = true
|
||||||
|
ij_java_builder_methods = none
|
||||||
|
ij_java_call_parameters_new_line_after_left_paren = false
|
||||||
|
ij_java_call_parameters_right_paren_on_new_line = false
|
||||||
|
ij_java_call_parameters_wrap = off
|
||||||
|
ij_java_case_statement_on_separate_line = true
|
||||||
|
ij_java_catch_on_new_line = false
|
||||||
|
ij_java_class_annotation_wrap = split_into_lines
|
||||||
|
ij_java_class_brace_style = end_of_line
|
||||||
|
ij_java_class_count_to_use_import_on_demand = 99
|
||||||
|
ij_java_class_names_in_javadoc = 1
|
||||||
|
ij_java_do_not_indent_top_level_class_members = false
|
||||||
|
ij_java_do_not_wrap_after_single_annotation = false
|
||||||
|
ij_java_do_not_wrap_after_single_annotation_in_parameter = false
|
||||||
|
ij_java_do_while_brace_force = never
|
||||||
|
ij_java_doc_add_blank_line_after_description = true
|
||||||
|
ij_java_doc_add_blank_line_after_param_comments = false
|
||||||
|
ij_java_doc_add_blank_line_after_return = false
|
||||||
|
ij_java_doc_add_p_tag_on_empty_lines = true
|
||||||
|
ij_java_doc_align_exception_comments = true
|
||||||
|
ij_java_doc_align_param_comments = true
|
||||||
|
ij_java_doc_do_not_wrap_if_one_line = false
|
||||||
|
ij_java_doc_enable_formatting = true
|
||||||
|
ij_java_doc_enable_leading_asterisks = true
|
||||||
|
ij_java_doc_indent_on_continuation = false
|
||||||
|
ij_java_doc_keep_empty_lines = true
|
||||||
|
ij_java_doc_keep_empty_parameter_tag = true
|
||||||
|
ij_java_doc_keep_empty_return_tag = true
|
||||||
|
ij_java_doc_keep_empty_throws_tag = true
|
||||||
|
ij_java_doc_keep_invalid_tags = true
|
||||||
|
ij_java_doc_param_description_on_new_line = false
|
||||||
|
ij_java_doc_preserve_line_breaks = false
|
||||||
|
ij_java_doc_use_throws_not_exception_tag = true
|
||||||
|
ij_java_else_on_new_line = false
|
||||||
|
ij_java_enum_constants_wrap = off
|
||||||
|
ij_java_extends_keyword_wrap = off
|
||||||
|
ij_java_extends_list_wrap = off
|
||||||
|
ij_java_field_annotation_wrap = split_into_lines
|
||||||
|
ij_java_finally_on_new_line = false
|
||||||
|
ij_java_for_brace_force = never
|
||||||
|
ij_java_for_statement_new_line_after_left_paren = false
|
||||||
|
ij_java_for_statement_right_paren_on_new_line = false
|
||||||
|
ij_java_for_statement_wrap = off
|
||||||
|
ij_java_generate_final_locals = false
|
||||||
|
ij_java_generate_final_parameters = false
|
||||||
|
ij_java_if_brace_force = never
|
||||||
|
ij_java_imports_layout = android.**,|,com.**,|,junit.**,|,net.**,|,org.**,|,java.**,|,javax.**,|,*,|,$*,|
|
||||||
|
ij_java_indent_case_from_switch = true
|
||||||
|
ij_java_insert_inner_class_imports = false
|
||||||
|
ij_java_insert_override_annotation = true
|
||||||
|
ij_java_keep_blank_lines_before_right_brace = 2
|
||||||
|
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
|
||||||
|
ij_java_keep_blank_lines_in_code = 2
|
||||||
|
ij_java_keep_blank_lines_in_declarations = 2
|
||||||
|
ij_java_keep_builder_methods_indents = false
|
||||||
|
ij_java_keep_control_statement_in_one_line = true
|
||||||
|
ij_java_keep_first_column_comment = true
|
||||||
|
ij_java_keep_indents_on_empty_lines = false
|
||||||
|
ij_java_keep_line_breaks = true
|
||||||
|
ij_java_keep_multiple_expressions_in_one_line = false
|
||||||
|
ij_java_keep_simple_blocks_in_one_line = false
|
||||||
|
ij_java_keep_simple_classes_in_one_line = false
|
||||||
|
ij_java_keep_simple_lambdas_in_one_line = false
|
||||||
|
ij_java_keep_simple_methods_in_one_line = false
|
||||||
|
ij_java_label_indent_absolute = false
|
||||||
|
ij_java_label_indent_size = 0
|
||||||
|
ij_java_lambda_brace_style = end_of_line
|
||||||
|
ij_java_layout_static_imports_separately = true
|
||||||
|
ij_java_line_comment_add_space = false
|
||||||
|
ij_java_line_comment_add_space_on_reformat = false
|
||||||
|
ij_java_line_comment_at_first_column = true
|
||||||
|
ij_java_method_annotation_wrap = split_into_lines
|
||||||
|
ij_java_method_brace_style = end_of_line
|
||||||
|
ij_java_method_call_chain_wrap = off
|
||||||
|
ij_java_method_parameters_new_line_after_left_paren = false
|
||||||
|
ij_java_method_parameters_right_paren_on_new_line = false
|
||||||
|
ij_java_method_parameters_wrap = off
|
||||||
|
ij_java_modifier_list_wrap = false
|
||||||
|
ij_java_multi_catch_types_wrap = normal
|
||||||
|
ij_java_names_count_to_use_import_on_demand = 99
|
||||||
|
ij_java_new_line_after_lparen_in_annotation = false
|
||||||
|
ij_java_new_line_after_lparen_in_record_header = false
|
||||||
|
ij_java_parameter_annotation_wrap = off
|
||||||
|
ij_java_parentheses_expression_new_line_after_left_paren = false
|
||||||
|
ij_java_parentheses_expression_right_paren_on_new_line = false
|
||||||
|
ij_java_place_assignment_sign_on_next_line = false
|
||||||
|
ij_java_prefer_longer_names = true
|
||||||
|
ij_java_prefer_parameters_wrap = false
|
||||||
|
ij_java_record_components_wrap = normal
|
||||||
|
ij_java_repeat_synchronized = true
|
||||||
|
ij_java_replace_instanceof_and_cast = false
|
||||||
|
ij_java_replace_null_check = true
|
||||||
|
ij_java_replace_sum_lambda_with_method_ref = true
|
||||||
|
ij_java_resource_list_new_line_after_left_paren = false
|
||||||
|
ij_java_resource_list_right_paren_on_new_line = false
|
||||||
|
ij_java_resource_list_wrap = off
|
||||||
|
ij_java_rparen_on_new_line_in_annotation = false
|
||||||
|
ij_java_rparen_on_new_line_in_record_header = false
|
||||||
|
ij_java_space_after_closing_angle_bracket_in_type_argument = false
|
||||||
|
ij_java_space_after_colon = true
|
||||||
|
ij_java_space_after_comma = true
|
||||||
|
ij_java_space_after_comma_in_type_arguments = true
|
||||||
|
ij_java_space_after_for_semicolon = true
|
||||||
|
ij_java_space_after_quest = true
|
||||||
|
ij_java_space_after_type_cast = true
|
||||||
|
ij_java_space_before_annotation_array_initializer_left_brace = false
|
||||||
|
ij_java_space_before_annotation_parameter_list = false
|
||||||
|
ij_java_space_before_array_initializer_left_brace = false
|
||||||
|
ij_java_space_before_catch_keyword = false
|
||||||
|
ij_java_space_before_catch_left_brace = false
|
||||||
|
ij_java_space_before_catch_parentheses = false
|
||||||
|
ij_java_space_before_class_left_brace = false
|
||||||
|
ij_java_space_before_colon = true
|
||||||
|
ij_java_space_before_colon_in_foreach = true
|
||||||
|
ij_java_space_before_comma = false
|
||||||
|
ij_java_space_before_do_left_brace = false
|
||||||
|
ij_java_space_before_else_keyword = false
|
||||||
|
ij_java_space_before_else_left_brace = false
|
||||||
|
ij_java_space_before_finally_keyword = false
|
||||||
|
ij_java_space_before_finally_left_brace = false
|
||||||
|
ij_java_space_before_for_left_brace = false
|
||||||
|
ij_java_space_before_for_parentheses = false
|
||||||
|
ij_java_space_before_for_semicolon = false
|
||||||
|
ij_java_space_before_if_left_brace = false
|
||||||
|
ij_java_space_before_if_parentheses = false
|
||||||
|
ij_java_space_before_method_call_parentheses = false
|
||||||
|
ij_java_space_before_method_left_brace = false
|
||||||
|
ij_java_space_before_method_parentheses = false
|
||||||
|
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
|
||||||
|
ij_java_space_before_quest = true
|
||||||
|
ij_java_space_before_switch_left_brace = false
|
||||||
|
ij_java_space_before_switch_parentheses = false
|
||||||
|
ij_java_space_before_synchronized_left_brace = false
|
||||||
|
ij_java_space_before_synchronized_parentheses = false
|
||||||
|
ij_java_space_before_try_left_brace = false
|
||||||
|
ij_java_space_before_try_parentheses = false
|
||||||
|
ij_java_space_before_type_parameter_list = false
|
||||||
|
ij_java_space_before_while_keyword = false
|
||||||
|
ij_java_space_before_while_left_brace = false
|
||||||
|
ij_java_space_before_while_parentheses = false
|
||||||
|
ij_java_space_inside_one_line_enum_braces = false
|
||||||
|
ij_java_space_within_empty_array_initializer_braces = false
|
||||||
|
ij_java_space_within_empty_method_call_parentheses = false
|
||||||
|
ij_java_space_within_empty_method_parentheses = false
|
||||||
|
ij_java_spaces_around_additive_operators = false
|
||||||
|
ij_java_spaces_around_annotation_eq = true
|
||||||
|
ij_java_spaces_around_assignment_operators = false
|
||||||
|
ij_java_spaces_around_bitwise_operators = false
|
||||||
|
ij_java_spaces_around_equality_operators = false
|
||||||
|
ij_java_spaces_around_lambda_arrow = false
|
||||||
|
ij_java_spaces_around_logical_operators = true
|
||||||
|
ij_java_spaces_around_method_ref_dbl_colon = false
|
||||||
|
ij_java_spaces_around_multiplicative_operators = false
|
||||||
|
ij_java_spaces_around_relational_operators = false
|
||||||
|
ij_java_spaces_around_shift_operators = false
|
||||||
|
ij_java_spaces_around_type_bounds_in_type_parameters = true
|
||||||
|
ij_java_spaces_around_unary_operator = false
|
||||||
|
ij_java_spaces_within_angle_brackets = false
|
||||||
|
ij_java_spaces_within_annotation_parentheses = false
|
||||||
|
ij_java_spaces_within_array_initializer_braces = false
|
||||||
|
ij_java_spaces_within_braces = false
|
||||||
|
ij_java_spaces_within_brackets = false
|
||||||
|
ij_java_spaces_within_cast_parentheses = false
|
||||||
|
ij_java_spaces_within_catch_parentheses = false
|
||||||
|
ij_java_spaces_within_for_parentheses = false
|
||||||
|
ij_java_spaces_within_if_parentheses = false
|
||||||
|
ij_java_spaces_within_method_call_parentheses = false
|
||||||
|
ij_java_spaces_within_method_parentheses = false
|
||||||
|
ij_java_spaces_within_parentheses = false
|
||||||
|
ij_java_spaces_within_record_header = false
|
||||||
|
ij_java_spaces_within_switch_parentheses = false
|
||||||
|
ij_java_spaces_within_synchronized_parentheses = false
|
||||||
|
ij_java_spaces_within_try_parentheses = false
|
||||||
|
ij_java_spaces_within_while_parentheses = false
|
||||||
|
ij_java_special_else_if_treatment = true
|
||||||
|
ij_java_subclass_name_suffix = Impl
|
||||||
|
ij_java_ternary_operation_signs_on_next_line = false
|
||||||
|
ij_java_ternary_operation_wrap = off
|
||||||
|
ij_java_test_name_suffix = Test
|
||||||
|
ij_java_throws_keyword_wrap = off
|
||||||
|
ij_java_throws_list_wrap = off
|
||||||
|
ij_java_use_external_annotations = false
|
||||||
|
ij_java_use_fq_class_names = false
|
||||||
|
ij_java_use_relative_indents = false
|
||||||
|
ij_java_use_single_class_imports = true
|
||||||
|
ij_java_variable_annotation_wrap = off
|
||||||
|
ij_java_visibility = public
|
||||||
|
ij_java_while_brace_force = never
|
||||||
|
ij_java_while_on_new_line = false
|
||||||
|
ij_java_wrap_comments = false
|
||||||
|
ij_java_wrap_first_method_in_call_chain = false
|
||||||
|
ij_java_wrap_long_lines = false
|
||||||
|
|
||||||
|
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
||||||
|
ij_continuation_indent_size = 4
|
||||||
|
ij_xml_align_attributes = false
|
||||||
|
ij_xml_align_text = false
|
||||||
|
ij_xml_attribute_wrap = normal
|
||||||
|
ij_xml_block_comment_add_space = false
|
||||||
|
ij_xml_block_comment_at_first_column = true
|
||||||
|
ij_xml_keep_blank_lines = 2
|
||||||
|
ij_xml_keep_indents_on_empty_lines = false
|
||||||
|
ij_xml_keep_line_breaks = false
|
||||||
|
ij_xml_keep_line_breaks_in_text = true
|
||||||
|
ij_xml_keep_whitespaces = false
|
||||||
|
ij_xml_keep_whitespaces_around_cdata = preserve
|
||||||
|
ij_xml_keep_whitespaces_inside_cdata = false
|
||||||
|
ij_xml_line_comment_at_first_column = true
|
||||||
|
ij_xml_space_after_tag_name = false
|
||||||
|
ij_xml_space_around_equals_in_attribute = false
|
||||||
|
ij_xml_space_inside_empty_tag = true
|
||||||
|
ij_xml_text_wrap = normal
|
||||||
|
ij_xml_use_custom_settings = true
|
||||||
@@ -9,10 +9,10 @@ android {
|
|||||||
applicationId "org.joinmastodon.android"
|
applicationId "org.joinmastodon.android"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 54
|
versionCode 89
|
||||||
versionName "1.2.3"
|
versionName "2.4.1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
|
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "ka-rGE", "kab", "ko-rKR", "lt-rLT", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -37,6 +37,9 @@ android {
|
|||||||
githubRelease{
|
githubRelease{
|
||||||
initWith release
|
initWith release
|
||||||
}
|
}
|
||||||
|
githubDebug{
|
||||||
|
initWith debug
|
||||||
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
@@ -53,11 +56,25 @@ android {
|
|||||||
githubRelease{
|
githubRelease{
|
||||||
setRoot "src/github"
|
setRoot "src/github"
|
||||||
}
|
}
|
||||||
|
githubDebug{
|
||||||
|
setRoot "src/github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lintOptions{
|
lintOptions{
|
||||||
checkReleaseBuilds false
|
checkReleaseBuilds false
|
||||||
abortOnError false
|
abortOnError false
|
||||||
}
|
}
|
||||||
|
buildFeatures{
|
||||||
|
aidl true
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
dependenciesInfo{
|
||||||
|
// Disables dependency metadata when building APKs.
|
||||||
|
includeInApk false
|
||||||
|
// Disables dependency metadata when building Android App Bundles.
|
||||||
|
includeInBundle false
|
||||||
|
}
|
||||||
|
namespace 'org.joinmastodon.android'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -69,14 +86,17 @@ dependencies {
|
|||||||
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
|
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
|
||||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||||
implementation 'me.grishka.appkit:appkit:1.2.7'
|
implementation 'me.grishka.litex:palette:1.0.0'
|
||||||
|
implementation 'me.grishka.appkit:appkit:1.2.16'
|
||||||
implementation 'com.google.code.gson:gson:2.8.9'
|
implementation 'com.google.code.gson:gson:2.8.9'
|
||||||
implementation 'org.jsoup:jsoup:1.14.3'
|
implementation 'org.jsoup:jsoup:1.14.3'
|
||||||
implementation 'com.squareup:otto:1.3.8'
|
implementation 'com.squareup:otto:1.3.8'
|
||||||
implementation 'de.psdev:async-otto:1.0.3'
|
implementation 'de.psdev:async-otto:1.0.3'
|
||||||
|
implementation 'com.google.zxing:core:3.5.3'
|
||||||
|
implementation 'org.microg:safe-parcel:1.5.0'
|
||||||
implementation 'org.parceler:parceler-api:1.1.12'
|
implementation 'org.parceler:parceler-api:1.1.12'
|
||||||
annotationProcessor 'org.parceler:parceler:1.1.12'
|
annotationProcessor 'org.parceler:parceler:1.1.12'
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||||
|
|
||||||
def appCenterSdkVersion = "4.4.2"
|
def appCenterSdkVersion = "4.4.2"
|
||||||
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
||||||
@@ -84,8 +104,8 @@ dependencies {
|
|||||||
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
||||||
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
|
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:core:1.4.1-alpha05'
|
androidTestImplementation 'androidx.test:core:1.5.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha05'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'
|
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha05'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
}
|
}
|
||||||
|
|||||||
20
mastodon/ci_signing.gradle
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Included into build.gradle when running in a CI pipeline
|
||||||
|
|
||||||
|
android{
|
||||||
|
signingConfigs{
|
||||||
|
release{
|
||||||
|
keyAlias "key0"
|
||||||
|
keyPassword System.getenv("KEYSTORE_PASSWORD")
|
||||||
|
storeFile file(System.getenv("KEYSTORE_FILE"))
|
||||||
|
storePassword System.getenv("KEYSTORE_PASSWORD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes{
|
||||||
|
release{
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
githubRelease{
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
mastodon/proguard-rules.pro
vendored
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
# Keep all model classes as they're used with gson and their names are shown in errors
|
# Keep all model classes as they're used with gson and their names are shown in errors
|
||||||
-keep public class org.joinmastodon.android.model.**{
|
-keep public class org.joinmastodon.android.model.**{
|
||||||
<fields>;
|
*;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Inner classes in api requests are used with gson
|
# Inner classes in api requests are used with gson
|
||||||
@@ -51,3 +51,7 @@
|
|||||||
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
|
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
|
||||||
|
|
||||||
-keepattributes LineNumberTable
|
-keepattributes LineNumberTable
|
||||||
|
-keepattributes Signature
|
||||||
|
-keep class com.google.gson.reflect.TypeToken { *; }
|
||||||
|
-keep class * extends com.google.gson.reflect.TypeToken
|
||||||
|
-keep public class * implements java.lang.reflect.Type
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.*;
|
|||||||
@LargeTest
|
@LargeTest
|
||||||
public class StoreScreenshotsGenerator{
|
public class StoreScreenshotsGenerator{
|
||||||
private static final String PHOTO_FILE="IMG_1010.jpg";
|
private static final String PHOTO_FILE="IMG_1010.jpg";
|
||||||
|
private static final long LOAD_WAIT_TIMEOUT=20_000;
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public ActivityScenarioRule<MainActivity> activityScenarioRule=new ActivityScenarioRule<>(MainActivity.class);
|
public ActivityScenarioRule<MainActivity> activityScenarioRule=new ActivityScenarioRule<>(MainActivity.class);
|
||||||
@@ -84,14 +85,14 @@ public class StoreScreenshotsGenerator{
|
|||||||
AccountSession session=AccountSessionManager.getInstance().getAccount(AccountSessionManager.getInstance().getLastActiveAccountID());
|
AccountSession session=AccountSessionManager.getInstance().getAccount(AccountSessionManager.getInstance().getLastActiveAccountID());
|
||||||
MastodonApp.context.deleteDatabase(session.getID()+".db");
|
MastodonApp.context.deleteDatabase(session.getID()+".db");
|
||||||
|
|
||||||
onView(isRoot()).perform(waitId(R.id.more, 5000));
|
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT));
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
takeScreenshot("HomeTimeline");
|
takeScreenshot("HomeTimeline");
|
||||||
|
|
||||||
GlobalUserPreferences.theme=GlobalUserPreferences.ThemePreference.DARK;
|
GlobalUserPreferences.theme=GlobalUserPreferences.ThemePreference.DARK;
|
||||||
activityScenarioRule.getScenario().recreate();
|
activityScenarioRule.getScenario().recreate();
|
||||||
|
|
||||||
onView(isRoot()).perform(waitId(R.id.more, 5000));
|
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT));
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
takeScreenshot("HomeTimeline_Dark");
|
takeScreenshot("HomeTimeline_Dark");
|
||||||
|
|
||||||
@@ -100,8 +101,8 @@ public class StoreScreenshotsGenerator{
|
|||||||
|
|
||||||
activityScenarioRule.getScenario().onActivity(activity->UiUtils.openProfileByID(activity, session.getID(), args.getString("profileAccountID")));
|
activityScenarioRule.getScenario().onActivity(activity->UiUtils.openProfileByID(activity, session.getID(), args.getString("profileAccountID")));
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
onView(isRoot()).perform(waitId(R.id.avatar_border, 5000)); // wait for profile to load
|
onView(isRoot()).perform(waitId(R.id.avatar_border, LOAD_WAIT_TIMEOUT)); // wait for profile to load
|
||||||
onView(isRoot()).perform(waitId(R.id.more, 5000)); // wait for timeline to load
|
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT)); // wait for timeline to load
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
takeScreenshot("Profile");
|
takeScreenshot("Profile");
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
|||||||
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
|
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
|
||||||
return;
|
return;
|
||||||
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", 0);
|
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", 0);
|
||||||
if(timeSinceLastCheck>CHECK_PERIOD){
|
if(timeSinceLastCheck>CHECK_PERIOD || forceUpdate){
|
||||||
setState(UpdateState.CHECKING);
|
setState(UpdateState.CHECKING);
|
||||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||||
}
|
}
|
||||||
@@ -109,23 +109,26 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
|||||||
try(Response resp=call.execute()){
|
try(Response resp=call.execute()){
|
||||||
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
|
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
|
||||||
String tag=obj.get("tag_name").getAsString();
|
String tag=obj.get("tag_name").getAsString();
|
||||||
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)");
|
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)(?:\\.(\\d+))?");
|
||||||
Matcher matcher=pattern.matcher(tag);
|
Matcher matcher=pattern.matcher(tag);
|
||||||
if(!matcher.find()){
|
if(!matcher.find()){
|
||||||
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
|
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int newMajor=Integer.parseInt(matcher.group(1)), newMinor=Integer.parseInt(matcher.group(2)), newRevision=Integer.parseInt(matcher.group(3));
|
int newMajor=Integer.parseInt(matcher.group(1)), newMinor=Integer.parseInt(matcher.group(2)), newRevision=matcher.group(3)!=null ? Integer.parseInt(matcher.group(3)) : 0;
|
||||||
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
|
Matcher curMatcher=pattern.matcher(BuildConfig.VERSION_NAME);
|
||||||
if(!matcher.find()){
|
if(!curMatcher.find()){
|
||||||
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
|
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int curMajor=Integer.parseInt(matcher.group(1)), curMinor=Integer.parseInt(matcher.group(2)), curRevision=Integer.parseInt(matcher.group(3));
|
int curMajor=Integer.parseInt(curMatcher.group(1)), curMinor=Integer.parseInt(curMatcher.group(2)), curRevision=matcher.group(3)!=null ? Integer.parseInt(curMatcher.group(3)) : 0;
|
||||||
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
|
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
|
||||||
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
|
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
|
||||||
if(newVersion>curVersion || BuildConfig.DEBUG){
|
if(newVersion>curVersion || forceUpdate){
|
||||||
String version=newMajor+"."+newMinor+"."+newRevision;
|
forceUpdate=false;
|
||||||
|
String version=newMajor+"."+newMinor;
|
||||||
|
if(matcher.group(3)!=null)
|
||||||
|
version+="."+newRevision;
|
||||||
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
|
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
|
||||||
for(JsonElement el:obj.getAsJsonArray("assets")){
|
for(JsonElement el:obj.getAsJsonArray("assets")){
|
||||||
JsonObject asset=el.getAsJsonObject();
|
JsonObject asset=el.getAsJsonObject();
|
||||||
@@ -295,6 +298,15 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(){
|
||||||
|
getPrefs().edit().clear().apply();
|
||||||
|
File apk=getUpdateApkFile();
|
||||||
|
if(apk.exists())
|
||||||
|
apk.delete();
|
||||||
|
state=UpdateState.NO_UPDATE;
|
||||||
|
}
|
||||||
|
|
||||||
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
|
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
||||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||||
@@ -19,6 +20,10 @@
|
|||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.TRANSLATE" />
|
<action android:name="android.intent.action.TRANSLATE" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<data android:scheme="http"/>
|
||||||
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@@ -28,15 +33,30 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:localeConfig="@xml/locales_config"
|
android:localeConfig="@xml/locales_config"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:theme="@style/Theme.Mastodon.AutoLightDark"
|
android:theme="@style/Theme.Mastodon.AutoLightDark"
|
||||||
android:largeHeap="true">
|
android:largeHeap="true">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.mlkit.vision.DEPENDENCIES"
|
||||||
|
android:value="barcode_ui"/>
|
||||||
|
|
||||||
<activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize" android:launchMode="singleTask">
|
<activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize" android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:scheme="https" android:host="mastodon.social" android:pathPrefix="/@"/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:scheme="https" android:host="mastodon.online" android:pathPrefix="/@"/>
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".OAuthActivity" android:exported="true" android:configChanges="orientation|screenSize" android:launchMode="singleTask">
|
<activity android:name=".OAuthActivity" android:exported="true" android:configChanges="orientation|screenSize" android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -73,6 +93,14 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:name=".TweakedFileProvider"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
|
android:exported="false">
|
||||||
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/fileprovider_paths"/>
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.google.android.gms.common.api;
|
||||||
|
|
||||||
|
parcelable Status;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.google.android.gms.common.api.internal;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.api.Status;
|
||||||
|
|
||||||
|
interface IStatusCallback {
|
||||||
|
void onResult(in Status status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.gms.common.internal;
|
||||||
|
parcelable ConnectionInfo;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.google.android.gms.common.internal;
|
||||||
|
|
||||||
|
parcelable GetServiceRequest;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.gms.common.internal;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import com.google.android.gms.common.internal.ConnectionInfo;
|
||||||
|
|
||||||
|
interface IGmsCallbacks {
|
||||||
|
void onPostInitComplete(int statusCode, IBinder binder, in Bundle params);
|
||||||
|
void onAccountValidationComplete(int statusCode, in Bundle params);
|
||||||
|
void onPostInitCompleteWithConnectionInfo(int statusCode, IBinder binder, in ConnectionInfo info);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.google.android.gms.common.internal;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.internal.IGmsCallbacks;
|
||||||
|
import com.google.android.gms.common.internal.GetServiceRequest;
|
||||||
|
|
||||||
|
interface IGmsServiceBroker {
|
||||||
|
void getService(IGmsCallbacks callback, in GetServiceRequest request) = 45;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall;
|
||||||
|
|
||||||
|
parcelable ModuleAvailabilityResponse;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall;
|
||||||
|
|
||||||
|
parcelable ModuleInstallIntentResponse;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall;
|
||||||
|
|
||||||
|
parcelable ModuleInstallResponse;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall;
|
||||||
|
|
||||||
|
parcelable ModuleInstallStatusUpdate;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall.internal;
|
||||||
|
|
||||||
|
parcelable ApiFeatureRequest;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall.internal;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.api.Status;
|
||||||
|
import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse;
|
||||||
|
import com.google.android.gms.common.moduleinstall.ModuleInstallIntentResponse;
|
||||||
|
import com.google.android.gms.common.moduleinstall.ModuleInstallResponse;
|
||||||
|
|
||||||
|
interface IModuleInstallCallbacks {
|
||||||
|
void onModuleAvailabilityResponse(in Status status, in ModuleAvailabilityResponse response) = 0;
|
||||||
|
void onModuleInstallResponse(in Status status, in ModuleInstallResponse response) = 1;
|
||||||
|
void onModuleInstallIntentResponse(in Status status, in ModuleInstallIntentResponse response) = 2;
|
||||||
|
void onStatus(in Status status) = 3;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall.internal;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.api.internal.IStatusCallback;
|
||||||
|
import com.google.android.gms.common.moduleinstall.internal.ApiFeatureRequest;
|
||||||
|
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallCallbacks;
|
||||||
|
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallStatusListener;
|
||||||
|
|
||||||
|
interface IModuleInstallService {
|
||||||
|
void areModulesAvailable(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request) = 0;
|
||||||
|
void installModules(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request, IModuleInstallStatusListener listener) = 1;
|
||||||
|
void getInstallModulesIntent(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request) = 2;
|
||||||
|
void releaseModules(IStatusCallback callback, in ApiFeatureRequest request) = 3;
|
||||||
|
void unregisterListener(IStatusCallback callback, IModuleInstallStatusListener listener) = 5;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall.internal;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate;
|
||||||
|
|
||||||
|
interface IModuleInstallStatusListener {
|
||||||
|
void onModuleInstallStatusUpdate(in ModuleInstallStatusUpdate statusUpdate) = 0;
|
||||||
|
}
|
||||||
51
mastodon/src/main/assets/server_about_template.htm
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
*{
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
body{
|
||||||
|
background: {{colorSurface}};
|
||||||
|
padding: 16px 16px 0 16px;
|
||||||
|
margin: 0;
|
||||||
|
color: {{colorOnSurface}};
|
||||||
|
font-family: Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
-webkit-tap-highlight-color: {{colorPrimaryTransparent}};
|
||||||
|
}
|
||||||
|
a{
|
||||||
|
text-decoration: none;
|
||||||
|
color: {{colorPrimary}};
|
||||||
|
}
|
||||||
|
p, h1, h2, h3, h4, h5, h6, ul, ol{
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
h1, h2{
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
h3, h4, h5, h6{
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
b, strong{
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
ul, ol{
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
}
|
||||||
|
ul>li, ol>li{
|
||||||
|
padding-inline-start: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{content}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.google.android.gms.common;
|
||||||
|
|
||||||
|
import org.microg.safeparcel.AutoSafeParcelable;
|
||||||
|
import org.microg.safeparcel.SafeParceled;
|
||||||
|
|
||||||
|
public class Feature extends AutoSafeParcelable{
|
||||||
|
@SafeParceled(1)
|
||||||
|
public String name;
|
||||||
|
@SafeParceled(2)
|
||||||
|
public int oldVersion;
|
||||||
|
@SafeParceled(3)
|
||||||
|
public long version=-1;
|
||||||
|
|
||||||
|
public static final Creator<Feature> CREATOR=new AutoCreator<>(Feature.class);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.google.android.gms.common.api;
|
||||||
|
|
||||||
|
import org.microg.safeparcel.AutoSafeParcelable;
|
||||||
|
import org.microg.safeparcel.SafeParceled;
|
||||||
|
|
||||||
|
public class Scope extends AutoSafeParcelable{
|
||||||
|
@SafeParceled(1)
|
||||||
|
public int versionCode=1;
|
||||||
|
@SafeParceled(2)
|
||||||
|
public String scopeUri;
|
||||||
|
|
||||||
|
public static final Creator<Scope> CREATOR=new AutoCreator<>(Scope.class);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.google.android.gms.common.api;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.googleservices.ConnectionResult;
|
||||||
|
import org.microg.safeparcel.AutoSafeParcelable;
|
||||||
|
import org.microg.safeparcel.SafeParceled;
|
||||||
|
|
||||||
|
public class Status extends AutoSafeParcelable{
|
||||||
|
@SafeParceled(1000)
|
||||||
|
public int versionCode;
|
||||||
|
@SafeParceled(1)
|
||||||
|
public int statusCode;
|
||||||
|
@SafeParceled(2)
|
||||||
|
public String statusMessage;
|
||||||
|
@SafeParceled(3)
|
||||||
|
public PendingIntent pendingIntent;
|
||||||
|
@SafeParceled(4)
|
||||||
|
public ConnectionResult connectionResult;
|
||||||
|
|
||||||
|
public static final Creator<Status> CREATOR=new AutoCreator<>(Status.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString(){
|
||||||
|
return "Status{"+
|
||||||
|
"versionCode="+versionCode+
|
||||||
|
", statusCode="+statusCode+
|
||||||
|
", statusMessage='"+statusMessage+'\''+
|
||||||
|
", pendingIntent="+pendingIntent+
|
||||||
|
", connectionResult="+connectionResult+
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.google.android.gms.common.internal;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.Feature;
|
||||||
|
|
||||||
|
import org.microg.safeparcel.AutoSafeParcelable;
|
||||||
|
import org.microg.safeparcel.SafeParceled;
|
||||||
|
|
||||||
|
public class ConnectionInfo extends AutoSafeParcelable{
|
||||||
|
@SafeParceled(1)
|
||||||
|
public Bundle params;
|
||||||
|
@SafeParceled(2)
|
||||||
|
public Feature[] features;
|
||||||
|
@SafeParceled(3)
|
||||||
|
public int unknown3;
|
||||||
|
|
||||||
|
public static final Creator<ConnectionInfo> CREATOR=new AutoCreator<>(ConnectionInfo.class);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.google.android.gms.common.internal;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.accounts.Account;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.Feature;
|
||||||
|
import com.google.android.gms.common.api.Scope;
|
||||||
|
|
||||||
|
import org.microg.safeparcel.AutoSafeParcelable;
|
||||||
|
import org.microg.safeparcel.SafeParceled;
|
||||||
|
|
||||||
|
public class GetServiceRequest extends AutoSafeParcelable{
|
||||||
|
@SafeParceled(1)
|
||||||
|
int versionCode=6;
|
||||||
|
@SafeParceled(2)
|
||||||
|
public int serviceId;
|
||||||
|
@SafeParceled(3)
|
||||||
|
public int gmsVersion;
|
||||||
|
@SafeParceled(4)
|
||||||
|
public String packageName;
|
||||||
|
@SafeParceled(5)
|
||||||
|
public IBinder accountAccessor;
|
||||||
|
@SafeParceled(6)
|
||||||
|
public Scope[] scopes;
|
||||||
|
@SafeParceled(7)
|
||||||
|
public Bundle extras;
|
||||||
|
@SafeParceled(8)
|
||||||
|
public Account account;
|
||||||
|
@SafeParceled(9)
|
||||||
|
@Deprecated
|
||||||
|
long field9;
|
||||||
|
@SafeParceled(10)
|
||||||
|
public Feature[] defaultFeatures;
|
||||||
|
@SafeParceled(11)
|
||||||
|
public Feature[] apiFeatures;
|
||||||
|
@SafeParceled(12)
|
||||||
|
boolean supportsConnectionInfo;
|
||||||
|
@SafeParceled(13)
|
||||||
|
int field13;
|
||||||
|
@SafeParceled(14)
|
||||||
|
boolean field14;
|
||||||
|
@SafeParceled(15)
|
||||||
|
String attributionTag;
|
||||||
|
|
||||||
|
public static final Creator<GetServiceRequest> CREATOR=new AutoCreator<>(GetServiceRequest.class);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall;
|
||||||
|
|
||||||
|
import org.microg.safeparcel.AutoSafeParcelable;
|
||||||
|
import org.microg.safeparcel.SafeParceled;
|
||||||
|
|
||||||
|
public class ModuleAvailabilityResponse extends AutoSafeParcelable{
|
||||||
|
@SafeParceled(1)
|
||||||
|
public boolean modulesAvailable;
|
||||||
|
@SafeParceled(2)
|
||||||
|
public int availabilityStatus;
|
||||||
|
|
||||||
|
public static final Creator<ModuleAvailabilityResponse> CREATOR=new AutoCreator<>(ModuleAvailabilityResponse.class);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
|
||||||
|
import org.microg.safeparcel.AutoSafeParcelable;
|
||||||
|
import org.microg.safeparcel.SafeParceled;
|
||||||
|
|
||||||
|
public class ModuleInstallIntentResponse extends AutoSafeParcelable{
|
||||||
|
@SafeParceled(1)
|
||||||
|
public PendingIntent pendingIntent;
|
||||||
|
|
||||||
|
public static final Creator<ModuleInstallIntentResponse> CREATOR=new AutoCreator<>(ModuleInstallIntentResponse.class);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall;
|
||||||
|
|
||||||
|
import org.microg.safeparcel.AutoSafeParcelable;
|
||||||
|
import org.microg.safeparcel.SafeParceled;
|
||||||
|
|
||||||
|
public class ModuleInstallResponse extends AutoSafeParcelable{
|
||||||
|
@SafeParceled(1)
|
||||||
|
public int sessionID;
|
||||||
|
@SafeParceled(2)
|
||||||
|
public boolean shouldUnregisterListener;
|
||||||
|
|
||||||
|
public static final Creator<ModuleInstallResponse> CREATOR=new AutoCreator<>(ModuleInstallResponse.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString(){
|
||||||
|
return "ModuleInstallResponse{"+
|
||||||
|
"sessionID="+sessionID+
|
||||||
|
", shouldUnregisterListener="+shouldUnregisterListener+
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall;
|
||||||
|
|
||||||
|
import org.microg.safeparcel.AutoSafeParcelable;
|
||||||
|
import org.microg.safeparcel.SafeParceled;
|
||||||
|
|
||||||
|
public class ModuleInstallStatusUpdate extends AutoSafeParcelable{
|
||||||
|
public static final int STATE_UNKNOWN = 0;
|
||||||
|
/**
|
||||||
|
* The request is pending and will be processed soon.
|
||||||
|
*/
|
||||||
|
public static final int STATE_PENDING = 1;
|
||||||
|
/**
|
||||||
|
* The optional module download is in progress.
|
||||||
|
*/
|
||||||
|
public static final int STATE_DOWNLOADING = 2;
|
||||||
|
/**
|
||||||
|
* The optional module download has been canceled.
|
||||||
|
*/
|
||||||
|
public static final int STATE_CANCELED = 3;
|
||||||
|
/**
|
||||||
|
* Installation is completed; the optional modules are available to the client app.
|
||||||
|
*/
|
||||||
|
public static final int STATE_COMPLETED = 4;
|
||||||
|
/**
|
||||||
|
* The optional module download or installation has failed.
|
||||||
|
*/
|
||||||
|
public static final int STATE_FAILED = 5;
|
||||||
|
/**
|
||||||
|
* The optional modules have been downloaded and the installation is in progress.
|
||||||
|
*/
|
||||||
|
public static final int STATE_INSTALLING = 6;
|
||||||
|
/**
|
||||||
|
* The optional module download has been paused.
|
||||||
|
* <p>
|
||||||
|
* This usually happens when connectivity requirements can't be met during download. Once the connectivity requirements
|
||||||
|
* are met, the download will be resumed automatically.
|
||||||
|
*/
|
||||||
|
public static final int STATE_DOWNLOAD_PAUSED = 7;
|
||||||
|
|
||||||
|
@SafeParceled(1)
|
||||||
|
public int sessionID;
|
||||||
|
@SafeParceled(2)
|
||||||
|
public int installState;
|
||||||
|
@SafeParceled(3)
|
||||||
|
public Long bytesDownloaded;
|
||||||
|
@SafeParceled(4)
|
||||||
|
public Long totalBytesToDownload;
|
||||||
|
@SafeParceled(5)
|
||||||
|
public int errorCode;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString(){
|
||||||
|
return "ModuleInstallStatusUpdate{"+
|
||||||
|
"sessionID="+sessionID+
|
||||||
|
", installState="+installState+
|
||||||
|
", bytesDownloaded="+bytesDownloaded+
|
||||||
|
", totalBytesToDownload="+totalBytesToDownload+
|
||||||
|
", errorCode="+errorCode+
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<ModuleInstallStatusUpdate> CREATOR=new AutoCreator<>(ModuleInstallStatusUpdate.class);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.google.android.gms.common.moduleinstall.internal;
|
||||||
|
|
||||||
|
import com.google.android.gms.common.Feature;
|
||||||
|
|
||||||
|
import org.microg.safeparcel.AutoSafeParcelable;
|
||||||
|
import org.microg.safeparcel.SafeParceled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ApiFeatureRequest extends AutoSafeParcelable{
|
||||||
|
@SafeParceled(value=1, subClass=Feature.class)
|
||||||
|
public List<Feature> features;
|
||||||
|
@SafeParceled(2)
|
||||||
|
public boolean urgent;
|
||||||
|
@SafeParceled(3)
|
||||||
|
public String sessionId;
|
||||||
|
@SafeParceled(4)
|
||||||
|
public String callingPackage;
|
||||||
|
|
||||||
|
public static final Creator<ApiFeatureRequest> CREATOR=new AutoCreator<>(ApiFeatureRequest.class);
|
||||||
|
}
|
||||||
@@ -31,7 +31,6 @@ import org.joinmastodon.android.ui.text.HtmlParser;
|
|||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -57,6 +56,7 @@ public class AudioPlayerService extends Service{
|
|||||||
private static HashSet<Callback> callbacks=new HashSet<>();
|
private static HashSet<Callback> callbacks=new HashSet<>();
|
||||||
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener=this::onAudioFocusChanged;
|
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener=this::onAudioFocusChanged;
|
||||||
private boolean resumeAfterAudioFocusGain;
|
private boolean resumeAfterAudioFocusGain;
|
||||||
|
private boolean isBuffering=true;
|
||||||
|
|
||||||
private BroadcastReceiver receiver=new BroadcastReceiver(){
|
private BroadcastReceiver receiver=new BroadcastReceiver(){
|
||||||
@Override
|
@Override
|
||||||
@@ -176,6 +176,7 @@ public class AudioPlayerService extends Service{
|
|||||||
player.setOnErrorListener(this::onPlayerError);
|
player.setOnErrorListener(this::onPlayerError);
|
||||||
player.setOnCompletionListener(this::onPlayerCompletion);
|
player.setOnCompletionListener(this::onPlayerCompletion);
|
||||||
player.setOnSeekCompleteListener(this::onPlayerSeekCompleted);
|
player.setOnSeekCompleteListener(this::onPlayerSeekCompleted);
|
||||||
|
player.setOnInfoListener(this::onPlayerInfo);
|
||||||
try{
|
try{
|
||||||
player.setDataSource(this, Uri.parse(attachment.url));
|
player.setDataSource(this, Uri.parse(attachment.url));
|
||||||
player.prepareAsync();
|
player.prepareAsync();
|
||||||
@@ -187,7 +188,9 @@ public class AudioPlayerService extends Service{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onPlayerPrepared(MediaPlayer mp){
|
private void onPlayerPrepared(MediaPlayer mp){
|
||||||
|
Log.i(TAG, "onPlayerPrepared");
|
||||||
playerReady=true;
|
playerReady=true;
|
||||||
|
isBuffering=false;
|
||||||
player.start();
|
player.start();
|
||||||
updateSessionState(false);
|
updateSessionState(false);
|
||||||
}
|
}
|
||||||
@@ -205,6 +208,21 @@ public class AudioPlayerService extends Service{
|
|||||||
stopSelf();
|
stopSelf();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean onPlayerInfo(MediaPlayer mp, int what, int extra){
|
||||||
|
switch(what){
|
||||||
|
case MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
|
||||||
|
isBuffering=true;
|
||||||
|
updateSessionState(false);
|
||||||
|
}
|
||||||
|
case MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
|
||||||
|
isBuffering=false;
|
||||||
|
updateSessionState(false);
|
||||||
|
}
|
||||||
|
default -> Log.i(TAG, "onPlayerInfo() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void onAudioFocusChanged(int change){
|
private void onAudioFocusChanged(int change){
|
||||||
switch(change){
|
switch(change){
|
||||||
case AudioManager.AUDIOFOCUS_LOSS -> {
|
case AudioManager.AUDIOFOCUS_LOSS -> {
|
||||||
@@ -212,7 +230,7 @@ public class AudioPlayerService extends Service{
|
|||||||
pause(false);
|
pause(false);
|
||||||
}
|
}
|
||||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||||
resumeAfterAudioFocusGain=true;
|
resumeAfterAudioFocusGain=isPlaying();
|
||||||
pause(false);
|
pause(false);
|
||||||
}
|
}
|
||||||
case AudioManager.AUDIOFOCUS_GAIN -> {
|
case AudioManager.AUDIOFOCUS_GAIN -> {
|
||||||
@@ -232,12 +250,16 @@ public class AudioPlayerService extends Service{
|
|||||||
|
|
||||||
private void updateSessionState(boolean removeNotification){
|
private void updateSessionState(boolean removeNotification){
|
||||||
session.setPlaybackState(new PlaybackState.Builder()
|
session.setPlaybackState(new PlaybackState.Builder()
|
||||||
.setState(player.isPlaying() ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, player.getCurrentPosition(), 1f)
|
.setState(switch(getPlayState()){
|
||||||
|
case PLAYING -> PlaybackState.STATE_PLAYING;
|
||||||
|
case PAUSED -> PlaybackState.STATE_PAUSED;
|
||||||
|
case BUFFERING -> PlaybackState.STATE_BUFFERING;
|
||||||
|
}, player.getCurrentPosition(), 1f)
|
||||||
.setActions(PlaybackState.ACTION_STOP | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SEEK_TO)
|
.setActions(PlaybackState.ACTION_STOP | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SEEK_TO)
|
||||||
.build());
|
.build());
|
||||||
updateNotification(!player.isPlaying(), removeNotification);
|
updateNotification(!player.isPlaying(), removeNotification);
|
||||||
for(Callback cb:callbacks)
|
for(Callback cb:callbacks)
|
||||||
cb.onPlayStateChanged(attachment.id, player.isPlaying(), player.getCurrentPosition());
|
cb.onPlayStateChanged(attachment.id, getPlayState(), player.getCurrentPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateNotification(boolean dismissable, boolean removeNotification){
|
private void updateNotification(boolean dismissable, boolean removeNotification){
|
||||||
@@ -310,6 +332,12 @@ public class AudioPlayerService extends Service{
|
|||||||
return attachment.id;
|
return attachment.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PlayState getPlayState(){
|
||||||
|
if(isBuffering)
|
||||||
|
return PlayState.BUFFERING;
|
||||||
|
return player.isPlaying() ? PlayState.PLAYING : PlayState.PAUSED;
|
||||||
|
}
|
||||||
|
|
||||||
public static void registerCallback(Callback cb){
|
public static void registerCallback(Callback cb){
|
||||||
callbacks.add(cb);
|
callbacks.add(cb);
|
||||||
}
|
}
|
||||||
@@ -333,7 +361,13 @@ public class AudioPlayerService extends Service{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public interface Callback{
|
public interface Callback{
|
||||||
void onPlayStateChanged(String attachmentID, boolean playing, int position);
|
void onPlayStateChanged(String attachmentID, PlayState state, int position);
|
||||||
void onPlaybackStopped(String attachmentID);
|
void onPlaybackStopped(String attachmentID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum PlayState{
|
||||||
|
PLAYING,
|
||||||
|
PAUSED,
|
||||||
|
BUFFERING
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,841 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.joinmastodon.android;
|
||||||
|
|
||||||
|
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
|
||||||
|
import static org.xmlpull.v1.XmlPullParser.START_TAG;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ContentProvider;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.ProviderInfo;
|
||||||
|
import android.content.res.XmlResourceParser;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.MatrixCursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileProvider is a special subclass of {@link ContentProvider} that facilitates secure sharing
|
||||||
|
* of files associated with an app by creating a <code>content://</code> {@link Uri} for a file
|
||||||
|
* instead of a <code>file:///</code> {@link Uri}.
|
||||||
|
* <p>
|
||||||
|
* A content URI allows you to grant read and write access using
|
||||||
|
* temporary access permissions. When you create an {@link Intent} containing
|
||||||
|
* a content URI, in order to send the content URI
|
||||||
|
* to a client app, you can also call {@link Intent#setFlags(int) Intent.setFlags()} to add
|
||||||
|
* permissions. These permissions are available to the client app for as long as the stack for
|
||||||
|
* a receiving {@link android.app.Activity} is active. For an {@link Intent} going to a
|
||||||
|
* {@link android.app.Service}, the permissions are available as long as the
|
||||||
|
* {@link android.app.Service} is running.
|
||||||
|
* <p>
|
||||||
|
* In comparison, to control access to a <code>file:///</code> {@link Uri} you have to modify the
|
||||||
|
* file system permissions of the underlying file. The permissions you provide become available to
|
||||||
|
* <em>any</em> app, and remain in effect until you change them. This level of access is
|
||||||
|
* fundamentally insecure.
|
||||||
|
* <p>
|
||||||
|
* The increased level of file access security offered by a content URI
|
||||||
|
* makes FileProvider a key part of Android's security infrastructure.
|
||||||
|
* <p>
|
||||||
|
* This overview of FileProvider includes the following topics:
|
||||||
|
* </p>
|
||||||
|
* <ol>
|
||||||
|
* <li><a href="#ProviderDefinition">Defining a FileProvider</a></li>
|
||||||
|
* <li><a href="#SpecifyFiles">Specifying Available Files</a></li>
|
||||||
|
* <li><a href="#GetUri">Retrieving the Content URI for a File</li>
|
||||||
|
* <li><a href="#Permissions">Granting Temporary Permissions to a URI</a></li>
|
||||||
|
* <li><a href="#ServeUri">Serving a Content URI to Another App</a></li>
|
||||||
|
* </ol>
|
||||||
|
* <h3 id="ProviderDefinition">Defining a FileProvider</h3>
|
||||||
|
* <p>
|
||||||
|
* Since the default functionality of FileProvider includes content URI generation for files, you
|
||||||
|
* don't need to define a subclass in code. Instead, you can include a FileProvider in your app
|
||||||
|
* by specifying it entirely in XML. To specify the FileProvider component itself, add a
|
||||||
|
* <code><a href="{@docRoot}guide/topics/manifest/provider-element.html"><provider></a></code>
|
||||||
|
* element to your app manifest. Set the <code>android:name</code> attribute to
|
||||||
|
* <code>androidx.core.content.FileProvider</code>. Set the <code>android:authorities</code>
|
||||||
|
* attribute to a URI authority based on a domain you control; for example, if you control the
|
||||||
|
* domain <code>mydomain.com</code> you should use the authority
|
||||||
|
* <code>com.mydomain.fileprovider</code>. Set the <code>android:exported</code> attribute to
|
||||||
|
* <code>false</code>; the FileProvider does not need to be public. Set the
|
||||||
|
* <a href="{@docRoot}guide/topics/manifest/provider-element.html#gprmsn"
|
||||||
|
* >android:grantUriPermissions</a> attribute to <code>true</code>, to allow you
|
||||||
|
* to grant temporary access to files. For example:
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<manifest>
|
||||||
|
* ...
|
||||||
|
* <application>
|
||||||
|
* ...
|
||||||
|
* <provider
|
||||||
|
* android:name="androidx.core.content.FileProvider"
|
||||||
|
* android:authorities="com.mydomain.fileprovider"
|
||||||
|
* android:exported="false"
|
||||||
|
* android:grantUriPermissions="true">
|
||||||
|
* ...
|
||||||
|
* </provider>
|
||||||
|
* ...
|
||||||
|
* </application>
|
||||||
|
*</manifest></pre>
|
||||||
|
* <p>
|
||||||
|
* If you want to override any of the default behavior of FileProvider methods, extend
|
||||||
|
* the FileProvider class and use the fully-qualified class name in the <code>android:name</code>
|
||||||
|
* attribute of the <code><provider></code> element.
|
||||||
|
* <h3 id="SpecifyFiles">Specifying Available Files</h3>
|
||||||
|
* A FileProvider can only generate a content URI for files in directories that you specify
|
||||||
|
* beforehand. To specify a directory, specify the its storage area and path in XML, using child
|
||||||
|
* elements of the <code><paths></code> element.
|
||||||
|
* For example, the following <code>paths</code> element tells FileProvider that you intend to
|
||||||
|
* request content URIs for the <code>images/</code> subdirectory of your private file area.
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
* <files-path name="my_images" path="images/"/>
|
||||||
|
* ...
|
||||||
|
*</paths>
|
||||||
|
*</pre>
|
||||||
|
* <p>
|
||||||
|
* The <code><paths></code> element must contain one or more of the following child elements:
|
||||||
|
* </p>
|
||||||
|
* <dl>
|
||||||
|
* <dt>
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<files-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the <code>files/</code> subdirectory of your app's internal storage
|
||||||
|
* area. This subdirectory is the same as the value returned by {@link Context#getFilesDir()
|
||||||
|
* Context.getFilesDir()}.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <pre>
|
||||||
|
*<cache-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* <dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the cache subdirectory of your app's internal storage area. The root path
|
||||||
|
* of this subdirectory is the same as the value returned by {@link Context#getCacheDir()
|
||||||
|
* getCacheDir()}.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<external-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the root of the external storage area. The root path of this subdirectory
|
||||||
|
* is the same as the value returned by
|
||||||
|
* {@link Environment#getExternalStorageDirectory() Environment.getExternalStorageDirectory()}.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<external-files-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the root of your app's external storage area. The root path of this
|
||||||
|
* subdirectory is the same as the value returned by
|
||||||
|
* {@code Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)}.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<external-cache-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the root of your app's external cache area. The root path of this
|
||||||
|
* subdirectory is the same as the value returned by
|
||||||
|
* {@link Context#getExternalCacheDir() Context.getExternalCacheDir()}.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<external-media-path name="<i>name</i>" path="<i>path</i>" />
|
||||||
|
*</pre>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* Represents files in the root of your app's external media area. The root path of this
|
||||||
|
* subdirectory is the same as the value returned by the first result of
|
||||||
|
* {@link Context#getExternalMediaDirs() Context.getExternalMediaDirs()}.
|
||||||
|
* <p><strong>Note:</strong> this directory is only available on API 21+ devices.</p>
|
||||||
|
* </dd>
|
||||||
|
* </dl>
|
||||||
|
* <p>
|
||||||
|
* These child elements all use the same attributes:
|
||||||
|
* </p>
|
||||||
|
* <dl>
|
||||||
|
* <dt>
|
||||||
|
* <code>name="<i>name</i>"</code>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* A URI path segment. To enforce security, this value hides the name of the subdirectory
|
||||||
|
* you're sharing. The subdirectory name for this value is contained in the
|
||||||
|
* <code>path</code> attribute.
|
||||||
|
* </dd>
|
||||||
|
* <dt>
|
||||||
|
* <code>path="<i>path</i>"</code>
|
||||||
|
* </dt>
|
||||||
|
* <dd>
|
||||||
|
* The subdirectory you're sharing. While the <code>name</code> attribute is a URI path
|
||||||
|
* segment, the <code>path</code> value is an actual subdirectory name. Notice that the
|
||||||
|
* value refers to a <b>subdirectory</b>, not an individual file or files. You can't
|
||||||
|
* share a single file by its file name, nor can you specify a subset of files using
|
||||||
|
* wildcards.
|
||||||
|
* </dd>
|
||||||
|
* </dl>
|
||||||
|
* <p>
|
||||||
|
* You must specify a child element of <code><paths></code> for each directory that contains
|
||||||
|
* files for which you want content URIs. For example, these XML elements specify two directories:
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
* <files-path name="my_images" path="images/"/>
|
||||||
|
* <files-path name="my_docs" path="docs/"/>
|
||||||
|
*</paths>
|
||||||
|
*</pre>
|
||||||
|
* <p>
|
||||||
|
* Put the <code><paths></code> element and its children in an XML file in your project.
|
||||||
|
* For example, you can add them to a new file called <code>res/xml/file_paths.xml</code>.
|
||||||
|
* To link this file to the FileProvider, add a
|
||||||
|
* <a href="{@docRoot}guide/topics/manifest/meta-data-element.html"><meta-data></a> element
|
||||||
|
* as a child of the <code><provider></code> element that defines the FileProvider. Set the
|
||||||
|
* <code><meta-data></code> element's "android:name" attribute to
|
||||||
|
* <code>android.support.FILE_PROVIDER_PATHS</code>. Set the element's "android:resource" attribute
|
||||||
|
* to <code>@xml/file_paths</code> (notice that you don't specify the <code>.xml</code>
|
||||||
|
* extension). For example:
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*<provider
|
||||||
|
* android:name="androidx.core.content.FileProvider"
|
||||||
|
* android:authorities="com.mydomain.fileprovider"
|
||||||
|
* android:exported="false"
|
||||||
|
* android:grantUriPermissions="true">
|
||||||
|
* <meta-data
|
||||||
|
* android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
* android:resource="@xml/file_paths" />
|
||||||
|
*</provider>
|
||||||
|
*</pre>
|
||||||
|
* <h3 id="GetUri">Generating the Content URI for a File</h3>
|
||||||
|
* <p>
|
||||||
|
* To share a file with another app using a content URI, your app has to generate the content URI.
|
||||||
|
* To generate the content URI, create a new {@link File} for the file, then pass the {@link File}
|
||||||
|
* to {@link #getUriForFile(Context, String, File) getUriForFile()}. You can send the content URI
|
||||||
|
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()} to another app in an
|
||||||
|
* {@link Intent}. The client app that receives the content URI can open the file
|
||||||
|
* and access its contents by calling
|
||||||
|
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
|
||||||
|
* ContentResolver.openFileDescriptor} to get a {@link ParcelFileDescriptor}.
|
||||||
|
* <p>
|
||||||
|
* For example, suppose your app is offering files to other apps with a FileProvider that has the
|
||||||
|
* authority <code>com.mydomain.fileprovider</code>. To get a content URI for the file
|
||||||
|
* <code>default_image.jpg</code> in the <code>images/</code> subdirectory of your internal storage
|
||||||
|
* add the following code:
|
||||||
|
* <pre class="prettyprint">
|
||||||
|
*File imagePath = new File(Context.getFilesDir(), "images");
|
||||||
|
*File newFile = new File(imagePath, "default_image.jpg");
|
||||||
|
*Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
|
||||||
|
*</pre>
|
||||||
|
* As a result of the previous snippet,
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()} returns the content URI
|
||||||
|
* <code>content://com.mydomain.fileprovider/my_images/default_image.jpg</code>.
|
||||||
|
* <h3 id="Permissions">Granting Temporary Permissions to a URI</h3>
|
||||||
|
* To grant an access permission to a content URI returned from
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()}, do one of the following:
|
||||||
|
* <ul>
|
||||||
|
* <li>
|
||||||
|
* Call the method
|
||||||
|
* {@link Context#grantUriPermission(String, Uri, int)
|
||||||
|
* Context.grantUriPermission(package, Uri, mode_flags)} for the <code>content://</code>
|
||||||
|
* {@link Uri}, using the desired mode flags. This grants temporary access permission for the
|
||||||
|
* content URI to the specified package, according to the value of the
|
||||||
|
* the <code>mode_flags</code> parameter, which you can set to
|
||||||
|
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}
|
||||||
|
* or both. The permission remains in effect until you revoke it by calling
|
||||||
|
* {@link Context#revokeUriPermission(Uri, int) revokeUriPermission()} or until the device
|
||||||
|
* reboots.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* Put the content URI in an {@link Intent} by calling {@link Intent#setData(Uri) setData()}.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* Next, call the method {@link Intent#setFlags(int) Intent.setFlags()} with either
|
||||||
|
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} or
|
||||||
|
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} or both.
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* Finally, send the {@link Intent} to
|
||||||
|
* another app. Most often, you do this by calling
|
||||||
|
* {@link android.app.Activity#setResult(int, Intent) setResult()}.
|
||||||
|
* <p>
|
||||||
|
* Permissions granted in an {@link Intent} remain in effect while the stack of the receiving
|
||||||
|
* {@link android.app.Activity} is active. When the stack finishes, the permissions are
|
||||||
|
* automatically removed. Permissions granted to one {@link android.app.Activity} in a client
|
||||||
|
* app are automatically extended to other components of that app.
|
||||||
|
* </p>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
* <h3 id="ServeUri">Serving a Content URI to Another App</h3>
|
||||||
|
* <p>
|
||||||
|
* There are a variety of ways to serve the content URI for a file to a client app. One common way
|
||||||
|
* is for the client app to start your app by calling
|
||||||
|
* {@link android.app.Activity#startActivityForResult(Intent, int, Bundle) startActivityResult()},
|
||||||
|
* which sends an {@link Intent} to your app to start an {@link android.app.Activity} in your app.
|
||||||
|
* In response, your app can immediately return a content URI to the client app or present a user
|
||||||
|
* interface that allows the user to pick a file. In the latter case, once the user picks the file
|
||||||
|
* your app can return its content URI. In both cases, your app returns the content URI in an
|
||||||
|
* {@link Intent} sent via {@link android.app.Activity#setResult(int, Intent) setResult()}.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* You can also put the content URI in a {@link android.content.ClipData} object and then add the
|
||||||
|
* object to an {@link Intent} you send to a client app. To do this, call
|
||||||
|
* {@link Intent#setClipData(ClipData) Intent.setClipData()}. When you use this approach, you can
|
||||||
|
* add multiple {@link android.content.ClipData} objects to the {@link Intent}, each with its own
|
||||||
|
* content URI. When you call {@link Intent#setFlags(int) Intent.setFlags()} on the {@link Intent}
|
||||||
|
* to set temporary access permissions, the same permissions are applied to all of the content
|
||||||
|
* URIs.
|
||||||
|
* </p>
|
||||||
|
* <p class="note">
|
||||||
|
* <strong>Note:</strong> The {@link Intent#setClipData(ClipData) Intent.setClipData()} method is
|
||||||
|
* only available in platform version 16 (Android 4.1) and later. If you want to maintain
|
||||||
|
* compatibility with previous versions, you should send one content URI at a time in the
|
||||||
|
* {@link Intent}. Set the action to {@link Intent#ACTION_SEND} and put the URI in data by calling
|
||||||
|
* {@link Intent#setData setData()}.
|
||||||
|
* </p>
|
||||||
|
* <h3 id="">More Information</h3>
|
||||||
|
* <p>
|
||||||
|
* To learn more about FileProvider, see the Android training class
|
||||||
|
* <a href="{@docRoot}training/secure-file-sharing/index.html">Sharing Files Securely with URIs</a>.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public class FileProvider extends ContentProvider {
|
||||||
|
private static final String[] COLUMNS = {
|
||||||
|
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
|
||||||
|
|
||||||
|
private static final String
|
||||||
|
META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";
|
||||||
|
|
||||||
|
private static final String TAG_ROOT_PATH = "root-path";
|
||||||
|
private static final String TAG_FILES_PATH = "files-path";
|
||||||
|
private static final String TAG_CACHE_PATH = "cache-path";
|
||||||
|
private static final String TAG_EXTERNAL = "external-path";
|
||||||
|
private static final String TAG_EXTERNAL_FILES = "external-files-path";
|
||||||
|
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
|
||||||
|
private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
|
||||||
|
|
||||||
|
private static final String ATTR_NAME = "name";
|
||||||
|
private static final String ATTR_PATH = "path";
|
||||||
|
|
||||||
|
private static final File DEVICE_ROOT = new File("/");
|
||||||
|
|
||||||
|
@GuardedBy("sCache")
|
||||||
|
private static HashMap<String, PathStrategy> sCache = new HashMap<String, PathStrategy>();
|
||||||
|
|
||||||
|
private PathStrategy mStrategy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default FileProvider implementation does not need to be initialized. If you want to
|
||||||
|
* override this method, you must provide your own subclass of FileProvider.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean onCreate() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After the FileProvider is instantiated, this method is called to provide the system with
|
||||||
|
* information about the provider.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context} for the current component.
|
||||||
|
* @param info A {@link ProviderInfo} for the new provider.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
|
||||||
|
super.attachInfo(context, info);
|
||||||
|
|
||||||
|
// Sanity check our security
|
||||||
|
if (info.exported) {
|
||||||
|
throw new SecurityException("Provider must not be exported");
|
||||||
|
}
|
||||||
|
if (!info.grantUriPermissions) {
|
||||||
|
throw new SecurityException("Provider must grant uri permissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
mStrategy = getPathStrategy(context, info.authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a content URI for a given {@link File}. Specific temporary
|
||||||
|
* permissions for the content URI can be set with
|
||||||
|
* {@link Context#grantUriPermission(String, Uri, int)}, or added
|
||||||
|
* to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then
|
||||||
|
* {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are
|
||||||
|
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
|
||||||
|
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a
|
||||||
|
* <code>content</code> {@link Uri} for file paths defined in their <code><paths></code>
|
||||||
|
* meta-data element. See the Class Overview for more information.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context} for the current component.
|
||||||
|
* @param authority The authority of a {@link FileProvider} defined in a
|
||||||
|
* {@code <provider>} element in your app's manifest.
|
||||||
|
* @param file A {@link File} pointing to the filename for which you want a
|
||||||
|
* <code>content</code> {@link Uri}.
|
||||||
|
* @return A content URI for the file.
|
||||||
|
* @throws IllegalArgumentException When the given {@link File} is outside
|
||||||
|
* the paths supported by the provider.
|
||||||
|
*/
|
||||||
|
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
|
||||||
|
@NonNull File file) {
|
||||||
|
final PathStrategy strategy = getPathStrategy(context, authority);
|
||||||
|
return strategy.getUriForFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a content URI returned by
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file
|
||||||
|
* managed by the FileProvider.
|
||||||
|
* FileProvider reports the column names defined in {@link OpenableColumns}:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link OpenableColumns#DISPLAY_NAME}</li>
|
||||||
|
* <li>{@link OpenableColumns#SIZE}</li>
|
||||||
|
* </ul>
|
||||||
|
* For more information, see
|
||||||
|
* {@link ContentProvider#query(Uri, String[], String, String[], String)
|
||||||
|
* ContentProvider.query()}.
|
||||||
|
*
|
||||||
|
* @param uri A content URI returned by {@link #getUriForFile}.
|
||||||
|
* @param projection The list of columns to put into the {@link Cursor}. If null all columns are
|
||||||
|
* included.
|
||||||
|
* @param selection Selection criteria to apply. If null then all data that matches the content
|
||||||
|
* URI is returned.
|
||||||
|
* @param selectionArgs An array of {@link String}, containing arguments to bind to
|
||||||
|
* the <i>selection</i> parameter. The <i>query</i> method scans <i>selection</i> from left to
|
||||||
|
* right and iterates through <i>selectionArgs</i>, replacing the current "?" character in
|
||||||
|
* <i>selection</i> with the value at the current position in <i>selectionArgs</i>. The
|
||||||
|
* values are bound to <i>selection</i> as {@link String} values.
|
||||||
|
* @param sortOrder A {@link String} containing the column name(s) on which to sort
|
||||||
|
* the resulting {@link Cursor}.
|
||||||
|
* @return A {@link Cursor} containing the results of the query.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
|
||||||
|
@Nullable String[] selectionArgs,
|
||||||
|
@Nullable String sortOrder) {
|
||||||
|
// ContentProvider has already checked granted permissions
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
|
||||||
|
if (projection == null) {
|
||||||
|
projection = COLUMNS;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] cols = new String[projection.length];
|
||||||
|
Object[] values = new Object[projection.length];
|
||||||
|
int i = 0;
|
||||||
|
for (String col : projection) {
|
||||||
|
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
||||||
|
cols[i] = OpenableColumns.DISPLAY_NAME;
|
||||||
|
values[i++] = file.getName();
|
||||||
|
} else if (OpenableColumns.SIZE.equals(col)) {
|
||||||
|
cols[i] = OpenableColumns.SIZE;
|
||||||
|
values[i++] = file.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cols = copyOf(cols, i);
|
||||||
|
values = copyOf(values, i);
|
||||||
|
|
||||||
|
final MatrixCursor cursor = new MatrixCursor(cols, 1);
|
||||||
|
cursor.addRow(values);
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the MIME type of a content URI returned by
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||||
|
*
|
||||||
|
* @param uri A content URI returned by
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||||
|
* @return If the associated file has an extension, the MIME type associated with that
|
||||||
|
* extension; otherwise <code>application/octet-stream</code>.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getType(@NonNull Uri uri) {
|
||||||
|
// ContentProvider has already checked granted permissions
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
|
||||||
|
final int lastDot = file.getName().lastIndexOf('.');
|
||||||
|
if (lastDot >= 0) {
|
||||||
|
final String extension = file.getName().substring(lastDot + 1);
|
||||||
|
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||||
|
if (mime != null) {
|
||||||
|
return mime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, this method throws an {@link UnsupportedOperationException}. You must
|
||||||
|
* subclass FileProvider if you want to provide different functionality.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||||
|
throw new UnsupportedOperationException("No external inserts");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, this method throws an {@link UnsupportedOperationException}. You must
|
||||||
|
* subclass FileProvider if you want to provide different functionality.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int update(@NonNull Uri uri, ContentValues values, @Nullable String selection,
|
||||||
|
@Nullable String[] selectionArgs) {
|
||||||
|
throw new UnsupportedOperationException("No external updates");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the file associated with the specified content URI, as
|
||||||
|
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this
|
||||||
|
* method does <b>not</b> throw an {@link IOException}; you must check its return value.
|
||||||
|
*
|
||||||
|
* @param uri A content URI for a file, as returned by
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||||
|
* @param selection Ignored. Set to {@code null}.
|
||||||
|
* @param selectionArgs Ignored. Set to {@code null}.
|
||||||
|
* @return 1 if the delete succeeds; otherwise, 0.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int delete(@NonNull Uri uri, @Nullable String selection,
|
||||||
|
@Nullable String[] selectionArgs) {
|
||||||
|
// ContentProvider has already checked granted permissions
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
return file.delete() ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, FileProvider automatically returns the
|
||||||
|
* {@link ParcelFileDescriptor} for a file associated with a <code>content://</code>
|
||||||
|
* {@link Uri}. To get the {@link ParcelFileDescriptor}, call
|
||||||
|
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
|
||||||
|
* ContentResolver.openFileDescriptor}.
|
||||||
|
*
|
||||||
|
* To override this method, you must provide your own subclass of FileProvider.
|
||||||
|
*
|
||||||
|
* @param uri A content URI associated with a file, as returned by
|
||||||
|
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||||
|
* @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and
|
||||||
|
* write access, or "rwt" for read and write access that truncates any existing file.
|
||||||
|
* @return A new {@link ParcelFileDescriptor} with which you can access the file.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
|
||||||
|
throws FileNotFoundException {
|
||||||
|
// ContentProvider has already checked granted permissions
|
||||||
|
final File file = mStrategy.getFileForUri(uri);
|
||||||
|
final int fileMode = modeToMode(mode);
|
||||||
|
return ParcelFileDescriptor.open(file, fileMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@link PathStrategy} for given authority, either by parsing or
|
||||||
|
* returning from cache.
|
||||||
|
*/
|
||||||
|
private static PathStrategy getPathStrategy(Context context, String authority) {
|
||||||
|
PathStrategy strat;
|
||||||
|
synchronized (sCache) {
|
||||||
|
strat = sCache.get(authority);
|
||||||
|
if (strat == null) {
|
||||||
|
try {
|
||||||
|
strat = parsePathStrategy(context, authority);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
|
||||||
|
} catch (XmlPullParserException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
|
||||||
|
}
|
||||||
|
sCache.put(authority, strat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and return {@link PathStrategy} for given authority as defined in
|
||||||
|
* {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}.
|
||||||
|
*
|
||||||
|
* @see #getPathStrategy(Context, String)
|
||||||
|
*/
|
||||||
|
private static PathStrategy parsePathStrategy(Context context, String authority)
|
||||||
|
throws IOException, XmlPullParserException {
|
||||||
|
final SimplePathStrategy strat = new SimplePathStrategy(authority);
|
||||||
|
|
||||||
|
final ProviderInfo info = context.getPackageManager()
|
||||||
|
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
|
||||||
|
if (info == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Couldn't find meta-data for provider with authority " + authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
final XmlResourceParser in = info.loadXmlMetaData(
|
||||||
|
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
|
||||||
|
if (in == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
|
||||||
|
}
|
||||||
|
|
||||||
|
int type;
|
||||||
|
while ((type = in.next()) != END_DOCUMENT) {
|
||||||
|
if (type == START_TAG) {
|
||||||
|
final String tag = in.getName();
|
||||||
|
|
||||||
|
final String name = in.getAttributeValue(null, ATTR_NAME);
|
||||||
|
String path = in.getAttributeValue(null, ATTR_PATH);
|
||||||
|
|
||||||
|
File target = null;
|
||||||
|
if (TAG_ROOT_PATH.equals(tag)) {
|
||||||
|
target = DEVICE_ROOT;
|
||||||
|
} else if (TAG_FILES_PATH.equals(tag)) {
|
||||||
|
target = context.getFilesDir();
|
||||||
|
} else if (TAG_CACHE_PATH.equals(tag)) {
|
||||||
|
target = context.getCacheDir();
|
||||||
|
} else if (TAG_EXTERNAL.equals(tag)) {
|
||||||
|
target = Environment.getExternalStorageDirectory();
|
||||||
|
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
|
||||||
|
File[] externalFilesDirs = context.getExternalFilesDirs(null);
|
||||||
|
if (externalFilesDirs.length > 0) {
|
||||||
|
target = externalFilesDirs[0];
|
||||||
|
}
|
||||||
|
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
|
||||||
|
File[] externalCacheDirs = context.getExternalCacheDirs();
|
||||||
|
if (externalCacheDirs.length > 0) {
|
||||||
|
target = externalCacheDirs[0];
|
||||||
|
}
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||||
|
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
|
||||||
|
File[] externalMediaDirs = context.getExternalMediaDirs();
|
||||||
|
if (externalMediaDirs.length > 0) {
|
||||||
|
target = externalMediaDirs[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target != null) {
|
||||||
|
strat.addRoot(name, buildPath(target, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy for mapping between {@link File} and {@link Uri}.
|
||||||
|
* <p>
|
||||||
|
* Strategies must be symmetric so that mapping a {@link File} to a
|
||||||
|
* {@link Uri} and then back to a {@link File} points at the original
|
||||||
|
* target.
|
||||||
|
* <p>
|
||||||
|
* Strategies must remain consistent across app launches, and not rely on
|
||||||
|
* dynamic state. This ensures that any generated {@link Uri} can still be
|
||||||
|
* resolved if your process is killed and later restarted.
|
||||||
|
*
|
||||||
|
* @see SimplePathStrategy
|
||||||
|
*/
|
||||||
|
interface PathStrategy {
|
||||||
|
/**
|
||||||
|
* Return a {@link Uri} that represents the given {@link File}.
|
||||||
|
*/
|
||||||
|
Uri getUriForFile(File file);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a {@link File} that represents the given {@link Uri}.
|
||||||
|
*/
|
||||||
|
File getFileForUri(Uri uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy that provides access to files living under a narrow whitelist of
|
||||||
|
* filesystem roots. It will throw {@link SecurityException} if callers try
|
||||||
|
* accessing files outside the configured roots.
|
||||||
|
* <p>
|
||||||
|
* For example, if configured with
|
||||||
|
* {@code addRoot("myfiles", context.getFilesDir())}, then
|
||||||
|
* {@code context.getFileStreamPath("foo.txt")} would map to
|
||||||
|
* {@code content://myauthority/myfiles/foo.txt}.
|
||||||
|
*/
|
||||||
|
static class SimplePathStrategy implements PathStrategy {
|
||||||
|
private final String mAuthority;
|
||||||
|
private final HashMap<String, File> mRoots = new HashMap<String, File>();
|
||||||
|
|
||||||
|
SimplePathStrategy(String authority) {
|
||||||
|
mAuthority = authority;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a mapping from a name to a filesystem root. The provider only offers
|
||||||
|
* access to files that live under configured roots.
|
||||||
|
*/
|
||||||
|
void addRoot(String name, File root) {
|
||||||
|
if (TextUtils.isEmpty(name)) {
|
||||||
|
throw new IllegalArgumentException("Name must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Resolve to canonical path to keep path checking fast
|
||||||
|
root = root.getCanonicalFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to resolve canonical path for " + root, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
mRoots.put(name, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getUriForFile(File file) {
|
||||||
|
String path;
|
||||||
|
try {
|
||||||
|
path = file.getCanonicalPath();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the most-specific root path
|
||||||
|
Map.Entry<String, File> mostSpecific = null;
|
||||||
|
for (Map.Entry<String, File> root : mRoots.entrySet()) {
|
||||||
|
final String rootPath = root.getValue().getPath();
|
||||||
|
if (path.startsWith(rootPath) && (mostSpecific == null
|
||||||
|
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
|
||||||
|
mostSpecific = root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mostSpecific == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to find configured root that contains " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start at first char of path under root
|
||||||
|
final String rootPath = mostSpecific.getValue().getPath();
|
||||||
|
if (rootPath.endsWith("/")) {
|
||||||
|
path = path.substring(rootPath.length());
|
||||||
|
} else {
|
||||||
|
path = path.substring(rootPath.length() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the tag and path separately
|
||||||
|
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
|
||||||
|
return new Uri.Builder().scheme("content")
|
||||||
|
.authority(mAuthority).encodedPath(path).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getFileForUri(Uri uri) {
|
||||||
|
String path = uri.getEncodedPath();
|
||||||
|
|
||||||
|
final int splitIndex = path.indexOf('/', 1);
|
||||||
|
final String tag = Uri.decode(path.substring(1, splitIndex));
|
||||||
|
path = Uri.decode(path.substring(splitIndex + 1));
|
||||||
|
|
||||||
|
final File root = mRoots.get(tag);
|
||||||
|
if (root == null) {
|
||||||
|
throw new IllegalArgumentException("Unable to find configured root for " + uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = new File(root, path);
|
||||||
|
try {
|
||||||
|
file = file.getCanonicalFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.getPath().startsWith(root.getPath())) {
|
||||||
|
throw new SecurityException("Resolved path jumped beyond configured root");
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copied from ContentResolver.java
|
||||||
|
*/
|
||||||
|
private static int modeToMode(String mode) {
|
||||||
|
int modeBits;
|
||||||
|
if ("r".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
|
||||||
|
} else if ("w".equals(mode) || "wt".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE
|
||||||
|
| ParcelFileDescriptor.MODE_TRUNCATE;
|
||||||
|
} else if ("wa".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE
|
||||||
|
| ParcelFileDescriptor.MODE_APPEND;
|
||||||
|
} else if ("rw".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE;
|
||||||
|
} else if ("rwt".equals(mode)) {
|
||||||
|
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
||||||
|
| ParcelFileDescriptor.MODE_CREATE
|
||||||
|
| ParcelFileDescriptor.MODE_TRUNCATE;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid mode: " + mode);
|
||||||
|
}
|
||||||
|
return modeBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File buildPath(File base, String... segments) {
|
||||||
|
File cur = base;
|
||||||
|
for (String segment : segments) {
|
||||||
|
if (segment != null) {
|
||||||
|
cur = new File(cur, segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] copyOf(String[] original, int newLength) {
|
||||||
|
final String[] result = new String[newLength];
|
||||||
|
System.arraycopy(original, 0, result, 0, newLength);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object[] copyOf(Object[] original, int newLength) {
|
||||||
|
final Object[] result = new Object[newLength];
|
||||||
|
System.arraycopy(original, 0, result, 0, newLength);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,21 +3,31 @@ package org.joinmastodon.android;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
public class GlobalUserPreferences{
|
public class GlobalUserPreferences{
|
||||||
public static boolean playGifs;
|
public static boolean playGifs;
|
||||||
public static boolean useCustomTabs;
|
public static boolean useCustomTabs;
|
||||||
public static boolean trueBlackTheme;
|
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
|
||||||
public static ThemePreference theme;
|
public static ThemePreference theme;
|
||||||
|
|
||||||
private static SharedPreferences getPrefs(){
|
private static SharedPreferences getPrefs(){
|
||||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static SharedPreferences getPreReplyPrefs(){
|
||||||
|
return MastodonApp.context.getSharedPreferences("pre_reply_sheets", Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
public static void load(){
|
public static void load(){
|
||||||
SharedPreferences prefs=getPrefs();
|
SharedPreferences prefs=getPrefs();
|
||||||
playGifs=prefs.getBoolean("playGifs", true);
|
playGifs=prefs.getBoolean("playGifs", true);
|
||||||
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
|
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
|
||||||
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
|
altTextReminders=prefs.getBoolean("altTextReminders", false);
|
||||||
|
confirmUnfollow=prefs.getBoolean("confirmUnfollow", false);
|
||||||
|
confirmBoost=prefs.getBoolean("confirmBoost", false);
|
||||||
|
confirmDeletePost=prefs.getBoolean("confirmDeletePost", true);
|
||||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,14 +35,50 @@ public class GlobalUserPreferences{
|
|||||||
getPrefs().edit()
|
getPrefs().edit()
|
||||||
.putBoolean("playGifs", playGifs)
|
.putBoolean("playGifs", playGifs)
|
||||||
.putBoolean("useCustomTabs", useCustomTabs)
|
.putBoolean("useCustomTabs", useCustomTabs)
|
||||||
.putBoolean("trueBlackTheme", trueBlackTheme)
|
|
||||||
.putInt("theme", theme.ordinal())
|
.putInt("theme", theme.ordinal())
|
||||||
|
.putBoolean("altTextReminders", altTextReminders)
|
||||||
|
.putBoolean("confirmUnfollow", confirmUnfollow)
|
||||||
|
.putBoolean("confirmBoost", confirmBoost)
|
||||||
|
.putBoolean("confirmDeletePost", confirmDeletePost)
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isOptedOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
|
||||||
|
if(getPreReplyPrefs().getBoolean("opt_out_"+type, false))
|
||||||
|
return true;
|
||||||
|
if(account==null)
|
||||||
|
return false;
|
||||||
|
String accountKey=account.acct;
|
||||||
|
if(!accountKey.contains("@"))
|
||||||
|
accountKey+="@"+AccountSessionManager.get(accountID).domain;
|
||||||
|
return getPreReplyPrefs().getBoolean("opt_out_"+type+"_"+accountKey.toLowerCase(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void optOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
|
||||||
|
String key;
|
||||||
|
if(account==null){
|
||||||
|
key="opt_out_"+type;
|
||||||
|
}else{
|
||||||
|
String accountKey=account.acct;
|
||||||
|
if(!accountKey.contains("@"))
|
||||||
|
accountKey+="@"+AccountSessionManager.get(accountID).domain;
|
||||||
|
key="opt_out_"+type+"_"+accountKey.toLowerCase();
|
||||||
|
}
|
||||||
|
getPreReplyPrefs().edit().putBoolean(key, true).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void resetPreReplySheets(){
|
||||||
|
getPreReplyPrefs().edit().clear().apply();
|
||||||
|
}
|
||||||
|
|
||||||
public enum ThemePreference{
|
public enum ThemePreference{
|
||||||
AUTO,
|
AUTO,
|
||||||
LIGHT,
|
LIGHT,
|
||||||
DARK
|
DARK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum PreReplySheetType{
|
||||||
|
OLD_POST,
|
||||||
|
NON_MUTUAL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import android.Manifest;
|
|||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageInstaller;
|
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.BadParcelableException;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.ObjectValidationException;
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
|
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||||
@@ -20,6 +23,7 @@ import org.joinmastodon.android.fragments.SplashFragment;
|
|||||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
|
import org.joinmastodon.android.model.SearchResults;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
@@ -28,46 +32,20 @@ import java.lang.reflect.InvocationTargetException;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import me.grishka.appkit.FragmentStackActivity;
|
import me.grishka.appkit.FragmentStackActivity;
|
||||||
|
import me.grishka.appkit.Nav;
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
|
||||||
public class MainActivity extends FragmentStackActivity{
|
public class MainActivity extends FragmentStackActivity{
|
||||||
|
private static final String TAG="MainActivity";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||||
UiUtils.setUserPreferredTheme(this);
|
UiUtils.setUserPreferredTheme(this);
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
if(savedInstanceState==null){
|
if(savedInstanceState==null){
|
||||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
restartHomeFragment();
|
||||||
showFragmentClearingBackStack(new SplashFragment());
|
|
||||||
}else{
|
|
||||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
|
||||||
AccountSession session;
|
|
||||||
Bundle args=new Bundle();
|
|
||||||
Intent intent=getIntent();
|
|
||||||
if(intent.getBooleanExtra("fromNotification", false)){
|
|
||||||
String accountID=intent.getStringExtra("accountID");
|
|
||||||
try{
|
|
||||||
session=AccountSessionManager.getInstance().getAccount(accountID);
|
|
||||||
if(!intent.hasExtra("notification"))
|
|
||||||
args.putString("tab", "notifications");
|
|
||||||
}catch(IllegalStateException x){
|
|
||||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
|
||||||
}
|
|
||||||
args.putString("account", session.getID());
|
|
||||||
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
|
||||||
fragment.setArguments(args);
|
|
||||||
showFragmentClearingBackStack(fragment);
|
|
||||||
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
|
|
||||||
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
|
||||||
showFragmentForNotification(notification, session.getID());
|
|
||||||
}else if(intent.getBooleanExtra("compose", false)){
|
|
||||||
showCompose();
|
|
||||||
}else{
|
|
||||||
maybeRequestNotificationsPermission();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(BuildConfig.BUILD_TYPE.startsWith("appcenter")){
|
if(BuildConfig.BUILD_TYPE.startsWith("appcenter")){
|
||||||
@@ -105,11 +83,55 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
}
|
}
|
||||||
}else if(intent.getBooleanExtra("compose", false)){
|
}else if(intent.getBooleanExtra("compose", false)){
|
||||||
showCompose();
|
showCompose();
|
||||||
|
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||||
|
handleURL(intent.getData(), null);
|
||||||
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
||||||
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
|
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void handleURL(Uri uri, String accountID){
|
||||||
|
if(uri==null)
|
||||||
|
return;
|
||||||
|
if(!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme()))
|
||||||
|
return;
|
||||||
|
AccountSession session;
|
||||||
|
if(accountID==null)
|
||||||
|
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||||
|
else
|
||||||
|
session=AccountSessionManager.get(accountID);
|
||||||
|
if(session==null || !session.activated)
|
||||||
|
return;
|
||||||
|
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch, GetSearchResults.Type type){
|
||||||
|
new GetSearchResults(q, type, true, null, 0, 0)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(SearchResults result){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
if(result.statuses!=null && !result.statuses.isEmpty()){
|
||||||
|
args.putParcelable("status", Parcels.wrap(result.statuses.get(0)));
|
||||||
|
Nav.go(MainActivity.this, ThreadFragment.class, args);
|
||||||
|
}else if(result.accounts!=null && !result.accounts.isEmpty()){
|
||||||
|
args.putParcelable("profileAccount", Parcels.wrap(result.accounts.get(0)));
|
||||||
|
Nav.go(MainActivity.this, ProfileFragment.class, args);
|
||||||
|
}else{
|
||||||
|
Toast.makeText(MainActivity.this, fromSearch ? R.string.no_search_results : R.string.link_not_supported, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(MainActivity.this);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(this, progressText, true)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
private void showFragmentForNotification(Notification notification, String accountID){
|
private void showFragmentForNotification(Notification notification, String accountID){
|
||||||
Fragment fragment;
|
Fragment fragment;
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
@@ -148,4 +170,47 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
|
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void restartHomeFragment(){
|
||||||
|
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
||||||
|
showFragmentClearingBackStack(new SplashFragment());
|
||||||
|
}else{
|
||||||
|
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
||||||
|
AccountSession session;
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
Intent intent=getIntent();
|
||||||
|
if(intent.getBooleanExtra("fromNotification", false)){
|
||||||
|
String accountID=intent.getStringExtra("accountID");
|
||||||
|
try{
|
||||||
|
session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
|
if(!intent.hasExtra("notification"))
|
||||||
|
args.putString("tab", "notifications");
|
||||||
|
}catch(IllegalStateException x){
|
||||||
|
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||||
|
}
|
||||||
|
args.putString("account", session.getID());
|
||||||
|
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
||||||
|
fragment.setArguments(args);
|
||||||
|
showFragmentClearingBackStack(fragment);
|
||||||
|
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
|
||||||
|
// Parcelables might not be compatible across app versions so this protects against possible crashes
|
||||||
|
// when a notification was received, then the app was updated, and then the user opened the notification
|
||||||
|
try{
|
||||||
|
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||||
|
showFragmentForNotification(notification, session.getID());
|
||||||
|
}catch(BadParcelableException x){
|
||||||
|
Log.w(TAG, x);
|
||||||
|
}
|
||||||
|
}else if(intent.getBooleanExtra("compose", false)){
|
||||||
|
showCompose();
|
||||||
|
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||||
|
handleURL(intent.getData(), null);
|
||||||
|
}else{
|
||||||
|
maybeRequestNotificationsPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ package org.joinmastodon.android;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
|
|
||||||
import me.grishka.appkit.imageloader.ImageCache;
|
import me.grishka.appkit.imageloader.ImageCache;
|
||||||
import me.grishka.appkit.utils.NetworkUtils;
|
import me.grishka.appkit.utils.NetworkUtils;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
@@ -30,5 +29,8 @@ public class MastodonApp extends Application{
|
|||||||
|
|
||||||
PushSubscriptionManager.tryRegisterFCM();
|
PushSubscriptionManager.tryRegisterFCM();
|
||||||
GlobalUserPreferences.load();
|
GlobalUserPreferences.load();
|
||||||
|
if(BuildConfig.DEBUG){
|
||||||
|
WebView.setWebContentsDebuggingEnabled(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
Log.w(TAG, "onReceive: account for id '"+pushAccountID+"' not found");
|
Log.w(TAG, "onReceive: account for id '"+pushAccountID+"' not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(account.getLocalPreferences().getNotificationsPauseEndTime()>System.currentTimeMillis()){
|
||||||
|
Log.i(TAG, "onReceive: dropping notification because user has paused notifications for this account");
|
||||||
|
return;
|
||||||
|
}
|
||||||
String accountID=account.getID();
|
String accountID=account.getID();
|
||||||
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
||||||
new GetNotificationByID(pn.notificationId+"")
|
new GetNotificationByID(pn.notificationId+"")
|
||||||
@@ -114,6 +118,8 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
List<NotificationChannel> channels=Arrays.stream(PushNotification.Type.values())
|
List<NotificationChannel> channels=Arrays.stream(PushNotification.Type.values())
|
||||||
.map(type->{
|
.map(type->{
|
||||||
NotificationChannel channel=new NotificationChannel(accountID+"_"+type, context.getString(type.localizedName), NotificationManager.IMPORTANCE_DEFAULT);
|
NotificationChannel channel=new NotificationChannel(accountID+"_"+type, context.getString(type.localizedName), NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
|
channel.setLightColor(context.getColor(R.color.primary_700));
|
||||||
|
channel.enableLights(true);
|
||||||
channel.setGroup(accountID);
|
channel.setGroup(accountID);
|
||||||
return channel;
|
return channel;
|
||||||
})
|
})
|
||||||
@@ -143,6 +149,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
.setShowWhen(true)
|
.setShowWhen(true)
|
||||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
|
.setLights(context.getColor(R.color.primary_700), 500, 1000)
|
||||||
.setColor(context.getColor(R.color.primary_700));
|
.setColor(context.getColor(R.color.primary_700));
|
||||||
if(avatar!=null){
|
if(avatar!=null){
|
||||||
builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar));
|
builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar));
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package org.joinmastodon.android;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
public class TweakedFileProvider extends FileProvider{
|
||||||
|
private static final String TAG="TweakedFileProvider";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType(@NonNull Uri uri){
|
||||||
|
Log.d(TAG, "getType() called with: uri = ["+uri+"]");
|
||||||
|
if(uri.getPathSegments().get(0).equals("image_cache")){
|
||||||
|
Log.i(TAG, "getType: HERE!");
|
||||||
|
return "image/jpeg"; // might as well be a png but image decoding APIs don't care, needs to be image/* though
|
||||||
|
}
|
||||||
|
return super.getType(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
|
||||||
|
Log.d(TAG, "query() called with: uri = ["+uri+"], projection = ["+Arrays.toString(projection)+"], selection = ["+selection+"], selectionArgs = ["+Arrays.toString(selectionArgs)+"], sortOrder = ["+sortOrder+"]");
|
||||||
|
return super.query(uri, projection, selection, selectionArgs, sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
|
||||||
|
Log.d(TAG, "openFile() called with: uri = ["+uri+"], mode = ["+mode+"]");
|
||||||
|
return super.openFile(uri, mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,25 +9,34 @@ import android.os.Handler;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
import org.joinmastodon.android.BuildConfig;
|
import org.joinmastodon.android.BuildConfig;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
|
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
import org.joinmastodon.android.model.PaginatedResponse;
|
||||||
import org.joinmastodon.android.model.SearchResult;
|
import org.joinmastodon.android.model.SearchResult;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
@@ -35,13 +44,16 @@ import me.grishka.appkit.utils.WorkerThread;
|
|||||||
|
|
||||||
public class CacheController{
|
public class CacheController{
|
||||||
private static final String TAG="CacheController";
|
private static final String TAG="CacheController";
|
||||||
private static final int DB_VERSION=2;
|
private static final int DB_VERSION=3;
|
||||||
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||||
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
private final String accountID;
|
private final String accountID;
|
||||||
private DatabaseHelper db;
|
private DatabaseHelper db;
|
||||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||||
|
private boolean loadingNotifications;
|
||||||
|
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||||
|
private List<FollowList> lists;
|
||||||
|
|
||||||
private static final int POST_FLAG_GAP_AFTER=1;
|
private static final int POST_FLAG_GAP_AFTER=1;
|
||||||
|
|
||||||
@@ -57,28 +69,23 @@ public class CacheController{
|
|||||||
cancelDelayedClose();
|
cancelDelayedClose();
|
||||||
databaseThread.postRunnable(()->{
|
databaseThread.postRunnable(()->{
|
||||||
try{
|
try{
|
||||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
|
||||||
if(!forceReload){
|
if(!forceReload){
|
||||||
SQLiteDatabase db=getOrOpenDatabase();
|
SQLiteDatabase db=getOrOpenDatabase();
|
||||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||||
if(cursor.getCount()==count){
|
if(cursor.getCount()==count){
|
||||||
ArrayList<Status> result=new ArrayList<>();
|
ArrayList<Status> result=new ArrayList<>();
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
String newMaxID;
|
String newMaxID;
|
||||||
outer:
|
|
||||||
do{
|
do{
|
||||||
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
|
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
|
||||||
status.postprocess();
|
status.postprocess();
|
||||||
int flags=cursor.getInt(1);
|
int flags=cursor.getInt(1);
|
||||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||||
newMaxID=status.id;
|
newMaxID=status.id;
|
||||||
for(Filter filter:filters){
|
|
||||||
if(filter.matches(status))
|
|
||||||
continue outer;
|
|
||||||
}
|
|
||||||
result.add(status);
|
result.add(status);
|
||||||
}while(cursor.moveToNext());
|
}while(cursor.moveToNext());
|
||||||
String _newMaxID=newMaxID;
|
String _newMaxID=newMaxID;
|
||||||
|
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||||
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -90,7 +97,9 @@ public class CacheController{
|
|||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
ArrayList<Status> filtered=new ArrayList<>(result);
|
||||||
|
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
|
||||||
|
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||||
putHomeTimeline(result, maxID==null);
|
putHomeTimeline(result, maxID==null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +122,7 @@ public class CacheController{
|
|||||||
runOnDbThread((db)->{
|
runOnDbThread((db)->{
|
||||||
if(clear)
|
if(clear)
|
||||||
db.delete("home_timeline", null, null);
|
db.delete("home_timeline", null, null);
|
||||||
ContentValues values=new ContentValues(3);
|
ContentValues values=new ContentValues(4);
|
||||||
for(Status s:posts){
|
for(Status s:posts){
|
||||||
values.put("id", s.id);
|
values.put("id", s.id);
|
||||||
values.put("json", MastodonAPIController.gson.toJson(s));
|
values.put("json", MastodonAPIController.gson.toJson(s));
|
||||||
@@ -121,6 +130,7 @@ public class CacheController{
|
|||||||
if(s.hasGapAfter)
|
if(s.hasGapAfter)
|
||||||
flags|=POST_FLAG_GAP_AFTER;
|
flags|=POST_FLAG_GAP_AFTER;
|
||||||
values.put("flags", flags);
|
values.put("flags", flags);
|
||||||
|
values.put("time", s.createdAt.getEpochSecond());
|
||||||
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -130,28 +140,27 @@ public class CacheController{
|
|||||||
cancelDelayedClose();
|
cancelDelayedClose();
|
||||||
databaseThread.postRunnable(()->{
|
databaseThread.postRunnable(()->{
|
||||||
try{
|
try{
|
||||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
|
if(!onlyMentions && loadingNotifications){
|
||||||
|
synchronized(pendingNotificationsCallbacks){
|
||||||
|
pendingNotificationsCallbacks.add(callback);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(!forceReload){
|
if(!forceReload){
|
||||||
SQLiteDatabase db=getOrOpenDatabase();
|
SQLiteDatabase db=getOrOpenDatabase();
|
||||||
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||||
if(cursor.getCount()==count){
|
if(cursor.getCount()==count){
|
||||||
ArrayList<Notification> result=new ArrayList<>();
|
ArrayList<Notification> result=new ArrayList<>();
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
String newMaxID;
|
String newMaxID;
|
||||||
outer:
|
|
||||||
do{
|
do{
|
||||||
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
|
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
|
||||||
ntf.postprocess();
|
ntf.postprocess();
|
||||||
newMaxID=ntf.id;
|
newMaxID=ntf.id;
|
||||||
if(ntf.status!=null){
|
|
||||||
for(Filter filter:filters){
|
|
||||||
if(filter.matches(ntf.status))
|
|
||||||
continue outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.add(ntf);
|
result.add(ntf);
|
||||||
}while(cursor.moveToNext());
|
}while(cursor.moveToNext());
|
||||||
String _newMaxID=newMaxID;
|
String _newMaxID=newMaxID;
|
||||||
|
AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS);
|
||||||
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
|
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -159,26 +168,40 @@ public class CacheController{
|
|||||||
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
|
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(!onlyMentions)
|
||||||
|
loadingNotifications=true;
|
||||||
new GetNotifications(maxID, count, onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class))
|
new GetNotifications(maxID, count, onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class))
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Notification> result){
|
public void onSuccess(List<Notification> result){
|
||||||
callback.onSuccess(new PaginatedResponse<>(result.stream().filter(ntf->{
|
ArrayList<Notification> filtered=new ArrayList<>(result);
|
||||||
if(ntf.status!=null){
|
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
|
||||||
for(Filter filter:filters){
|
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id);
|
||||||
if(filter.matches(ntf.status)){
|
callback.onSuccess(res);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id));
|
|
||||||
putNotifications(result, onlyMentions, maxID==null);
|
putNotifications(result, onlyMentions, maxID==null);
|
||||||
|
if(!onlyMentions){
|
||||||
|
loadingNotifications=false;
|
||||||
|
synchronized(pendingNotificationsCallbacks){
|
||||||
|
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||||
|
cb.onSuccess(res);
|
||||||
|
}
|
||||||
|
pendingNotificationsCallbacks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error){
|
public void onError(ErrorResponse error){
|
||||||
callback.onError(error);
|
callback.onError(error);
|
||||||
|
if(!onlyMentions){
|
||||||
|
loadingNotifications=false;
|
||||||
|
synchronized(pendingNotificationsCallbacks){
|
||||||
|
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||||
|
cb.onError(error);
|
||||||
|
}
|
||||||
|
pendingNotificationsCallbacks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
@@ -196,7 +219,7 @@ public class CacheController{
|
|||||||
String table=onlyMentions ? "notifications_mentions" : "notifications_all";
|
String table=onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||||
if(clear)
|
if(clear)
|
||||||
db.delete(table, null, null);
|
db.delete(table, null, null);
|
||||||
ContentValues values=new ContentValues(3);
|
ContentValues values=new ContentValues(4);
|
||||||
for(Notification n:notifications){
|
for(Notification n:notifications){
|
||||||
if(n.type==null){
|
if(n.type==null){
|
||||||
continue;
|
continue;
|
||||||
@@ -204,6 +227,7 @@ public class CacheController{
|
|||||||
values.put("id", n.id);
|
values.put("id", n.id);
|
||||||
values.put("json", MastodonAPIController.gson.toJson(n));
|
values.put("json", MastodonAPIController.gson.toJson(n));
|
||||||
values.put("type", n.type.ordinal());
|
values.put("type", n.type.ordinal());
|
||||||
|
values.put("time", n.createdAt.getEpochSecond());
|
||||||
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -288,6 +312,99 @@ public class CacheController{
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void reloadLists(Callback<List<FollowList>> callback){
|
||||||
|
new GetLists()
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<FollowList> result){
|
||||||
|
result.sort(Comparator.comparing(l->l.title));
|
||||||
|
lists=result;
|
||||||
|
if(callback!=null)
|
||||||
|
callback.onSuccess(result);
|
||||||
|
writeListsToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
if(callback!=null)
|
||||||
|
callback.onError(error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FollowList> loadListsFromFile(){
|
||||||
|
File file=getListsFile();
|
||||||
|
if(!file.exists())
|
||||||
|
return null;
|
||||||
|
try(InputStreamReader in=new InputStreamReader(new FileInputStream(file))){
|
||||||
|
return MastodonAPIController.gson.fromJson(in, new TypeToken<List<FollowList>>(){}.getType());
|
||||||
|
}catch(Exception x){
|
||||||
|
Log.w(TAG, "failed to read lists from cache file", x);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeListsToFile(){
|
||||||
|
databaseThread.postRunnable(()->{
|
||||||
|
try(OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream(getListsFile()))){
|
||||||
|
MastodonAPIController.gson.toJson(lists, out);
|
||||||
|
}catch(IOException x){
|
||||||
|
Log.w(TAG, "failed to write lists to cache file", x);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getLists(Callback<List<FollowList>> callback){
|
||||||
|
if(lists!=null){
|
||||||
|
if(callback!=null)
|
||||||
|
callback.onSuccess(lists);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
databaseThread.postRunnable(()->{
|
||||||
|
List<FollowList> lists=loadListsFromFile();
|
||||||
|
if(lists!=null){
|
||||||
|
this.lists=lists;
|
||||||
|
if(callback!=null)
|
||||||
|
uiHandler.post(()->callback.onSuccess(lists));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reloadLists(callback);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getListsFile(){
|
||||||
|
return new File(MastodonApp.context.getFilesDir(), "lists_"+accountID+".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addList(FollowList list){
|
||||||
|
if(lists==null)
|
||||||
|
return;
|
||||||
|
lists.add(list);
|
||||||
|
lists.sort(Comparator.comparing(l->l.title));
|
||||||
|
writeListsToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteList(String id){
|
||||||
|
if(lists==null)
|
||||||
|
return;
|
||||||
|
lists.removeIf(l->l.id.equals(id));
|
||||||
|
writeListsToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateList(FollowList list){
|
||||||
|
if(lists==null)
|
||||||
|
return;
|
||||||
|
for(int i=0;i<lists.size();i++){
|
||||||
|
if(lists.get(i).id.equals(list.id)){
|
||||||
|
lists.set(i, list);
|
||||||
|
lists.sort(Comparator.comparing(l->l.title));
|
||||||
|
writeListsToFile();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class DatabaseHelper extends SQLiteOpenHelper{
|
private class DatabaseHelper extends SQLiteOpenHelper{
|
||||||
|
|
||||||
public DatabaseHelper(){
|
public DatabaseHelper(){
|
||||||
@@ -300,30 +417,36 @@ public class CacheController{
|
|||||||
CREATE TABLE `home_timeline` (
|
CREATE TABLE `home_timeline` (
|
||||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||||
`json` TEXT NOT NULL,
|
`json` TEXT NOT NULL,
|
||||||
`flags` INTEGER NOT NULL DEFAULT 0
|
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
db.execSQL("""
|
db.execSQL("""
|
||||||
CREATE TABLE `notifications_all` (
|
CREATE TABLE `notifications_all` (
|
||||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||||
`json` TEXT NOT NULL,
|
`json` TEXT NOT NULL,
|
||||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||||
`type` INTEGER NOT NULL
|
`type` INTEGER NOT NULL,
|
||||||
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
db.execSQL("""
|
db.execSQL("""
|
||||||
CREATE TABLE `notifications_mentions` (
|
CREATE TABLE `notifications_mentions` (
|
||||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||||
`json` TEXT NOT NULL,
|
`json` TEXT NOT NULL,
|
||||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||||
`type` INTEGER NOT NULL
|
`type` INTEGER NOT NULL,
|
||||||
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
createRecentSearchesTable(db);
|
createRecentSearchesTable(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
||||||
if(oldVersion==1){
|
if(oldVersion<2){
|
||||||
createRecentSearchesTable(db);
|
createRecentSearchesTable(db);
|
||||||
}
|
}
|
||||||
|
if(oldVersion<3){
|
||||||
|
addTimeColumns(db);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createRecentSearchesTable(SQLiteDatabase db){
|
private void createRecentSearchesTable(SQLiteDatabase db){
|
||||||
@@ -334,6 +457,15 @@ public class CacheController{
|
|||||||
`time` INTEGER NOT NULL
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addTimeColumns(SQLiteDatabase db){
|
||||||
|
db.execSQL("DELETE FROM `home_timeline`");
|
||||||
|
db.execSQL("DELETE FROM `notifications_all`");
|
||||||
|
db.execSQL("DELETE FROM `notifications_mentions`");
|
||||||
|
db.execSQL("ALTER TABLE `home_timeline` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||||
|
db.execSQL("ALTER TABLE `notifications_all` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||||
|
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -44,7 +45,11 @@ public class MastodonAPIController{
|
|||||||
.registerTypeAdapter(LocalDate.class, new IsoLocalDateTypeAdapter())
|
.registerTypeAdapter(LocalDate.class, new IsoLocalDateTypeAdapter())
|
||||||
.create();
|
.create();
|
||||||
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
||||||
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
|
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(60, TimeUnit.SECONDS)
|
||||||
|
.build();
|
||||||
|
|
||||||
private AccountSession session;
|
private AccountSession session;
|
||||||
|
|
||||||
@@ -86,17 +91,20 @@ public class MastodonAPIController{
|
|||||||
synchronized(req){
|
synchronized(req){
|
||||||
req.okhttpCall=call;
|
req.okhttpCall=call;
|
||||||
}
|
}
|
||||||
|
if(req.timeout>0){
|
||||||
|
call.timeout().timeout(req.timeout, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
|
Log.d(TAG, logTag(session)+"Sending request: "+hreq);
|
||||||
|
|
||||||
call.enqueue(new Callback(){
|
call.enqueue(new Callback(){
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
||||||
if(call.isCanceled())
|
if(req.canceled)
|
||||||
return;
|
return;
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
|
Log.w(TAG, logTag(session)+""+hreq+" failed", e);
|
||||||
synchronized(req){
|
synchronized(req){
|
||||||
req.okhttpCall=null;
|
req.okhttpCall=null;
|
||||||
}
|
}
|
||||||
@@ -105,10 +113,10 @@ public class MastodonAPIController{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
||||||
if(call.isCanceled())
|
if(req.canceled)
|
||||||
return;
|
return;
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" received response: "+response);
|
Log.d(TAG, logTag(session)+hreq+" received response: "+response);
|
||||||
synchronized(req){
|
synchronized(req){
|
||||||
req.okhttpCall=null;
|
req.okhttpCall=null;
|
||||||
}
|
}
|
||||||
@@ -119,20 +127,24 @@ public class MastodonAPIController{
|
|||||||
try{
|
try{
|
||||||
if(BuildConfig.DEBUG){
|
if(BuildConfig.DEBUG){
|
||||||
JsonElement respJson=JsonParser.parseReader(reader);
|
JsonElement respJson=JsonParser.parseReader(reader);
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
|
Log.d(TAG, logTag(session)+"response body: "+respJson);
|
||||||
if(req.respTypeToken!=null)
|
if(req.respTypeToken!=null)
|
||||||
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
||||||
else
|
else if(req.respClass!=null)
|
||||||
respObj=gson.fromJson(respJson, req.respClass);
|
respObj=gson.fromJson(respJson, req.respClass);
|
||||||
|
else
|
||||||
|
respObj=null;
|
||||||
}else{
|
}else{
|
||||||
if(req.respTypeToken!=null)
|
if(req.respTypeToken!=null)
|
||||||
respObj=gson.fromJson(reader, req.respTypeToken.getType());
|
respObj=gson.fromJson(reader, req.respTypeToken.getType());
|
||||||
else
|
else if(req.respClass!=null)
|
||||||
respObj=gson.fromJson(reader, req.respClass);
|
respObj=gson.fromJson(reader, req.respClass);
|
||||||
|
else
|
||||||
|
respObj=null;
|
||||||
}
|
}
|
||||||
}catch(JsonIOException|JsonSyntaxException x){
|
}catch(JsonIOException|JsonSyntaxException x){
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
Log.w(TAG, logTag(session)+response+" error parsing or reading body", x);
|
||||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -141,19 +153,19 @@ public class MastodonAPIController{
|
|||||||
req.validateAndPostprocessResponse(respObj, response);
|
req.validateAndPostprocessResponse(respObj, response);
|
||||||
}catch(IOException x){
|
}catch(IOException x){
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
Log.w(TAG, logTag(session)+response+" error post-processing or validating response", x);
|
||||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" parsed successfully: "+respObj);
|
Log.d(TAG, logTag(session)+response+" parsed successfully: "+respObj);
|
||||||
|
|
||||||
req.onSuccess(respObj);
|
req.onSuccess(respObj);
|
||||||
}else{
|
}else{
|
||||||
try{
|
try{
|
||||||
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
|
Log.w(TAG, logTag(session)+response+" received error: "+error);
|
||||||
if(error.has("details")){
|
if(error.has("details")){
|
||||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
||||||
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
||||||
@@ -188,7 +200,7 @@ public class MastodonAPIController{
|
|||||||
});
|
});
|
||||||
}catch(Exception x){
|
}catch(Exception x){
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
Log.w(TAG, logTag(session)+"error creating and sending http request", x);
|
||||||
req.onError(x.getLocalizedMessage(), 0, x);
|
req.onError(x.getLocalizedMessage(), 0, x);
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -201,4 +213,8 @@ public class MastodonAPIController{
|
|||||||
public static OkHttpClient getHttpClient(){
|
public static OkHttpClient getHttpClient(){
|
||||||
return httpClient;
|
return httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String logTag(AccountSession session){
|
||||||
|
return "["+(session==null ? "no-auth" : session.getID())+"] ";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
Token token;
|
Token token;
|
||||||
boolean canceled;
|
boolean canceled;
|
||||||
Map<String, String> headers;
|
Map<String, String> headers;
|
||||||
|
long timeout;
|
||||||
private ProgressDialog progressDialog;
|
private ProgressDialog progressDialog;
|
||||||
protected boolean removeUnsupportedItems;
|
protected boolean removeUnsupportedItems;
|
||||||
|
|
||||||
@@ -127,6 +128,10 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
headers.put(key, value);
|
headers.put(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void setTimeout(long timeout){
|
||||||
|
this.timeout=timeout;
|
||||||
|
}
|
||||||
|
|
||||||
protected String getPathPrefix(){
|
protected String getPathPrefix(){
|
||||||
return "/api/v1";
|
return "/api/v1";
|
||||||
}
|
}
|
||||||
@@ -149,6 +154,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public RequestBody getRequestBody() throws IOException{
|
public RequestBody getRequestBody() throws IOException{
|
||||||
|
if(requestBody instanceof RequestBody rb)
|
||||||
|
return rb;
|
||||||
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ public class PushSubscriptionManager{
|
|||||||
private String accountID;
|
private String accountID;
|
||||||
private PrivateKey privateKey;
|
private PrivateKey privateKey;
|
||||||
private PublicKey publicKey;
|
private PublicKey publicKey;
|
||||||
private PublicKey serverKey;
|
|
||||||
private byte[] authKey;
|
private byte[] authKey;
|
||||||
|
|
||||||
public PushSubscriptionManager(String accountID){
|
public PushSubscriptionManager(String accountID){
|
||||||
@@ -162,10 +161,6 @@ public class PushSubscriptionManager{
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(PushSubscription result){
|
public void onSuccess(PushSubscription result){
|
||||||
MastodonAPIController.runInBackground(()->{
|
MastodonAPIController.runInBackground(()->{
|
||||||
result.serverKey=result.serverKey.replace('/','_');
|
|
||||||
result.serverKey=result.serverKey.replace('+','-');
|
|
||||||
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
|
|
||||||
|
|
||||||
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
|
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
|
||||||
if(session==null)
|
if(session==null)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.joinmastodon.android.api;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
public abstract class ResultlessMastodonAPIRequest extends MastodonAPIRequest<Void>{
|
||||||
|
public ResultlessMastodonAPIRequest(HttpMethod method, String path){
|
||||||
|
super(method, path, (Class<Void>)null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.RequiredField;
|
||||||
|
import org.joinmastodon.android.model.BaseModel;
|
||||||
|
|
||||||
|
public class CheckInviteLink extends MastodonAPIRequest<CheckInviteLink.Response>{
|
||||||
|
public CheckInviteLink(String path){
|
||||||
|
super(HttpMethod.GET, path, Response.class);
|
||||||
|
addHeader("Accept", "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getPathPrefix(){
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Response extends BaseModel{
|
||||||
|
@RequiredField
|
||||||
|
public String inviteCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class GetAccountFeaturedHashtags extends MastodonAPIRequest<List<Hashtag>>{
|
||||||
|
public GetAccountFeaturedHashtags(String id){
|
||||||
|
super(HttpMethod.GET, "/accounts/"+id+"/featured_tags", new TypeToken<>(){});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.FollowList;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class GetAccountLists extends MastodonAPIRequest<List<FollowList>>{
|
||||||
|
public GetAccountLists(String id){
|
||||||
|
super(HttpMethod.GET, "/accounts/"+id+"/lists", new TypeToken<>(){});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.joinmastodon.android.api.requests.accounts;
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
@@ -10,7 +12,7 @@ import java.util.List;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
||||||
public GetAccountStatuses(String id, String maxID, String minID, int limit, @NonNull Filter filter){
|
public GetAccountStatuses(String id, String maxID, String minID, int limit, @NonNull Filter filter, String hashtag){
|
||||||
super(HttpMethod.GET, "/accounts/"+id+"/statuses", new TypeToken<>(){});
|
super(HttpMethod.GET, "/accounts/"+id+"/statuses", new TypeToken<>(){});
|
||||||
if(maxID!=null)
|
if(maxID!=null)
|
||||||
addQueryParameter("max_id", maxID);
|
addQueryParameter("max_id", maxID);
|
||||||
@@ -27,7 +29,10 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
|||||||
addQueryParameter("exclude_reblogs", "true");
|
addQueryParameter("exclude_reblogs", "true");
|
||||||
}
|
}
|
||||||
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
|
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
|
||||||
|
case PINNED -> addQueryParameter("pinned", "true");
|
||||||
}
|
}
|
||||||
|
if(!TextUtils.isEmpty(hashtag))
|
||||||
|
addQueryParameter("tagged", hashtag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Filter{
|
public enum Filter{
|
||||||
@@ -35,6 +40,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
|||||||
INCLUDE_REPLIES,
|
INCLUDE_REPLIES,
|
||||||
MEDIA,
|
MEDIA,
|
||||||
NO_REBLOGS,
|
NO_REBLOGS,
|
||||||
OWN_POSTS_AND_REPLIES
|
OWN_POSTS_AND_REPLIES,
|
||||||
|
PINNED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,23 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||||||
import org.joinmastodon.android.model.Token;
|
import org.joinmastodon.android.model.Token;
|
||||||
|
|
||||||
public class RegisterAccount extends MastodonAPIRequest<Token>{
|
public class RegisterAccount extends MastodonAPIRequest<Token>{
|
||||||
public RegisterAccount(String username, String email, String password, String locale, String reason){
|
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone, String inviteCode){
|
||||||
super(HttpMethod.POST, "/accounts", Token.class);
|
super(HttpMethod.POST, "/accounts", Token.class);
|
||||||
setRequestBody(new Body(username, email, password, locale, reason));
|
setRequestBody(new Body(username, email, password, locale, reason, timezone, inviteCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Body{
|
private static class Body{
|
||||||
public String username, email, password, locale, reason;
|
public String username, email, password, locale, reason, timeZone, inviteCode;
|
||||||
public boolean agreement=true;
|
public boolean agreement=true;
|
||||||
|
|
||||||
public Body(String username, String email, String password, String locale, String reason){
|
public Body(String username, String email, String password, String locale, String reason, String timeZone, String inviteCode){
|
||||||
this.username=username;
|
this.username=username;
|
||||||
this.email=email;
|
this.email=email;
|
||||||
this.password=password;
|
this.password=password;
|
||||||
this.locale=locale;
|
this.locale=locale;
|
||||||
this.reason=reason;
|
this.reason=reason;
|
||||||
|
this.timeZone=timeZone;
|
||||||
|
this.inviteCode=inviteCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SearchAccounts extends MastodonAPIRequest<List<Account>>{
|
||||||
|
public SearchAccounts(String q, int limit, int offset, boolean resolve, boolean following){
|
||||||
|
super(HttpMethod.GET, "/accounts/search", new TypeToken<>(){});
|
||||||
|
addQueryParameter("q", q);
|
||||||
|
if(limit>0)
|
||||||
|
addQueryParameter("limit", limit+"");
|
||||||
|
if(offset>0)
|
||||||
|
addQueryParameter("offset", offset+"");
|
||||||
|
if(resolve)
|
||||||
|
addQueryParameter("resolve", "true");
|
||||||
|
if(following)
|
||||||
|
addQueryParameter("following", "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||||||
private Uri avatar, cover;
|
private Uri avatar, cover;
|
||||||
private File avatarFile, coverFile;
|
private File avatarFile, coverFile;
|
||||||
private List<AccountField> fields;
|
private List<AccountField> fields;
|
||||||
|
private Boolean discoverable, indexable;
|
||||||
|
|
||||||
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
|
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
|
||||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||||
@@ -41,6 +42,12 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||||||
this.fields=fields;
|
this.fields=fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UpdateAccountCredentials setDiscoverableIndexable(boolean discoverable, boolean indexable){
|
||||||
|
this.discoverable=discoverable;
|
||||||
|
this.indexable=indexable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RequestBody getRequestBody() throws IOException{
|
public RequestBody getRequestBody() throws IOException{
|
||||||
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
||||||
@@ -58,6 +65,7 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||||||
}else if(coverFile!=null){
|
}else if(coverFile!=null){
|
||||||
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
||||||
}
|
}
|
||||||
|
if(fields!=null){
|
||||||
if(fields.isEmpty()){
|
if(fields.isEmpty()){
|
||||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||||
}else{
|
}else{
|
||||||
@@ -67,6 +75,11 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if(discoverable!=null)
|
||||||
|
bldr.addFormDataPart("discoverable", discoverable.toString());
|
||||||
|
if(indexable!=null)
|
||||||
|
bldr.addFormDataPart("indexable", indexable.toString());
|
||||||
|
|
||||||
return bldr.build();
|
return bldr.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Preferences;
|
||||||
|
import org.joinmastodon.android.model.StatusPrivacy;
|
||||||
|
|
||||||
|
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
|
||||||
|
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable, Boolean indexable){
|
||||||
|
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||||
|
setRequestBody(new Request(locked, discoverable, indexable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Request{
|
||||||
|
public Boolean locked, discoverable, indexable;
|
||||||
|
public RequestSource source;
|
||||||
|
|
||||||
|
public Request(Boolean locked, Boolean discoverable, Boolean indexable, RequestSource source){
|
||||||
|
this.locked=locked;
|
||||||
|
this.discoverable=discoverable;
|
||||||
|
this.indexable=indexable;
|
||||||
|
this.source=source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RequestSource{
|
||||||
|
public StatusPrivacy privacy;
|
||||||
|
public String language;
|
||||||
|
|
||||||
|
public RequestSource(StatusPrivacy privacy, String language){
|
||||||
|
this.privacy=privacy;
|
||||||
|
this.language=language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.catalog;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class GetCatalogDefaultInstances extends MastodonAPIRequest<List<CatalogDefaultInstance>>{
|
||||||
|
public GetCatalogDefaultInstances(){
|
||||||
|
super(HttpMethod.GET, null, new TypeToken<>(){});
|
||||||
|
setTimeout(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getURL(){
|
||||||
|
return Uri.parse("https://api.joinmastodon.org/default-servers");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.filters;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Filter;
|
||||||
|
import org.joinmastodon.android.model.FilterAction;
|
||||||
|
import org.joinmastodon.android.model.FilterContext;
|
||||||
|
import org.joinmastodon.android.model.FilterKeyword;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class CreateFilter extends MastodonAPIRequest<Filter>{
|
||||||
|
public CreateFilter(String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words){
|
||||||
|
super(HttpMethod.POST, "/filters", Filter.class);
|
||||||
|
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, words.stream().map(w->new KeywordAttribute(null, null, w.keyword, w.wholeWord)).collect(Collectors.toList())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getPathPrefix(){
|
||||||
|
return "/api/v2";
|
||||||
|
}
|
||||||
|
}
|
||||||