Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66558aaf5d | ||
|
|
1b272cb019 | ||
|
|
50a8754277 | ||
|
|
b619cd31a4 | ||
|
|
45284affb9 | ||
|
|
78ceef7a17 | ||
|
|
530c7f3caf | ||
|
|
94a6f7fe01 | ||
|
|
b13d60e8d9 | ||
|
|
87ed4be31b | ||
|
|
71e760840d | ||
|
|
49f9460c68 | ||
|
|
f074e868fc | ||
|
|
37bf22bdf5 | ||
|
|
4db0a67900 | ||
|
|
fa9c2b8234 | ||
|
|
d24991709a | ||
|
|
3940d65eda | ||
|
|
61a73eea87 | ||
|
|
bc3fbcb758 | ||
|
|
e5d5b9609e | ||
|
|
f0ea636ee2 | ||
|
|
72dad4cb19 | ||
|
|
5089126f64 | ||
|
|
edaa06aa68 | ||
|
|
70edcfe911 | ||
|
|
a34a405f60 | ||
|
|
a034888866 | ||
|
|
f60674f2e8 | ||
|
|
8647341aba | ||
|
|
3ea30dc57d | ||
|
|
3e6cd068ad | ||
|
|
706db80731 | ||
|
|
c9e4aeb444 | ||
|
|
817df9c5a5 | ||
|
|
8ad1f07631 | ||
|
|
f61cf5a137 | ||
|
|
7e1dfffb57 | ||
|
|
ef910c4f6b | ||
|
|
f30a3d6077 | ||
|
|
1f7a48bc45 | ||
|
|
d444eb5ae3 | ||
|
|
b99041541b | ||
|
|
ea0da14cdd | ||
|
|
f8d8f15b9c | ||
|
|
5eab1d5012 | ||
|
|
81b888ab6f | ||
|
|
3cbf399328 | ||
|
|
3665c73e7a | ||
|
|
0eff82a10a | ||
|
|
b2efc82aa3 | ||
|
|
5273f42b75 | ||
|
|
9bce215588 | ||
|
|
8cad725d16 | ||
|
|
2050848498 | ||
|
|
10dd2b9f59 | ||
|
|
eae02be45a | ||
|
|
b1a2192e77 | ||
|
|
d24c2d72fb | ||
|
|
8652040a01 | ||
|
|
beb6488ff9 | ||
|
|
691ed64e17 | ||
|
|
483b2e59f4 | ||
|
|
6c2d3aa7f3 | ||
|
|
0724d7f387 | ||
|
|
f52d0e94ab | ||
|
|
9c9d682695 | ||
|
|
fbb3ebb3dd | ||
|
|
6717715a7e | ||
|
|
a814c8f2cd | ||
|
|
278abf074e | ||
|
|
09d0e51160 | ||
|
|
7da44e3694 | ||
|
|
f18a8017fd | ||
|
|
0b9434d658 | ||
|
|
611ac35ad4 | ||
|
|
50e3b72bd2 | ||
|
|
ee1b557f1f | ||
|
|
6f7f0356cc | ||
|
|
b38d426d37 | ||
|
|
705811697e | ||
|
|
1fcfd28cd7 | ||
|
|
6438787134 | ||
|
|
199eb7db9a | ||
|
|
7b3e84826a | ||
|
|
e3d3263eb9 | ||
|
|
7fa9c6fd78 | ||
|
|
9c844ac9ca | ||
|
|
a44e9a847f | ||
|
|
b6da3d1ab6 | ||
|
|
3fdff255c8 | ||
|
|
e510870672 | ||
|
|
46078ca5e3 | ||
|
|
dee309370d | ||
|
|
af177b4c6d | ||
|
|
44174be084 | ||
|
|
bf22f28f75 | ||
|
|
a11a2ae397 | ||
|
|
99637b1a36 | ||
|
|
40b67b3f10 | ||
|
|
95c90808eb | ||
|
|
64c2aa0d12 | ||
|
|
7856e5523e | ||
|
|
3e59d4fb88 | ||
|
|
9ecdb6da75 | ||
|
|
31149f9d1e | ||
|
|
e28ab66159 | ||
|
|
d85b87e45d | ||
|
|
4b44fd5905 | ||
|
|
3ac26f1387 | ||
|
|
bceac49697 | ||
|
|
8866f306d6 | ||
|
|
e6f3ecbc7e | ||
|
|
aceb489374 | ||
|
|
9f6ac297ce | ||
|
|
f70839d406 | ||
|
|
855427376a | ||
|
|
38c1337c39 | ||
|
|
7da1540913 | ||
|
|
6dbe06a941 | ||
|
|
a27ba17521 | ||
|
|
354bd3468e | ||
|
|
0b2aa3edab | ||
|
|
f3245b3c20 | ||
|
|
91b4f0e75d | ||
|
|
cba7cc879c | ||
|
|
d2c5d00907 | ||
|
|
afb9e36bca | ||
|
|
22583d08e7 | ||
|
|
5c1b0caac5 | ||
|
|
2045521a97 | ||
|
|
95e4ba0f93 | ||
|
|
41c2ae9289 | ||
|
|
aa1a7a1c38 | ||
|
|
b75898f0c1 | ||
|
|
2e299ebef3 | ||
|
|
21d5e097f6 | ||
|
|
6f8b7244c4 | ||
|
|
d305ae7fa3 | ||
|
|
8059291f71 | ||
|
|
b2dff44df1 | ||
|
|
ffe160a6e7 | ||
|
|
248d62ebf7 | ||
|
|
575bf6905f | ||
|
|
0c342fe733 | ||
|
|
e67fa4ac75 | ||
|
|
b6309ac9a9 | ||
|
|
33b2b02267 | ||
|
|
6cfc723dfc | ||
|
|
5934465197 | ||
|
|
30d4674389 | ||
|
|
54ab886ef4 | ||
|
|
68f3d73f27 | ||
|
|
d3c9487ba8 | ||
|
|
73ec2c5fa2 | ||
|
|
f457473399 | ||
|
|
4f5555a2cd | ||
|
|
a5eabd5cec | ||
|
|
b085ca4d3b | ||
|
|
6766b10f29 | ||
|
|
ec887d4a16 | ||
|
|
a66a6953fc | ||
|
|
155f5b0b81 | ||
|
|
5fb6ea7799 | ||
|
|
3e39e20788 | ||
|
|
f5f33ed0e8 | ||
|
|
41524ceb4c | ||
|
|
a3a081b17d | ||
|
|
d07255dd3b | ||
|
|
8092fb8c02 | ||
|
|
542b469bda | ||
|
|
f102a6dd79 | ||
|
|
ccef9eff91 | ||
|
|
cb2d0d7edd | ||
|
|
68b540a635 | ||
|
|
541fffbe29 | ||
|
|
c11beffb5a | ||
|
|
bf5559fa8c | ||
|
|
8a4e232a3b | ||
|
|
b2b574a6ed | ||
|
|
bf18356bdf | ||
|
|
0961e859c5 | ||
|
|
785f22f039 | ||
|
|
e1fe4e4994 | ||
|
|
8a5e40f91d | ||
|
|
2f938d5f6a | ||
|
|
68c86390e2 | ||
|
|
b576942bd4 | ||
|
|
cca75dca9b | ||
|
|
6e5e9ffc3c | ||
|
|
38df8bc316 | ||
|
|
a498e48470 | ||
|
|
5ea80d9bdd | ||
|
|
51565edf35 | ||
|
|
c11be1123e | ||
|
|
89b1edc565 | ||
|
|
51b1a9517c | ||
|
|
6ae82b73c0 | ||
|
|
7e6cd9d996 | ||
|
|
dd5886f6d8 | ||
|
|
c5ba588cc3 | ||
|
|
b8b9e61378 | ||
|
|
31ca07feb0 | ||
|
|
3fd4a48208 | ||
|
|
ba36426905 | ||
|
|
ffdaca06a6 | ||
|
|
b682fa0c43 | ||
|
|
f9f9b59415 | ||
|
|
46a911cef1 | ||
|
|
6539de70f6 | ||
|
|
7a5389fdd1 | ||
|
|
76974e43d7 | ||
|
|
97c259a7b6 | ||
|
|
1e3e71c93b | ||
|
|
16d85fd93d | ||
|
|
085829ea08 | ||
|
|
015488b4e4 | ||
|
|
46ade526c9 | ||
|
|
f11eee1f3c | ||
|
|
6241a66d2d | ||
|
|
6cba45c140 | ||
|
|
dfcdf04d1c | ||
|
|
a8864d36ad | ||
|
|
6e058b4f93 | ||
|
|
7d1b6b7ee0 | ||
|
|
78b8ba345c | ||
|
|
5dcee37fa6 | ||
|
|
bae5c77574 | ||
|
|
51a45a6f56 | ||
|
|
32a480e21d | ||
|
|
c7c731ba5f | ||
|
|
bbf09490de | ||
|
|
e9d2b6af63 | ||
|
|
c0f3658e2f | ||
|
|
7a2a21cc20 | ||
|
|
b31bbe8002 | ||
|
|
d53e7640fd | ||
|
|
26a57c0874 | ||
|
|
d28fe6c7ff | ||
|
|
57a5b70e77 | ||
|
|
3a335d2839 | ||
|
|
2b074ae6f7 | ||
|
|
f725f50f83 | ||
|
|
623a74d812 | ||
|
|
916bc74779 | ||
|
|
d35641f517 | ||
|
|
77b2e3ad24 | ||
|
|
a8e67039c2 | ||
|
|
7b8dc691e3 | ||
|
|
1efc26d98f | ||
|
|
bce3b085db | ||
|
|
8c23e503bc | ||
|
|
3f165f5f80 | ||
|
|
024d54780e | ||
|
|
0b420d23c1 | ||
|
|
a0d95338d6 | ||
|
|
fee0c14800 | ||
|
|
e743dd3cf5 | ||
|
|
1a1af661aa | ||
|
|
2e0c8383c5 | ||
|
|
6a3699d2ea | ||
|
|
2c9942d069 | ||
|
|
ace62606b9 | ||
|
|
ab97ea91f0 | ||
|
|
d2b43d5ca9 | ||
|
|
210f90766d | ||
|
|
61d2386401 | ||
|
|
749f0a9d11 | ||
|
|
2e5cd4be96 | ||
|
|
a6848d5e4e | ||
|
|
984c4d81ad | ||
|
|
bb490815c2 | ||
|
|
aa2315612d | ||
|
|
e03be127aa | ||
|
|
3f1e46e723 | ||
|
|
f42bd111ee | ||
|
|
b98be37baf | ||
|
|
c7bb3485e5 | ||
|
|
3eb0e92961 | ||
|
|
01872b0981 | ||
|
|
7f741f9517 | ||
|
|
15e99b27d5 | ||
|
|
251537026e | ||
|
|
aa3621bd7f | ||
|
|
55c8f4402d | ||
|
|
838588a2ea | ||
|
|
0c93e20971 | ||
|
|
c220bffdbc | ||
|
|
72cccedc60 | ||
|
|
ddbadcf671 | ||
|
|
184a652a21 | ||
|
|
c13f9e4084 | ||
|
|
0acb5fc51c | ||
|
|
3a789caf53 | ||
|
|
471e1bfefe | ||
|
|
8caa1386c9 | ||
|
|
8dc36fd58e | ||
|
|
5ee3a5618b | ||
|
|
3e2cf54d88 | ||
|
|
2c1ff372ef | ||
|
|
35b53f1615 | ||
|
|
c525e048ae | ||
|
|
246c434bce | ||
|
|
c368d78516 | ||
|
|
6cef736aa5 | ||
|
|
509579cff0 | ||
|
|
5808646932 | ||
|
|
d0173e0b8c | ||
|
|
d22f8477bb | ||
|
|
05bd0a125a | ||
|
|
ebecc5af19 | ||
|
|
7ed8caa7df | ||
|
|
4ff7a02cf4 | ||
|
|
646771b573 | ||
|
|
b1d01b49da | ||
|
|
dbc059ac33 | ||
|
|
8fd00c61f9 | ||
|
|
22f8535000 | ||
|
|
5f1e4cb34d | ||
|
|
544cb3c9c7 | ||
|
|
199d05b32d | ||
|
|
e8ebde087d | ||
|
|
0d1a238dc3 | ||
|
|
fe98599f6e | ||
|
|
914ca80d4c | ||
|
|
38ecc6c5ed | ||
|
|
c042226428 | ||
|
|
40275ce377 | ||
|
|
7d01bf4a8f | ||
|
|
62800e0d14 | ||
|
|
51b3427289 | ||
|
|
aeb5a16333 | ||
|
|
f0871041e9 | ||
|
|
e67f8e1cca | ||
|
|
bef0e36ecb | ||
|
|
ef2fa5b3cc | ||
|
|
e3bee07ec4 | ||
|
|
1455be36a2 | ||
|
|
b57bf89f3c | ||
|
|
106a9daa1c | ||
|
|
3d7726e93a | ||
|
|
b1202af131 | ||
|
|
b1dc0ec4a5 | ||
|
|
bd5396dec3 | ||
|
|
7de9dced00 | ||
|
|
38990629e1 | ||
|
|
6ca814c3d9 | ||
|
|
841085c585 | ||
|
|
5dc70e48af | ||
|
|
fbb1141e46 | ||
|
|
7b757600a8 | ||
|
|
e4435e3121 | ||
|
|
576b698cd6 | ||
|
|
c03ed8027e | ||
|
|
6dbb4970dc | ||
|
|
d1383116b7 | ||
|
|
62528cbd46 | ||
|
|
a1fee26f98 | ||
|
|
7f6056ecc2 | ||
|
|
4706196f95 | ||
|
|
a4fde9154f | ||
|
|
5474536802 | ||
|
|
73c71e5c4b | ||
|
|
3e57ad3f75 | ||
|
|
efde9ea8f2 | ||
|
|
0cf7a53a2f | ||
|
|
80b33a8058 | ||
|
|
8430b28fda | ||
|
|
6292cabbb7 | ||
|
|
db804107ef | ||
|
|
214f541d02 | ||
|
|
2a04e43d51 | ||
|
|
1d93e439ce | ||
|
|
be4a62470e | ||
|
|
b2c3c365ff | ||
|
|
e605090dfe | ||
|
|
0addf44aa5 | ||
|
|
ed7c41589c | ||
|
|
bd2ee36aa6 | ||
|
|
a596520964 | ||
|
|
84cad99f20 | ||
|
|
303b721c8d | ||
|
|
ca74962010 | ||
|
|
ef6f687dce | ||
|
|
20d2d868bc | ||
|
|
51d4ba170c | ||
|
|
4b5b4b646b | ||
|
|
45e3c0e2db | ||
|
|
4224e564a5 | ||
|
|
b6a80a6bc6 | ||
|
|
565fe66958 | ||
|
|
00c91abbf0 | ||
|
|
fd3672b40c | ||
|
|
41e863f0bc | ||
|
|
29d9ab78be | ||
|
|
02ecd6e86b | ||
|
|
99cc479e09 | ||
|
|
bc6908b453 | ||
|
|
b75a2508da | ||
|
|
14655fa683 | ||
|
|
93b43a58db | ||
|
|
b7b8553c23 | ||
|
|
d274f16b12 | ||
|
|
e9305be96f | ||
|
|
551d706b7f | ||
|
|
6368bc35ef | ||
|
|
ef44f0e9f7 | ||
|
|
029b24969c | ||
|
|
3c5ead03e2 | ||
|
|
85a5f2042d | ||
|
|
35018cdad2 | ||
|
|
94adebd220 | ||
|
|
7b365afdcd | ||
|
|
9391fdc868 | ||
|
|
f8c3aa1172 | ||
|
|
9550541aaa | ||
|
|
bf0785148e | ||
|
|
cfd5a23b3d | ||
|
|
f6e4f0ab78 | ||
|
|
e30d5b02b4 | ||
|
|
7c62c9baf1 | ||
|
|
c037dfb533 | ||
|
|
15dc794093 | ||
|
|
0c6884481e | ||
|
|
0e0128b8b2 | ||
|
|
669108744d | ||
|
|
18cb5845a6 | ||
|
|
728a068a7b | ||
|
|
e6dd86719f | ||
|
|
8f932de25d | ||
|
|
93c9408419 | ||
|
|
a567936a7a | ||
|
|
ce3e7f7f81 | ||
|
|
6aace451df | ||
|
|
323859bed2 | ||
|
|
c1e199c188 | ||
|
|
817a027c55 | ||
|
|
73db5b0a9e | ||
|
|
73ab8213fd | ||
|
|
7789e20408 | ||
|
|
4aceb4244e | ||
|
|
843be6f760 | ||
|
|
3505e1ecf8 | ||
|
|
fc7986b654 | ||
|
|
258f61705a | ||
|
|
2e89f36b8f | ||
|
|
e288629ccb | ||
|
|
47a4acc6b7 | ||
|
|
39eb2a9c01 | ||
|
|
3582d6dd0a | ||
|
|
d053c1f79b | ||
|
|
bd375abe5c | ||
|
|
1c48b27046 | ||
|
|
3281f13fd1 | ||
|
|
af61146925 | ||
|
|
c423bee933 | ||
|
|
efe9bd68e7 | ||
|
|
fe15e06da7 | ||
|
|
aadaa2637e | ||
|
|
86fe039a99 | ||
|
|
5a414331fb | ||
|
|
0093dcb697 | ||
|
|
7d8e04f3d3 | ||
|
|
dbad949d28 | ||
|
|
d5b90d53f6 | ||
|
|
f5fae0ca73 | ||
|
|
8ce329afeb | ||
|
|
e958c342ea | ||
|
|
cb281783a5 | ||
|
|
5a3dd7eb57 | ||
|
|
b8c91011e5 | ||
|
|
acdc328cdc | ||
|
|
9a055c829b | ||
|
|
736fb18f34 | ||
|
|
6887c67642 | ||
|
|
bc571c1f42 | ||
|
|
bb6efbb43d | ||
|
|
1e495c483f | ||
|
|
62525a6075 | ||
|
|
508fbb3679 | ||
|
|
bac586faf7 | ||
|
|
8fe5a7a78e | ||
|
|
12d52c0798 | ||
|
|
d29f100bbe | ||
|
|
111908d84c | ||
|
|
6f79f3b232 | ||
|
|
853908d04f | ||
|
|
cb13ee6869 | ||
|
|
81e9d07802 | ||
|
|
66a40447a3 | ||
|
|
825b2f75ba | ||
|
|
81a6d84126 | ||
|
|
55d0c6905c | ||
|
|
50169b11dd | ||
|
|
718767919e | ||
|
|
23c295e82e | ||
|
|
5e4f8b67fb | ||
|
|
e77abf814f | ||
|
|
484ec75afd | ||
|
|
bddeb135a4 | ||
|
|
459a309dbe | ||
|
|
7bf77f2157 | ||
|
|
0a5a9f8a51 | ||
|
|
523d6dd5b4 | ||
|
|
3aca226373 | ||
|
|
2422f33469 | ||
|
|
06b60a25d0 | ||
|
|
5d51914842 | ||
|
|
61e8784dbb | ||
|
|
1d5265a0bd | ||
|
|
54c27ba534 | ||
|
|
8b24fa3326 | ||
|
|
8a3f0dd809 | ||
|
|
6821d1f379 | ||
|
|
0c823faf5b | ||
|
|
7caadb8d54 | ||
|
|
a2066c73b1 | ||
|
|
42198912a4 | ||
|
|
51978b7bf9 | ||
|
|
7011c50040 | ||
|
|
bffd099935 | ||
|
|
f109e44271 | ||
|
|
89ea48ffd9 | ||
|
|
bcd764308d | ||
|
|
88b232fd90 | ||
|
|
610a8b7e6c | ||
|
|
e8a75a102a | ||
|
|
4fd229cb47 | ||
|
|
81798596c8 | ||
|
|
489c98ed01 | ||
|
|
8ad8df921a | ||
|
|
a04723bdfe | ||
|
|
daf407be5c | ||
|
|
886314f153 | ||
|
|
06c0a42186 | ||
|
|
383a69ad31 | ||
|
|
2aa6c4eb37 | ||
|
|
98f8b6b826 | ||
|
|
e2fb1a0479 | ||
|
|
8d777e9046 | ||
|
|
c81062992a | ||
|
|
1956a3066a | ||
|
|
98620999f6 | ||
|
|
6b1d2f69b2 | ||
|
|
c5ab58c49f | ||
|
|
d45dd6ab19 | ||
|
|
511f6931b3 | ||
|
|
60e43fdf45 | ||
|
|
870a28b64f | ||
|
|
aa6db44911 | ||
|
|
26905f0742 | ||
|
|
ff41a035d1 | ||
|
|
28574c4737 | ||
|
|
2895da5ce8 | ||
|
|
d179e598c5 | ||
|
|
d948f94642 | ||
|
|
4d62af638d | ||
|
|
d4c41cd362 | ||
|
|
b7b376123e | ||
|
|
62c096ee3c | ||
|
|
fd4837ed4b | ||
|
|
fcd1467d93 | ||
|
|
e861c81a60 | ||
|
|
41815e9bf2 | ||
|
|
9a24a4b934 | ||
|
|
075cbf8516 | ||
|
|
191e8c6ef0 | ||
|
|
4fc83c7acc | ||
|
|
e31127d595 | ||
|
|
a71c50e836 | ||
|
|
7d25c311a0 | ||
|
|
c4ca844851 | ||
|
|
4678e389d3 | ||
|
|
468036440e | ||
|
|
10be67bd24 | ||
|
|
62ed89eb54 | ||
|
|
0de2d07f5f | ||
|
|
ea3ec63943 | ||
|
|
0a19b06a9d | ||
|
|
5e55df428d | ||
|
|
6c4ce7361d | ||
|
|
01ad482f61 | ||
|
|
24cfabe841 | ||
|
|
1a9dfd4b16 | ||
|
|
ff36d827a7 | ||
|
|
1310043b67 | ||
|
|
01488c7605 | ||
|
|
99baba9cba | ||
|
|
7f4aebdb73 | ||
|
|
4696c38514 | ||
|
|
ce1a9be259 | ||
|
|
5397d7a085 | ||
|
|
a3814cb6a1 | ||
|
|
f571558813 | ||
|
|
4e29b00eac | ||
|
|
9487bbd680 | ||
|
|
a83749f160 | ||
|
|
e3fa431b06 | ||
|
|
d646ad0921 | ||
|
|
a629ec0067 | ||
|
|
067f568d1c | ||
|
|
5dd7fd23be | ||
|
|
961433eebe | ||
|
|
f83c50ef40 | ||
|
|
85ff28c2db | ||
|
|
47a7aeb224 | ||
|
|
f243e80997 | ||
|
|
84c7617063 | ||
|
|
982fed28e0 | ||
|
|
52d9292837 | ||
|
|
0db2fabef4 | ||
|
|
52531b46f0 | ||
|
|
7408e67789 | ||
|
|
9612d86545 | ||
|
|
fed6475003 | ||
|
|
aed463219a | ||
|
|
e6574ede9d | ||
|
|
c89bc7c42a | ||
|
|
29ba041279 | ||
|
|
ebde0efa54 | ||
|
|
c58f6249f1 | ||
|
|
40f98870d8 | ||
|
|
04f414606e | ||
|
|
250a86b07e | ||
|
|
490a592445 | ||
|
|
225b2f5412 | ||
|
|
5b009fc416 | ||
|
|
6211b43ea5 | ||
|
|
91e2da9f48 | ||
|
|
4a699fea63 | ||
|
|
3b104c72ab | ||
|
|
157341b14a | ||
|
|
327c90c55a | ||
|
|
4e364fa704 | ||
|
|
079178a02b | ||
|
|
1ceb6e4aad | ||
|
|
1e955fcaf5 | ||
|
|
c353b25664 | ||
|
|
8a9d832813 | ||
|
|
882aa159ef | ||
|
|
35d0a5102c | ||
|
|
28b77ddbb5 | ||
|
|
ee2afe042a | ||
|
|
80eb19a118 | ||
|
|
e17ed72439 | ||
|
|
670b84bf2a | ||
|
|
6af8795ca2 | ||
|
|
2423844b8d | ||
|
|
b0a464ab0a | ||
|
|
a221864770 | ||
|
|
4ed008976e | ||
|
|
8c863c92a3 | ||
|
|
af68af14e7 | ||
|
|
2c136bc222 | ||
|
|
d7928e36d4 | ||
|
|
2db387892a | ||
|
|
57c24c4933 | ||
|
|
a60ec4d645 | ||
|
|
7849b450e8 | ||
|
|
bf679a9c4e | ||
|
|
b83f70afbe | ||
|
|
5e4cc29087 | ||
|
|
c185be0e5c | ||
|
|
817a5b102f | ||
|
|
2d3f7d8853 | ||
|
|
f2276cdde3 | ||
|
|
e5c9281c51 | ||
|
|
b4293c69b4 | ||
|
|
b064cb857f | ||
|
|
352dca246e | ||
|
|
42ea6516c7 | ||
|
|
06ce70131a | ||
|
|
32009bffd5 | ||
|
|
80400634e5 | ||
|
|
58b63e899a | ||
|
|
131b362fb4 | ||
|
|
c8c25e9d52 | ||
|
|
2fae678e2e | ||
|
|
06f97b68b7 | ||
|
|
a71cd1d183 | ||
|
|
508ccb8079 | ||
|
|
477683db4f | ||
|
|
11b16b8edd | ||
|
|
a5a326d0da | ||
|
|
19df5897f4 | ||
|
|
65918143ad | ||
|
|
bc22de91a1 | ||
|
|
a1284f9111 | ||
|
|
f88f2d732a | ||
|
|
e9f9922750 | ||
|
|
ce6c88a4db | ||
|
|
edb82bed8a | ||
|
|
4b4c3b2bfa | ||
|
|
2a57366029 | ||
|
|
510391593c | ||
|
|
a4aa57a6a4 | ||
|
|
dbcc84f65b | ||
|
|
8342fd2cbe | ||
|
|
0cf5eeaebd | ||
|
|
e7e7ac415c | ||
|
|
0bfd224bdb | ||
|
|
d830ba4a7d | ||
|
|
4969064f17 | ||
|
|
fefc23d2d9 | ||
|
|
1f70d680dc | ||
|
|
4d6bfe8008 | ||
|
|
67f910e278 | ||
|
|
ec897c9467 | ||
|
|
816161eda7 | ||
|
|
6a741a46b8 | ||
|
|
e736ba614f | ||
|
|
e863598fe7 | ||
|
|
30727d95a3 | ||
|
|
74b250ccda | ||
|
|
c018363dfa | ||
|
|
4dbc91ec5a | ||
|
|
7aebbd0540 | ||
|
|
82bb0c14c4 | ||
|
|
a6a6c8d48b | ||
|
|
1814657f2d | ||
|
|
c5524035d6 | ||
|
|
66be656f11 | ||
|
|
ec067e4af0 | ||
|
|
643d9350a5 | ||
|
|
1230baad84 | ||
|
|
55b375e2b0 | ||
|
|
d573c6a721 | ||
|
|
81e2b6833c | ||
|
|
12e4aa7749 | ||
|
|
1d29c94e19 | ||
|
|
80c9d2b827 | ||
|
|
ae72b7ca78 | ||
|
|
8b5596d682 | ||
|
|
f59a138f3e | ||
|
|
d8dc6bf8c4 | ||
|
|
2e35cb4f86 | ||
|
|
3a862cc89b | ||
|
|
40bf34f2e2 | ||
|
|
0458a46039 | ||
|
|
c1f93e0f80 | ||
|
|
9fd0a7962b | ||
|
|
850832083b | ||
|
|
e49c7ac89b | ||
|
|
ad96ccaab7 | ||
|
|
d4baabaf67 | ||
|
|
e6bd441d44 | ||
|
|
7485b77519 | ||
|
|
44b380230a | ||
|
|
557c329607 | ||
|
|
75a734596b | ||
|
|
2940da47c9 | ||
|
|
9ee08258cb | ||
|
|
e07d4aff8c | ||
|
|
3cdc9b50fc | ||
|
|
9c053a8e53 | ||
|
|
f0bc4fa38c | ||
|
|
1e6b350952 | ||
|
|
042eb4deb8 | ||
|
|
bf45c55a9c | ||
|
|
6d7e62c3b8 | ||
|
|
0fb38d4aa8 | ||
|
|
9d8780feff | ||
|
|
aee0177afc | ||
|
|
9fbca97566 | ||
|
|
7bbb492c67 | ||
|
|
208cf3a137 | ||
|
|
4caa48e0e2 | ||
|
|
290ebce885 | ||
|
|
856ee6fc07 | ||
|
|
dca5830e77 | ||
|
|
53f56c1546 | ||
|
|
033a625d3b | ||
|
|
20d0197847 | ||
|
|
3ece264976 | ||
|
|
723407765b | ||
|
|
baa97fa947 | ||
|
|
bb10308d7f | ||
|
|
47a177887f | ||
|
|
287736f60c | ||
|
|
e37c9fd0eb | ||
|
|
e756fd4549 | ||
|
|
8f468d58a4 | ||
|
|
cbaea4f3f8 | ||
|
|
e5828506ca | ||
|
|
45aaa00850 | ||
|
|
a1a4490c64 | ||
|
|
9e4b29f464 | ||
|
|
f09101b32a | ||
|
|
dc748d8609 | ||
|
|
987b839a66 | ||
|
|
0b0f5fa08f | ||
|
|
ed02fd897a | ||
|
|
aace8c10c1 | ||
|
|
84c4672191 | ||
|
|
6cd375d293 | ||
|
|
d095abb91f | ||
|
|
65609b0207 | ||
|
|
b89d28a63b | ||
|
|
bb71a05a44 | ||
|
|
d1873159fd | ||
|
|
7effa7bfa4 | ||
|
|
f3f90b9db9 | ||
|
|
283abaeb31 | ||
|
|
66a72b53eb | ||
|
|
43cf285e91 | ||
|
|
47048e9d79 | ||
|
|
1c9423208f | ||
|
|
918efccbfa | ||
|
|
5df9420aa1 | ||
|
|
6ccdcdd862 | ||
|
|
06dc7e9398 | ||
|
|
aa3e0ee054 | ||
|
|
ccedf46139 | ||
|
|
b360b43961 | ||
|
|
4dda5210d6 | ||
|
|
45c35ea0e7 | ||
|
|
5cf81fafba | ||
|
|
58c2b69ff4 | ||
|
|
0b096152f2 | ||
|
|
3af3299821 | ||
|
|
3af6ba2c65 | ||
|
|
0257c205af | ||
|
|
e64f7dd397 | ||
|
|
a72566511b | ||
|
|
b1b7628544 | ||
|
|
7b456c088e | ||
|
|
6116d5a5bc | ||
|
|
f11b7613f0 | ||
|
|
006b834d40 | ||
|
|
36b400eebf | ||
|
|
c1131b1c22 | ||
|
|
6b96c0e0d4 | ||
|
|
ddb5b8914c | ||
|
|
7cc0cd6308 | ||
|
|
9eea33b0c5 | ||
|
|
889bc6c9b4 | ||
|
|
3af89c8779 | ||
|
|
0339b903ac | ||
|
|
380e7ef00e | ||
|
|
2a83e8fcda | ||
|
|
26bbc70010 | ||
|
|
d62e659cf9 | ||
|
|
715ec6595d | ||
|
|
21da8b4c64 | ||
|
|
8978350a2b | ||
|
|
9a3cf9c3d1 | ||
|
|
3b586727cd | ||
|
|
d7c7fc5a5d | ||
|
|
03ee79127e | ||
|
|
1c1a7366eb | ||
|
|
d93e3b795a | ||
|
|
1f6618b9ea | ||
|
|
22fcde08ce | ||
|
|
e5b5aef2d9 | ||
|
|
ea72833e4b | ||
|
|
a2467f439b | ||
|
|
f15e601cd9 | ||
|
|
6fe7ca4d59 | ||
|
|
fa929d56e1 | ||
|
|
44a605fbbf | ||
|
|
da2dd06866 | ||
|
|
a23880735a | ||
|
|
88cfa5f1a5 | ||
|
|
0226f52501 | ||
|
|
c01a869a0e | ||
|
|
9f7d27a617 | ||
|
|
6d2b445218 | ||
|
|
664c7b5d1a | ||
|
|
54b2fa5fe6 | ||
|
|
eaa2bc37af | ||
|
|
45b313ec2d | ||
|
|
15e86003d9 | ||
|
|
2ace136796 | ||
|
|
a4478994e2 | ||
|
|
936d2fc34f | ||
|
|
ab25fbaf89 | ||
|
|
29c582d117 | ||
|
|
cca89e5dd3 | ||
|
|
de191ca264 | ||
|
|
246e7b8b20 | ||
|
|
8e8348774d | ||
|
|
f5aae178af | ||
|
|
8926eef694 | ||
|
|
6942e278ba | ||
|
|
ddb1950416 | ||
|
|
dbf07e54de | ||
|
|
36c1ed195a | ||
|
|
1aca77fc27 | ||
|
|
ae1c484207 | ||
|
|
999dd6ce16 | ||
|
|
addfa238b8 | ||
|
|
1f73a17208 | ||
|
|
6a2fe4857d | ||
|
|
446556a97d | ||
|
|
d96313ab9e | ||
|
|
dcaef26b17 | ||
|
|
795d03d926 | ||
|
|
e3d1121d5b | ||
|
|
0f241a2aa5 | ||
|
|
7211812071 | ||
|
|
93778d9a25 | ||
|
|
45b8e21ed8 | ||
|
|
e1ed76b766 | ||
|
|
c946fe369f | ||
|
|
7b8935c8ca | ||
|
|
36fc823469 | ||
|
|
34cf17b147 | ||
|
|
72fb47ba99 | ||
|
|
39dea34c31 | ||
|
|
650d6bd5b6 | ||
|
|
ae5bd9b175 | ||
|
|
b436457045 | ||
|
|
3d74a80577 | ||
|
|
96e697c391 | ||
|
|
b7039a6375 | ||
|
|
8e3b3c5b59 | ||
|
|
e447965235 | ||
|
|
1e95f0e388 | ||
|
|
8a64aff600 | ||
|
|
a3a7fe5613 | ||
|
|
db254ecfae | ||
|
|
5d3b11ffa3 | ||
|
|
c54affaa40 | ||
|
|
45d7061699 | ||
|
|
748f5ac2ee | ||
|
|
5c60d87ef2 | ||
|
|
0dbdb34827 | ||
|
|
58a1e1be9f | ||
|
|
6160c206cb | ||
|
|
3b19d47cc8 | ||
|
|
12a256a1e2 | ||
|
|
d9047ea395 | ||
|
|
0c00c03f68 | ||
|
|
7ee6b3c6c3 | ||
|
|
91e6b212e7 | ||
|
|
c56381b389 | ||
|
|
882f3eeeb2 | ||
|
|
da02215ea4 | ||
|
|
a698d22129 | ||
|
|
2e427e7887 | ||
|
|
ca5f3d3ba3 | ||
|
|
2b86f4671e | ||
|
|
b068695114 | ||
|
|
a1dfe6836c | ||
|
|
bd1d932825 | ||
|
|
5330811035 | ||
|
|
ae341362f0 | ||
|
|
6cca3d5a57 | ||
|
|
99f588305f | ||
|
|
c459362f34 | ||
|
|
02fa3126a8 | ||
|
|
a6577f779d | ||
|
|
4d1080a465 | ||
|
|
de8e0319e2 | ||
|
|
f1e0413466 | ||
|
|
cf64528466 | ||
|
|
8f69b7a850 | ||
|
|
1e012b7953 | ||
|
|
2b6376ddd0 | ||
|
|
de4f27b5f8 | ||
|
|
7066301e8f | ||
|
|
ae14c6b05d | ||
|
|
b2a24ffdba | ||
|
|
62cdda749e | ||
|
|
bf8e7e6f0d | ||
|
|
5b1fa24e3d | ||
|
|
6fc8e8fda7 | ||
|
|
957e81aa30 | ||
|
|
22aee8b192 | ||
|
|
82dfa5a9dd | ||
|
|
cc07342a61 | ||
|
|
c61e833b2c | ||
|
|
53d96b3ccd | ||
|
|
e5b5b728ba | ||
|
|
a8875131d7 | ||
|
|
d40d7e603c | ||
|
|
90f3e4e94a | ||
|
|
4851eef6d6 | ||
|
|
ec63b64d2e | ||
|
|
f7f7b2feaf | ||
|
|
90c73fb445 | ||
|
|
3d59a8c8bc | ||
|
|
f14150f8de | ||
|
|
5b362894f2 |
@@ -0,0 +1 @@
|
||||
local.properties
|
||||
@@ -0,0 +1,19 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{kt,kts}]
|
||||
ij_kotlin_imports_layout = *
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_standard_discouraged-comment-location = disabled
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report
|
||||
---
|
||||
|
||||
## 🐛 Describe the bug
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
## ⚠️ Current behavior
|
||||
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
## ✅ Expected behavior
|
||||
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
## 💣 Steps to reproduce
|
||||
|
||||
<!-- How we can reproduce the behavior: -->
|
||||
|
||||
## 📷 Screenshots
|
||||
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
## 📱 Tech info
|
||||
|
||||
- Device: <!-- e.g. Nexus One -->
|
||||
- OS: <!-- e.g. 7.1.1 -->
|
||||
- App version: <!-- e.g. 1.0.0 -->
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
---
|
||||
|
||||
## ⚠️ Is your feature request related to a problem? Please describe
|
||||
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
## 💡 Describe the solution you'd like
|
||||
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
## 🤚 Do you want to develop this feature yourself?
|
||||
|
||||
<!-- Put an `x` symbol into braces of desired choice. -->
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
@@ -0,0 +1,27 @@
|
||||
## 🚀 Description
|
||||
<!-- Describe your changes in detail -->
|
||||
|
||||
## 📄 Motivation and Context
|
||||
<!-- Why is this change required? What problem does it solve? -->
|
||||
<!-- If it fixes an open issue, please link to the issue here. -->
|
||||
|
||||
## 🧪 How Has This Been Tested?
|
||||
<!-- Please describe in detail how you tested your changes. -->
|
||||
<!-- Include details of your testing environment, tests ran to see how -->
|
||||
<!-- your change affects other areas of the code, etc. -->
|
||||
|
||||
## 📷 Screenshots (if appropriate)
|
||||
<!-- Please provide a screenshot of your change -->
|
||||
|
||||
## 📦 Types of changes
|
||||
<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
|
||||
## ✅ Checklist
|
||||
<!-- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||
- [ ] My code follows the code style of this project.
|
||||
- [ ] My change requires a change to the documentation.
|
||||
- [ ] I have updated the documentation accordingly.
|
||||
@@ -0,0 +1,192 @@
|
||||
name: Android release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
unit_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Unit tests
|
||||
run: ./gradlew allTests
|
||||
|
||||
- name: Generate code coverage
|
||||
run: ./gradlew shared:koverXmlReport
|
||||
|
||||
- name: Upload test coverage
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: code-coverage.xml
|
||||
path: shared/build/coverage-report/result.xml
|
||||
|
||||
- name: Static checks
|
||||
run: ./gradlew spotlessCheck
|
||||
build:
|
||||
needs: unit_tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
format: [
|
||||
{
|
||||
type: "apk",
|
||||
command: "androidApp:assembleRelease
|
||||
-Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile
|
||||
-Pandroid.testoptions.manageddevices.emulator.gpu=\"swiftshader_indirect\"
|
||||
-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
|
||||
-Pandroid.experimental.androidTest.numManagedDeviceShards=1
|
||||
-Pandroid.experimental.testOptions.managedDevices.maxConcurrentDevices=1
|
||||
--no-configuration-cache",
|
||||
output: "apk"
|
||||
},
|
||||
{
|
||||
type: "aab",
|
||||
command: "androidApp:generateReleaseBaseline
|
||||
-Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile
|
||||
-Pandroid.testoptions.manageddevices.emulator.gpu=\"swiftshader_indirect\"
|
||||
-Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
|
||||
-Pandroid.experimental.androidTest.numManagedDeviceShards=1
|
||||
-Pandroid.experimental.testOptions.managedDevices.maxConcurrentDevices=1
|
||||
--no-configuration-cache
|
||||
androidApp:bundleRelease",
|
||||
output: "bundle"
|
||||
}
|
||||
]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Clean GMD
|
||||
run: ./gradlew cleanManagedDevices --unused-only
|
||||
|
||||
- name: Decode Keystore
|
||||
env:
|
||||
ENCODED_KEYSTORE: ${{ secrets.KEYSTORE_ENCODED }}
|
||||
run: |
|
||||
echo $ENCODED_KEYSTORE > keystore-b64.txt
|
||||
base64 -d keystore-b64.txt > ./androidApp/keystore.jks
|
||||
|
||||
- name: Build release
|
||||
env:
|
||||
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
|
||||
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
||||
KEYSTORE_PATH: keystore.jks
|
||||
run: ./gradlew ${{ matrix.format.command }}
|
||||
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: app-release.${{ matrix.format.type }}
|
||||
path: androidApp/build/outputs/${{ matrix.format.output }}/release/*.${{ matrix.format.type }}
|
||||
|
||||
- name: Upload mapping folder
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mapping-${{ matrix.format.type }}
|
||||
path: androidApp/build/outputs/mapping/
|
||||
|
||||
- name: Set up Ruby
|
||||
if: matrix.format.type == 'aab'
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '4.0.6'
|
||||
|
||||
- name: Install fastlane
|
||||
if: matrix.format.type == 'aab'
|
||||
run: gem install fastlane -NV
|
||||
|
||||
- name: Write Play Store service account JSON
|
||||
if: matrix.format.type == 'aab'
|
||||
env:
|
||||
GOOGLE_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }}
|
||||
run: echo "$GOOGLE_SERVICE_ACCOUNT_JSON" > play-service-account.json
|
||||
|
||||
- name: Upload AAB to Play Store internal track
|
||||
if: matrix.format.type == 'aab'
|
||||
run: |
|
||||
AAB_PATH=$(ls androidApp/build/outputs/bundle/release/*.aab | head -n 1)
|
||||
fastlane android internal aab:"$AAB_PATH" json_key:play-service-account.json
|
||||
|
||||
release:
|
||||
needs: build
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Tag Release
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git tag android-${{ github.event.inputs.version }}
|
||||
git push origin android-${{ github.event.inputs.version }}
|
||||
|
||||
- name: Download APK
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: app-release.apk
|
||||
|
||||
- name: Download APK mapping
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: mapping-apk
|
||||
path: mapping-apk
|
||||
|
||||
- name: Zip APK mapping
|
||||
run: |
|
||||
zip -r mapping-apk.zip mapping-apk/
|
||||
|
||||
- name: Download AAB
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: app-release.aab
|
||||
|
||||
- name: Download AAB mapping
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: mapping-aab
|
||||
path: mapping-aab
|
||||
|
||||
- name: Zip AAB mapping
|
||||
run: |
|
||||
zip -r mapping-aab.zip mapping-aab/
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: "*.apk,*.aab,mapping-apk.zip,mapping-aab.zip"
|
||||
draft: true
|
||||
name: "[Android] ${{ github.event.inputs.version }}"
|
||||
tag: android-${{ github.event.inputs.version }}
|
||||
@@ -1,28 +0,0 @@
|
||||
name: Build and test
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
unit_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: Unit tests
|
||||
run: ./gradlew testDebugUnitTest
|
||||
- name: Generate code coverage
|
||||
run: ./gradlew app:koverXmlReportDebug
|
||||
- name: Upload test coverage
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: code-coverage.xml
|
||||
path: app/build/coverage-report/result.xml
|
||||
@@ -1,117 +0,0 @@
|
||||
name: Build and release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
unit_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2.10.0
|
||||
- name: Unit tests
|
||||
run: ./gradlew testDebugUnitTest
|
||||
- name: Generate code coverage
|
||||
run: ./gradlew app:koverXmlReportDebug
|
||||
- name: Upload test coverage
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: code-coverage.xml
|
||||
path: app/build/coverage-report/result.xml
|
||||
build:
|
||||
needs: unit_tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
format: [
|
||||
{ type: "apk", command: "assembleRelease", output: "apk" },
|
||||
{ type: "aab", command: "bundleRelease", output: "bundle" }
|
||||
]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2.10.0
|
||||
- name: Decode Keystore
|
||||
env:
|
||||
ENCODED_KEYSTORE: ${{ secrets.KEYSTORE_ENCODED }}
|
||||
run: |
|
||||
echo $ENCODED_KEYSTORE > keystore-b64.txt
|
||||
base64 -d keystore-b64.txt > ./app/keystore.jks
|
||||
- name: Build release
|
||||
env:
|
||||
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
|
||||
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
||||
KEYSTORE_PATH: keystore.jks
|
||||
run: ./gradlew ${{ matrix.format.command }}
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: app-release.${{ matrix.format.type }}
|
||||
path: app/build/outputs/${{ matrix.format.output }}/release/*.${{ matrix.format.type }}
|
||||
- name: Upload mapping folder
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mapping-${{ matrix.format.type }}
|
||||
path: app/build/outputs/mapping/
|
||||
release:
|
||||
needs: build
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Tag Release
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git tag ${{ github.event.inputs.version }}
|
||||
git push origin ${{ github.event.inputs.version }}
|
||||
- name: Download APK
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: app-release.apk
|
||||
- name: Download APK mapping
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: mapping-apk
|
||||
path: mapping-apk
|
||||
- name: Zip APK mapping
|
||||
run: |
|
||||
zip -r mapping-apk.zip mapping-apk/
|
||||
- name: Download AAB
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: app-release.aab
|
||||
- name: Download AAB mapping
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: mapping-aab
|
||||
path: mapping-aab
|
||||
- name: Zip AAB mapping
|
||||
run: |
|
||||
zip -r mapping-aab.zip mapping-aab/
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: "app-release.apk,app-release.aab,mapping-apk.zip,mapping-aab.zip"
|
||||
draft: true
|
||||
name: ${{ github.event.inputs.version }}
|
||||
tag: ${{ github.event.inputs.version }}
|
||||
@@ -0,0 +1,277 @@
|
||||
name: Desktop release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
code_quality:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
- name: Unit tests
|
||||
run: ./gradlew test
|
||||
- name: Static checks
|
||||
run: ./gradlew spotlessCheck
|
||||
build_msi:
|
||||
needs: code_quality
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
- name: Build MSI
|
||||
run: |
|
||||
./gradlew :desktopApp:packageReleaseMsi
|
||||
- name: Upload MSI
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: CPU.Info-${{ github.event.inputs.version }}.msi
|
||||
path: desktopApp/build/compose/binaries/main-release/msi/*.msi
|
||||
- name: Install MSIX Packaging Tool
|
||||
run: |
|
||||
winget install --id Microsoft.MSIXPackagingTool --accept-source-agreements --accept-package-agreements
|
||||
shell: pwsh
|
||||
- name: Create MSIX conversion template
|
||||
run: |
|
||||
$msiPath = (Get-ChildItem -Path "desktopApp/build/compose/binaries/main-release/msi/*.msi" | Select-Object -First 1).FullName
|
||||
@"
|
||||
<MsixPackagingToolTemplate
|
||||
xmlns="http://schemas.microsoft.com/appx/msixpackagingtool/template/2018">
|
||||
<Installer Path="$msiPath" />
|
||||
<PackageInformation
|
||||
PackageName="${{ secrets.MSIX_PACKAGE_NAME }}"
|
||||
PackageDisplayName="${{ secrets.MSIX_PACKAGE_DISPLAY_NAME }}"
|
||||
PublisherName="${{ secrets.MSIX_PUBLISHER_NAME }}"
|
||||
PublisherDisplayName="${{ secrets.MSIX_PUBLISHER_DISPLAY_NAME }}"
|
||||
Version="${{ github.event.inputs.version }}.0"
|
||||
/>
|
||||
<SaveLocation PackagePath="${{ github.workspace }}\output" />
|
||||
</MsixPackagingToolTemplate>
|
||||
"@ | Out-File -FilePath template.xml -Encoding utf8
|
||||
shell: pwsh
|
||||
- name: Convert MSI to MSIX
|
||||
id: convert_msix
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path output
|
||||
$attempts = 3
|
||||
for ($i = 1; $i -le $attempts; $i++) {
|
||||
MsixPackagingTool.exe create-package --template template.xml
|
||||
if ($LASTEXITCODE -eq 0) { break }
|
||||
if ($i -eq $attempts) {
|
||||
Write-Error "MSIX conversion failed after $attempts attempts."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
shell: pwsh
|
||||
- name: Show MSIX Packaging Tool logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
$logPath = Join-Path $env:LOCALAPPDATA "Packages\Microsoft.MSIXPackagingTool_8wekyb3d8bbwe\LocalState\DiagOutputDir\Logs\Log.txt"
|
||||
if (Test-Path $logPath) {
|
||||
Write-Host "MSIX Packaging Tool log: $logPath"
|
||||
Get-Content $logPath -Tail 400
|
||||
} else {
|
||||
Write-Warning "MSIX Packaging Tool log not found at expected path: $logPath"
|
||||
}
|
||||
shell: pwsh
|
||||
- name: Assert MSIX artifact exists
|
||||
run: |
|
||||
$msix = Get-ChildItem -Path "output/*.msix" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $msix) {
|
||||
Write-Error "MSIX conversion finished without producing an .msix file."
|
||||
exit 1
|
||||
}
|
||||
shell: pwsh
|
||||
- name: Upload MSIX
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: CPU.Info-${{ github.event.inputs.version }}.msix
|
||||
path: output/*.msix
|
||||
|
||||
build_linux_x64_uber_jar:
|
||||
needs: code_quality
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
- name: Build Uber JAR
|
||||
run: ./gradlew :desktopApp:packageReleaseUberJarForCurrentOS
|
||||
- name: Upload Uber JAR
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: CPU-Info-linux-x64-${{ github.event.inputs.version }}-release.jar
|
||||
path: desktopApp/build/compose/jars/*.jar
|
||||
|
||||
build_mac_uber_jar:
|
||||
needs: code_quality
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
- name: Build Uber JAR
|
||||
run: ./gradlew :desktopApp:packageReleaseUberJarForCurrentOS
|
||||
- name: Upload Uber JAR
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: CPU-Info-macos-arm64-${{ github.event.inputs.version }}-release.jar
|
||||
path: desktopApp/build/compose/jars/*.jar
|
||||
|
||||
build_mac_dmg:
|
||||
needs: code_quality
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-26
|
||||
arch: arm64
|
||||
- runner: macos-15-intel
|
||||
arch: x64
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: set up JDK 18
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 18
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Cache KMP tooling
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.konan
|
||||
key: ${{ runner.os }}-v1-${{ hashFiles('*.versions.toml') }}
|
||||
|
||||
- name: Import certs
|
||||
uses: apple-actions/import-codesign-certs@v7
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.MAC_NOTARIZATION_CERT }}
|
||||
p12-password: ${{ secrets.MAC_NOTARIZATION_CERT_PASS }}
|
||||
|
||||
- name: Create path variables
|
||||
id: path_variables
|
||||
run: |
|
||||
name="CPU-Info-${{ matrix.arch }}-${{ github.event.inputs.version }}.dmg"
|
||||
path="desktopApp/build/compose/binaries/main-release/dmg/CPU-Info-${{ github.event.inputs.version }}.dmg"
|
||||
echo "RELEASE_NAME=$name" >> $GITHUB_ENV
|
||||
echo "RELEASE_PATH=$path" >> $GITHUB_ENV
|
||||
|
||||
- name: Create and notarize DMG
|
||||
run: |
|
||||
./gradlew desktopApp:notarizeReleaseDmg -PmacOsNotarization=true \
|
||||
-Pcompose.desktop.mac.notarization.appleID=$MAC_NOTARIZATION_ID \
|
||||
-Pcompose.desktop.mac.notarization.password=$MAC_NOTARIZATION_PWD \
|
||||
-Pcompose.desktop.mac.notarization.teamID=$MAC_APPSTORE_TEAM_ID
|
||||
env:
|
||||
MAC_NOTARIZATION_ID: ${{ secrets.MAC_NOTARIZATION_ID }}
|
||||
MAC_NOTARIZATION_PWD: ${{ secrets.MAC_NOTARIZATION_PWD }}
|
||||
MAC_APPSTORE_TEAM_ID: ${{ secrets.MAC_APPSTORE_TEAM_ID }}
|
||||
RELEASE_PATH: ${{ env.RELEASE_PATH }}
|
||||
|
||||
- name: Show notarization logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== notarizeReleaseDmg stdout logs ==="
|
||||
find desktopApp/build/compose/logs/notarizeReleaseDmg -name "*-out.txt" 2>/dev/null | sort | while read f; do
|
||||
echo "--- $f ---"
|
||||
cat "$f"
|
||||
done
|
||||
echo "=== notarizeReleaseDmg stderr logs ==="
|
||||
find desktopApp/build/compose/logs/notarizeReleaseDmg -name "*-err.txt" 2>/dev/null | sort | while read f; do
|
||||
echo "--- $f ---"
|
||||
cat "$f"
|
||||
done
|
||||
|
||||
- name: Upload DMG
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.RELEASE_NAME }}
|
||||
path: ${{ env.RELEASE_PATH }}
|
||||
|
||||
tag_and_create_release:
|
||||
needs: [ build_msi, build_linux_x64_uber_jar, build_mac_dmg, build_mac_uber_jar ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Download MSI
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: CPU.Info-${{ github.event.inputs.version }}.msi
|
||||
- name: Download MSIX
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: CPU.Info-${{ github.event.inputs.version }}.msix
|
||||
- name: Download Linux x64 Uber JAR
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: CPU-Info-linux-x64-${{ github.event.inputs.version }}-release.jar
|
||||
- name: Download Mac x64 DMG
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: CPU-Info-x64-${{ github.event.inputs.version }}.dmg
|
||||
- name: Rename Mac x64 DMG file
|
||||
run: mv CPU-Info-${{ github.event.inputs.version }}.dmg CPU-Info-macos-x64-${{ github.event.inputs.version }}.dmg
|
||||
- name: Download Mac arm64 DMG
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: CPU-Info-arm64-${{ github.event.inputs.version }}.dmg
|
||||
- name: Rename Mac arm64 DMG file
|
||||
run: mv CPU-Info-${{ github.event.inputs.version }}.dmg CPU-Info-macos-arm64-${{ github.event.inputs.version }}.dmg
|
||||
- name: Download Mac OS Uber JAR
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: CPU-Info-macos-arm64-${{ github.event.inputs.version }}-release.jar
|
||||
- name: Tag Release
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git tag jvm-${{ github.event.inputs.version }}
|
||||
git push origin jvm-${{ github.event.inputs.version }}
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: "*.msi, *.msix, *.jar, *.dmg"
|
||||
draft: true
|
||||
name: "[JVM] ${{ github.event.inputs.version }}"
|
||||
tag: jvm-${{ github.event.inputs.version }}
|
||||
@@ -0,0 +1,134 @@
|
||||
name: iOS release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
code_quality:
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
- name: Unit tests
|
||||
run: ./gradlew testDebugUnitTest
|
||||
- name: Static checks
|
||||
run: ./gradlew spotlessCheck
|
||||
build_and_release:
|
||||
needs: code_quality
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
|
||||
- name: Cache KMP tooling
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.konan
|
||||
key: ${{ runner.os }}-v1-${{ hashFiles('*.versions.toml') }}
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: latest-stable
|
||||
- name: Import certs
|
||||
uses: apple-actions/import-codesign-certs@v7
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.IOS_CERTIFICATES_P12 }}
|
||||
p12-password: ${{ secrets.IOS_CERTIFICATES_PASSWORD }}
|
||||
- name: Download provisioning profiles
|
||||
uses: apple-actions/download-provisioning-profiles@v6
|
||||
with:
|
||||
bundle-id: ${{ secrets.IOS_BUNDLE_ID }}
|
||||
issuer-id: ${{ secrets.IOS_APPSTORE_ISSUER_ID }}
|
||||
api-key-id: ${{ secrets.IOS_APPSTORE_KEY_ID }}
|
||||
api-private-key: ${{ secrets.IOS_APPSTORE_PRIVATE_KEY }}
|
||||
- name: Build archive
|
||||
run: |
|
||||
cd iosApp
|
||||
|
||||
xcrun xcodebuild \
|
||||
-scheme "iosApp" \
|
||||
-configuration "Release" \
|
||||
-sdk "iphoneos" \
|
||||
-parallelizeTargets \
|
||||
-showBuildTimingSummary \
|
||||
-disableAutomaticPackageResolution \
|
||||
-derivedDataPath "${RUNNER_TEMP}/Build/DerivedData" \
|
||||
-archivePath "${RUNNER_TEMP}/Build/Archives/cpuinfo.xcarchive" \
|
||||
-resultBundlePath "${RUNNER_TEMP}/Build/Artifacts/cpuinfo.xcresult" \
|
||||
-destination "generic/platform=iOS" \
|
||||
DEVELOPMENT_TEAM="${{ secrets.IOS_APPSTORE_TEAM_ID }}" \
|
||||
PRODUCT_BUNDLE_IDENTIFIER="${{ secrets.IOS_BUNDLE_ID }}" \
|
||||
CODE_SIGN_STYLE="Manual" \
|
||||
PROVISIONING_PROFILE_SPECIFIER="${{ secrets.IOS_DEV_PROVISIONING_PROFILE_NAME }}" \
|
||||
archive
|
||||
- name: "Generate ExportOptions.plist"
|
||||
run: |
|
||||
cat <<EOF > ${RUNNER_TEMP}/Build/ExportOptions.plist
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>destination</key>
|
||||
<string>export</string>
|
||||
<key>method</key>
|
||||
<string>app-store</string>
|
||||
<key>signingStyle</key>
|
||||
<string>manual</string>
|
||||
<key>generateAppStoreInformation</key>
|
||||
<true/>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>teamID</key>
|
||||
<string>${{ secrets.IOS_APPSTORE_TEAM_ID }}</string>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>${{ secrets.IOS_BUNDLE_ID }}</key>
|
||||
<string>${{ secrets.IOS_DIST_PROVISIONING_PROFILE_NAME }}</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
- id: build_ipa
|
||||
name: Build IPA
|
||||
run: |
|
||||
xcrun xcodebuild \
|
||||
-exportArchive \
|
||||
-exportOptionsPlist "${RUNNER_TEMP}/Build/ExportOptions.plist" \
|
||||
-archivePath "${RUNNER_TEMP}/Build/Archives/cpuinfo.xcarchive" \
|
||||
-exportPath "${RUNNER_TEMP}/Build/Archives/cpuinfo.xcarchive" \
|
||||
PRODUCT_BUNDLE_IDENTIFIER="${{ secrets.IOS_BUNDLE_ID }}"
|
||||
|
||||
echo "ipa_path=${RUNNER_TEMP}/Build/Archives/cpuinfo.xcarchive/CPU Info.ipa" >> $GITHUB_ENV
|
||||
- uses: Apple-Actions/upload-testflight-build@v5
|
||||
with:
|
||||
app-path: ${{ env.ipa_path }}
|
||||
issuer-id: ${{ secrets.IOS_APPSTORE_ISSUER_ID }}
|
||||
api-key-id: ${{ secrets.IOS_APPSTORE_KEY_ID }}
|
||||
api-private-key: ${{ secrets.IOS_APPSTORE_PRIVATE_KEY }}
|
||||
- name: Tag Release
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git tag ios-${{ github.event.inputs.version }}
|
||||
git push origin ios-${{ github.event.inputs.version }}
|
||||
@@ -0,0 +1,85 @@
|
||||
name: Mac App Store release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
code_quality:
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up JDK 18
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 18
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
- name: Unit tests
|
||||
run: ./gradlew testDebugUnitTest
|
||||
- name: Static checks
|
||||
run: ./gradlew spotlessCheck
|
||||
build_and_release:
|
||||
needs: code_quality
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: set up JDK 18
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 18
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Cache KMP tooling
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.konan
|
||||
key: ${{ runner.os }}-v1-${{ hashFiles('*.versions.toml') }}
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: latest-stable
|
||||
|
||||
- name: Import certs
|
||||
uses: apple-actions/import-codesign-certs@v7
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.MAC_CERTIFICATES_P12 }}
|
||||
p12-password: ${{ secrets.MAC_CERTIFICATES_PASSWORD }}
|
||||
|
||||
- name: Create Embedded Provision Profile
|
||||
run: |
|
||||
echo "$EMBEDDED_PROVISION" > desktopApp/embedded.provisionprofile.b64
|
||||
base64 -d -i desktopApp/embedded.provisionprofile.b64 > desktopApp/embedded.provisionprofile
|
||||
env:
|
||||
EMBEDDED_PROVISION: ${{ secrets.MAC_EMBEDDED_PROVISION }}
|
||||
|
||||
- name: Create Runtime Provision Profile
|
||||
run: |
|
||||
echo "$RUNTIME_PROVISION" > desktopApp/runtime.provisionprofile.b64
|
||||
base64 -d -i desktopApp/runtime.provisionprofile.b64 > desktopApp/runtime.provisionprofile
|
||||
env:
|
||||
RUNTIME_PROVISION: ${{ secrets.MAC_RUNTIME_PROVISION }}
|
||||
|
||||
- name: Create PKG
|
||||
run: ./gradlew packageReleasePkg -PmacOsAppStoreRelease=true
|
||||
|
||||
- uses: Apple-Actions/upload-testflight-build@v5
|
||||
with:
|
||||
app-type: 'osx'
|
||||
app-path: desktopApp/build/compose/binaries/main-release/pkg/CPU-Info-${{ github.event.inputs.version }}.pkg
|
||||
issuer-id: ${{ secrets.IOS_APPSTORE_ISSUER_ID }}
|
||||
api-key-id: ${{ secrets.IOS_APPSTORE_KEY_ID }}
|
||||
api-private-key: ${{ secrets.IOS_APPSTORE_PRIVATE_KEY }}
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Shared tests
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
unit_tests:
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
- name: Unit tests
|
||||
run: ./gradlew allTests
|
||||
- name: Generate code coverage
|
||||
run: ./gradlew shared:koverXmlReport
|
||||
- name: Upload test coverage
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: code-coverage.xml
|
||||
path: shared/build/coverage-report/result.xml
|
||||
- name: Static checks
|
||||
run: ./gradlew spotlessCheck
|
||||
@@ -0,0 +1,142 @@
|
||||
name: Wear OS release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
unit_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: corretto
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
- name: Unit tests
|
||||
run: ./gradlew allTests
|
||||
- name: Generate code coverage
|
||||
run: './gradlew shared:koverXmlReport'
|
||||
- name: Upload test coverage
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: code-coverage.xml
|
||||
path: shared/build/coverage-report/result.xml
|
||||
- name: Static checks
|
||||
run: ./gradlew spotlessCheck
|
||||
build:
|
||||
needs: unit_tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
format:
|
||||
- type: apk
|
||||
command: 'wearOsApp:assembleRelease'
|
||||
output: apk
|
||||
- type: aab
|
||||
command: 'wearOsApp:bundleRelease'
|
||||
output: bundle
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: corretto
|
||||
cache: gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
- name: Decode Keystore
|
||||
env:
|
||||
ENCODED_KEYSTORE: '${{ secrets.KEYSTORE_ENCODED }}'
|
||||
run: |
|
||||
echo $ENCODED_KEYSTORE > keystore-b64.txt
|
||||
base64 -d keystore-b64.txt > ./wearOsApp/keystore.jks
|
||||
- name: Build release
|
||||
env:
|
||||
KEYSTORE_ALIAS: '${{ secrets.KEYSTORE_ALIAS }}'
|
||||
KEYSTORE_PASS: '${{ secrets.KEYSTORE_PASS }}'
|
||||
KEYSTORE_PATH: keystore.jks
|
||||
run: './gradlew ${{ matrix.format.command }}'
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: 'app-release.${{ matrix.format.type }}'
|
||||
path: >-
|
||||
wearOsApp/build/outputs/${{ matrix.format.output }}/release/*.${{
|
||||
matrix.format.type }}
|
||||
- name: Upload mapping folder
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: 'mapping-${{ matrix.format.type }}'
|
||||
path: wearOsApp/build/outputs/mapping/
|
||||
- name: Set up Ruby
|
||||
if: matrix.format.type == 'aab'
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '4.0.6'
|
||||
- name: Install fastlane
|
||||
if: matrix.format.type == 'aab'
|
||||
run: gem install fastlane -NV
|
||||
- name: Write Play Store service account JSON
|
||||
if: matrix.format.type == 'aab'
|
||||
env:
|
||||
GOOGLE_SERVICE_ACCOUNT_JSON: '${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }}'
|
||||
run: echo "$GOOGLE_SERVICE_ACCOUNT_JSON" > play-service-account.json
|
||||
- name: Upload AAB to Play Store internal track
|
||||
if: matrix.format.type == 'aab'
|
||||
run: |
|
||||
AAB_PATH=$(ls wearOsApp/build/outputs/bundle/release/*.aab | head -n 1)
|
||||
fastlane android internal aab:"$AAB_PATH" json_key:play-service-account.json track:wear:internal
|
||||
release:
|
||||
needs: build
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Tag Release
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git tag wear-${{ github.event.inputs.version }}
|
||||
git push origin wear-${{ github.event.inputs.version }}
|
||||
- name: Download APK
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: app-release.apk
|
||||
- name: Download APK mapping
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: mapping-apk
|
||||
path: mapping-apk
|
||||
- name: Zip APK mapping
|
||||
run: |
|
||||
zip -r mapping-apk.zip mapping-apk/
|
||||
- name: Download AAB
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: app-release.aab
|
||||
- name: Download AAB mapping
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: mapping-aab
|
||||
path: mapping-aab
|
||||
- name: Zip AAB mapping
|
||||
run: |
|
||||
zip -r mapping-aab.zip mapping-aab/
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: '*.apk,*.aab,mapping-apk.zip,mapping-aab.zip'
|
||||
draft: true
|
||||
name: '[WearOS] ${{ github.event.inputs.version }}'
|
||||
tag: 'wear-${{ github.event.inputs.version }}'
|
||||
@@ -5,7 +5,18 @@ local.properties
|
||||
build/
|
||||
captures/
|
||||
.cxx/
|
||||
.kotlin/
|
||||
|
||||
.idea/
|
||||
!/.idea/codeStyles
|
||||
!/.idea/icon.png
|
||||
|
||||
captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcodeproj/project.xcworkspace/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="" withSubpackages="true" static="false" module="true" />
|
||||
<package name="android" withSubpackages="true" static="true" />
|
||||
<package name="androidx" withSubpackages="true" static="true" />
|
||||
<package name="com" withSubpackages="true" static="true" />
|
||||
<package name="junit" withSubpackages="true" static="true" />
|
||||
<package name="net" withSubpackages="true" static="true" />
|
||||
<package name="org" withSubpackages="true" static="true" />
|
||||
<package name="java" withSubpackages="true" static="true" />
|
||||
<package name="javax" withSubpackages="true" static="true" />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="androidx" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="junit" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="net" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="org" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Project
|
||||
|
||||
This is a compose multiplatform project build for Android, iOS, JVM and web. App is focused around
|
||||
hardware and software of the devices. App is built using MVVM architecture with AndroidX lifecycle
|
||||
and Kotlin Coroutines for asynchronous programming. It is using DataStore for key-value storage.
|
||||
|
||||
# Commands
|
||||
|
||||
- Run tests: `./gradlew allTests`
|
||||
- Format code: `./gradlew spotlessApply`
|
||||
|
||||
# Code style
|
||||
|
||||
- Use 4 spaces for indentation
|
||||
- Pass data to VM with Interactor
|
||||
- From VM expose data as StateFlow using single UiState
|
||||
- For events use Channel<Event>(Channel.BUFFERED) and observe them in compose with ObserveAsEvents
|
||||
- In VM use `on` prefix for callback functions like `onEvent`, `onClick` etc.
|
||||
- Use composeResources for all strings
|
||||
@@ -1,10 +1,10 @@
|
||||

|
||||

|
||||
|
||||
<img src="info/icon_glow.png" width="100" height="100" />
|
||||
<img src="info/icon_glow.png" width="100" height="100" alt="CPU Info Icon" />
|
||||
|
||||
CPU Info (refactor-in-progress ⛏)
|
||||
=================================
|
||||
CPU Info provides information about Android device hardware and software.
|
||||
# CPU Info
|
||||
|
||||
CPU Info provides comprehensive information about your device's hardware and software.
|
||||
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
@@ -17,25 +17,42 @@ alt="Get it on HUAWEI AppGallery"
|
||||
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C102414279?channelId=Main+badge+&id=fb28f69db40840f8b79b541cc4a13775&s=1378847C6B5A46F97603F316DD1450C7D3F74C023A62827B43619C2D41F2341D&detailType=0&v=&callType=AGDLINK&installType=0000)
|
||||
[<img src="https://raw.githubusercontent.com/kamgurgul/cpu-info/master/info/get-it-on-github.png"
|
||||
alt='Get it on GitHub' height="80">](https://github.com/kamgurgul/cpu-info/releases/latest)
|
||||
[<img src="https://raw.githubusercontent.com/kamgurgul/cpu-info/master/info/amazon-badge.png"
|
||||
alt='Available on Amazon AppStore' height="80">](https://www.amazon.com/Kamil-Gurgul-KG-Soft-Info/dp/B088FYQTYR/ref=sr_1_5?keywords=cpu+info&qid=1661020642&s=mobile-apps&sr=1-5)
|
||||
[<img src="info/app-store-badge.png"
|
||||
alt="Download on the App Store"
|
||||
height="80">](https://apps.apple.com/us/app/cpu-info/id6560116815)
|
||||
[<img src="info/flathub_badge.png"
|
||||
alt="Get it on Flathub"
|
||||
height="80">](https://flathub.org/apps/com.kgurgul.cpuinfo)
|
||||
[<img src="info/ms_badge.png"
|
||||
alt="Get it from Microsoft"
|
||||
height="80">](https://apps.microsoft.com/detail/9phxq0f4knbl)
|
||||
[<img src="info/get-it-on-homebrew.png"
|
||||
alt="Get it from Homebrew"
|
||||
height="80">](https://formulae.brew.sh/cask/cpu-info)
|
||||
|
||||
TODO
|
||||
====
|
||||
## Supported platforms
|
||||
|
||||
* Add more information about battery
|
||||
* Bring back apps size - use StorageStatsManager if possible
|
||||
* Unit and UI tests
|
||||
* Add Kotlin Multiplatform module or test circuit
|
||||
* Add translations framework
|
||||
| Android | Android TV | Wear OS | iOS | Desktop | Wasm |
|
||||
|:-------:|:----------:|:-------:|:---:|:-------:|:-------------------------------------------------------:|
|
||||
| ✅ | ✅ | ✅ | ✅ | ✅ | ✅<br/>[In preview](https://kgurgul.com/assets/cpuinfo/) |
|
||||
|
||||
Alternative stores
|
||||
==================
|
||||
## Tech stack
|
||||
|
||||
* [Amazon](https://www.amazon.com/Kamil-Gurgul-KG-Soft-Info/dp/B088FYQTYR/ref=sr_1_5?keywords=cpu+info&qid=1661020642&s=mobile-apps&sr=1-5)
|
||||
* [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html)
|
||||
* [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/)
|
||||
* [Jetpack ViewModel/Lifecycle](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-lifecycle.html)
|
||||
* [Coroutines](https://github.com/Kotlin/kotlinx.coroutines)
|
||||
* [Koin](https://github.com/InsertKoinIO/koin)
|
||||
* [Coil](https://github.com/coil-kt/coil)
|
||||
* [DataStore](https://developer.android.com/kotlin/multiplatform/datastore)
|
||||
* [pytorch/cpuinfo](https://github.com/pytorch/cpuinfo)
|
||||
* [OSHI](https://github.com/oshi/oshi)
|
||||
|
||||
License
|
||||
-------
|
||||
## License
|
||||
|
||||
Copyright 2017 KG Soft
|
||||
Copyright KG Soft
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,54 @@
|
||||
import com.kgurgul.AndroidVersions
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidTest)
|
||||
alias(libs.plugins.baselineprofile)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.kgurgul.cpuinfo.baselineprofile"
|
||||
compileSdk = AndroidVersions.COMPILE_SDK
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 28
|
||||
targetSdk = AndroidVersions.TARGET_SDK
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
targetProjectPath = ":androidApp"
|
||||
|
||||
testOptions.managedDevices.allDevices {
|
||||
create<com.android.build.api.dsl.ManagedVirtualDevice>("pixel6Api33") {
|
||||
device = "Pixel 6"
|
||||
apiLevel = 33
|
||||
systemImageSource = "aosp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
managedDevices += "pixel6Api33"
|
||||
useConnectedDevices = false
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.benchmark.macro.junit4)
|
||||
implementation(libs.androidx.test.ext)
|
||||
implementation(libs.androidx.test.espresso.core)
|
||||
implementation(libs.androidx.test.uiautomator)
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants { v ->
|
||||
v.instrumentationRunnerArguments.put(
|
||||
"targetAppId",
|
||||
v.testedApks.map { v.artifacts.getBuiltArtifactsLoader().load(it)?.applicationId!! },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest />
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.baselineprofile
|
||||
|
||||
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class BaselineProfileGenerator {
|
||||
|
||||
@get:Rule val rule = BaselineProfileRule()
|
||||
|
||||
@Test
|
||||
fun generate() {
|
||||
val packageName =
|
||||
InstrumentationRegistry.getArguments().getString("targetAppId")
|
||||
?: throw Exception("targetAppId not passed as instrumentation runner arg")
|
||||
rule.collect(packageName = packageName, includeInStartupProfile = true) {
|
||||
pressHome()
|
||||
startActivityAndWait()
|
||||
waitForAsyncContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.baselineprofile
|
||||
|
||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.UiScrollable
|
||||
import androidx.test.uiautomator.UiSelector
|
||||
import androidx.test.uiautomator.Until
|
||||
|
||||
fun MacrobenchmarkScope.waitForAsyncContent() {
|
||||
device.wait(Until.hasObject(By.res("cpu_info_lazy_column")), 5_000)
|
||||
val element = UiScrollable(UiSelector().scrollable(true))
|
||||
element.scrollToEnd(3)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.baselineprofile
|
||||
|
||||
import androidx.benchmark.macro.BaselineProfileMode
|
||||
import androidx.benchmark.macro.CompilationMode
|
||||
import androidx.benchmark.macro.StartupMode
|
||||
import androidx.benchmark.macro.StartupTimingMetric
|
||||
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class StartupBenchmarks {
|
||||
|
||||
@get:Rule val rule = MacrobenchmarkRule()
|
||||
|
||||
@Test fun startupCompilationNone() = benchmark(CompilationMode.None())
|
||||
|
||||
@Test
|
||||
fun startupCompilationBaselineProfiles() =
|
||||
benchmark(CompilationMode.Partial(BaselineProfileMode.Require))
|
||||
|
||||
private fun benchmark(compilationMode: CompilationMode) {
|
||||
val packageName =
|
||||
InstrumentationRegistry.getArguments().getString("targetAppId")
|
||||
?: throw Exception("targetAppId not passed as instrumentation runner arg")
|
||||
rule.measureRepeated(
|
||||
packageName = packageName,
|
||||
metrics = listOf(StartupTimingMetric()),
|
||||
compilationMode = compilationMode,
|
||||
startupMode = StartupMode.COLD,
|
||||
iterations = 10,
|
||||
setupBlock = { pressHome() },
|
||||
measureBlock = {
|
||||
startActivityAndWait()
|
||||
waitForAsyncContent()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import com.github.jk1.license.filter.DependencyFilter
|
||||
import com.github.jk1.license.filter.LicenseBundleNormalizer
|
||||
import com.github.jk1.license.render.JsonReportRenderer
|
||||
import com.github.jk1.license.render.ReportRenderer
|
||||
import com.kgurgul.AndroidSigningConfig
|
||||
import com.kgurgul.AndroidVersions
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.baselineprofile)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.licenses)
|
||||
alias(libs.plugins.serialization)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = AndroidVersions.COMPILE_SDK
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.kgurgul.cpuinfo"
|
||||
|
||||
minSdk = AndroidVersions.MIN_SDK
|
||||
targetSdk = AndroidVersions.TARGET_SDK
|
||||
versionCode = AndroidVersions.VERSION_CODE
|
||||
versionName = AndroidVersions.VERSION_NAME
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
namespace = "com.kgurgul.cpuinfo"
|
||||
|
||||
signingConfigs {
|
||||
getByName("debug") {
|
||||
val debugSigningConfig = AndroidSigningConfig.getDebugProperties(rootProject.rootDir)
|
||||
storeFile = file(debugSigningConfig.getProperty(AndroidSigningConfig.KEY_PATH))
|
||||
keyAlias = debugSigningConfig.getProperty(AndroidSigningConfig.KEY_ALIAS)
|
||||
keyPassword = debugSigningConfig.getProperty(AndroidSigningConfig.KEY_PASS)
|
||||
storePassword = debugSigningConfig.getProperty(AndroidSigningConfig.KEY_PASS)
|
||||
}
|
||||
create("release") {
|
||||
val releaseSigningConfig =
|
||||
AndroidSigningConfig.getReleaseProperties(rootProject.rootDir)
|
||||
storeFile = file(releaseSigningConfig.getProperty(AndroidSigningConfig.KEY_PATH))
|
||||
keyAlias = releaseSigningConfig.getProperty(AndroidSigningConfig.KEY_ALIAS)
|
||||
keyPassword = releaseSigningConfig.getProperty(AndroidSigningConfig.KEY_PASS)
|
||||
storePassword = releaseSigningConfig.getProperty(AndroidSigningConfig.KEY_PASS)
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
debug {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isMinifyEnabled = false
|
||||
enableUnitTestCoverage = true
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
useLegacyPackaging = false
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isReturnDefaultValues = true
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
animationsDisabled = true
|
||||
execution = "ANDROIDX_TEST_ORCHESTRATOR"
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
saveInSrc = true
|
||||
}
|
||||
|
||||
composeCompiler {
|
||||
// Opt-in only: the Compose compiler metrics/reports writer crashes on Windows
|
||||
// ("Invalid file path") under Kotlin 2.4.0. Enable with -PenableComposeCompilerReports=true.
|
||||
if (providers.gradleProperty("enableComposeCompilerReports").orNull == "true") {
|
||||
reportsDestination = layout.buildDirectory.dir("compose_compiler")
|
||||
metricsDestination = layout.buildDirectory.dir("compose_compiler")
|
||||
}
|
||||
}
|
||||
|
||||
licenseReport {
|
||||
renderers = arrayOf<ReportRenderer>(JsonReportRenderer("licenses.json"))
|
||||
filters = arrayOf<DependencyFilter>(LicenseBundleNormalizer())
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.shared)
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestImplementation(libs.androidx.test.rules)
|
||||
androidTestImplementation(libs.androidx.test.ext)
|
||||
androidTestImplementation(libs.androidx.test.espresso.core)
|
||||
androidTestImplementation(libs.androidx.test.espresso.contrib)
|
||||
androidTestImplementation(libs.compose.ui.test)
|
||||
|
||||
debugImplementation(libs.compose.ui.testManifest)
|
||||
|
||||
androidTestUtil(libs.androidx.test.orchestrator)
|
||||
|
||||
"baselineProfile"(projects.androidApp.baselineprofile)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Remove logs
|
||||
-assumenosideeffects class co.touchlab.kermit.Logger {
|
||||
public *** v(...);
|
||||
public *** d(...);
|
||||
public *** i(...);
|
||||
public *** e(...);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* Copyright 2017 KG Soft
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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
|
||||
* 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,
|
||||
@@ -13,20 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:JvmName("TestExtensions")
|
||||
|
||||
package com.kgurgul.cpuinfo.core
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.hamcrest.CoreMatchers
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import com.kgurgul.cpuinfo.HostActivity
|
||||
import org.junit.Rule
|
||||
|
||||
/**
|
||||
* Kotlin definition for [CoreMatchers. is] method to avoid problems with 'is' keyword
|
||||
*/
|
||||
fun <T> isk(value: T) = CoreMatchers.`is`(value)!!
|
||||
abstract class BaseTestSuit {
|
||||
|
||||
fun getString(@StringRes id: Int): String = InstrumentationRegistry.getInstrumentation()
|
||||
.targetContext
|
||||
.getString(id)
|
||||
@get:Rule val androidComposeRule = createAndroidComposeRule<HostActivity>()
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* Copyright 2017 KG Soft
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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
|
||||
* 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,
|
||||
@@ -13,70 +13,18 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kgurgul.cpuinfo.core
|
||||
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.ui.test.SemanticsNodeInteraction
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsToggleable
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.ViewInteraction
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.BoundedMatcher
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasChildCount
|
||||
import androidx.test.espresso.matcher.ViewMatchers.hasMinimumChildCount
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import com.kgurgul.cpuinfo.core.CustomMatchers.atPosition
|
||||
import org.hamcrest.Description
|
||||
import org.hamcrest.Matcher
|
||||
|
||||
|
||||
/**
|
||||
* Singleton with custom matchers.
|
||||
*
|
||||
* @author kgurgul
|
||||
*/
|
||||
object CustomMatchers {
|
||||
|
||||
/**
|
||||
* Matcher for toolbar title
|
||||
*/
|
||||
fun withToolbarTitle(textMatcher: Matcher<CharSequence>): Matcher<Any> {
|
||||
return object : BoundedMatcher<Any, Toolbar>(Toolbar::class.java) {
|
||||
|
||||
public override fun matchesSafely(toolbar: Toolbar): Boolean {
|
||||
return textMatcher.matches(toolbar.title)
|
||||
}
|
||||
|
||||
override fun describeTo(description: Description) {
|
||||
description.appendText("with toolbar title: ")
|
||||
textMatcher.describeTo(description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Matcher for [RecyclerView] items
|
||||
*/
|
||||
fun atPosition(position: Int, itemMatcher: Matcher<View>): Matcher<View> {
|
||||
return object : BoundedMatcher<View, RecyclerView>(RecyclerView::class.java) {
|
||||
|
||||
override fun describeTo(description: Description) {
|
||||
description.appendText("has item at position $position: ")
|
||||
itemMatcher.describeTo(description)
|
||||
}
|
||||
|
||||
override fun matchesSafely(view: RecyclerView): Boolean {
|
||||
val viewHolder = view.findViewHolderForAdapterPosition(position) ?: return false
|
||||
return itemMatcher.matches(viewHolder.itemView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ViewInteraction.isDisplayed(): Boolean {
|
||||
return try {
|
||||
@@ -172,15 +120,6 @@ fun ViewInteraction.hasElementsCount(count: Int): Boolean {
|
||||
}
|
||||
}
|
||||
|
||||
fun ViewInteraction.hasItemAtPosition(position: Int, itemMatcher: Matcher<View>): Boolean {
|
||||
return try {
|
||||
check(matches(atPosition(position, itemMatcher)))
|
||||
true
|
||||
} catch (e: Throwable) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun SemanticsNodeInteraction.isToggleable(): Boolean {
|
||||
return try {
|
||||
assertIsToggleable()
|
||||
@@ -204,8 +143,6 @@ fun SemanticsNodeInteraction.isNotDisplayed(): Boolean {
|
||||
}
|
||||
|
||||
fun SemanticsNodeInteractionCollection.hasAnyElement(): Boolean {
|
||||
val matchedNodes = fetchSemanticsNodes(
|
||||
atLeastOneRootRequired = true,
|
||||
)
|
||||
val matchedNodes = fetchSemanticsNodes(atLeastOneRootRequired = true)
|
||||
return matchedNodes.isNotEmpty()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.pages
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import com.kgurgul.cpuinfo.shared.Res
|
||||
import com.kgurgul.cpuinfo.shared.cpu
|
||||
import com.kgurgul.cpuinfo.ui.components.TEST_TAG_CPU_TOP_APP_BAR
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
class InfoContainerPage(composeContent: ComposeContentTestRule) {
|
||||
|
||||
private val toolbarTitle = composeContent.onNodeWithTag(TEST_TAG_CPU_TOP_APP_BAR)
|
||||
private val cpuTab = composeContent.onNodeWithText(runBlocking { getString(Res.string.cpu) })
|
||||
|
||||
fun assertViewDisplayed() {
|
||||
toolbarTitle.assertIsDisplayed()
|
||||
cpuTab.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* Copyright 2017 KG Soft
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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
|
||||
* 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,
|
||||
@@ -13,7 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kgurgul.cpuinfo.testsuits.information
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
@@ -28,6 +27,7 @@ class InfoTestSuit : BaseTestSuit() {
|
||||
private val infoContainerPage = InfoContainerPage(androidComposeRule)
|
||||
|
||||
@Test
|
||||
fun checkInfoTabs() {
|
||||
fun checkInitialScreenDisplayed() {
|
||||
infoContainerPage.assertViewDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.uitestutils.conditionwatcher
|
||||
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
@Synchronized
|
||||
fun waitForCondition(timeoutMillis: Long = 5000L, condition: () -> Boolean) {
|
||||
val step = 250L
|
||||
var totalTime = 0L
|
||||
|
||||
do {
|
||||
if (condition()) {
|
||||
break
|
||||
}
|
||||
totalTime += step
|
||||
Thread.sleep(step)
|
||||
if (totalTime > timeoutMillis) {
|
||||
throw TimeoutException("Condition timed out after: ${totalTime}ms")
|
||||
}
|
||||
} while (true)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE" />
|
||||
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.READ_BASIC_PHONE_STATE" />
|
||||
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.wifi"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".CpuInfoApp"
|
||||
android:allowBackup="true"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:extractNativeLibs="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:resizeableActivity="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="false"
|
||||
android:theme="@style/AppThemeBase"
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
<activity
|
||||
android:name="com.kgurgul.cpuinfo.HostActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Launcher"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="kgurgul.com" />
|
||||
<data android:pathPattern="/assets/cpuinfo.*" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.kgurgul.cpuinfo.tv.TvActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Launcher"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".features.cputile.CpuTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_cpu_low"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo
|
||||
|
||||
import android.app.Application
|
||||
import com.kgurgul.cpuinfo.appinitializers.AppInitializerComponent
|
||||
import com.kgurgul.cpuinfo.di.androidModule
|
||||
import com.kgurgul.cpuinfo.di.sharedModule
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
|
||||
class CpuInfoApp : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startKoin {
|
||||
androidContext(this@CpuInfoApp)
|
||||
modules(androidModule, sharedModule)
|
||||
}
|
||||
AppInitializerComponent().init(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kgurgul.cpuinfo.features.HostScreen
|
||||
import com.kgurgul.cpuinfo.features.HostViewModel
|
||||
import com.kgurgul.cpuinfo.ui.shouldUseDarkTheme
|
||||
import com.kgurgul.cpuinfo.ui.theme.CpuInfoTheme
|
||||
import com.kgurgul.cpuinfo.ui.theme.DarkColors
|
||||
import com.kgurgul.cpuinfo.ui.theme.LightColors
|
||||
import com.kgurgul.cpuinfo.ui.theme.darkPrimary
|
||||
import com.kgurgul.cpuinfo.ui.theme.lightPrimary
|
||||
import com.kgurgul.cpuinfo.utils.AndroidShortcutManager
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class HostActivity : ComponentActivity() {
|
||||
|
||||
private val viewModel: HostViewModel by viewModel()
|
||||
private val androidShortcutManager: AndroidShortcutManager by inject()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
var uiState: HostViewModel.UiState by mutableStateOf(HostViewModel.UiState())
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.uiStateFlow.onEach { uiState = it }.collect()
|
||||
}
|
||||
}
|
||||
splashScreen.setKeepOnScreenCondition { uiState.isLoading }
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
val darkTheme = shouldUseDarkTheme(uiState)
|
||||
val systemBarScrim = (if (darkTheme) darkPrimary else lightPrimary).toArgb()
|
||||
DisposableEffect(darkTheme) {
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.dark(systemBarScrim),
|
||||
navigationBarStyle = SystemBarStyle.dark(systemBarScrim),
|
||||
)
|
||||
onDispose {}
|
||||
}
|
||||
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
val colors =
|
||||
when {
|
||||
dynamicColor && darkTheme -> {
|
||||
val dynamicDarkColors = dynamicDarkColorScheme(LocalContext.current)
|
||||
DarkColors.copy(
|
||||
secondary = dynamicDarkColors.secondaryFixed,
|
||||
onSecondary = dynamicDarkColors.onSecondaryFixed,
|
||||
)
|
||||
}
|
||||
|
||||
dynamicColor && !darkTheme -> {
|
||||
val dynamicLightColors = dynamicLightColorScheme(LocalContext.current)
|
||||
LightColors.copy(
|
||||
secondary = dynamicLightColors.secondaryFixed,
|
||||
onSecondary = dynamicLightColors.onSecondaryFixed,
|
||||
)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColors
|
||||
else -> LightColors
|
||||
}
|
||||
LaunchedEffect(uiState.isApplicationSectionVisible) {
|
||||
androidShortcutManager.createShortcuts(uiState.isApplicationSectionVisible)
|
||||
}
|
||||
CpuInfoTheme(useDarkTheme = darkTheme, colors = colors) {
|
||||
HostScreen(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.features.cputile
|
||||
|
||||
import android.graphics.drawable.Icon
|
||||
@@ -7,34 +22,32 @@ import androidx.annotation.RequiresApi
|
||||
import com.kgurgul.cpuinfo.R
|
||||
import com.kgurgul.cpuinfo.data.provider.CpuDataProvider
|
||||
import com.kgurgul.cpuinfo.utils.IDispatchersProvider
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import com.kgurgul.cpuinfo.utils.formatHz
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@AndroidEntryPoint
|
||||
class CpuTileService : TileService(), CoroutineScope {
|
||||
class CpuTileService : TileService(), CoroutineScope, KoinComponent {
|
||||
|
||||
@Inject
|
||||
lateinit var cpuDataProvider: CpuDataProvider
|
||||
private val cpuDataProvider: CpuDataProvider by inject()
|
||||
private val dispatchersProvider: IDispatchersProvider by inject()
|
||||
|
||||
@Inject
|
||||
lateinit var dispatchersProvider: IDispatchersProvider
|
||||
|
||||
private val job = SupervisorJob()
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + dispatchersProvider.main
|
||||
get() = SupervisorJob() + dispatchersProvider.main
|
||||
|
||||
private var refreshingJob: Job? = null
|
||||
|
||||
private val minMaxAvg: Pair<Long, Long> by lazy {
|
||||
val cpuCount = cpuDataProvider.getNumberOfCores()
|
||||
val cpuCount = cpuDataProvider.getNumberOfLogicalCores()
|
||||
val minFreq = mutableListOf<Long>()
|
||||
val maxFreq = mutableListOf<Long>()
|
||||
for (i in 0 until cpuCount) {
|
||||
@@ -49,7 +62,7 @@ class CpuTileService : TileService(), CoroutineScope {
|
||||
mapOf(
|
||||
CPULoad.Low to Icon.createWithResource(this, R.drawable.ic_cpu_low),
|
||||
CPULoad.Medium to Icon.createWithResource(this, R.drawable.ic_cpu_med),
|
||||
CPULoad.High to Icon.createWithResource(this, R.drawable.ic_cpu_high)
|
||||
CPULoad.High to Icon.createWithResource(this, R.drawable.ic_cpu_high),
|
||||
)
|
||||
}
|
||||
private val defaultIcon by lazy { Icon.createWithResource(this, R.drawable.ic_cpu_high) }
|
||||
@@ -59,11 +72,13 @@ class CpuTileService : TileService(), CoroutineScope {
|
||||
refreshingJob?.cancel()
|
||||
refreshingJob = launch {
|
||||
while (true) {
|
||||
val load = withContext(dispatchersProvider.io) { averageCPUFreq() }
|
||||
qsTile.label = "Avg ${load}MHz"
|
||||
qsTile.icon = getLoadIcon(load)
|
||||
qsTile.updateTile()
|
||||
delay(REFRESHING_DELAY_MS)
|
||||
val load = getAverageCPUFreq()
|
||||
qsTile?.run {
|
||||
label = "Avg ${formatHz(load)}"
|
||||
icon = getLoadIcon(load)
|
||||
updateTile()
|
||||
}
|
||||
delay(REFRESHING_DELAY.milliseconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,37 +89,42 @@ class CpuTileService : TileService(), CoroutineScope {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
job.cancel()
|
||||
coroutineContext.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun getLoadIcon(avgLoad: Long): Icon {
|
||||
val freqDiff = minMaxAvg.second - minMaxAvg.first
|
||||
val freqThirds = freqDiff / 3
|
||||
val loadEnum = when {
|
||||
avgLoad >= minMaxAvg.second - freqThirds -> CPULoad.High
|
||||
minMaxAvg.second - freqThirds > avgLoad && avgLoad >= minMaxAvg.first + freqThirds -> CPULoad.Medium
|
||||
else -> CPULoad.Low
|
||||
}
|
||||
val loadEnum =
|
||||
when {
|
||||
avgLoad >= minMaxAvg.second - freqThirds -> CPULoad.High
|
||||
minMaxAvg.second - freqThirds > avgLoad &&
|
||||
avgLoad >= minMaxAvg.first + freqThirds -> CPULoad.Medium
|
||||
|
||||
else -> CPULoad.Low
|
||||
}
|
||||
return icons.getOrDefault(loadEnum, defaultIcon)
|
||||
}
|
||||
|
||||
private fun averageCPUFreq(): Long {
|
||||
val cpuCount = cpuDataProvider.getNumberOfCores()
|
||||
var sumFreq = 0L
|
||||
for (i in 0 until cpuCount) {
|
||||
sumFreq += cpuDataProvider.getCurrentFreq(i)
|
||||
private suspend fun getAverageCPUFreq(): Long {
|
||||
return withContext(dispatchersProvider.io) {
|
||||
val cpuCount = cpuDataProvider.getNumberOfLogicalCores()
|
||||
var sumFreq = 0L
|
||||
for (i in 0 until cpuCount) {
|
||||
sumFreq += cpuDataProvider.getCurrentFreq(i)
|
||||
}
|
||||
sumFreq / cpuCount
|
||||
}
|
||||
return (sumFreq / cpuCount)
|
||||
}
|
||||
|
||||
enum class CPULoad {
|
||||
Low,
|
||||
Medium,
|
||||
High
|
||||
High,
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REFRESHING_DELAY_MS = 1000L
|
||||
private const val REFRESHING_DELAY = 1000L
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 KG Soft
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -13,38 +13,36 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kgurgul.cpuinfo.features
|
||||
package com.kgurgul.cpuinfo.tv
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kgurgul.cpuinfo.domain.model.DarkThemeConfig
|
||||
import com.kgurgul.cpuinfo.features.HostViewModel
|
||||
import com.kgurgul.cpuinfo.tv.features.TvHostScreen
|
||||
import com.kgurgul.cpuinfo.ui.shouldUseDarkTheme
|
||||
import com.kgurgul.cpuinfo.ui.theme.CpuInfoTheme
|
||||
import com.kgurgul.cpuinfo.ui.theme.darkPrimary
|
||||
import com.kgurgul.cpuinfo.ui.theme.lightPrimary
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HostActivity : AppCompatActivity() {
|
||||
class TvActivity : ComponentActivity() {
|
||||
|
||||
private val viewModel: HostViewModel by viewModels()
|
||||
private val viewModel: HostViewModel by viewModel()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
@@ -53,9 +51,7 @@ class HostActivity : AppCompatActivity() {
|
||||
var uiState: HostViewModel.UiState by mutableStateOf(HostViewModel.UiState())
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.uiStateFlow
|
||||
.onEach { uiState = it }
|
||||
.collect()
|
||||
viewModel.uiStateFlow.onEach { uiState = it }.collect()
|
||||
}
|
||||
}
|
||||
splashScreen.setKeepOnScreenCondition { uiState.isLoading }
|
||||
@@ -64,7 +60,7 @@ class HostActivity : AppCompatActivity() {
|
||||
|
||||
setContent {
|
||||
val darkTheme = shouldUseDarkTheme(uiState)
|
||||
val systemBarScrim = if (darkTheme) darkPrimary else lightPrimary
|
||||
val systemBarScrim = (if (darkTheme) darkPrimary else lightPrimary).toArgb()
|
||||
DisposableEffect(darkTheme) {
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.dark(systemBarScrim),
|
||||
@@ -72,22 +68,7 @@ class HostActivity : AppCompatActivity() {
|
||||
)
|
||||
onDispose {}
|
||||
}
|
||||
CpuInfoTheme(
|
||||
useDarkTheme = darkTheme,
|
||||
) {
|
||||
HostScreen(
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
CpuInfoTheme(useDarkTheme = darkTheme) { TvHostScreen(viewModel = viewModel) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun shouldUseDarkTheme(
|
||||
uiState: HostViewModel.UiState,
|
||||
): Boolean = when (uiState.darkThemeConfig) {
|
||||
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme()
|
||||
DarkThemeConfig.LIGHT -> false
|
||||
DarkThemeConfig.DARK -> true
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features
|
||||
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.tv.material3.Icon
|
||||
import androidx.tv.material3.NavigationDrawer
|
||||
import androidx.tv.material3.NavigationDrawerItem
|
||||
import androidx.tv.material3.NavigationDrawerItemDefaults
|
||||
import com.kgurgul.cpuinfo.features.HostViewModel
|
||||
import com.kgurgul.cpuinfo.shared.Res
|
||||
import com.kgurgul.cpuinfo.shared.applications
|
||||
import com.kgurgul.cpuinfo.shared.hardware
|
||||
import com.kgurgul.cpuinfo.shared.ic_android
|
||||
import com.kgurgul.cpuinfo.shared.ic_cpu
|
||||
import com.kgurgul.cpuinfo.shared.ic_settings
|
||||
import com.kgurgul.cpuinfo.shared.ic_temperature
|
||||
import com.kgurgul.cpuinfo.shared.settings
|
||||
import com.kgurgul.cpuinfo.shared.temp
|
||||
import com.kgurgul.cpuinfo.tv.features.applications.TvApplicationsBaseRoute
|
||||
import com.kgurgul.cpuinfo.tv.features.applications.tvApplicationsScreen
|
||||
import com.kgurgul.cpuinfo.tv.features.information.TvInformationBaseRoute
|
||||
import com.kgurgul.cpuinfo.tv.features.information.tvInformationScreen
|
||||
import com.kgurgul.cpuinfo.tv.features.settings.TvSettingsBaseRoute
|
||||
import com.kgurgul.cpuinfo.tv.features.settings.tvSettingsScreen
|
||||
import com.kgurgul.cpuinfo.tv.features.temperature.TvTemperaturesBaseRoute
|
||||
import com.kgurgul.cpuinfo.tv.features.temperature.tvTemperaturesScreen
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingMedium
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import com.kgurgul.cpuinfo.utils.navigation.TopLevelRoute
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun TvHostScreen(viewModel: HostViewModel = koinViewModel()) {
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
TvHostScreen(uiState = uiState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvHostScreen(uiState: HostViewModel.UiState) {
|
||||
val navController = rememberNavController()
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
NavigationDrawer(
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
||||
drawerContent = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxHeight().padding(spacingMedium).selectableGroup(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement =
|
||||
Arrangement.spacedBy(spacingSmall, Alignment.CenterVertically),
|
||||
) {
|
||||
buildTopLevelRoutes(isApplicationsVisible = uiState.isApplicationSectionVisible)
|
||||
.forEach { topLevelRoute ->
|
||||
NavigationDrawerItem(
|
||||
selected =
|
||||
currentDestination?.hierarchy?.any {
|
||||
it.hasRoute(topLevelRoute.route::class)
|
||||
} == true,
|
||||
onClick = {
|
||||
navController.navigate(topLevelRoute.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
painter = painterResource(topLevelRoute.icon),
|
||||
contentDescription = stringResource(topLevelRoute.name),
|
||||
)
|
||||
},
|
||||
colors =
|
||||
NavigationDrawerItemDefaults.colors(
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceTint,
|
||||
focusedContentColor = MaterialTheme.colorScheme.onBackground,
|
||||
selectedContentColor = MaterialTheme.colorScheme.onBackground,
|
||||
selectedContainerColor =
|
||||
MaterialTheme.colorScheme.surfaceTint.copy(alpha = 0.4f),
|
||||
pressedContentColor = MaterialTheme.colorScheme.onBackground,
|
||||
pressedContainerColor =
|
||||
MaterialTheme.colorScheme.surfaceTint.copy(alpha = 0.4f),
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(topLevelRoute.name),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = TvInformationBaseRoute,
|
||||
enterTransition = { fadeIn() },
|
||||
exitTransition = { fadeOut() },
|
||||
popEnterTransition = { fadeIn() },
|
||||
popExitTransition = { fadeOut() },
|
||||
) {
|
||||
tvInformationScreen()
|
||||
tvApplicationsScreen()
|
||||
tvTemperaturesScreen()
|
||||
tvSettingsScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildTopLevelRoutes(isApplicationsVisible: Boolean) = buildList {
|
||||
add(
|
||||
TopLevelRoute(
|
||||
name = Res.string.hardware,
|
||||
route = TvInformationBaseRoute,
|
||||
icon = Res.drawable.ic_cpu,
|
||||
)
|
||||
)
|
||||
if (isApplicationsVisible) {
|
||||
add(
|
||||
TopLevelRoute(
|
||||
name = Res.string.applications,
|
||||
route = TvApplicationsBaseRoute,
|
||||
icon = Res.drawable.ic_android,
|
||||
)
|
||||
)
|
||||
}
|
||||
add(
|
||||
TopLevelRoute(
|
||||
name = Res.string.temp,
|
||||
route = TvTemperaturesBaseRoute,
|
||||
icon = Res.drawable.ic_temperature,
|
||||
)
|
||||
)
|
||||
add(
|
||||
TopLevelRoute(
|
||||
name = Res.string.settings,
|
||||
route = TvSettingsBaseRoute,
|
||||
icon = Res.drawable.ic_settings,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features.applications
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.tv.material3.Icon
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.LocalPlatformContext
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import com.kgurgul.cpuinfo.domain.model.ExtendedApplicationData
|
||||
import com.kgurgul.cpuinfo.features.applications.ApplicationsScreenTestData
|
||||
import com.kgurgul.cpuinfo.features.applications.ApplicationsViewModel
|
||||
import com.kgurgul.cpuinfo.features.applications.registerUninstallListener
|
||||
import com.kgurgul.cpuinfo.shared.Res
|
||||
import com.kgurgul.cpuinfo.shared.apps_open
|
||||
import com.kgurgul.cpuinfo.shared.apps_settings
|
||||
import com.kgurgul.cpuinfo.shared.apps_show_system_apps
|
||||
import com.kgurgul.cpuinfo.shared.apps_sort_order
|
||||
import com.kgurgul.cpuinfo.shared.apps_uninstall
|
||||
import com.kgurgul.cpuinfo.shared.ic_apk_document_filled
|
||||
import com.kgurgul.cpuinfo.shared.ic_apk_document_outlined
|
||||
import com.kgurgul.cpuinfo.shared.ok
|
||||
import com.kgurgul.cpuinfo.shared.refresh
|
||||
import com.kgurgul.cpuinfo.shared.search
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvAlertDialog
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvButton
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvIconButton
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvListItem
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvWideButton
|
||||
import com.kgurgul.cpuinfo.ui.components.CpuPullToRefreshBox
|
||||
import com.kgurgul.cpuinfo.ui.components.CpuSnackbar
|
||||
import com.kgurgul.cpuinfo.ui.components.CpuTextField
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingMedium
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingXSmall
|
||||
import com.kgurgul.cpuinfo.utils.navigation.NavigationConst
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Serializable
|
||||
data object TvApplicationsBaseRoute {
|
||||
|
||||
@Serializable data object TvApplicationsRoute
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.tvApplicationsScreen() {
|
||||
navigation<TvApplicationsBaseRoute>(
|
||||
startDestination = TvApplicationsBaseRoute.TvApplicationsRoute,
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<TvApplicationsBaseRoute>(
|
||||
basePath = NavigationConst.BASE_URL + NavigationConst.APPLICATIONS
|
||||
)
|
||||
),
|
||||
) {
|
||||
composable<TvApplicationsBaseRoute.TvApplicationsRoute> { TvApplicationsScreen() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvApplicationsScreen(viewModel: ApplicationsViewModel = koinViewModel()) {
|
||||
registerUninstallListener(onRefresh = viewModel::onRefreshApplications)
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle()
|
||||
TvApplicationsScreen(
|
||||
uiState = uiState,
|
||||
onAppClicked = viewModel::onApplicationClicked,
|
||||
onRefreshApplications = viewModel::onRefreshApplications,
|
||||
onSnackbarDismissed = viewModel::onSnackbarDismissed,
|
||||
onAppUninstallClicked = viewModel::onAppUninstallClicked,
|
||||
onAppSettingsClicked = viewModel::onAppSettingsClicked,
|
||||
onSystemAppsSwitched = viewModel::onSystemAppsSwitched,
|
||||
onSortOrderChange = viewModel::onSortOrderChange,
|
||||
searchQuery = searchQuery,
|
||||
onSearchQueryChanged = viewModel::onSearchQueryChanged,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TvApplicationsScreen(
|
||||
uiState: ApplicationsViewModel.UiState,
|
||||
onAppClicked: (packageName: String) -> Unit,
|
||||
onRefreshApplications: () -> Unit,
|
||||
onSnackbarDismissed: () -> Unit,
|
||||
onAppUninstallClicked: (id: String) -> Unit,
|
||||
onAppSettingsClicked: (id: String) -> Unit,
|
||||
onSystemAppsSwitched: (enabled: Boolean) -> Unit,
|
||||
onSortOrderChange: (ascending: Boolean) -> Unit,
|
||||
searchQuery: String,
|
||||
onSearchQueryChanged: (String) -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
val snackbarMessageString = uiState.snackbarMessage?.let { stringResource(it) }
|
||||
LaunchedEffect(snackbarMessageString) {
|
||||
scope.launch {
|
||||
if (snackbarMessageString != null) {
|
||||
val result = snackbarHostState.showSnackbar(snackbarMessageString)
|
||||
if (result == SnackbarResult.Dismissed) {
|
||||
onSnackbarDismissed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var optionsDialogId by remember { mutableStateOf("") }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
withSystemApps = uiState.withSystemApps,
|
||||
onRefreshApplications = onRefreshApplications,
|
||||
onSystemAppsSwitched = onSystemAppsSwitched,
|
||||
isSortAscending = uiState.isSortAscending,
|
||||
onSortOrderChange = onSortOrderChange,
|
||||
searchQuery = searchQuery,
|
||||
onSearchQueryChanged = onSearchQueryChanged,
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal),
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) { data -> CpuSnackbar(data) } },
|
||||
) { innerPaddingModifier ->
|
||||
CpuPullToRefreshBox(
|
||||
isRefreshing = uiState.isLoading,
|
||||
enabled = false,
|
||||
onRefresh = { onRefreshApplications() },
|
||||
modifier = Modifier.fillMaxSize().padding(innerPaddingModifier),
|
||||
) {
|
||||
ApplicationsList(
|
||||
appList = uiState.applications,
|
||||
onAppClicked = { optionsDialogId = it },
|
||||
)
|
||||
}
|
||||
OptionsDialog(
|
||||
isVisible = optionsDialogId.isNotEmpty(),
|
||||
onDismissRequest = { optionsDialogId = "" },
|
||||
appId = optionsDialogId,
|
||||
onOpenAppClicked = onAppClicked,
|
||||
onSettingsClicked = onAppSettingsClicked,
|
||||
onUninstallAppClicked = onAppUninstallClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
withSystemApps: Boolean,
|
||||
onRefreshApplications: () -> Unit,
|
||||
onSystemAppsSwitched: (enabled: Boolean) -> Unit,
|
||||
isSortAscending: Boolean,
|
||||
onSortOrderChange: (ascending: Boolean) -> Unit,
|
||||
searchQuery: String,
|
||||
onSearchQueryChanged: (String) -> Unit,
|
||||
) {
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
BackHandler(enabled = showSearch) { showSearch = false }
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(spacingMedium, Alignment.End),
|
||||
modifier = Modifier.fillMaxWidth().padding(spacingMedium),
|
||||
) {
|
||||
AnimatedVisibility(visible = showSearch) {
|
||||
SearchTextField(
|
||||
searchQuery = searchQuery,
|
||||
onSearchQueryChanged = onSearchQueryChanged,
|
||||
onSearchClosed = { showSearch = false },
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = !showSearch) {
|
||||
TvIconButton(onClick = { showSearch = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = stringResource(Res.string.search),
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(visible = !showSearch) {
|
||||
TvIconButton(onClick = onRefreshApplications) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = stringResource(Res.string.refresh),
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(visible = !showSearch) {
|
||||
TvIconButton(onClick = { onSortOrderChange(!isSortAscending) }) {
|
||||
val icon =
|
||||
if (isSortAscending) {
|
||||
Icons.Default.KeyboardArrowDown
|
||||
} else {
|
||||
Icons.Default.KeyboardArrowUp
|
||||
}
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = stringResource(Res.string.apps_sort_order),
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(visible = !showSearch) {
|
||||
val systemAppIcon =
|
||||
if (withSystemApps) {
|
||||
Res.drawable.ic_apk_document_filled
|
||||
} else {
|
||||
Res.drawable.ic_apk_document_outlined
|
||||
}
|
||||
TvIconButton(onClick = { onSystemAppsSwitched(!withSystemApps) }) {
|
||||
Icon(
|
||||
painter = painterResource(systemAppIcon),
|
||||
contentDescription = stringResource(Res.string.apps_show_system_apps),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ApplicationsList(
|
||||
appList: ImmutableList<ExtendedApplicationData>,
|
||||
onAppClicked: (packageName: String) -> Unit,
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize().padding(spacingMedium)) {
|
||||
items(items = appList, key = { item -> item.packageName }) { item ->
|
||||
TvListItem(
|
||||
onClick = { onAppClicked(item.packageName) },
|
||||
modifier = Modifier.animateItem(),
|
||||
) {
|
||||
Row { ApplicationItem(appData = item, modifier = Modifier.weight(1f)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ApplicationItem(appData: ExtendedApplicationData, modifier: Modifier = Modifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
|
||||
AsyncImage(
|
||||
model =
|
||||
ImageRequest.Builder(LocalPlatformContext.current)
|
||||
.data(appData.appIconUri)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = appData.name,
|
||||
modifier = Modifier.size(50.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.size(spacingMedium))
|
||||
SelectionContainer {
|
||||
Column(modifier = Modifier.padding(horizontal = spacingXSmall)) {
|
||||
Text(
|
||||
text = appData.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = appData.packageName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = appData.versionName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OptionsDialog(
|
||||
isVisible: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
appId: String,
|
||||
onOpenAppClicked: (id: String) -> Unit,
|
||||
onSettingsClicked: (id: String) -> Unit,
|
||||
onUninstallAppClicked: (id: String) -> Unit,
|
||||
) {
|
||||
if (isVisible) {
|
||||
TvAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
text = {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(horizontal = spacingSmall),
|
||||
verticalArrangement = Arrangement.spacedBy(spacingSmall),
|
||||
) {
|
||||
item(key = "__open") {
|
||||
TvWideButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onOpenAppClicked(appId)
|
||||
},
|
||||
title = { Text(text = stringResource(Res.string.apps_open)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
item(key = "__settings") {
|
||||
TvWideButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onSettingsClicked(appId)
|
||||
},
|
||||
title = { Text(text = stringResource(Res.string.apps_settings)) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
item(key = "__uninstall") {
|
||||
TvWideButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onUninstallAppClicked(appId)
|
||||
},
|
||||
title = { Text(text = stringResource(Res.string.apps_uninstall)) },
|
||||
icon = {
|
||||
Icon(imageVector = Icons.Default.Delete, contentDescription = null)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TvButton(onClick = onDismissRequest) {
|
||||
androidx.tv.material3.Text(text = stringResource(Res.string.ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchTextField(
|
||||
searchQuery: String,
|
||||
onSearchQueryChanged: (String) -> Unit,
|
||||
onSearchClosed: () -> Unit,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
val onSearchExplicitlyTriggered = {
|
||||
keyboardController?.hide()
|
||||
onSearchClosed()
|
||||
}
|
||||
|
||||
CpuTextField(
|
||||
colors =
|
||||
TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
cursorColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = stringResource(Res.string.search),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
onValueChange = { if ("\n" !in it) onSearchQueryChanged(it) },
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
|
||||
.padding(spacingSmall)
|
||||
.focusRequester(focusRequester)
|
||||
.onKeyEvent {
|
||||
if (it.key == Key.Enter) {
|
||||
if (searchQuery.isBlank()) return@onKeyEvent false
|
||||
onSearchExplicitlyTriggered()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
.testTag(ApplicationsScreenTestData.SEARCH_TEST_TAG),
|
||||
shape = RoundedCornerShape(32.dp),
|
||||
value = searchQuery,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onSearch = {
|
||||
if (searchQuery.isBlank()) return@KeyboardActions
|
||||
onSearchExplicitlyTriggered()
|
||||
}
|
||||
),
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
)
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
@file:OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||
|
||||
package com.kgurgul.cpuinfo.tv.features.information
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.navDeepLink
|
||||
import androidx.tv.material3.Tab
|
||||
import androidx.tv.material3.TabRow
|
||||
import androidx.tv.material3.TabRowDefaults
|
||||
import com.kgurgul.cpuinfo.features.information.InfoContainerViewModel
|
||||
import com.kgurgul.cpuinfo.features.information.InfoContainerViewModel.Companion.ANDROID_POS
|
||||
import com.kgurgul.cpuinfo.features.information.InfoContainerViewModel.Companion.CPU_POS
|
||||
import com.kgurgul.cpuinfo.features.information.InfoContainerViewModel.Companion.GPU_POS
|
||||
import com.kgurgul.cpuinfo.features.information.InfoContainerViewModel.Companion.HARDWARE_POS
|
||||
import com.kgurgul.cpuinfo.features.information.InfoContainerViewModel.Companion.INFO_PAGE_AMOUNT
|
||||
import com.kgurgul.cpuinfo.features.information.InfoContainerViewModel.Companion.RAM_POS
|
||||
import com.kgurgul.cpuinfo.features.information.InfoContainerViewModel.Companion.SCREEN_POS
|
||||
import com.kgurgul.cpuinfo.features.information.InfoContainerViewModel.Companion.SENSORS_POS
|
||||
import com.kgurgul.cpuinfo.features.information.InfoContainerViewModel.Companion.STORAGE_POS
|
||||
import com.kgurgul.cpuinfo.features.information.getTabTitle
|
||||
import com.kgurgul.cpuinfo.tv.features.information.cpu.TvCpuInfoScreen
|
||||
import com.kgurgul.cpuinfo.tv.features.information.gpu.TvGpuInfoScreen
|
||||
import com.kgurgul.cpuinfo.tv.features.information.hardware.TvHardwareInfoScreen
|
||||
import com.kgurgul.cpuinfo.tv.features.information.os.TvOsInfoScreen
|
||||
import com.kgurgul.cpuinfo.tv.features.information.ram.TvRamInfoScreen
|
||||
import com.kgurgul.cpuinfo.tv.features.information.screen.TvScreenInfoScreen
|
||||
import com.kgurgul.cpuinfo.tv.features.information.sensors.TvSensorsInfoScreen
|
||||
import com.kgurgul.cpuinfo.tv.features.information.storage.TvStorageInfoScreen
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingMedium
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import com.kgurgul.cpuinfo.utils.navigation.NavigationConst
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Serializable
|
||||
data object TvInformationBaseRoute {
|
||||
|
||||
@Serializable object TvInformationRoute
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.tvInformationScreen() {
|
||||
navigation<TvInformationBaseRoute>(
|
||||
startDestination = TvInformationBaseRoute.TvInformationRoute,
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<TvInformationBaseRoute>(
|
||||
basePath = NavigationConst.BASE_URL + NavigationConst.INFORMATION
|
||||
)
|
||||
),
|
||||
) {
|
||||
composable<TvInformationBaseRoute.TvInformationRoute> { TvInfoContainerScreen() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvInfoContainerScreen(viewModel: InfoContainerViewModel = koinViewModel()) {
|
||||
TvInfoContainerScreen(onPageChanged = viewModel::onPageChanged)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvInfoContainerScreen(onPageChanged: (Int) -> Unit) {
|
||||
Scaffold(contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)) {
|
||||
paddingValues ->
|
||||
InfoContainer(
|
||||
paddingValues = paddingValues,
|
||||
onPageChanged = onPageChanged,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoContainer(
|
||||
paddingValues: PaddingValues,
|
||||
onPageChanged: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val tabTitles = (0 until INFO_PAGE_AMOUNT).map { getTabTitle(position = it) }
|
||||
var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||
Column(modifier = Modifier.padding(top = paddingValues.calculateTopPadding()).then(modifier)) {
|
||||
TabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
indicator = { tabPositions, doesTabRowHaveFocus ->
|
||||
TabRowDefaults.PillIndicator(
|
||||
currentTabPosition = tabPositions[selectedTabIndex],
|
||||
doesTabRowHaveFocus = doesTabRowHaveFocus,
|
||||
activeColor = MaterialTheme.colorScheme.surfaceTint,
|
||||
inactiveColor = MaterialTheme.colorScheme.surfaceTint.copy(alpha = 0.4f),
|
||||
)
|
||||
},
|
||||
modifier =
|
||||
Modifier.align(Alignment.CenterHorizontally).focusRestorer().padding(spacingMedium),
|
||||
) {
|
||||
tabTitles.forEachIndexed { index, tab ->
|
||||
key(index) {
|
||||
Tab(
|
||||
selected = index == selectedTabIndex,
|
||||
onFocus = { selectedTabIndex = index },
|
||||
) {
|
||||
Text(
|
||||
text = tab,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
horizontal = spacingMedium,
|
||||
vertical = spacingSmall,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = selectedTabIndex,
|
||||
label = "screen_transition",
|
||||
transitionSpec = {
|
||||
fadeIn(animationSpec = tween(220, delayMillis = 90))
|
||||
.togetherWith(fadeOut(animationSpec = tween(90)))
|
||||
},
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
start = paddingValues.calculateStartPadding(LayoutDirection.Ltr),
|
||||
end = paddingValues.calculateEndPadding(LayoutDirection.Ltr),
|
||||
),
|
||||
) { targetPos ->
|
||||
when (targetPos) {
|
||||
CPU_POS -> TvCpuInfoScreen()
|
||||
GPU_POS -> TvGpuInfoScreen()
|
||||
RAM_POS -> TvRamInfoScreen()
|
||||
STORAGE_POS -> TvStorageInfoScreen()
|
||||
SCREEN_POS -> TvScreenInfoScreen()
|
||||
ANDROID_POS -> TvOsInfoScreen()
|
||||
HARDWARE_POS -> TvHardwareInfoScreen()
|
||||
SENSORS_POS -> TvSensorsInfoScreen()
|
||||
else -> throw IllegalArgumentException("Unknown position")
|
||||
}
|
||||
onPageChanged(targetPos)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features.information.cpu
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.kgurgul.cpuinfo.domain.model.CpuData
|
||||
import com.kgurgul.cpuinfo.domain.model.getName
|
||||
import com.kgurgul.cpuinfo.domain.model.getValue
|
||||
import com.kgurgul.cpuinfo.features.information.base.InformationRow
|
||||
import com.kgurgul.cpuinfo.features.information.cpu.CpuInfoViewModel
|
||||
import com.kgurgul.cpuinfo.shared.Res
|
||||
import com.kgurgul.cpuinfo.shared.cpu_current_frequency
|
||||
import com.kgurgul.cpuinfo.shared.cpu_frequency_stopped
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvListItem
|
||||
import com.kgurgul.cpuinfo.ui.components.CpuProgressBar
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import com.kgurgul.cpuinfo.utils.formatHz
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun TvCpuInfoScreen(viewModel: CpuInfoViewModel = koinViewModel()) {
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
TvCpuInfoScreen(uiState = uiState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvCpuInfoScreen(uiState: CpuInfoViewModel.UiState) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(spacingSmall),
|
||||
verticalArrangement = Arrangement.spacedBy(spacingSmall),
|
||||
modifier = Modifier.fillMaxSize().testTag(TvCpuInfoScreenTestTags.LAZY_COLUMN),
|
||||
) {
|
||||
uiState.cpuData?.let { cpuData ->
|
||||
cpuData.frequencies.forEachIndexed { i, frequency ->
|
||||
item(key = "__frequency_$i") {
|
||||
TvListItem { FrequencyItem(index = i, frequency = frequency) }
|
||||
}
|
||||
}
|
||||
items(cpuData.cpuItems) { itemValue ->
|
||||
TvListItem {
|
||||
InformationRow(
|
||||
title = itemValue.getName(),
|
||||
value = itemValue.getValue(),
|
||||
isLastItem = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FrequencyItem(index: Int, frequency: CpuData.Frequency) {
|
||||
val currentFreq =
|
||||
if (frequency.current != -1L) {
|
||||
stringResource(Res.string.cpu_current_frequency, index, formatHz(frequency.current))
|
||||
} else {
|
||||
stringResource(Res.string.cpu_frequency_stopped, index)
|
||||
}
|
||||
val minFreq =
|
||||
if (frequency.min != -1L) {
|
||||
formatHz(0)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val maxFreq =
|
||||
if (frequency.max != -1L) {
|
||||
formatHz(frequency.max)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val progress =
|
||||
if (frequency.current != -1L && frequency.max != 0L) {
|
||||
frequency.current.toFloat() / frequency.max.toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
CpuProgressBar(label = currentFreq, progress = progress, minMaxValues = minFreq to maxFreq)
|
||||
}
|
||||
|
||||
object TvCpuInfoScreenTestTags {
|
||||
const val LAZY_COLUMN = "cpu_info_lazy_column"
|
||||
const val SOCKET_NAME = "cpu_info_socket_name"
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features.information.gpu
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.kgurgul.cpuinfo.domain.model.getKey
|
||||
import com.kgurgul.cpuinfo.domain.model.getName
|
||||
import com.kgurgul.cpuinfo.domain.model.getValue
|
||||
import com.kgurgul.cpuinfo.features.information.base.InformationRow
|
||||
import com.kgurgul.cpuinfo.features.information.gpu.GpuInfoViewModel
|
||||
import com.kgurgul.cpuinfo.features.information.gpu.InternalGLSurfaceView
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvListItem
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun TvGpuInfoScreen(viewModel: GpuInfoViewModel = koinViewModel()) {
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
Box {
|
||||
InternalGLSurfaceView(
|
||||
onGlInfoReceived = { vendor, renderer, extensions ->
|
||||
viewModel.onGlInfoReceived(vendor, renderer, extensions)
|
||||
}
|
||||
)
|
||||
TvGpuInfoScreen(uiState = uiState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvGpuInfoScreen(uiState: GpuInfoViewModel.UiState) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(spacingSmall),
|
||||
verticalArrangement = Arrangement.spacedBy(spacingSmall),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(uiState.gpuData, key = { itemValue -> itemValue.getKey() }) { itemValue ->
|
||||
TvListItem {
|
||||
InformationRow(
|
||||
title = itemValue.getName(),
|
||||
value = itemValue.getValue(),
|
||||
isLastItem = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features.information.hardware
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.kgurgul.cpuinfo.domain.model.getName
|
||||
import com.kgurgul.cpuinfo.domain.model.getValue
|
||||
import com.kgurgul.cpuinfo.features.information.base.InformationRow
|
||||
import com.kgurgul.cpuinfo.features.information.hardware.HardwareInfoViewModel
|
||||
import com.kgurgul.cpuinfo.features.information.hardware.RegisterPowerPlugListener
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvListItem
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun TvHardwareInfoScreen(viewModel: HardwareInfoViewModel = koinViewModel()) {
|
||||
RegisterPowerPlugListener(onRefresh = viewModel::refreshHardwareInfo)
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
TvHardwareInfoScreen(uiState = uiState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvHardwareInfoScreen(uiState: HardwareInfoViewModel.UiState) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(spacingSmall),
|
||||
verticalArrangement = Arrangement.spacedBy(spacingSmall),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(uiState.hardwareItems) { itemValue ->
|
||||
TvListItem {
|
||||
InformationRow(
|
||||
title = itemValue.getName(),
|
||||
value = itemValue.getValue(),
|
||||
isLastItem = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features.information.os
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.kgurgul.cpuinfo.domain.model.getKey
|
||||
import com.kgurgul.cpuinfo.domain.model.getName
|
||||
import com.kgurgul.cpuinfo.domain.model.getValue
|
||||
import com.kgurgul.cpuinfo.features.information.base.InformationRow
|
||||
import com.kgurgul.cpuinfo.features.information.os.OsInfoViewModel
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvListItem
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun TvOsInfoScreen(viewModel: OsInfoViewModel = koinViewModel()) {
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
TvOsInfoScreen(uiState = uiState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvOsInfoScreen(uiState: OsInfoViewModel.UiState) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(spacingSmall),
|
||||
verticalArrangement = Arrangement.spacedBy(spacingSmall),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(uiState.items, key = { itemValue -> itemValue.getKey() }) { itemValue ->
|
||||
TvListItem {
|
||||
InformationRow(
|
||||
title = itemValue.getName(),
|
||||
value = itemValue.getValue(),
|
||||
isLastItem = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features.information.ram
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.kgurgul.cpuinfo.features.information.ram.RamInfoViewModel
|
||||
import com.kgurgul.cpuinfo.shared.Res
|
||||
import com.kgurgul.cpuinfo.shared.available_memory
|
||||
import com.kgurgul.cpuinfo.shared.threshold
|
||||
import com.kgurgul.cpuinfo.shared.total_memory
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvListItem
|
||||
import com.kgurgul.cpuinfo.ui.components.CpuProgressBar
|
||||
import com.kgurgul.cpuinfo.ui.components.ItemValueRow
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import com.kgurgul.cpuinfo.utils.Utils
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun TvRamInfoScreen(viewModel: RamInfoViewModel = koinViewModel()) {
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
TvRamInfoScreen(uiState = uiState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvRamInfoScreen(uiState: RamInfoViewModel.UiState) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(spacingSmall),
|
||||
verticalArrangement = Arrangement.spacedBy(spacingSmall),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
uiState.ramData?.let { ramData ->
|
||||
item(key = "__total") {
|
||||
TvListItem {
|
||||
ItemValueRow(
|
||||
title = stringResource(Res.string.total_memory),
|
||||
value = Utils.convertBytesToMega(ramData.total),
|
||||
)
|
||||
}
|
||||
}
|
||||
item(key = "__available") {
|
||||
TvListItem {
|
||||
ItemValueRow(
|
||||
title = stringResource(Res.string.available_memory),
|
||||
value =
|
||||
"${Utils.convertBytesToMega(ramData.available)} " +
|
||||
"(${ramData.availablePercentage}%)",
|
||||
)
|
||||
}
|
||||
}
|
||||
if (ramData.threshold != -1L) {
|
||||
item(key = "__threshold") {
|
||||
TvListItem {
|
||||
ItemValueRow(
|
||||
title = stringResource(Res.string.threshold),
|
||||
value = Utils.convertBytesToMega(ramData.threshold),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item(key = "__progress") {
|
||||
val usedPercentage = (100 - ramData.availablePercentage) / 100f
|
||||
TvListItem {
|
||||
CpuProgressBar(
|
||||
progress = usedPercentage,
|
||||
minMaxValues =
|
||||
Utils.convertBytesToMega(0L) to Utils.convertBytesToMega(ramData.total),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features.information.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.kgurgul.cpuinfo.domain.model.getKey
|
||||
import com.kgurgul.cpuinfo.domain.model.getName
|
||||
import com.kgurgul.cpuinfo.domain.model.getValue
|
||||
import com.kgurgul.cpuinfo.features.information.base.InformationRow
|
||||
import com.kgurgul.cpuinfo.features.information.screen.ScreenInfoViewModel
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvListItem
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun TvScreenInfoScreen(viewModel: ScreenInfoViewModel = koinViewModel()) {
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
TvScreenInfoScreen(uiState = uiState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvScreenInfoScreen(uiState: ScreenInfoViewModel.UiState) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(spacingSmall),
|
||||
verticalArrangement = Arrangement.spacedBy(spacingSmall),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(uiState.items, key = { itemValue -> itemValue.getKey() }) { itemValue ->
|
||||
TvListItem {
|
||||
InformationRow(
|
||||
title = itemValue.getName(),
|
||||
value = itemValue.getValue(),
|
||||
isLastItem = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features.information.sensors
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.kgurgul.cpuinfo.domain.model.asString
|
||||
import com.kgurgul.cpuinfo.features.information.sensors.SensorsInfoViewModel
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvListItem
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingXSmall
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun TvSensorsInfoScreen(viewModel: SensorsInfoViewModel = koinViewModel()) {
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
TvSensorsInfoScreen(uiState = uiState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvSensorsInfoScreen(uiState: SensorsInfoViewModel.UiState) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(spacingSmall),
|
||||
verticalArrangement = Arrangement.spacedBy(spacingSmall),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(uiState.sensors, key = { sensorData -> sensorData.id }) { sensorData ->
|
||||
TvListItem { SensorItem(title = sensorData.name.asString(), value = sensorData.value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SensorItem(title: String, value: String) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Spacer(modifier = Modifier.size(spacingXSmall))
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = .7f),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features.information.storage
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.kgurgul.cpuinfo.domain.model.asString
|
||||
import com.kgurgul.cpuinfo.features.information.storage.StorageInfoViewModel
|
||||
import com.kgurgul.cpuinfo.features.information.storage.registerStorageMountingListener
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvListItem
|
||||
import com.kgurgul.cpuinfo.ui.components.CpuProgressBar
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import com.kgurgul.cpuinfo.utils.Utils
|
||||
import kotlin.math.roundToInt
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
fun TvStorageInfoScreen(viewModel: StorageInfoViewModel = koinViewModel()) {
|
||||
registerStorageMountingListener(viewModel::onRefreshStorage)
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
TvStorageInfoScreen(uiState = uiState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvStorageInfoScreen(uiState: StorageInfoViewModel.UiState) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(spacingSmall),
|
||||
verticalArrangement = Arrangement.spacedBy(spacingSmall),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(uiState.storageItems, key = { it.id }) { storageItem ->
|
||||
val label = buildString {
|
||||
val notFormattedLabel = storageItem.label.asString().trim()
|
||||
if (notFormattedLabel.isNotEmpty()) {
|
||||
append(notFormattedLabel)
|
||||
append(": ")
|
||||
}
|
||||
}
|
||||
val totalReadable = Utils.humanReadableByteCount(storageItem.storageTotal)
|
||||
val usedReadable = Utils.humanReadableByteCount(storageItem.storageUsed)
|
||||
val progress =
|
||||
if (storageItem.storageTotal != 0L)
|
||||
storageItem.storageUsed.toFloat() / storageItem.storageTotal.toFloat()
|
||||
else 0f
|
||||
val usedPercent = (progress * 100.0).roundToInt()
|
||||
val storageDesc = "$label$usedReadable / $totalReadable ($usedPercent%)"
|
||||
val minMaxValues =
|
||||
Utils.humanReadableByteCount(0) to
|
||||
Utils.humanReadableByteCount(storageItem.storageTotal)
|
||||
TvListItem {
|
||||
CpuProgressBar(
|
||||
label = storageDesc,
|
||||
progress = progress,
|
||||
progressHeight = 32.dp,
|
||||
prefixImageRes = storageItem.iconDrawable,
|
||||
minMaxValues = minMaxValues,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.RadioButtonDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.navDeepLink
|
||||
import com.kgurgul.cpuinfo.features.settings.SettingsViewModel
|
||||
import com.kgurgul.cpuinfo.features.settings.getTemperatureUnit
|
||||
import com.kgurgul.cpuinfo.features.settings.getThemeName
|
||||
import com.kgurgul.cpuinfo.shared.Res
|
||||
import com.kgurgul.cpuinfo.shared.cancel
|
||||
import com.kgurgul.cpuinfo.shared.general
|
||||
import com.kgurgul.cpuinfo.shared.pref_theme
|
||||
import com.kgurgul.cpuinfo.shared.pref_theme_choose
|
||||
import com.kgurgul.cpuinfo.shared.temperature_unit
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvAlertDialog
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvButton
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvListItem
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingLarge
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingMedium
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import com.kgurgul.cpuinfo.utils.navigation.NavigationConst
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Serializable
|
||||
data object TvSettingsBaseRoute {
|
||||
|
||||
@Serializable data object TvSettingsRoute
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.tvSettingsScreen() {
|
||||
navigation<TvSettingsBaseRoute>(
|
||||
startDestination = TvSettingsBaseRoute.TvSettingsRoute,
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<TvSettingsBaseRoute>(
|
||||
basePath = NavigationConst.BASE_URL + NavigationConst.SETTINGS
|
||||
)
|
||||
),
|
||||
) {
|
||||
composable<TvSettingsBaseRoute.TvSettingsRoute> { TvSettingsScreen() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvSettingsScreen(viewModel: SettingsViewModel = koinViewModel()) {
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
TvSettingsScreen(
|
||||
uiState = uiState,
|
||||
onTemperatureOptionClicked = viewModel::setTemperatureUnit,
|
||||
onThemeOptionClicked = viewModel::setTheme,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvSettingsScreen(
|
||||
uiState: SettingsViewModel.UiState,
|
||||
onTemperatureOptionClicked: (Int) -> Unit,
|
||||
onThemeOptionClicked: (String) -> Unit,
|
||||
) {
|
||||
Scaffold { paddingValues ->
|
||||
var isTemperatureDialogVisible by remember { mutableStateOf(false) }
|
||||
var isThemeDialogVisible by remember { mutableStateOf(false) }
|
||||
SettingsList(
|
||||
uiState = uiState,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
onThemeItemClicked = { isThemeDialogVisible = true },
|
||||
onTemperatureItemClicked = { isTemperatureDialogVisible = true },
|
||||
)
|
||||
TemperatureUnitDialog(
|
||||
isDialogVisible = isTemperatureDialogVisible,
|
||||
onDismissRequest = { isTemperatureDialogVisible = false },
|
||||
currentSelection = uiState.temperatureUnit,
|
||||
options = uiState.temperatureDialogOptions,
|
||||
onOptionClicked = onTemperatureOptionClicked,
|
||||
)
|
||||
ThemeDialog(
|
||||
isDialogVisible = isThemeDialogVisible,
|
||||
onDismissRequest = { isThemeDialogVisible = false },
|
||||
currentSelection = uiState.theme,
|
||||
options = uiState.themeDialogOptions,
|
||||
onOptionClicked = onThemeOptionClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsList(
|
||||
uiState: SettingsViewModel.UiState,
|
||||
onThemeItemClicked: () -> Unit,
|
||||
onTemperatureItemClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(contentPadding = PaddingValues(spacingMedium), modifier = modifier) {
|
||||
item(key = "__generalHeader") {
|
||||
Text(
|
||||
text = stringResource(Res.string.general),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
)
|
||||
Spacer(modifier = Modifier.requiredSize(spacingMedium))
|
||||
}
|
||||
item(key = "__themeItem") {
|
||||
SettingsItem(
|
||||
title = stringResource(Res.string.pref_theme),
|
||||
subtitle = getThemeName(option = uiState.theme),
|
||||
onClick = onThemeItemClicked,
|
||||
)
|
||||
}
|
||||
item(key = "__temperatureItem") {
|
||||
SettingsItem(
|
||||
title = stringResource(Res.string.temperature_unit),
|
||||
subtitle = getTemperatureUnit(option = uiState.temperatureUnit),
|
||||
onClick = onTemperatureItemClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsItem(title: String, subtitle: String, onClick: () -> Unit) {
|
||||
TvListItem(onClick = onClick) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(vertical = spacingMedium)
|
||||
.padding(start = spacingLarge)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TemperatureUnitDialog(
|
||||
isDialogVisible: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
currentSelection: Int,
|
||||
options: ImmutableList<Int>,
|
||||
onOptionClicked: (Int) -> Unit,
|
||||
) {
|
||||
if (isDialogVisible) {
|
||||
TvAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(Res.string.temperature_unit)) },
|
||||
text = {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(spacingSmall),
|
||||
modifier = Modifier.verticalScroll(scrollState),
|
||||
) {
|
||||
for (option in options) {
|
||||
TvListItem(
|
||||
onClick = {
|
||||
onOptionClicked(option)
|
||||
onDismissRequest()
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(spacingMedium),
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = spacingSmall),
|
||||
) {
|
||||
RadioButton(
|
||||
selected = option == currentSelection,
|
||||
onClick = null,
|
||||
colors =
|
||||
RadioButtonDefaults.colors(
|
||||
selectedColor = MaterialTheme.colorScheme.tertiary
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = getTemperatureUnit(option = option),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TvButton(onClick = onDismissRequest) {
|
||||
androidx.tv.material3.Text(text = stringResource(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeDialog(
|
||||
isDialogVisible: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
currentSelection: String,
|
||||
options: ImmutableList<String>,
|
||||
onOptionClicked: (String) -> Unit,
|
||||
) {
|
||||
if (isDialogVisible) {
|
||||
TvAlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(Res.string.pref_theme_choose)) },
|
||||
text = {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(spacingSmall),
|
||||
modifier = Modifier.verticalScroll(scrollState),
|
||||
) {
|
||||
for (option in options) {
|
||||
TvListItem(
|
||||
onClick = {
|
||||
onOptionClicked(option)
|
||||
onDismissRequest()
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(spacingMedium),
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = spacingSmall),
|
||||
) {
|
||||
RadioButton(
|
||||
selected = option == currentSelection,
|
||||
onClick = null,
|
||||
colors =
|
||||
RadioButtonDefaults.colors(
|
||||
selectedColor = MaterialTheme.colorScheme.tertiary
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = getThemeName(option = option),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TvButton(onClick = onDismissRequest) {
|
||||
androidx.tv.material3.Text(text = stringResource(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.features.temperature
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.navDeepLink
|
||||
import com.kgurgul.cpuinfo.domain.model.TemperatureItem
|
||||
import com.kgurgul.cpuinfo.domain.model.asString
|
||||
import com.kgurgul.cpuinfo.features.temperature.TemperatureFormatter
|
||||
import com.kgurgul.cpuinfo.features.temperature.TemperatureViewModel
|
||||
import com.kgurgul.cpuinfo.shared.Res
|
||||
import com.kgurgul.cpuinfo.shared.no_temp_data
|
||||
import com.kgurgul.cpuinfo.shared.no_temp_data_admin_required
|
||||
import com.kgurgul.cpuinfo.tv.ui.components.TvListItem
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingMedium
|
||||
import com.kgurgul.cpuinfo.ui.theme.spacingSmall
|
||||
import com.kgurgul.cpuinfo.utils.navigation.NavigationConst
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Serializable
|
||||
data object TvTemperaturesBaseRoute {
|
||||
|
||||
@Serializable data object TvTemperaturesRoute
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.tvTemperaturesScreen() {
|
||||
navigation<TvTemperaturesBaseRoute>(
|
||||
startDestination = TvTemperaturesBaseRoute.TvTemperaturesRoute,
|
||||
deepLinks =
|
||||
listOf(
|
||||
navDeepLink<TvTemperaturesBaseRoute>(
|
||||
basePath = NavigationConst.BASE_URL + NavigationConst.TEMPERATURES
|
||||
)
|
||||
),
|
||||
) {
|
||||
composable<TvTemperaturesBaseRoute.TvTemperaturesRoute> { TvTemperatureScreen() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvTemperatureScreen(viewModel: TemperatureViewModel = koinViewModel()) {
|
||||
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
|
||||
TvTemperatureScreen(uiState = uiState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvTemperatureScreen(uiState: TemperatureViewModel.UiState) {
|
||||
Scaffold(contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)) {
|
||||
paddingValues ->
|
||||
val paddingModifier = Modifier.padding(paddingValues)
|
||||
if (uiState.temperatureItems.isEmpty()) {
|
||||
TvEmptyTemperatureList(
|
||||
isAdminRequired = uiState.isAdminRequired,
|
||||
modifier = paddingModifier,
|
||||
)
|
||||
} else {
|
||||
TvTemperatureList(
|
||||
temperatureItems = uiState.temperatureItems,
|
||||
temperatureFormatter = uiState.temperatureFormatter,
|
||||
modifier = paddingModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TvTemperatureList(
|
||||
temperatureItems: ImmutableList<TemperatureItem>,
|
||||
temperatureFormatter: TemperatureFormatter,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize().padding(spacingMedium).then(modifier)) {
|
||||
items(items = temperatureItems, key = { item -> item.id }) { item ->
|
||||
TvTemperatureItem(item = item, temperatureFormatter = temperatureFormatter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TvTemperatureItem(item: TemperatureItem, temperatureFormatter: TemperatureFormatter) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
TvListItem {
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
Icon(
|
||||
painter = painterResource(item.icon),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.requiredSize(60.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.requiredSize(spacingSmall))
|
||||
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
|
||||
Text(
|
||||
text = item.name.asString(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Spacer(modifier = Modifier.requiredSize(spacingSmall))
|
||||
if (!item.temperature.isNaN()) {
|
||||
var formattedTemp by remember { mutableStateOf("") }
|
||||
LaunchedEffect(item.temperature) {
|
||||
coroutineScope.launch {
|
||||
formattedTemp = temperatureFormatter.format(item.temperature)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = formattedTemp,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TvEmptyTemperatureList(isAdminRequired: Boolean, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxSize().padding(spacingMedium).then(modifier),
|
||||
) {
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
if (isAdminRequired) {
|
||||
Res.string.no_temp_data_admin_required
|
||||
} else {
|
||||
Res.string.no_temp_data
|
||||
}
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.ui.components
|
||||
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun TvAlertDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
confirmButton: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
text: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = title,
|
||||
tonalElevation = 0.dp,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
text = text,
|
||||
confirmButton = confirmButton,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.ButtonDefaults
|
||||
import androidx.tv.material3.IconButton
|
||||
import androidx.tv.material3.IconButtonDefaults
|
||||
import androidx.tv.material3.OutlinedIconButton
|
||||
import androidx.tv.material3.OutlinedIconButtonDefaults
|
||||
import androidx.tv.material3.WideButton
|
||||
import androidx.tv.material3.WideButtonDefaults
|
||||
|
||||
@Composable
|
||||
fun TvIconButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
colors =
|
||||
IconButtonDefaults.colors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(alpha = 0.2f),
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceTint.copy(alpha = 0.5f),
|
||||
focusedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvOutlinedIconButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
OutlinedIconButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
colors =
|
||||
OutlinedIconButtonDefaults.colors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.onSurface,
|
||||
focusedContentColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
colors =
|
||||
ButtonDefaults.colors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceTint.copy(alpha = 0.7f),
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceTint,
|
||||
focusedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvWideButton(
|
||||
onClick: () -> Unit,
|
||||
title: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: (@Composable () -> Unit)? = null,
|
||||
subtitle: (@Composable () -> Unit)? = null,
|
||||
enabled: Boolean = true,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
WideButton(
|
||||
onClick = onClick,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
icon = icon,
|
||||
contentColor =
|
||||
WideButtonDefaults.contentColor(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
focusedColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
scale = WideButtonDefaults.scale(focusedScale = 1.05f),
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
background = { TvWideButtonBackground(enabled, interactionSource) },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TvWideButtonBackground(enabled: Boolean, interactionSource: MutableInteractionSource) {
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
val isPressed = interactionSource.collectIsPressedAsState().value
|
||||
|
||||
val backgroundColor =
|
||||
when {
|
||||
!enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.01f)
|
||||
isPressed -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
isFocused -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
|
||||
else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize().background(backgroundColor))
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.tv.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.tv.material3.DenseListItem
|
||||
import androidx.tv.material3.ListItemColors
|
||||
import androidx.tv.material3.ListItemDefaults
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
|
||||
@Composable
|
||||
fun TvListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
colors: ListItemColors =
|
||||
ListItemDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.4f)
|
||||
),
|
||||
headlineContent: @Composable () -> Unit,
|
||||
) {
|
||||
DenseListItem(
|
||||
selected = false,
|
||||
onClick = onClick,
|
||||
scale = ListItemDefaults.scale(focusedScale = 1.01f),
|
||||
colors = colors,
|
||||
headlineContent = headlineContent,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="320dp"
|
||||
android:height="180dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group
|
||||
android:scaleX="0.6666667"
|
||||
android:scaleY="0.6666667"
|
||||
android:translateX="18"
|
||||
android:translateY="18">
|
||||
<group
|
||||
android:scaleX="0.5625"
|
||||
android:translateX="23.625">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m62.72,30l1.08,0a0.42,0.43 90.01,0 0,-0.01 0.07q0,0.95 0,1.9a0.63,0.63 90,0 0,0.01 0.09l0.68,0l0,4.3l2.14,0l0,-4.3l0.68,0c0,-0.04 0.01,-0.07 0.01,-0.1q0,-0.94 0,-1.88A0.57,0.57 89.99,0 0,67.3 30l1.08,0l0,2.06l0.69,0l0,4.31c0.2,0 0.37,-0.01 0.55,0 0.44,0.04 0.88,-0.1 1.32,0.08a0.63,0.63 90,0 1,0.29 0.18,0.69 0.69,89.99 0,0 0.12,0.12 0.37,0.37 90,0 1,0.18 0.29,0.11 0.11,90.01 0,0 0.02,0.08c0.1,0.05 0.07,0.14 0.07,0.22 0,0.58 0,1.15 0,1.73 0,0.05 0,0.09 0,0.15l4.31,0l0,0.68l0.18,0 1.89,0l0,1.08l-2.07,0l0,0.69l-4.3,0l0,1.96l4.3,0l0,0.67l2.06,0l0,1.08l-2.07,0l0,0.69l-4.3,0l0,2.25l4.3,0l0,0.68l2.06,0l0,1.08l-2.07,0l0,0.69l-4.3,0l0,2.06l4.3,0l0,0.68l2.06,0l0,1.08l-2.07,0l0,0.69l-4.3,0l0,2.25l4.3,0l0,0.68l2.06,0l0,1.08l-2.06,0l0,0.69l-4.3,0l0,2.06l4.31,0l0,0.69l0.16,0 1.89,0l0,1.06c-0.05,0 -0.09,0.01 -0.14,0.01l-1.93,0l0,0.69l-4.3,0l0,2.35l4.31,0l0,0.68l0.16,0 1.89,0l0,1.08l-2.07,0l0,0.69l-4.31,0c0,0.05 0,0.1 0,0.14 0,0.43 0,0.85 0,1.28a0.49,0.49 90.01,0 1,-0.03 0.14,1.06 1.06,90 0,1 -0.06,0.13 1.11,1.11 90,0 1,-0.13 0.25,2.23 2.23,90 0,1 -0.27,0.22 1.45,1.45 89.99,0 1,-0.45 0.18l-1.47,0 -0.14,0l0,4.31l-0.69,0c0,0.07 0,0.12 0,0.17q0,0.94 0,1.89l-1.06,0c0,-0.05 -0.01,-0.09 -0.01,-0.14q0,-0.89 0,-1.77l0,-0.15l-0.69,0l0,-4.26l-2.25,0l0,4.31l-0.69,0c0,0.07 0,0.12 0,0.17q0,0.94 0,1.89l-1.06,0c0,-0.05 -0.01,-0.09 -0.01,-0.14q0,-0.89 0,-1.77l0,-0.15l-0.69,0l0,-4.3l-2.06,0l0,4.3l-0.67,0l0,2.06l-1.08,0l0,-2.07l-0.69,0l0,-4.3l-2.15,0l0,4.31l-0.68,0l0,2.06l-1.08,0l0,-2.06l-0.69,0l0,-4.3l-2.15,0l0,4.31l-0.69,0l0,2.06l-1.08,0l0,-2.06l-0.69,0l0,-4.3l-2.15,0l0,4.31l-0.68,0c0,0.07 0,0.12 0,0.17q0,0.94 0,1.89l-1.08,0l0,-2.07l-0.69,0l0,-4.29l-2.15,0l0,4.31l-0.69,0c0,0.06 0,0.12 0,0.17q0,0.95 0,1.89l-1.09,0c0,-0.05 -0.01,-0.1 -0.01,-0.14q0,-0.89 0,-1.77l0,-0.15l-0.69,0l0,-4.31l-0.14,0c-0.52,0 -1.03,0.01 -1.55,-0.01a1.64,1.64 89.99,0 1,-0.41 -0.12,0.11 0.11,90 0,1 -0.03,-0.03c-0.05,-0.05 -0.1,-0.1 -0.15,-0.14 -0.13,-0.1 -0.27,-0.21 -0.27,-0.4a0.09,0.09 89.98,0 0,-0.02 -0.06c-0.08,-0.03 -0.07,-0.1 -0.07,-0.16 0,-0.5 0,-0.99 0,-1.49 0,-0.04 0,-0.08 0,-0.14l-4.31,0l0,-0.69l-0.16,0 -1.9,0l0,-1.08a0.42,0.43 90.01,0 0,0.07 0.01l1.9,0a0.61,0.61 89.99,0 0,0.09 -0.02l0,-0.68l4.3,0l0,-1.96l-4.3,0l0,-0.69l-2.06,0L30,62.91l2.06,0l0,-0.69l4.3,0l0,-2.25l-4.3,0l0,-0.69l-2.06,0l0,-1.08l2.06,0l0,-0.69l4.3,0l0,-2.06l-4.3,0l0,-0.68l-0.15,0 -1.9,0l0,-1.08l2.06,0l0,-0.69l4.3,0l0,-2.25l-4.3,0l0,-0.68l-0.15,0 -1.9,0l0,-1.08l2.06,0l0,-0.69l4.3,0l0,-2.06l-4.3,0L32.06,45.58c-0.04,0 -0.07,-0.01 -0.1,-0.01l-1.88,0a0.57,0.57 89.99,0 0,-0.08 0.01l0,-1.08l2.06,0L32.06,43.81l4.3,0l0,-2.35l-4.3,0l0,-0.69l-0.15,0 -1.9,0l0,-1.08a0.42,0.43 90.01,0 0,0.07 0.01l1.9,0a0.62,0.62 90,0 0,0.09 -0.02l0,-0.69l4.3,0c0,-0.06 0,-0.1 0,-0.14q0,-0.79 0,-1.59a0.36,0.36 90,0 1,0.02 -0.12c0.02,-0.05 0.05,-0.1 0.07,-0.14a0.29,0.29 89.99,0 1,0.03 -0.11c0.11,-0.07 0.14,-0.2 0.25,-0.27 0.11,-0.07 0.18,-0.17 0.32,-0.17a0.08,0.08 90,0 0,0.06 -0.02c0.04,-0.11 0.14,-0.07 0.21,-0.07 0.51,0 1.03,0 1.54,0l0.14,0l0,-4.31l0.68,0c0,-0.04 0.01,-0.07 0.01,-0.1q0,-0.94 0,-1.88a0.57,0.57 89.99,0 0,-0.01 -0.08l1.08,0l0,2.06l0.69,0l0,4.3l2.25,0l0,-4.3l0.68,0c0,-0.04 0.01,-0.07 0.01,-0.1q0,-0.94 0,-1.88a0.57,0.57 89.99,0 0,-0.01 -0.08l1.08,0l0,2.06l0.7,0l0,4.3l2.05,0l0,-4.3l0.68,0l0,-2.06l1.08,0l0,2.06l0.69,0l0,4.3l2.15,0l0,-4.3l0.69,0l0,-2.06l1.08,0l0,2.06l0.69,0l0,4.3l2.15,0l0,-4.3l0.68,0c0,-0.06 0,-0.1 0,-0.15q0,-0.95 0,-1.9l1.08,0l0,2.06l0.69,0l0,4.3l2.15,0l0,-4.3l0.69,0c0,-0.06 0,-0.11 0,-0.15q0.03,-0.95 0.03,-1.9zM38.63,53.91l0,0q0,7.15 0,14.3a1.29,1.29 90,0 0,0.06 0.47,0.88 0.88,89.99 0,0 0.25,0.37 0.85,0.85 89.99,0 0,0.57 0.22c0.06,0 0.13,0 0.19,0q14.09,0 28.18,0c0.27,0 0.53,0.05 0.81,-0.07a0.93,0.93 90,0 0,0.46 -0.38c0.08,-0.13 0.16,-0.29 0.13,-0.47a0.7,0.7 89.99,0 1,0 -0.13q0,-13.92 -0,-27.85c0,-0.39 0.09,-0.79 -0.08,-1.18a0.67,0.67 90.01,0 0,-0.32 -0.37,0.23 0.23,89.99 0,1 -0.05,-0.02 0.57,0.57 89.99,0 0,-0.39 -0.16l-26.61,0c-0.81,0 -1.62,0 -2.43,0a1.36,1.36 90,0 0,-0.36 0.1,0.05 0.05,90.01 0,0 -0.01,0.02 0.69,0.69 89.99,0 1,-0.12 0.15c-0.1,0.07 -0.18,0.14 -0.18,0.27a0.04,0.04 90,0 1,-0.02 0.03c-0.09,0.02 -0.06,0.1 -0.07,0.15 0,0.1 0,0.2 0,0.31q-0,7.13 -0,14.25z"
|
||||
android:strokeWidth="0.1" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m39.95,54q0,-6.25 0,-12.49c0,-0.12 0,-0.24 0.01,-0.35a0.5,0.5 90.01,0 1,0.06 -0.14,0.11 0.11,90.01 0,0 0.02,-0.04c-0.01,-0.2 0.11,-0.35 0.22,-0.48a1.27,1.27 90,0 1,0.43 -0.36c0.1,-0.05 0.22,-0.05 0.29,-0.1 0.17,-0.13 0.36,-0.08 0.53,-0.08q9.11,-0.01 18.23,0l6.78,0c0.11,0 0.23,0 0.34,0.01a0.44,0.44 90.01,0 1,0.13 0.06,0.11 0.11,90.01 0,0 0.04,0.02c0.2,-0.02 0.33,0.13 0.49,0.22a0.7,0.7 89.99,0 1,0.14 0.15c0.03,0.04 0.07,0.07 0.1,0.11 0.06,0.08 0.12,0.16 0.18,0.25a0.02,0.02 90.01,0 1,0 0.01c0,0.19 0.12,0.34 0.12,0.53 -0.01,1.23 -0,2.45 0,3.68q0,10.72 0,21.45c0,0.11 0,0.23 0,0.34 0,0.08 0.02,0.17 -0.07,0.23a0.11,0.11 90.01,0 0,-0.01 0.05,1.08 1.08,90 0,1 -0.29,0.52 3.01,3.01 89.99,0 0,-0.24 0.24,0.03 0.03,90 0,1 -0.01,0.01l-0.32,0.14c-0.05,0.02 -0.1,0.04 -0.14,0.06a0.42,0.42 90,0 1,-0.12 0.03c-0.11,0 -0.22,0 -0.33,0l-25.05,0c-0.07,0 -0.14,0 -0.21,0 -0.1,-0.01 -0.2,0.03 -0.27,-0.08a0.07,0.07 89.99,0 0,-0.05 -0.01,0.77 0.77,89.99 0,1 -0.48,-0.23 1.27,1.27 90,0 1,-0.35 -0.43c-0.04,-0.1 -0.04,-0.21 -0.1,-0.28 -0.13,-0.16 -0.08,-0.33 -0.08,-0.5q-0.01,-6.27 -0,-12.53zM66.38,65.89c0,-0.24 -0.24,-0.47 -0.49,-0.49 -0.2,-0.01 -0.49,0.29 -0.49,0.49 0,0.2 0.29,0.49 0.49,0.49 0.2,0 0.53,-0.29 0.49,-0.49z"
|
||||
android:strokeWidth="0.1" />
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_banner_background" />
|
||||
<foreground android:drawable="@drawable/ic_banner_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_banner_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
~ Copyright 2017 KG Soft
|
||||
~
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name" translatable="false">CPU Info</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,3 +0,0 @@
|
||||
/build
|
||||
src/debug/google-services.json
|
||||
src/release/google-services.json
|
||||
@@ -1,180 +0,0 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
kotlin("android")
|
||||
id("kotlin-parcelize")
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.kover)
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = Versions.COMPILE_SDK
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.kgurgul.cpuinfo"
|
||||
|
||||
minSdk = Versions.MIN_SDK
|
||||
targetSdk = Versions.TARGET_SDK
|
||||
versionCode = Versions.VERSION_CODE
|
||||
versionName = Versions.VERSION_NAME
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled = true
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments += "-DANDROID_STL=c++_static"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace = "com.kgurgul.cpuinfo"
|
||||
|
||||
ndkVersion = Versions.NDK_VERSION
|
||||
|
||||
signingConfigs {
|
||||
getByName("debug") {
|
||||
val debugSigningConfig = SigningConfig.getDebugProperties(rootProject.rootDir)
|
||||
storeFile = file(debugSigningConfig.getProperty(SigningConfig.KEY_PATH))
|
||||
keyAlias = debugSigningConfig.getProperty(SigningConfig.KEY_ALIAS)
|
||||
keyPassword = debugSigningConfig.getProperty(SigningConfig.KEY_PASS)
|
||||
storePassword = debugSigningConfig.getProperty(SigningConfig.KEY_PASS)
|
||||
}
|
||||
create("release") {
|
||||
val releaseSigningConfig = SigningConfig.getReleaseProperties(rootProject.rootDir)
|
||||
storeFile = file(releaseSigningConfig.getProperty(SigningConfig.KEY_PATH))
|
||||
keyAlias = releaseSigningConfig.getProperty(SigningConfig.KEY_ALIAS)
|
||||
keyPassword = releaseSigningConfig.getProperty(SigningConfig.KEY_PASS)
|
||||
storePassword = releaseSigningConfig.getProperty(SigningConfig.KEY_PASS)
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
||||
}
|
||||
debug {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isMinifyEnabled = false
|
||||
enableUnitTestCoverage = true
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isReturnDefaultValues = true
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
animationsDisabled = true
|
||||
execution = "ANDROIDX_TEST_ORCHESTRATOR"
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.kotlinx.immutable)
|
||||
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
implementation(libs.androidx.multidex)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
|
||||
val composeBom = platform(libs.androidx.compose.bom)
|
||||
implementation(composeBom)
|
||||
androidTestImplementation(composeBom)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.testManifest)
|
||||
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
|
||||
implementation(libs.timber)
|
||||
implementation(libs.relinker)
|
||||
implementation(libs.coil)
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.turbine)
|
||||
testImplementation(libs.androidx.test.core)
|
||||
testImplementation(libs.androidx.arch.core.testing)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.hilt.android.testing)
|
||||
testImplementation(libs.mockito.android)
|
||||
testImplementation(libs.mockito.kotlin)
|
||||
kspTest(libs.hilt.compiler)
|
||||
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestImplementation(libs.androidx.test.rules)
|
||||
androidTestImplementation(libs.androidx.test.ext)
|
||||
androidTestImplementation(libs.androidx.test.espresso.core)
|
||||
androidTestImplementation(libs.androidx.test.espresso.contrib)
|
||||
androidTestImplementation(libs.androidx.compose.ui.test)
|
||||
androidTestImplementation(libs.hilt.android.testing)
|
||||
kspAndroidTest(libs.hilt.compiler)
|
||||
|
||||
androidTestUtil(libs.androidx.test.orchestrator)
|
||||
}
|
||||
|
||||
koverReport {
|
||||
androidReports("debug") {
|
||||
filters {
|
||||
includes {
|
||||
packages(KoverConfig.includedPackages)
|
||||
}
|
||||
excludes {
|
||||
packages(KoverConfig.excludedPackages)
|
||||
classes(KoverConfig.excludedClasses)
|
||||
annotatedBy(KoverConfig.excludedAnnotations)
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
setReportDir(layout.buildDirectory.dir("coverage-report/html"))
|
||||
}
|
||||
|
||||
xml {
|
||||
setReportFile(layout.buildDirectory.file("coverage-report/result.xml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
# Kotlin
|
||||
-dontwarn kotlin.**
|
||||
|
||||
-keepclassmembers class **$WhenMappings {
|
||||
<fields>;
|
||||
}
|
||||
|
||||
# Dagger
|
||||
-dontwarn com.google.errorprone.annotations.*
|
||||
|
||||
# General
|
||||
-renamesourcefileattribute SourceFile
|
||||
-keepattributes Exceptions,InnerClasses,Signature,SourceFile,LineNumberTable,*Annotation*
|
||||
|
||||
# AIDL
|
||||
-keep class android.content.pm.IPackageStatsObserver$** {
|
||||
public <fields>;
|
||||
public <methods>;
|
||||
}
|
||||
-keep class android.content.pm.IPackageStatsObserver$Stub.** {
|
||||
public <fields>;
|
||||
public <methods>;
|
||||
}
|
||||
-keep interface android.content.pm.IPackageStatsObserver$** {*;}
|
||||
|
||||
# Coroutines
|
||||
# ServiceLoader support
|
||||
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
|
||||
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
|
||||
# Most of volatile fields are updated with AFU and should not be mangled
|
||||
-keepclassmembernames class kotlinx.** {
|
||||
volatile <fields>;
|
||||
}
|
||||
|
||||
# Remove logs
|
||||
-assumenosideeffects class timber.log.Timber* {
|
||||
public static *** v(...);
|
||||
public static *** d(...);
|
||||
public static *** i(...);
|
||||
public static *** e(...);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.core
|
||||
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import com.kgurgul.cpuinfo.features.HostActivity
|
||||
import org.junit.Rule
|
||||
|
||||
abstract class BaseTestSuit {
|
||||
|
||||
@get:Rule
|
||||
val androidComposeRule = createAndroidComposeRule<HostActivity>()
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.core
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.test.espresso.NoMatchingViewException
|
||||
import androidx.test.espresso.ViewAssertion
|
||||
import androidx.test.espresso.matcher.ViewMatchers.assertThat
|
||||
import org.hamcrest.Matcher
|
||||
|
||||
/**
|
||||
* Assertion for item count on [RecyclerView].
|
||||
* Examples:
|
||||
* - .check(RecyclerViewItemCountAssertion(5))
|
||||
* - .check(RecyclerViewItemCountAssertion(greaterThan(5)))
|
||||
* - .check(RecyclerViewItemCountAssertion(lessThan(5)))
|
||||
*
|
||||
* @author kgurgul
|
||||
*/
|
||||
class RecyclerViewItemCountAssertion : ViewAssertion {
|
||||
|
||||
private val matcher: Matcher<Int>
|
||||
|
||||
constructor(expectedCount: Int) {
|
||||
matcher = isk(expectedCount)
|
||||
}
|
||||
|
||||
constructor(matcher: Matcher<Int>) {
|
||||
this.matcher = matcher
|
||||
}
|
||||
|
||||
override fun check(view: View?, noViewFoundException: NoMatchingViewException?) {
|
||||
if (noViewFoundException != null) {
|
||||
throw noViewFoundException
|
||||
}
|
||||
|
||||
val recyclerView = view as RecyclerView
|
||||
val adapter = recyclerView.adapter
|
||||
assertThat(adapter?.itemCount, matcher)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.uitestutils.conditionwatcher
|
||||
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
@Synchronized
|
||||
fun waitForCondition(timeoutMillis: Long = 5000L, condition: () -> Boolean) {
|
||||
val step = 250L
|
||||
var totalTime = 0L
|
||||
|
||||
do {
|
||||
if (condition()) {
|
||||
break
|
||||
}
|
||||
totalTime += step
|
||||
Thread.sleep(step)
|
||||
if (totalTime > timeoutMillis) {
|
||||
throw TimeoutException("Condition timed out after: ${totalTime}ms")
|
||||
}
|
||||
} while (true)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package android.content.pm;
|
||||
import android.content.pm.PackageStats;
|
||||
/**
|
||||
* API for package data change related callbacks from the Package Manager.
|
||||
* Some usage scenarios include deletion of cache directory, generate
|
||||
* statistics related to code, data, cache usage
|
||||
*/
|
||||
oneway interface IPackageStatsObserver {
|
||||
|
||||
void onGetStatsCompleted(in PackageStats pStats, boolean succeeded);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// PackageStats.aidl
|
||||
package android.content.pm;
|
||||
|
||||
parcelable PackageStats;
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.appinitializers
|
||||
|
||||
import android.app.Application
|
||||
|
||||
interface AppInitializer {
|
||||
fun init(application: Application)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.appinitializers
|
||||
|
||||
import android.app.Application
|
||||
import javax.inject.Inject
|
||||
|
||||
class AppInitializers @Inject constructor(
|
||||
private val initializers: Set<@JvmSuppressWildcards AppInitializer>
|
||||
) {
|
||||
fun init(application: Application) {
|
||||
initializers.forEach {
|
||||
it.init(application)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.appinitializers
|
||||
|
||||
import android.app.Application
|
||||
import com.getkeepsafe.relinker.ReLinker
|
||||
import com.kgurgul.cpuinfo.data.provider.CpuDataNativeProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class NativeToolsInitializer @Inject constructor(
|
||||
private val cpuDataNativeProvider: CpuDataNativeProvider
|
||||
) : AppInitializer {
|
||||
|
||||
override fun init(application: Application) {
|
||||
ReLinker.loadLibrary(application, "cpuinfo-libs")
|
||||
cpuDataNativeProvider.initLibrary()
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.appinitializers
|
||||
|
||||
import android.app.Application
|
||||
import com.kgurgul.cpuinfo.BuildConfig
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimberInitializer @Inject constructor() : AppInitializer {
|
||||
|
||||
override fun init(application: Application) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.data
|
||||
|
||||
import com.kgurgul.cpuinfo.data.local.IUserPreferencesRepository
|
||||
import com.kgurgul.cpuinfo.data.local.UserPreferencesRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class DataModule {
|
||||
@Binds
|
||||
abstract fun bindUserPreferencesRepository(
|
||||
userPreferencesRepository: UserPreferencesRepository
|
||||
): IUserPreferencesRepository
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.data.local
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface IUserPreferencesRepository {
|
||||
|
||||
val userPreferencesFlow: Flow<UserPreferences>
|
||||
|
||||
suspend fun setApplicationsSortingOrder(isAscending: Boolean)
|
||||
|
||||
suspend fun setApplicationsWithSystemApps(withSystemApps: Boolean)
|
||||
|
||||
suspend fun setTemperatureUnit(temperatureUnit: Int)
|
||||
|
||||
suspend fun setTheme(theme: String)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.data.local
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class StubUserPreferencesRepository : IUserPreferencesRepository {
|
||||
override val userPreferencesFlow: Flow<UserPreferences>
|
||||
get() = flow { }
|
||||
|
||||
override suspend fun setApplicationsSortingOrder(isAscending: Boolean) {
|
||||
}
|
||||
|
||||
override suspend fun setApplicationsWithSystemApps(withSystemApps: Boolean) {
|
||||
}
|
||||
|
||||
override suspend fun setTemperatureUnit(temperatureUnit: Int) {
|
||||
}
|
||||
|
||||
override suspend fun setTheme(theme: String) {
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.data.local
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.kgurgul.cpuinfo.domain.model.DarkThemeConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class UserPreferencesRepository @Inject constructor(
|
||||
private val dataStore: DataStore<Preferences>
|
||||
) : IUserPreferencesRepository {
|
||||
|
||||
override val userPreferencesFlow: Flow<UserPreferences>
|
||||
get() = dataStore.data
|
||||
.catch { exception ->
|
||||
if (exception is IOException) {
|
||||
Timber.e(exception)
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}.map { preferences ->
|
||||
mapUserPreferences(preferences)
|
||||
}
|
||||
|
||||
override suspend fun setApplicationsSortingOrder(isAscending: Boolean) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.SORTING_APPS] = isAscending
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setApplicationsWithSystemApps(withSystemApps: Boolean) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.WITH_SYSTEM_APPS] = withSystemApps
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setTemperatureUnit(temperatureUnit: Int) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.TEMPERATURE_UNIT] = temperatureUnit
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setTheme(theme: String) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.THEME] = theme
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapUserPreferences(preferences: Preferences): UserPreferences {
|
||||
return UserPreferences(
|
||||
isApplicationsSortingAscending = preferences[PreferencesKeys.SORTING_APPS] ?: true,
|
||||
withSystemApps = preferences[PreferencesKeys.WITH_SYSTEM_APPS] ?: false,
|
||||
temperatureUnit = preferences[PreferencesKeys.TEMPERATURE_UNIT] ?: 0,
|
||||
theme = preferences[PreferencesKeys.THEME] ?: DarkThemeConfig.FOLLOW_SYSTEM.prefName,
|
||||
)
|
||||
}
|
||||
|
||||
private object PreferencesKeys {
|
||||
val SORTING_APPS = booleanPreferencesKey("sorting_apps")
|
||||
val WITH_SYSTEM_APPS = booleanPreferencesKey("with_system_apps")
|
||||
val TEMPERATURE_UNIT = intPreferencesKey("temperature_unit")
|
||||
val THEME = stringPreferencesKey("theme")
|
||||
}
|
||||
}
|
||||
|
||||
data class UserPreferences(
|
||||
val isApplicationsSortingAscending: Boolean,
|
||||
val withSystemApps: Boolean,
|
||||
val temperatureUnit: Int,
|
||||
val theme: String,
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.data.provider
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import javax.inject.Inject
|
||||
|
||||
class ApplicationsDataProvider @Inject constructor(
|
||||
private val packageManager: PackageManager
|
||||
) {
|
||||
|
||||
fun getInstalledApplications(withSystemApps: Boolean): List<ApplicationInfo> {
|
||||
val applications = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getInstalledApplications(
|
||||
PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong())
|
||||
)
|
||||
} else {
|
||||
packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
|
||||
}
|
||||
return if (withSystemApps) {
|
||||
applications
|
||||
} else {
|
||||
applications.filter { (it.flags and ApplicationInfo.FLAG_SYSTEM) == 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.data.provider
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Return battery status from ACTION_BATTERY_CHANGED broadcast receiver
|
||||
*/
|
||||
class BatteryStatusProvider @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context
|
||||
) {
|
||||
|
||||
/**
|
||||
* @return [Intent] with battery information
|
||||
*/
|
||||
fun getBatteryStatusIntent(): Intent? {
|
||||
val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
||||
return appContext.registerReceiver(null, iFilter)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return battery capacity from private API. In case of error it will return -1.
|
||||
*/
|
||||
@SuppressLint("PrivateApi")
|
||||
fun getBatteryCapacity(): Double {
|
||||
var capacity = -1.0
|
||||
try {
|
||||
val powerProfile = Class.forName("com.android.internal.os.PowerProfile")
|
||||
.getConstructor(Context::class.java).newInstance(appContext)
|
||||
capacity = Class
|
||||
.forName("com.android.internal.os.PowerProfile")
|
||||
.getMethod("getAveragePower", String::class.java)
|
||||
.invoke(powerProfile, "battery.capacity") as Double
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
return capacity
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.data.provider
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class CpuDataNativeProvider @Inject constructor() {
|
||||
|
||||
external fun initLibrary()
|
||||
|
||||
external fun getCpuName(): String
|
||||
|
||||
external fun hasArmNeon(): Boolean
|
||||
|
||||
external fun getL1dCaches(): IntArray?
|
||||
|
||||
external fun getL1iCaches(): IntArray?
|
||||
|
||||
external fun getL2Caches(): IntArray?
|
||||
|
||||
external fun getL3Caches(): IntArray?
|
||||
|
||||
external fun getL4Caches(): IntArray?
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.data.provider
|
||||
|
||||
import android.os.Build
|
||||
import timber.log.Timber
|
||||
import java.io.RandomAccessFile
|
||||
import javax.inject.Inject
|
||||
|
||||
class CpuDataProvider @Inject constructor() {
|
||||
|
||||
fun getAbi(): String {
|
||||
return Build.SUPPORTED_ABIS[0]
|
||||
}
|
||||
|
||||
fun getNumberOfCores(): Int {
|
||||
return Runtime.getRuntime().availableProcessors()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checking frequencies directories and return current value if exists (otherwise we can
|
||||
* assume that core is stopped - value -1)
|
||||
*/
|
||||
fun getCurrentFreq(coreNumber: Int): Long {
|
||||
val currentFreqPath = "${CPU_INFO_DIR}cpu$coreNumber/cpufreq/scaling_cur_freq"
|
||||
return try {
|
||||
RandomAccessFile(currentFreqPath, "r").use { it.readLine().toLong() / 1000 }
|
||||
} catch (e: Exception) {
|
||||
Timber.e("getCurrentFreq() - cannot read file")
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read max/min frequencies for specific [coreNumber]. Return [Pair] with min and max frequency
|
||||
* or [Pair] with -1.
|
||||
*/
|
||||
fun getMinMaxFreq(coreNumber: Int): Pair<Long, Long> {
|
||||
val minPath = "${CPU_INFO_DIR}cpu$coreNumber/cpufreq/cpuinfo_min_freq"
|
||||
val maxPath = "${CPU_INFO_DIR}cpu$coreNumber/cpufreq/cpuinfo_max_freq"
|
||||
return try {
|
||||
val minMhz = RandomAccessFile(minPath, "r").use { it.readLine().toLong() / 1000 }
|
||||
val maxMhz = RandomAccessFile(maxPath, "r").use { it.readLine().toLong() / 1000 }
|
||||
Pair(minMhz, maxMhz)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("getMinMaxFreq() - cannot read file")
|
||||
Pair(-1, -1)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CPU_INFO_DIR = "/sys/devices/system/cpu/"
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.kgurgul.cpuinfo.data.provider
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import com.kgurgul.cpuinfo.R
|
||||
import javax.inject.Inject
|
||||
|
||||
class GpuDataProvider @Inject constructor(
|
||||
private val activityManager: ActivityManager,
|
||||
private val packageManager: PackageManager,
|
||||
private val resources: Resources,
|
||||
) {
|
||||
|
||||
fun getGlEsVersion(): String {
|
||||
return activityManager.deviceConfigurationInfo.glEsVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain Vulkan version
|
||||
*/
|
||||
fun getVulkanVersion(): String {
|
||||
val default = resources.getString(R.string.unknown)
|
||||
if (Build.VERSION.SDK_INT < 24) {
|
||||
return default
|
||||
}
|
||||
|
||||
val vulkan = packageManager.systemAvailableFeatures.find {
|
||||
it.name == PackageManager.FEATURE_VULKAN_HARDWARE_VERSION
|
||||
}?.version ?: 0
|
||||
if (vulkan == 0) {
|
||||
return default
|
||||
}
|
||||
|
||||
// Extract versions from bit field
|
||||
// See: https://developer.android.com/reference/android/content/pm/PackageManager#FEATURE_VULKAN_HARDWARE_VERSION
|
||||
val major = vulkan shr 22 // Higher 10 bits
|
||||
val minor = vulkan shl 10 shr 22 // Middle 10 bits
|
||||
val patch = vulkan shl 20 shr 22 // Lower 12 bits
|
||||
//
|
||||
return "$major.$minor.$patch"
|
||||
}
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 KG Soft
|
||||
*
|
||||
* 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 com.kgurgul.cpuinfo.data.provider
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.hardware.ConsumerIrManager
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.BatteryManager
|
||||
import android.os.Build
|
||||
import com.kgurgul.cpuinfo.R
|
||||
import com.kgurgul.cpuinfo.features.temperature.TemperatureFormatter
|
||||
import com.kgurgul.cpuinfo.utils.Utils
|
||||
import com.kgurgul.cpuinfo.utils.round2
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.io.RandomAccessFile
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
class HardwareDataProvider @Inject constructor(
|
||||
private val resources: Resources,
|
||||
private val temperatureProvider: TemperatureProvider,
|
||||
private val temperatureFormatter: TemperatureFormatter,
|
||||
private val packageManager: PackageManager,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val batteryStatusProvider: BatteryStatusProvider,
|
||||
private val wifiManager: WifiManager,
|
||||
private val irManager: ConsumerIrManager?,
|
||||
private val cameraManager: CameraManager,
|
||||
) {
|
||||
|
||||
fun getData(): List<Pair<String, String>> {
|
||||
return buildList {
|
||||
add(Pair(resources.getString(R.string.battery), ""))
|
||||
addAll(getBatteryStatus())
|
||||
|
||||
if (hasCamera()) {
|
||||
add(Pair(resources.getString(R.string.cameras), ""))
|
||||
addAll(getCameraInfo())
|
||||
}
|
||||
|
||||
add(Pair(resources.getString(R.string.sound_card), ""))
|
||||
addAll(getSoundCardInfo())
|
||||
addAll(getWirelessInfo())
|
||||
addAll(getUsbInfo())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBatteryStatus(): ArrayList<Pair<String, String>> {
|
||||
val functionsList = ArrayList<Pair<String, String>>()
|
||||
|
||||
val batteryStatus = batteryStatusProvider.getBatteryStatusIntent()
|
||||
|
||||
if (batteryStatus != null) {
|
||||
// Level
|
||||
val level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
||||
val scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
|
||||
|
||||
if (level != -1 && scale != -1) {
|
||||
val batteryPct = level / scale.toFloat() * 100.0
|
||||
functionsList.add(
|
||||
Pair(
|
||||
resources.getString(R.string.level),
|
||||
"${batteryPct.round2()}%"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Health
|
||||
val health = batteryStatus.getIntExtra(BatteryManager.EXTRA_HEALTH, -1)
|
||||
if (health != -1) {
|
||||
functionsList.add(
|
||||
Pair(
|
||||
resources.getString(R.string.battery_health),
|
||||
getBatteryHealthStatus(health)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Voltage
|
||||
val voltage = batteryStatus.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1)
|
||||
if (voltage > 0) {
|
||||
functionsList.add(
|
||||
Pair(
|
||||
resources.getString(R.string.voltage),
|
||||
"${voltage / 1000.0}V"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature
|
||||
val temperature = temperatureProvider.getBatteryTemperature()
|
||||
if (temperature != null) {
|
||||
functionsList.add(
|
||||
Pair(
|
||||
resources.getString(R.string.temperature),
|
||||
temperatureFormatter.format(temperature)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Capacity
|
||||
val capacity = batteryStatusProvider.getBatteryCapacity().round2()
|
||||
if (capacity != -1.0) {
|
||||
functionsList.add(Pair(resources.getString(R.string.capacity), "${capacity}mAh"))
|
||||
}
|
||||
|
||||
if (batteryStatus != null) {
|
||||
// Technology
|
||||
val technology = batteryStatus.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY)
|
||||
Utils.addPairIfExists(
|
||||
functionsList,
|
||||
resources.getString(R.string.technology),
|
||||
technology
|
||||
)
|
||||
|
||||
// Are we charging / is charged?
|
||||
val status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
||||
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
||||
status == BatteryManager.BATTERY_STATUS_FULL
|
||||
|
||||
// How we charging?
|
||||
val chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
|
||||
val usbCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_USB
|
||||
val acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC
|
||||
|
||||
val charging =
|
||||
if (isCharging) resources.getString(R.string.yes)
|
||||
else resources.getString(R.string.no)
|
||||
functionsList.add(Pair(resources.getString(R.string.is_charging), charging))
|
||||
if (isCharging) {
|
||||
val chargingType: String = when {
|
||||
usbCharge -> "USB"
|
||||
acCharge -> "AC"
|
||||
else -> resources.getString(R.string.unknown)
|
||||
}
|
||||
functionsList.add(Pair(resources.getString(R.string.charging_type), chargingType))
|
||||
}
|
||||
}
|
||||
|
||||
return functionsList
|
||||
}
|
||||
|
||||
private fun getBatteryHealthStatus(healthInt: Int): String {
|
||||
return when (healthInt) {
|
||||
BatteryManager.BATTERY_HEALTH_COLD -> resources.getString(R.string.battery_cold)
|
||||
BatteryManager.BATTERY_HEALTH_GOOD -> resources.getString(R.string.battery_good)
|
||||
BatteryManager.BATTERY_HEALTH_DEAD -> resources.getString(R.string.battery_dead)
|
||||
BatteryManager.BATTERY_HEALTH_OVERHEAT -> resources.getString(R.string.battery_overheat)
|
||||
BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE -> resources.getString(R.string.battery_overvoltage)
|
||||
BatteryManager.BATTERY_HEALTH_UNKNOWN -> resources.getString(R.string.battery_unknown)
|
||||
BatteryManager.BATTERY_HEALTH_UNSPECIFIED_FAILURE -> resources.getString(R.string.battery_unspecified_failure)
|
||||
else -> resources.getString(R.string.battery_unknown)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Wi-Fi and Bluetooth mac address and Bluetooth LE support
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun getWirelessInfo(): List<Pair<String, String>> {
|
||||
val functionsList = mutableListOf<Pair<String, String>>()
|
||||
functionsList.add(resources.getString(R.string.wireless) to "")
|
||||
// Bluetooth
|
||||
functionsList.add(
|
||||
resources.getString(R.string.bluetooth) to getYesNoString(
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
|
||||
)
|
||||
)
|
||||
val hasBluetoothLe = getYesNoString(
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)
|
||||
)
|
||||
functionsList.add(resources.getString(R.string.bluetooth_le) to hasBluetoothLe)
|
||||
// GPS
|
||||
functionsList.add(
|
||||
"GPS" to getYesNoString(
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS)
|
||||
)
|
||||
)
|
||||
// NFC
|
||||
functionsList.add(
|
||||
"NFC" to getYesNoString(
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_NFC)
|
||||
)
|
||||
)
|
||||
functionsList.add(
|
||||
"NFC Card Emulation" to getYesNoString(
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)
|
||||
)
|
||||
)
|
||||
// Wi-Fi
|
||||
val hasWiFi = packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)
|
||||
functionsList.add("Wi-Fi" to getYesNoString(hasWiFi))
|
||||
if (hasWiFi) {
|
||||
functionsList.add(
|
||||
"Wi-Fi Aware" to getYesNoString(
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)
|
||||
)
|
||||
)
|
||||
functionsList.add(
|
||||
"Wi-Fi Direct" to getYesNoString(
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)
|
||||
)
|
||||
)
|
||||
functionsList.add(
|
||||
"Wi-Fi Passpoint" to getYesNoString(
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_PASSPOINT)
|
||||
)
|
||||
)
|
||||
functionsList.add("Wi-Fi 5Ghz" to getYesNoString(wifiManager.is5GHzBandSupported))
|
||||
functionsList.add("Wi-Fi P2P" to getYesNoString(wifiManager.isP2pSupported))
|
||||
}
|
||||
|
||||
try {
|
||||
val bluetoothMac = android.provider.Settings.Secure.getString(
|
||||
contentResolver,
|
||||
"bluetooth_address"
|
||||
)
|
||||
if (bluetoothMac != null && bluetoothMac.isNotEmpty())
|
||||
functionsList.add(resources.getString(R.string.bluetooth_mac) to bluetoothMac)
|
||||
} catch (e: Exception) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
// Wi-Fi mac
|
||||
val filePath = "/sys/class/net/wlan0/address"
|
||||
try {
|
||||
val reader = RandomAccessFile(filePath, "r")
|
||||
val value = reader.readLine()
|
||||
reader.close()
|
||||
functionsList.add(resources.getString(R.string.wifi_mac) to value)
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
|
||||
// IR
|
||||
val hasIr = irManager?.hasIrEmitter() ?: false
|
||||
functionsList.add(resources.getString(R.string.ir_emitter) to getYesNoString(hasIr))
|
||||
|
||||
return functionsList
|
||||
}
|
||||
|
||||
private fun getUsbInfo(): List<Pair<String, String>> {
|
||||
val featureList = mutableListOf<Pair<String, String>>()
|
||||
featureList.add("USB" to "")
|
||||
featureList.add(
|
||||
"OTG" to getYesNoString(
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST)
|
||||
)
|
||||
)
|
||||
return featureList
|
||||
}
|
||||
|
||||
/**
|
||||
* @return number of the sound card in device - default is 1
|
||||
*/
|
||||
private fun getSoundCardNumber(): Int {
|
||||
class AudioFilter : FileFilter {
|
||||
// http://alsa.opensrc.org/Proc_asound_documentation
|
||||
override fun accept(pathname: File): Boolean =
|
||||
Pattern.matches("card[0-7]+", pathname.name)
|
||||
}
|
||||
|
||||
return try {
|
||||
File("/proc/asound/").listFiles(AudioFilter())!!.size
|
||||
} catch (e: Exception) {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available data connected with sound card like ALSA version etc.
|
||||
*/
|
||||
private fun getSoundCardInfo(): List<Pair<String, String>> {
|
||||
val functionsList = mutableListOf<Pair<String, String>>()
|
||||
|
||||
val soundCardNumber = getSoundCardNumber()
|
||||
functionsList.add(Pair(resources.getString(R.string.amount), soundCardNumber.toString()))
|
||||
for (i in 0 until soundCardNumber) {
|
||||
functionsList.add(
|
||||
Pair(
|
||||
" ${resources.getString(R.string.card)} $i",
|
||||
tryToGetSoundCardId(i)
|
||||
)
|
||||
)
|
||||
}
|
||||
// ALSA
|
||||
val alsa = tryToGetAlsa()
|
||||
Utils.addPairIfExists(functionsList, "ALSA", alsa)
|
||||
|
||||
return functionsList
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to read id of the sound card.
|
||||
*
|
||||
* @param cardPosition position of the sound card in files
|
||||
* @return id from "id" file
|
||||
*/
|
||||
private fun tryToGetSoundCardId(cardPosition: Int): String {
|
||||
var id = resources.getString(R.string.unknown)
|
||||
val filePath = "/proc/asound/card$cardPosition/id"
|
||||
|
||||
var reader: RandomAccessFile? = null
|
||||
try {
|
||||
reader = RandomAccessFile(filePath, "r")
|
||||
id = reader.readLine()
|
||||
} catch (ignored: Exception) {
|
||||
} finally {
|
||||
reader?.close()
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ALSA version if exists, otherwise null
|
||||
*/
|
||||
private fun tryToGetAlsa(): String? {
|
||||
var alsa: String? = null
|
||||
val filePath = "/proc/asound/version"
|
||||
|
||||
var reader: RandomAccessFile? = null
|
||||
try {
|
||||
reader = RandomAccessFile(filePath, "r")
|
||||
val version = reader.readLine()
|
||||
alsa = version
|
||||
} catch (ignored: Exception) {
|
||||
} finally {
|
||||
reader?.close()
|
||||
}
|
||||
|
||||
return alsa
|
||||
}
|
||||
|
||||
private fun hasCamera(): Boolean =
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
|
||||
|
||||
/**
|
||||
* Get number, type and orientation of the cameras
|
||||
*/
|
||||
private fun getCameraInfo(): List<Pair<String, String>> {
|
||||
val functionsList = mutableListOf<Pair<String, String>>()
|
||||
|
||||
try {
|
||||
val cameraIdList = cameraManager.cameraIdList
|
||||
val numberOfCameras = cameraIdList.size
|
||||
functionsList.add(
|
||||
Pair(
|
||||
resources.getString(R.string.amount),
|
||||
numberOfCameras.toString()
|
||||
)
|
||||
)
|
||||
|
||||
val cameraName = resources.getString(R.string.camera)
|
||||
val cameraType = resources.getString(R.string.type)
|
||||
val cameraOrientation = resources.getString(R.string.orientation)
|
||||
for (cameraId in cameraIdList) {
|
||||
functionsList.add(Pair(" $cameraName $cameraId", " "))
|
||||
try {
|
||||
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
|
||||
val orientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)
|
||||
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
|
||||
functionsList.add(Pair(" $cameraType", getCameraFacing(facing)))
|
||||
functionsList.add(Pair(" $cameraOrientation", orientation.toString()))
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
val lensAmount = characteristics.physicalCameraIds.size
|
||||
if (lensAmount > 0) {
|
||||
functionsList.add(
|
||||
Pair(
|
||||
" ${resources.getString(R.string.camera_lens_number)}",
|
||||
lensAmount.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
functionsList.add(Pair(resources.getString(R.string.amount), "0"))
|
||||
}
|
||||
|
||||
return functionsList
|
||||
}
|
||||
|
||||
private fun getCameraFacing(facing: Int?): String =
|
||||
when (facing) {
|
||||
CameraCharacteristics.LENS_FACING_FRONT ->
|
||||
resources.getString(R.string.front)
|
||||
|
||||
CameraCharacteristics.LENS_FACING_BACK ->
|
||||
resources.getString(R.string.back)
|
||||
|
||||
CameraCharacteristics.LENS_FACING_EXTERNAL ->
|
||||
resources.getString(R.string.camera_external)
|
||||
|
||||
else -> resources.getString(R.string.unknown)
|
||||
}
|
||||
|
||||
private fun getYesNoString(yesValue: Boolean) = if (yesValue) {
|
||||
resources.getString(R.string.yes)
|
||||
} else {
|
||||
resources.getString(R.string.no)
|
||||
}
|
||||
}
|
||||