mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-22 12:00:35 +03:00
Compare commits
609 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1f9de0be7e | ||
|
550c486077 | ||
|
006c554456 | ||
|
6443881193 | ||
|
a98693bbdb | ||
|
03e555eb27 | ||
|
ef73d89491 | ||
|
4ad789d20b | ||
|
7c163c237a | ||
|
38432a3cdc | ||
|
53aee22794 | ||
|
9880f15017 | ||
|
0fc836cf83 | ||
|
6eb20d1c0b | ||
|
1f9c9505d7 | ||
|
2487d24371 | ||
|
c27b3759b1 | ||
|
481f4e0347 | ||
|
a9b47f5aef | ||
|
b53bb38631 | ||
|
a2a42e06b0 | ||
|
adf5b95243 | ||
|
3d82b3935d | ||
|
7ee66e51e8 | ||
|
b126cd594a | ||
|
31f654ee2f | ||
|
42c8edc6e7 | ||
|
8f7c7c33ff | ||
|
c105c0d02d | ||
|
e74fb3795a | ||
|
80923ca8ac | ||
|
f2fa530047 | ||
|
4cb6ab031b | ||
|
2d224e5f84 | ||
|
5a653bebb2 | ||
|
5566cb05ab | ||
|
7ee7aa23be | ||
|
5a2ae50c74 | ||
|
62d7e6bb6d | ||
|
5266fc0b0f | ||
|
612925bc7d | ||
|
81499a1b37 | ||
|
27fc8462d1 | ||
|
f3e205a452 | ||
|
84374f8c13 | ||
|
5e8bf249f8 | ||
|
3913e676b7 | ||
|
a70ee39403 | ||
|
05bd9f0d2c | ||
|
4d9388f8fe | ||
|
8acd7da959 | ||
|
c909848ed5 | ||
|
e5c8e874e3 | ||
|
9d5caed73d | ||
|
292086e9d6 | ||
|
5639fcfb8b | ||
|
ce39da4b79 | ||
|
4e89672d15 | ||
|
6725ef5a2c | ||
|
680cc90871 | ||
|
c19dc125a9 | ||
|
be8fa3c0d1 | ||
|
312a97d664 | ||
|
fca954102a | ||
|
9e078b4879 | ||
|
46e85c27a2 | ||
|
1124cef382 | ||
|
01ebf6e1aa | ||
|
b65050ae85 | ||
|
ddec1d7bdc | ||
|
888d34aeb9 | ||
|
0fb3d8aeca | ||
|
c56913733f | ||
|
76c61e1866 | ||
|
810bb9ddd1 | ||
|
68a2123293 | ||
|
4221b18e09 | ||
|
e7ed066eed | ||
|
1301f7e116 | ||
|
63b064b021 | ||
|
a9be77aa75 | ||
|
fc7dcc9f77 | ||
|
48461f82a8 | ||
|
81ea86fd01 | ||
|
8803f4444e | ||
|
38a97b2b91 | ||
|
6e7bcc4362 | ||
|
c12224e811 | ||
|
b8ef3cd177 | ||
|
2de773e5de | ||
|
fe5b63ad1f | ||
|
4aff5857fd | ||
|
a0d26cb3e7 | ||
|
639ef03bce | ||
|
b855e4bbe7 | ||
|
b83e349f5c | ||
|
abad0438e3 | ||
|
30f02c7eac | ||
|
64f1c3e172 | ||
|
f7f1f40668 | ||
|
6519945c7b | ||
|
94979aeaab | ||
|
37e43b73a7 | ||
|
2cf2cf31c4 | ||
|
9736ae451a | ||
|
26331c1a07 | ||
|
35ffe9ef10 | ||
|
976400d0e1 | ||
|
d2ef76a2c5 | ||
|
c0926b1e13 | ||
|
bf5d41ab30 | ||
|
e2716a5965 | ||
|
8351f1dc0a | ||
|
90997d5f4c | ||
|
747f071f2f | ||
|
9e76308cf2 | ||
|
c452030fe1 | ||
|
dbbeac6084 | ||
|
d56ec198a1 | ||
|
13238dfdc0 | ||
|
8063814bdc | ||
|
3d8abdc65b | ||
|
4832876e54 | ||
|
a0b0409930 | ||
|
8e6e9d4e8e | ||
|
ee6d03db35 | ||
|
364270d338 | ||
|
a4dc06fa8a | ||
|
5c94d995d4 | ||
|
366da1c30c | ||
|
e893510e79 | ||
|
67b359999e | ||
|
01569647f7 | ||
|
3ee20113ae | ||
|
6edff7e053 | ||
|
70cc124818 | ||
|
98bf943a86 | ||
|
58944bb0fd | ||
|
397e1b6ecc | ||
|
71336b3c9f | ||
|
cd47bcf082 | ||
|
4fb5d2a437 | ||
|
e8bf5a38b7 | ||
|
ddfaf520e5 | ||
|
4d1c7464b9 | ||
|
42c6eac97f | ||
|
2ae0a27588 | ||
|
f2e8c0c331 | ||
|
7ce522fd15 | ||
|
170c61ef84 | ||
|
87fdf94e61 | ||
|
fffe245df5 | ||
|
95194bed3e | ||
|
a9467e2fd8 | ||
|
4b99fdb788 | ||
|
727e53e096 | ||
|
69fc9b955a | ||
|
601d0ccdaa | ||
|
b19b1fb65a | ||
|
442f622b35 | ||
|
de523a6451 | ||
|
17260878ac | ||
|
c6d26cdd67 | ||
|
458fe5c36e | ||
|
f388aaf4d8 | ||
|
6ddff58afb | ||
|
1e176f995a | ||
|
22c61258cc | ||
|
288a1ad1c2 | ||
|
823465d6fd | ||
|
0786b634e5 | ||
|
7f4f944d79 | ||
|
264c84d540 | ||
|
e48c823de0 | ||
|
79f503beb3 | ||
|
34da90ad26 | ||
|
566f780621 | ||
|
c76bce5e94 | ||
|
27f0ee3a03 | ||
|
a1c21da7a6 | ||
|
401ea0fcf1 | ||
|
c062345f5e | ||
|
516cc94772 | ||
|
104df418cc | ||
|
5755363f2e | ||
|
5167f9e45e | ||
|
7a24be4b98 | ||
|
f9bafa93da | ||
|
8c7e8a8ae0 | ||
|
76f16cc9f1 | ||
|
907c94096b | ||
|
f6ad5be3e0 | ||
|
db0f3b21e1 | ||
|
709dff1a81 | ||
|
d2ed058d31 | ||
|
7ad16ad92c | ||
|
8ab3b2634d | ||
|
3349da6809 | ||
|
e581776e43 | ||
|
f292dc667e | ||
|
7b8d1dec9b | ||
|
2d943edd61 | ||
|
86464ba137 | ||
|
f2b47a257d | ||
|
09e87ce8a0 | ||
|
490461f840 | ||
|
fb123d926d | ||
|
d2a98a3022 | ||
|
896c946a5a | ||
|
560b593fcb | ||
|
fe9bce334a | ||
|
6af4d31aa3 | ||
|
0ec2bb4fce | ||
|
7f1dc14a4f | ||
|
8b2843410f | ||
|
deef540ddc | ||
|
cd101aef2a | ||
|
72bf0085b4 | ||
|
bfb7c85eac | ||
|
0a3717f448 | ||
|
fa3ca57a4e | ||
|
002a3250e4 | ||
|
ab10d39f75 | ||
|
82f6758649 | ||
|
979fb116e8 | ||
|
ae97ded344 | ||
|
71d45c89fb | ||
|
76a5705b2b | ||
|
6b000bc90f | ||
|
b87203bfe4 | ||
|
818cb96eb5 | ||
|
e9fdbd924b | ||
|
a0b9e738a5 | ||
|
0dcdd94149 | ||
|
410e4d9d96 | ||
|
68567731ad | ||
|
75ecfce1df | ||
|
ddf8ce7029 | ||
|
f99fb1c92a | ||
|
7fcb58963c | ||
|
f89b2da30d | ||
|
691bfbe594 | ||
|
7061b7a324 | ||
|
6bc520d551 | ||
|
ff20c671a2 | ||
|
37935a5f86 | ||
|
1f6df202b9 | ||
|
06496ddf37 | ||
|
509e33ac20 | ||
|
6bc09854ac | ||
|
fc637166bb | ||
|
6eb7c9d25c | ||
|
0492b0fa6f | ||
|
acbaae9f14 | ||
|
77c1329be5 | ||
|
aefdadbbd5 | ||
|
29a1527d24 | ||
|
18fd8f64c6 | ||
|
4f55a2a4a5 | ||
|
1fa5f4c2f8 | ||
|
2a9bf9dd29 | ||
|
a04517de90 | ||
|
21e4293adb | ||
|
358af365d2 | ||
|
62ba16eedd | ||
|
e7825fc90c | ||
|
1fba65ed3a | ||
|
7209779b64 | ||
|
ecee57e166 | ||
|
288f72b972 | ||
|
6be96ee8c9 | ||
|
65dd30eaa8 | ||
|
698360b77a | ||
|
b57b799feb | ||
|
2436537609 | ||
|
96ac6fcac7 | ||
|
9fa80d206b | ||
|
8990b4d622 | ||
|
211af57e8b | ||
|
9fd2a91f15 | ||
|
c85e2ef3f4 | ||
|
8ab840023a | ||
|
db278afe4a | ||
|
d08711eb0c | ||
|
500ffa8009 | ||
|
7124939467 | ||
|
4a21db2f73 | ||
|
fe555c076d | ||
|
b40b0048da | ||
|
e08acc27a7 | ||
|
c032d32e02 | ||
|
f202febb4d | ||
|
69b83fe538 | ||
|
c42439ef69 | ||
|
d420f70225 | ||
|
843cf8eb28 | ||
|
9b9e552b95 | ||
|
c3c241466c | ||
|
e8c56b1a06 | ||
|
aa3f44cdb0 | ||
|
f28fb5dbf3 | ||
|
45f4ad5ab0 | ||
|
8d241c17c8 | ||
|
cc17b28be7 | ||
|
e391746625 | ||
|
ae306e6eb3 | ||
|
11a30a15d9 | ||
|
dc4e85b21d | ||
|
3da9097c6b | ||
|
24fb2e4f43 | ||
|
8469a099b0 | ||
|
ea2048babf | ||
|
f3152c5e4d | ||
|
8b7fd123b7 | ||
|
c5fb4e25a3 | ||
|
162ae4dfac | ||
|
df3dc4221f | ||
|
ecfa17762c | ||
|
1df8b0bb41 | ||
|
fbec7610f3 | ||
|
e6441f1344 | ||
|
9a2a959972 | ||
|
f931c906c0 | ||
|
ed1191937b | ||
|
1d17274e00 | ||
|
e86bdca3eb | ||
|
ffdc43b6bf | ||
|
5c173f39f1 | ||
|
7e8467f852 | ||
|
ae7a200144 | ||
|
623564c28d | ||
|
971ed705fa | ||
|
725d9281bf | ||
|
8b9dc294c1 | ||
|
429c7d2473 | ||
|
ab50be3784 | ||
|
83fae89763 | ||
|
1f68a59af9 | ||
|
e1be29a6a8 | ||
|
ab16c45e54 | ||
|
024b2e8edb | ||
|
c04f90a6ac | ||
|
240e7fd8fb | ||
|
3de3844e75 | ||
|
01c8092cac | ||
|
a9494626bb | ||
|
ec852b0e74 | ||
|
1fe13e84ce | ||
|
d8cb11808c | ||
|
35334c93cc | ||
|
a98f50412f | ||
|
b0e8ddfa66 | ||
|
c18f07362f | ||
|
26e9fcd40f | ||
|
b2498367b1 | ||
|
f68eee5233 | ||
|
fd22c0f8b5 | ||
|
bfe24e5dfd | ||
|
63296829a8 | ||
|
dc9b261c48 | ||
|
bfec814b43 | ||
|
03d06896e9 | ||
|
5a4989186e | ||
|
eb380b9af9 | ||
|
01124c8ecf | ||
|
365067f1a0 | ||
|
6a97636c7a | ||
|
eb3aa0d202 | ||
|
53ea17d2b9 | ||
|
a1e339f850 | ||
|
4809317c63 | ||
|
c5c0497716 | ||
|
79ac73020b | ||
|
d9dfef4016 | ||
|
3bd3607220 | ||
|
e35a33edde | ||
|
c1013f6b80 | ||
|
21a27c691d | ||
|
25f3ff0bd3 | ||
|
4feb84bd83 | ||
|
4c049671db | ||
|
3a1251dd2a | ||
|
8dd7758bc6 | ||
|
296308cebf | ||
|
45ca0966fd | ||
|
77d3f8c11b | ||
|
7a453dbd30 | ||
|
0a6a12c70a | ||
|
448fdb70b0 | ||
|
5e4ec56ae7 | ||
|
8c42700676 | ||
|
22fda501f4 | ||
|
556d3bb90d | ||
|
0f18b20fa3 | ||
|
a8961e8e96 | ||
|
291528a309 | ||
|
2a7cdbeded | ||
|
c09739cea4 | ||
|
315271390f | ||
|
2df8d049d0 | ||
|
0643b1bd1c | ||
|
2b1291ae51 | ||
|
5ec0d8a96b | ||
|
71168983b6 | ||
|
bdcb1a505b | ||
|
94f29d270c | ||
|
98a61954a3 | ||
|
238d91c167 | ||
|
376748b298 | ||
|
13091948ea | ||
|
0cfd8e8240 | ||
|
7a17936855 | ||
|
1d7328c03e | ||
|
76da209c6d | ||
|
ddd9c29d7a | ||
|
fe696943a4 | ||
|
a5bfc08264 | ||
|
4c2c7252c3 | ||
|
b5dd0fdca2 | ||
|
a7ee33c7c0 | ||
|
7b56017844 | ||
|
0fb345dd24 | ||
|
4faf2db11f | ||
|
c118bd70f9 | ||
|
b0e850d17e | ||
|
a97c6e15e9 | ||
|
d084a35316 | ||
|
e8bfa5dc04 | ||
|
e729e3b063 | ||
|
8337f4a7e4 | ||
|
0ae32c3cfc | ||
|
49f3bd27ed | ||
|
1acfa917c9 | ||
|
9786cf2abf | ||
|
537d65affc | ||
|
5ab2ec08f0 | ||
|
e21477a5ee | ||
|
5daeddcc63 | ||
|
70d02f4493 | ||
|
58de991d64 | ||
|
90ffa32246 | ||
|
3d94de9e46 | ||
|
50aa2d0a2d | ||
|
0e2171b872 | ||
|
2c3f43f139 | ||
|
fd8afe0231 | ||
|
cd982814cf | ||
|
338fb618b9 | ||
|
842cb36ecb | ||
|
e06b8bc22e | ||
|
82fb45bd55 | ||
|
3a39902c4c | ||
|
88364b15d6 | ||
|
751b946092 | ||
|
39085f68b1 | ||
|
1beb67c497 | ||
|
2ba001894a | ||
|
0650ce0bba | ||
|
218f144848 | ||
|
83c9c188e9 | ||
|
a4e8a7f94d | ||
|
4f5d503ceb | ||
|
381e2e4b86 | ||
|
2a90fe4aab | ||
|
f37301e738 | ||
|
4e9cea87a8 | ||
|
fca5ffaa0c | ||
|
a0314a865c | ||
|
5da9a2819c | ||
|
2a02c94c8f | ||
|
96073125ca | ||
|
58bd663ac0 | ||
|
e689193df1 | ||
|
1aa388d48f | ||
|
8f84020cfa | ||
|
db88ff8431 | ||
|
2d1642170a | ||
|
0cb7952943 | ||
|
d750c84606 | ||
|
7abca537c9 | ||
|
ca2c5483c0 | ||
|
7b414a3a23 | ||
|
10767d2d5b | ||
|
404c7c05d5 | ||
|
0b2c8eccfe | ||
|
4882037098 | ||
|
138db03667 | ||
|
f59e039c49 | ||
|
e0679f99cf | ||
|
ffb78b166b | ||
|
5fcbf59e0e | ||
|
e62b8972e7 | ||
|
322457910c | ||
|
e9b602890a | ||
|
08d3618eb3 | ||
|
4f59c4d3ad | ||
|
9ca5a9257d | ||
|
6694d6f60b | ||
|
df7ff21cc9 | ||
|
732d44cb73 | ||
|
76eb89f5eb | ||
|
185762e164 | ||
|
33e4913761 | ||
|
aede9be97c | ||
|
97556a36e5 | ||
|
fb970ffb80 | ||
|
6de6cda7a4 | ||
|
5eed5c70b5 | ||
|
6e1078a256 | ||
|
c6c58262af | ||
|
2df89f4d81 | ||
|
e83026f29a | ||
|
233e4f7a67 | ||
|
dba12d147f | ||
|
ccdd994756 | ||
|
dfcac45669 | ||
|
f72fc1885c | ||
|
db40d95215 | ||
|
c2f4b58088 | ||
|
82d2596c66 | ||
|
ccd7f5881d | ||
|
c7edfbcae6 | ||
|
b1839c9562 | ||
|
dbef8307ea | ||
|
ee52070925 | ||
|
a406b8d211 | ||
|
367c1508b5 | ||
|
e5fce6a832 | ||
|
6b5c96ea74 | ||
|
d2faad60ca | ||
|
2d7c26f13d | ||
|
116e5aa4cf | ||
|
3e8f45a073 | ||
|
8090d4e039 | ||
|
9ca29cd11b | ||
|
76af2ffcc1 | ||
|
82d554a900 | ||
|
c70fcd7447 | ||
|
297f71a8c8 | ||
|
8c00388dc7 | ||
|
d92203ada6 | ||
|
f8d24a943d | ||
|
a1e84183d6 | ||
|
2b339ccc7d | ||
|
893ae7fb1e | ||
|
a828b19cce | ||
|
13f296b5ed | ||
|
aa2c460529 | ||
|
61b3b671a5 | ||
|
76d2fcdcc3 | ||
|
2fe8e5f229 | ||
|
4dcd495c96 | ||
|
6fe9f730e3 | ||
|
cb781cb76d | ||
|
037519a5d3 | ||
|
586b4f16e7 | ||
|
1f9afd92b6 | ||
|
64d6605c2a | ||
|
e2593406fa | ||
|
8647ea20f7 | ||
|
eddb5947d3 | ||
|
9ac48946f5 | ||
|
32e907905e | ||
|
7e9dfb0c72 | ||
|
4c9e0aaf87 | ||
|
d0b3371958 | ||
|
b406ec8d07 | ||
|
2255816a1b | ||
|
70f7d20fea | ||
|
5a9ca9f565 | ||
|
db97b03224 | ||
|
2ebb333a6e | ||
|
c72c6354e3 | ||
|
7a70c4565c | ||
|
be55eb514c | ||
|
0a81df23e7 | ||
|
46ad240306 | ||
|
d3151d934a | ||
|
0ad68f1f11 | ||
|
724c7856d9 | ||
|
36db1f7799 | ||
|
b4613c3b38 | ||
|
6ed8504d32 | ||
|
9c2ac56435 | ||
|
61e25e3c26 | ||
|
9c2eece929 | ||
|
145bcec047 | ||
|
efd840e5fe | ||
|
d9ff239b03 | ||
|
6f808c4ebc | ||
|
a43d2351ae | ||
|
839633c2f0 | ||
|
f6b43fd2fd | ||
|
735e3225f8 | ||
|
386bfd884c | ||
|
03ff59c0b8 | ||
|
28f06c0b09 | ||
|
2933e0d801 | ||
|
318d362b77 | ||
|
1b1914e7ad | ||
|
ae853d8d4a | ||
|
f840149275 | ||
|
990479e37d | ||
|
5e7837b4b9 | ||
|
a53b2bc2c0 | ||
|
c0536353eb | ||
|
e9b288cb6e | ||
|
9a4f1bfc60 | ||
|
924ad71855 |
@ -1,224 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
parameters:
|
||||
memory-config:
|
||||
type: string
|
||||
default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g"
|
||||
memory-config-debug:
|
||||
type: string
|
||||
default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g -verbose:gc -Xlog:gc*"
|
||||
|
||||
jobs:
|
||||
Done in GitLab CI:
|
||||
docker:
|
||||
- image: busybox:stable
|
||||
steps:
|
||||
- run:
|
||||
name: Done in GitLab CI
|
||||
command: echo This build will be done in GitLab CI
|
||||
Check Style:
|
||||
docker:
|
||||
- image: cimg/android:2023.02.1
|
||||
working_directory: ~/ultrasonic
|
||||
environment:
|
||||
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
|
||||
GRADLE_OPTS: << pipeline.parameters.memory-config >>
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
- ultrasonic-{{ .Branch }}
|
||||
- ultrasonic
|
||||
- run:
|
||||
name: Check Style
|
||||
command: ./gradlew -Pqc ktlintCheck
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.gradle
|
||||
key: ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
Static Analysis:
|
||||
docker:
|
||||
- image: cimg/android:2023.02.1
|
||||
working_directory: ~/ultrasonic
|
||||
environment:
|
||||
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
|
||||
GRADLE_OPTS: << pipeline.parameters.memory-config >>
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
- ultrasonic-{{ .Branch }}
|
||||
- ultrasonic
|
||||
- run:
|
||||
name: Check Style
|
||||
command: ./gradlew -Pqc detekt
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.gradle
|
||||
key: ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
Lint:
|
||||
docker:
|
||||
- image: cimg/android:2023.02.1
|
||||
working_directory: ~/ultrasonic
|
||||
environment:
|
||||
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
|
||||
GRADLE_OPTS: << pipeline.parameters.memory-config >>
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
- ultrasonic-{{ .Branch }}
|
||||
- ultrasonic
|
||||
- run:
|
||||
name: Lint
|
||||
command: ./gradlew :ultrasonic:lintRelease
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.gradle
|
||||
key: ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
Unit Tests:
|
||||
docker:
|
||||
- image: cimg/android:2023.02.1
|
||||
working_directory: ~/ultrasonic
|
||||
environment:
|
||||
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
|
||||
GRADLE_OPTS: << pipeline.parameters.memory-config >>
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
- ultrasonic-{{ .Branch }}
|
||||
- ultrasonic
|
||||
- run:
|
||||
name: Check Style
|
||||
command: ./gradlew ciTest testDebugUnitTest
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.gradle
|
||||
key: ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
Assemble Debug:
|
||||
docker:
|
||||
- image: cimg/android:2023.02.1
|
||||
working_directory: ~/ultrasonic
|
||||
environment:
|
||||
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
|
||||
GRADLE_OPTS: << pipeline.parameters.memory-config >>
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
- ultrasonic-{{ .Branch }}
|
||||
- ultrasonic
|
||||
- run:
|
||||
name: Assemble Debug
|
||||
command: ./gradlew assembleDebug
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.gradle
|
||||
key: ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
Assemble Release:
|
||||
docker:
|
||||
- image: cimg/android:2023.02.1
|
||||
working_directory: ~/ultrasonic
|
||||
environment:
|
||||
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
|
||||
GRADLE_OPTS: << pipeline.parameters.memory-config >>
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
- ultrasonic-{{ .Branch }}
|
||||
- ultrasonic
|
||||
- run:
|
||||
name: Assemble Release
|
||||
command: ./gradlew assembleRelease
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.gradle
|
||||
key: ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
|
||||
- store_artifacts:
|
||||
path: ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk
|
||||
destination: ultrasonic-release-unsigned
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
Check and Build:
|
||||
jobs:
|
||||
- Done in GitLab CI:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- develop
|
||||
- master
|
||||
tags:
|
||||
only: /.*/
|
||||
- Check Style:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- develop
|
||||
- master
|
||||
tags:
|
||||
ignore: /.*/
|
||||
- Static Analysis:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- develop
|
||||
- master
|
||||
tags:
|
||||
ignore: /.*/
|
||||
- Lint:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- develop
|
||||
- master
|
||||
tags:
|
||||
ignore: /.*/
|
||||
- Unit Tests:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- develop
|
||||
- master
|
||||
tags:
|
||||
ignore: /.*/
|
||||
- Assemble Debug:
|
||||
requires:
|
||||
- Check Style
|
||||
- Static Analysis
|
||||
- Lint
|
||||
- Unit Tests
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- develop
|
||||
- master
|
||||
tags:
|
||||
ignore: /.*/
|
||||
- Assemble Release:
|
||||
requires:
|
||||
- Check Style
|
||||
- Static Analysis
|
||||
- Lint
|
||||
- Unit Tests
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- develop
|
||||
- master
|
||||
tags:
|
||||
ignore: /.*/
|
2
.editorconfig
Normal file
2
.editorconfig
Normal file
@ -0,0 +1,2 @@
|
||||
[*.{kt,kts}]
|
||||
ktlint_code_style = android_studio
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,6 +18,7 @@ out/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
.kotlin/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
|
124
.gitlab-ci.yml
124
.gitlab-ci.yml
@ -1,7 +1,9 @@
|
||||
default:
|
||||
image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/cimg/android:2023.02.1
|
||||
cache:
|
||||
key: ${CI_COMMIT_REF_SLUG}
|
||||
image: registry.gitlab.com/ultrasonic/ci-android:1.2.0
|
||||
cache: &global_cache
|
||||
key:
|
||||
files:
|
||||
- gradle/wrapper/gradle-wrapper.properties
|
||||
paths:
|
||||
- .gradle/
|
||||
|
||||
@ -15,83 +17,79 @@ variables:
|
||||
PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/Ultrasonic/${CI_COMMIT_TAG}"
|
||||
PACKAGE_APK: "ultrasonic-${CI_COMMIT_TAG}.apk"
|
||||
PACKAGE_APK_IDSIG: "ultrasonic-${CI_COMMIT_TAG}.apk.idsig"
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
# The project id of https://gitlab.com/ultrasonic/ultrasonic/
|
||||
ROOT_PROJECT_ID: 37671564
|
||||
|
||||
stages:
|
||||
- Check
|
||||
- Assemble
|
||||
- Translations
|
||||
- APK
|
||||
- Build
|
||||
- Sign APK
|
||||
- Publish
|
||||
- Release
|
||||
|
||||
Check Style:
|
||||
stage: Check
|
||||
script: ./gradlew -Pqc ktlintCheck
|
||||
cache:
|
||||
# inherit all global cache settings
|
||||
<<: *global_cache
|
||||
policy: pull
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != "37671564"
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != $ROOT_PROJECT_ID
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
Static Analysis:
|
||||
stage: Check
|
||||
script: ./gradlew -Pqc detekt
|
||||
cache:
|
||||
# inherit all global cache settings
|
||||
<<: *global_cache
|
||||
policy: pull
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != "37671564"
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != $ROOT_PROJECT_ID
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
Lint:
|
||||
stage: Check
|
||||
script: ./gradlew :ultrasonic:lintRelease
|
||||
cache:
|
||||
# inherit all global cache settings
|
||||
<<: *global_cache
|
||||
policy: pull-push
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != "37671564"
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != $ROOT_PROJECT_ID
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
Unit Tests:
|
||||
stage: Check
|
||||
script: ./gradlew ciTest testDebugUnitTest
|
||||
cache:
|
||||
# inherit all global cache settings
|
||||
<<: *global_cache
|
||||
policy: pull-push
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != "37671564"
|
||||
|
||||
Assemble Debug:
|
||||
stage: Assemble
|
||||
script: ./gradlew assembleDebug
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != "37671564"
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != $ROOT_PROJECT_ID
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
Assemble Release:
|
||||
stage: Assemble
|
||||
script: ./gradlew assembleRelease
|
||||
stage: Build
|
||||
script:
|
||||
- sed -i 's/applicationId \"org.moire.ultrasonic\"/applicationId "org.moire.ultrasonic.gitlab"/' ultrasonic/build.gradle
|
||||
- ./gradlew assembleRelease
|
||||
artifacts:
|
||||
name: ultrasonic-release-unsigned-${CI_COMMIT_SHA}
|
||||
paths:
|
||||
- ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != "37671564"
|
||||
|
||||
Push Translations:
|
||||
stage: Translations
|
||||
image: alpine:latest
|
||||
script:
|
||||
- wget -O /tmp/tx-linux-amd64.tar.gz https://github.com/transifex/cli/releases/latest/download/tx-linux-amd64.tar.gz
|
||||
- tar xf /tmp/tx-linux-amd64.tar.gz -C /tmp
|
||||
- mv /tmp/tx /usr/bin/tx
|
||||
- tx push -s
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" && $CI_PROJECT_ID == "37671564"
|
||||
|
||||
Generate Signed Develop APK:
|
||||
stage: APK
|
||||
script:
|
||||
- openssl aes-256-cbc -K ${ULTRASONIC_KEYSTORE_KEY} -iv ${ULTRASONIC_KEYSTORE_IV} -in ultrasonic-keystore.enc -out ultrasonic-keystore -d
|
||||
- mkdir -p ${CI_PROJECT_DIR}/ultrasonic-release
|
||||
- ${ANDROID_HOME}/build-tools/*/zipalign -v 4 ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk ${CI_PROJECT_DIR}/ultrasonic-release/ultrasonic-${CI_COMMIT_SHA}.apk
|
||||
- ${ANDROID_HOME}/build-tools/*/apksigner sign --verbose --ks ${CI_PROJECT_DIR}/ultrasonic-keystore --ks-pass pass:${ULTRASONIC_KEYSTORE_STOREPASS} --key-pass pass:${ULTRASONIC_KEYSTORE_KEYPASS} ${CI_PROJECT_DIR}/ultrasonic-release/ultrasonic-${CI_COMMIT_SHA}.apk
|
||||
- ${ANDROID_HOME}/build-tools/*/apksigner verify --verbose ${CI_PROJECT_DIR}/ultrasonic-release/ultrasonic-${CI_COMMIT_SHA}.apk
|
||||
artifacts:
|
||||
name: ultrasonic-${CI_COMMIT_SHA}
|
||||
paths:
|
||||
- ultrasonic-release/
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" && $CI_PROJECT_ID == "37671564"
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != $ROOT_PROJECT_ID
|
||||
|
||||
# We generate a signed package for each commit to develop as well as when making a release.
|
||||
# Since the develop signed apk are not persistent they can be downloaded for around 3 weeks before Gitlab deletes them.
|
||||
Generate Signed APK:
|
||||
stage: APK
|
||||
stage: Sign APK
|
||||
# We don't need the gradle cache here
|
||||
cache: []
|
||||
script:
|
||||
- openssl aes-256-cbc -K ${ULTRASONIC_KEYSTORE_KEY} -iv ${ULTRASONIC_KEYSTORE_IV} -in ultrasonic-keystore.enc -out ultrasonic-keystore -d
|
||||
- mkdir -p ${CI_PROJECT_DIR}/ultrasonic-release
|
||||
@ -99,11 +97,17 @@ Generate Signed APK:
|
||||
- ${ANDROID_HOME}/build-tools/*/apksigner sign --verbose --ks ${CI_PROJECT_DIR}/ultrasonic-keystore --ks-pass pass:${ULTRASONIC_KEYSTORE_STOREPASS} --key-pass pass:${ULTRASONIC_KEYSTORE_KEYPASS} ${CI_PROJECT_DIR}/ultrasonic-release/${PACKAGE_APK}
|
||||
- ${ANDROID_HOME}/build-tools/*/apksigner verify --verbose ${CI_PROJECT_DIR}/ultrasonic-release/${PACKAGE_APK}
|
||||
artifacts:
|
||||
name: ultrasonic-${CI_COMMIT_TAG}
|
||||
name: $PACKAGE_APK
|
||||
paths:
|
||||
- ultrasonic-release/
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == "37671564"
|
||||
# Run when releasing a new tag
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
||||
# Or when adding a new commit to develop (but never inside merge events)
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" && $CI_PROJECT_ID == $ROOT_PROJECT_ID && $CI_PIPELINE_SOURCE != "merge_request_event"
|
||||
variables:
|
||||
PACKAGE_APK: ultrasonic-${CI_COMMIT_SHA}.apk
|
||||
|
||||
|
||||
Publish Signed APK:
|
||||
stage: Publish
|
||||
@ -114,7 +118,7 @@ Publish Signed APK:
|
||||
- |
|
||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ultrasonic-release/${PACKAGE_APK_IDSIG} "${PACKAGE_REGISTRY_URL}/${PACKAGE_APK_IDSIG}"
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == "37671564"
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
||||
|
||||
Release:
|
||||
stage: Release
|
||||
@ -124,4 +128,24 @@ Release:
|
||||
--assets-link "{\"name\":\"${PACKAGE_APK}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${PACKAGE_APK}\"}" \
|
||||
--assets-link "{\"name\":\"${PACKAGE_APK_IDSIG}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${PACKAGE_APK_IDSIG}\"}"
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == "37671564"
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
||||
|
||||
RoboTest:
|
||||
stage: Release
|
||||
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:latest
|
||||
# We don't need the gradle cache here
|
||||
cache: []
|
||||
script:
|
||||
- curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash
|
||||
- gcloud auth activate-service-account --key-file .secure_files/firebase-key.json
|
||||
- gcloud firebase test android run --project ultrasonic-61089 --type robo --app ultrasonic-release/${PACKAGE_APK} --robo-directives click:button1= --device model=Nexus6,version=21,locale=en,orientation=portrait --device model=Pixel3,version=28,locale=fr,orientation=landscape
|
||||
rules:
|
||||
# Run when releasing a new tag
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
||||
# or when requested by using [ROBO] inside the commit message and merging to develop
|
||||
# Would be nice to be able to run it in a MR as well, but currently not possible
|
||||
# Because it would not have access to the protected keys.
|
||||
- if: $CI_COMMIT_MESSAGE =~ /^\[ROBO\].*/ && $CI_PROJECT_ID == $ROOT_PROJECT_ID && $CI_COMMIT_REF_NAME == "develop" && $CI_PIPELINE_SOURCE != "merge_request_event"
|
||||
variables:
|
||||
PACKAGE_APK: ultrasonic-${CI_COMMIT_SHA}.apk
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
<!-- Please describe your changes here -->
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
@ -7,8 +6,6 @@
|
||||
- [ ] I ran `./gradlew -Pqc ktlintCheck`, `./gradlew -Pqc detekt` and
|
||||
`./gradlew :ultrasonic:lintRelease` and no problems found. See
|
||||
[CONTRIBUTING](CONTRIBUTING.md) for further information.
|
||||
- [ ] I'm using my own branch in my local copy. Ej, I want to merge
|
||||
`myuser/ultrasonic:my-new-contribution` into `develop`.
|
||||
- [ ] All commits [are
|
||||
signed](https://docs.gitlab.com/ee/user/project/repository/gpg_signed_commits/).
|
||||
- [ ] I agree to release my code and all other changes of this MR under the
|
||||
|
10
.gitlab/merge_request_templates/Release.md
Normal file
10
.gitlab/merge_request_templates/Release.md
Normal file
@ -0,0 +1,10 @@
|
||||
#### Before merge:
|
||||
- [ ] MR is targetting the master branch
|
||||
- [ ] **Squash commits must be disabled!**
|
||||
- [ ] RoboTests (5 physical, 10 virtual) on a Release apk return no errors
|
||||
- [ ] Release notes present
|
||||
|
||||
#### After merge
|
||||
- [ ] ``git fetch``
|
||||
- [ ] Create an annotated and signed tag: ``git tag -sa``
|
||||
- [ ] Push the tag to git:``git push --tags``
|
5
.idea/codeStyles/Project.xml
generated
5
.idea/codeStyles/Project.xml
generated
@ -1,11 +1,6 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value />
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
|
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
10
.tx/config
10
.tx/config
@ -1,10 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = fr_CA: fr-rCA, fr_FR: fr, es_ES: es, km_KH: km-rKH, th_TH: th, pl_PL: pl, zh_TW: zh-rTW, ru_RU: ru, pt_BR: pt-rBR, pt_PT: pt, he: iw, zh_HK: zh-rHK, da_DK: da-rDK, de_DE: de, bg_BG: bg, sr: sr, nl_NL: nl, zh_CN: zh-rCN, sv_SE: sv-rSE, el_GR: el, lt_LT: lt, it_IT: it, hu_HU: hu, kn_IN: kn-rIN, tr_TR: tr, cs_CZ: cs, id: in
|
||||
|
||||
[o:ultrasonic:p:ultrasonic:r:app]
|
||||
file_filter = ultrasonic/src/main/res/values-<lang>/strings.xml
|
||||
source_file = ultrasonic/src/main/res/values/strings.xml
|
||||
source_lang = en
|
||||
type = ANDROID
|
||||
|
@ -11,7 +11,7 @@ issue](https://gitlab.com/ultrasonic/ultrasonic/issues/new).
|
||||
## Contributing Translations
|
||||
|
||||
Interested in help to translate Ultrasonic? You can contribute in our
|
||||
[Transifex team](https://www.transifex.com/ultrasonic/ultrasonic/).
|
||||
[Weblate team](https://hosted.weblate.org/projects/ultrasonic/).
|
||||
|
||||
## Contributing Code
|
||||
|
||||
|
@ -31,12 +31,6 @@ If you want to use the version downloaded from F-Droid or from GitLab with
|
||||
First, see if your issue haven’t been yet reported [here][issues], otherwise
|
||||
open [a new issue][newissue].
|
||||
|
||||
### Known (not our) bugs
|
||||
|
||||
If you are using *Madsonic 5.1.X* several sections of Ultrasonic will not
|
||||
work. This is caused by bad implementation of Subsonic API by Madsonic. For
|
||||
more info about this you can read [this bug][madbug].
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING](CONTRIBUTING.md).
|
||||
@ -62,7 +56,6 @@ Full text of the license is available in the [LICENSE](LICENSE) file and
|
||||
[wikiaa]: https://gitlab.com/ultrasonic/ultrasonic/-/wikis/Using-Ultrasonic-with-Android-Auto
|
||||
[issues]: https://gitlab.com/ultrasonic/ultrasonic/-/issues
|
||||
[newissue]: https://gitlab.com/ultrasonic/ultrasonic/-/issues/new
|
||||
[madbug]: https://gitlab.com/ultrasonic/ultrasonic/-/issues/129
|
||||
[subsonic]: http://www.subsonic.org/
|
||||
[subapi]: http://www.subsonic.org/pages/api.jsp
|
||||
[airsonic]: https://github.com/airsonic-advanced/airsonic-advanced
|
||||
|
25
build.gradle
25
build.gradle
@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
apply from: 'gradle/versions.gradle'
|
||||
@ -10,8 +12,8 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
maven { url 'https://jitpack.io' }
|
||||
gradlePluginPortal()
|
||||
maven { url = "https://plugins.gradle.org/m2/" }
|
||||
}
|
||||
dependencies {
|
||||
classpath libs.gradle
|
||||
@ -27,25 +29,32 @@ allprojects {
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
// Set Kotlin JVM target to the same for all subprojects
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
tasks.withType(KotlinCompile).configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
jvmTarget = "21"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).tap {
|
||||
configureEach {
|
||||
options.compilerArgs.add("-Xlint:deprecation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wrapper {
|
||||
gradleVersion(libs.versions.gradle.get())
|
||||
distributionType("all")
|
||||
}
|
||||
gradleVersion = libs.versions.gradle.get()
|
||||
distributionType = "all"
|
||||
}
|
@ -52,7 +52,11 @@ style:
|
||||
active: true
|
||||
ForbiddenComment:
|
||||
active: true
|
||||
values: ['FIXME:', 'STOPSHIP:']
|
||||
comments:
|
||||
- reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
|
||||
value: 'FIXME:'
|
||||
- reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
|
||||
value: 'STOPSHIP:'
|
||||
WildcardImport:
|
||||
active: true
|
||||
MaxLineLength:
|
@ -1,12 +1,20 @@
|
||||
plugins {
|
||||
alias libs.plugins.ksp
|
||||
}
|
||||
|
||||
apply from: bootstrap.androidModule
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
dependencies {
|
||||
implementation libs.core
|
||||
implementation libs.roomRuntime
|
||||
implementation libs.roomKtx
|
||||
kapt libs.room
|
||||
ksp libs.room
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'org.moire.ultrasonic.subsonic.domain'
|
||||
namespace = 'org.moire.ultrasonic.subsonic.domain'
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ data class Album(
|
||||
override var genre: String? = null,
|
||||
override var starred: Boolean = false,
|
||||
override var path: String? = null,
|
||||
override var closeness: Int = 0,
|
||||
override var closeness: Int = 0
|
||||
) : MusicDirectory.Child() {
|
||||
override var isDirectory = true
|
||||
override var isVideo = false
|
||||
|
@ -13,10 +13,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
||||
var name: String? = null
|
||||
|
||||
@JvmOverloads
|
||||
fun getChildren(
|
||||
includeDirs: Boolean = true,
|
||||
includeFiles: Boolean = true
|
||||
): List<Child> {
|
||||
fun getChildren(includeDirs: Boolean = true, includeFiles: Boolean = true): List<Child> {
|
||||
if (includeDirs && includeFiles) {
|
||||
return toList()
|
||||
}
|
||||
|
@ -1,5 +1,14 @@
|
||||
plugins {
|
||||
alias libs.plugins.ksp
|
||||
}
|
||||
|
||||
apply from: bootstrap.kotlinModule
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api libs.retrofit
|
||||
api libs.jacksonConverter
|
||||
@ -13,7 +22,6 @@ dependencies {
|
||||
|
||||
testImplementation libs.kotlinJunit
|
||||
testImplementation libs.mockito
|
||||
testImplementation libs.mockitoInline
|
||||
testImplementation libs.mockitoKotlin
|
||||
testImplementation libs.kluent
|
||||
testImplementation libs.mockWebServer
|
||||
|
@ -8,7 +8,8 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
||||
* Base class for integration tests for [SubsonicAPIClient] class.
|
||||
*/
|
||||
abstract class SubsonicAPIClientTest {
|
||||
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
|
||||
@JvmField @Rule
|
||||
val mockWebServerRule = MockWebServerRule()
|
||||
|
||||
protected lateinit var config: SubsonicClientConfiguration
|
||||
protected lateinit var client: SubsonicAPIClient
|
||||
|
@ -11,7 +11,8 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
||||
* Base class for testing [okhttp3.Interceptor] implementations.
|
||||
*/
|
||||
abstract class BaseInterceptorTest {
|
||||
@Rule @JvmField val mockWebServerRule = MockWebServerRule()
|
||||
@Rule @JvmField
|
||||
val mockWebServerRule = MockWebServerRule()
|
||||
|
||||
lateinit var client: OkHttpClient
|
||||
|
||||
|
@ -92,7 +92,13 @@ internal class ApiVersionCheckWrapper(
|
||||
checkVersion(V1_4_0)
|
||||
checkParamVersion(musicFolderId, V1_12_0)
|
||||
return api.search2(
|
||||
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
|
||||
query,
|
||||
artistCount,
|
||||
artistOffset,
|
||||
albumCount,
|
||||
albumOffset,
|
||||
songCount,
|
||||
musicFolderId
|
||||
)
|
||||
}
|
||||
|
||||
@ -108,7 +114,13 @@ internal class ApiVersionCheckWrapper(
|
||||
checkVersion(V1_8_0)
|
||||
checkParamVersion(musicFolderId, V1_12_0)
|
||||
return api.search3(
|
||||
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
|
||||
query,
|
||||
artistCount,
|
||||
artistOffset,
|
||||
albumCount,
|
||||
albumOffset,
|
||||
songCount,
|
||||
musicFolderId
|
||||
)
|
||||
}
|
||||
|
||||
@ -228,7 +240,13 @@ internal class ApiVersionCheckWrapper(
|
||||
checkParamVersion(estimateContentLength, V1_8_0)
|
||||
checkParamVersion(converted, V1_14_0)
|
||||
return api.stream(
|
||||
id, maxBitRate, format, timeOffset, videoSize, estimateContentLength, converted
|
||||
id,
|
||||
maxBitRate,
|
||||
format,
|
||||
timeOffset,
|
||||
videoSize,
|
||||
estimateContentLength,
|
||||
converted
|
||||
)
|
||||
}
|
||||
|
||||
@ -335,8 +353,9 @@ internal class ApiVersionCheckWrapper(
|
||||
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
|
||||
// If it is true, it is probably the first call with this server
|
||||
if (!isRealProtocolVersion) return
|
||||
if (currentApiVersion < expectedVersion)
|
||||
if (currentApiVersion < expectedVersion) {
|
||||
throw ApiNotSupportedException(currentApiVersion)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {
|
||||
|
@ -90,10 +90,7 @@ interface SubsonicAPIDefinition {
|
||||
): Call<SubsonicResponse>
|
||||
|
||||
@GET("setRating.view")
|
||||
fun setRating(
|
||||
@Query("id") id: String,
|
||||
@Query("rating") rating: Int
|
||||
): Call<SubsonicResponse>
|
||||
fun setRating(@Query("id") id: String, @Query("rating") rating: Int): Call<SubsonicResponse>
|
||||
|
||||
@GET("getArtist.view")
|
||||
fun getArtist(@Query("id") id: String): Call<GetArtistResponse>
|
||||
@ -158,8 +155,7 @@ interface SubsonicAPIDefinition {
|
||||
@Query("public") public: Boolean? = null,
|
||||
@Query("songIdToAdd") songIdsToAdd: List<String>? = null,
|
||||
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null
|
||||
):
|
||||
Call<SubsonicResponse>
|
||||
): Call<SubsonicResponse>
|
||||
|
||||
@GET("getPodcasts.view")
|
||||
fun getPodcasts(
|
||||
@ -227,10 +223,7 @@ interface SubsonicAPIDefinition {
|
||||
|
||||
@Streaming
|
||||
@GET("getCoverArt.view")
|
||||
fun getCoverArt(
|
||||
@Query("id") id: String,
|
||||
@Query("size") size: Long? = null
|
||||
): Call<ResponseBody>
|
||||
fun getCoverArt(@Query("id") id: String, @Query("size") size: Long? = null): Call<ResponseBody>
|
||||
|
||||
@Streaming
|
||||
@GET("stream.view")
|
||||
|
@ -29,10 +29,12 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
||||
V1_13_0("5.3", "1.13.0"),
|
||||
V1_14_0("6.0", "1.14.0"),
|
||||
V1_15_0("6.1", "1.15.0"),
|
||||
V1_16_0("6.1.2", "1.16.0");
|
||||
V1_16_0("6.1.2", "1.16.0")
|
||||
;
|
||||
|
||||
companion object {
|
||||
@JvmStatic @Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions {
|
||||
val versionComponents = apiVersion.split(".")
|
||||
|
||||
@ -41,8 +43,11 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
||||
try {
|
||||
val majorVersion = versionComponents[0].toInt()
|
||||
val minorVersion = versionComponents[1].toInt()
|
||||
val patchVersion = if (versionComponents.size > 2) versionComponents[2].toInt()
|
||||
else 0
|
||||
val patchVersion = if (versionComponents.size > 2) {
|
||||
versionComponents[2].toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
when (majorVersion) {
|
||||
1 -> when {
|
||||
|
@ -48,7 +48,10 @@ class VersionAwareJacksonConverterFactory(
|
||||
retrofit: Retrofit
|
||||
): Converter<*, RequestBody>? {
|
||||
return jacksonConverterFactory?.requestBodyConverter(
|
||||
type, parameterAnnotations, methodAnnotations, retrofit
|
||||
type,
|
||||
parameterAnnotations,
|
||||
methodAnnotations,
|
||||
retrofit
|
||||
)
|
||||
}
|
||||
|
||||
@ -63,7 +66,7 @@ class VersionAwareJacksonConverterFactory(
|
||||
}
|
||||
}
|
||||
|
||||
class VersionAwareResponseBodyConverter<T> (
|
||||
class VersionAwareResponseBodyConverter<T>(
|
||||
private val notifier: (SubsonicAPIVersions) -> Unit = {},
|
||||
private val adapter: ObjectReader
|
||||
) : Converter<ResponseBody, T> {
|
||||
|
@ -6,6 +6,7 @@ import okhttp3.Interceptor.Chain
|
||||
import okhttp3.Response
|
||||
|
||||
internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000
|
||||
|
||||
// Allow 20 seconds extra timeout pear MB offset.
|
||||
internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
|
||||
|
||||
|
@ -23,7 +23,8 @@ enum class AlbumListType(val typeName: String) {
|
||||
SORTED_BY_ARTIST("alphabeticalByArtist"),
|
||||
STARRED("starred"),
|
||||
BY_YEAR("byYear"),
|
||||
BY_GENRE("byGenre");
|
||||
BY_GENRE("byGenre")
|
||||
;
|
||||
|
||||
override fun toString(): String {
|
||||
return typeName
|
||||
|
@ -16,7 +16,8 @@ enum class JukeboxAction(val action: String) {
|
||||
CLEAR("clear"),
|
||||
REMOVE("remove"),
|
||||
SHUFFLE("shuffle"),
|
||||
SET_GAIN("setGain");
|
||||
SET_GAIN("setGain")
|
||||
;
|
||||
|
||||
override fun toString(): String {
|
||||
return action
|
||||
|
@ -10,7 +10,8 @@ class BookmarksResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("bookmarks") private val bookmarksWrapper = BookmarkWrapper()
|
||||
@JsonProperty("bookmarks")
|
||||
private val bookmarksWrapper = BookmarkWrapper()
|
||||
|
||||
val bookmarkList: List<Bookmark> get() = bookmarksWrapper.bookmarkList
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ class ChatMessagesResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("chatMessages") private val wrapper = ChatMessagesWrapper()
|
||||
@JsonProperty("chatMessages")
|
||||
private val wrapper = ChatMessagesWrapper()
|
||||
|
||||
val chatMessages: List<ChatMessage> get() = wrapper.messagesList
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ class GenresResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("genres") private val genresWrapper = GenresWrapper()
|
||||
@JsonProperty("genres")
|
||||
private val genresWrapper = GenresWrapper()
|
||||
val genresList: List<Genre> get() = genresWrapper.genresList
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,8 @@ class GetAlbumList2Response(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("albumList2") private val albumWrapper2 = AlbumWrapper2()
|
||||
@JsonProperty("albumList2")
|
||||
private val albumWrapper2 = AlbumWrapper2()
|
||||
|
||||
val albumList: List<Album>
|
||||
get() = albumWrapper2.albumList
|
||||
|
@ -10,7 +10,8 @@ class GetAlbumListResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("albumList") private val albumWrapper = AlbumWrapper()
|
||||
@JsonProperty("albumList")
|
||||
private val albumWrapper = AlbumWrapper()
|
||||
|
||||
val albumList: List<Album>
|
||||
get() = albumWrapper.albumList
|
||||
|
@ -10,7 +10,8 @@ class GetPodcastsResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("podcasts") private val channelsWrapper = PodcastChannelWrapper()
|
||||
@JsonProperty("podcasts")
|
||||
private val channelsWrapper = PodcastChannelWrapper()
|
||||
|
||||
val podcastChannels: List<PodcastChannel>
|
||||
get() = channelsWrapper.channelsList
|
||||
|
@ -10,7 +10,8 @@ class GetRandomSongsResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("randomSongs") private val songsWrapper = RandomSongsWrapper()
|
||||
@JsonProperty("randomSongs")
|
||||
private val songsWrapper = RandomSongsWrapper()
|
||||
|
||||
val songsList
|
||||
get() = songsWrapper.songsList
|
||||
|
@ -10,7 +10,8 @@ class GetSongsByGenreResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("songsByGenre") private val songsByGenreList = SongsByGenreWrapper()
|
||||
@JsonProperty("songsByGenre")
|
||||
private val songsByGenreList = SongsByGenreWrapper()
|
||||
|
||||
val songsList get() = songsByGenreList.songsList
|
||||
}
|
||||
|
@ -11,11 +11,13 @@ class JukeboxResponse(
|
||||
error: SubsonicError?,
|
||||
var jukebox: JukeboxStatus = JukeboxStatus()
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonSetter("jukeboxStatus") fun setJukeboxStatus(jukebox: JukeboxStatus) {
|
||||
@JsonSetter("jukeboxStatus")
|
||||
fun setJukeboxStatus(jukebox: JukeboxStatus) {
|
||||
this.jukebox = jukebox
|
||||
}
|
||||
|
||||
@JsonSetter("jukeboxPlaylist") fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
|
||||
@JsonSetter("jukeboxPlaylist")
|
||||
fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
|
||||
this.jukebox = jukebox
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ class MusicFoldersResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("musicFolders") private val wrapper = MusicFoldersWrapper()
|
||||
@JsonProperty("musicFolders")
|
||||
private val wrapper = MusicFoldersWrapper()
|
||||
|
||||
val musicFolders get() = wrapper.musicFolders
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ class SharesResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("shares") private val wrappedShares = SharesWrapper()
|
||||
@JsonProperty("shares")
|
||||
private val wrappedShares = SharesWrapper()
|
||||
|
||||
val shares get() = wrappedShares.share
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ open class SubsonicResponse(
|
||||
) {
|
||||
@JsonDeserialize(using = Status.Companion.StatusJsonDeserializer::class)
|
||||
enum class Status(val jsonValue: String) {
|
||||
OK("ok"), ERROR("failed");
|
||||
OK("ok"),
|
||||
ERROR("failed");
|
||||
|
||||
companion object {
|
||||
fun getStatusFromJson(jsonValue: String) =
|
||||
|
@ -10,7 +10,8 @@ class VideosResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("videos") private val videosWrapper = VideosWrapper()
|
||||
@JsonProperty("videos")
|
||||
private val videosWrapper = VideosWrapper()
|
||||
|
||||
val videosList: List<MusicDirectoryChild> get() = videosWrapper.videosList
|
||||
}
|
||||
|
@ -18,7 +18,9 @@ class ProxyPasswordInterceptorTest {
|
||||
|
||||
private val proxyInterceptor = ProxyPasswordInterceptor(
|
||||
V1_12_0,
|
||||
mockPasswordHexInterceptor, mockPasswordMd5Interceptor, false
|
||||
mockPasswordHexInterceptor,
|
||||
mockPasswordMd5Interceptor,
|
||||
false
|
||||
)
|
||||
|
||||
@Test
|
||||
@ -40,8 +42,10 @@ class ProxyPasswordInterceptorTest {
|
||||
@Test
|
||||
fun `Should use hex password if forceHex is true`() {
|
||||
val interceptor = ProxyPasswordInterceptor(
|
||||
V1_16_0, mockPasswordHexInterceptor,
|
||||
mockPasswordMd5Interceptor, true
|
||||
V1_16_0,
|
||||
mockPasswordHexInterceptor,
|
||||
mockPasswordMd5Interceptor,
|
||||
true
|
||||
)
|
||||
|
||||
interceptor.intercept(mockChain)
|
||||
|
@ -1,27 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name)</ID>
|
||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:PlaylistsFragment.kt$PlaylistsFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken, additionalId: String? )</ID>
|
||||
<ID>LongMethod:SharesFragment.kt$SharesFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) )</ID>
|
||||
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
|
||||
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f</ID>
|
||||
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</ID>
|
||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
||||
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
|
||||
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable</ID>
|
||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
7
fastlane/metadata/android/en-US/changelogs/113.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/113.txt
Normal file
@ -0,0 +1,7 @@
|
||||
Bug fixes
|
||||
- #831: Version 4.1.1 and develop: 'jukebox on/off' no longer shown in 'Now Playing'.
|
||||
|
||||
Enhancements
|
||||
- #827: Make app full compliant Android Auto to publish in Play Store.
|
||||
- #878: "Play shuffled" option for playlists always begins with the first track.
|
||||
- #891: Dump config to log file when logging is enabled.
|
4
fastlane/metadata/android/en-US/changelogs/114.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/114.txt
Normal file
@ -0,0 +1,4 @@
|
||||
Bug fixes
|
||||
- Fix a crash when a ID3 tag date is in a wrong format.
|
||||
- Fix a crash on API 31 (newest Android).
|
||||
- Fix empty search results.
|
2
fastlane/metadata/android/en-US/changelogs/115.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/115.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Bug fixes
|
||||
- Fix a crash when downloading the album art.
|
8
fastlane/metadata/android/en-US/changelogs/116.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/116.txt
Normal file
@ -0,0 +1,8 @@
|
||||
Bug fixes
|
||||
- Fix various crashes
|
||||
|
||||
Changes since 4.2.0
|
||||
- #827: Make app full compliant Android Auto to publish in Play Store.
|
||||
- #878: "Play shuffled" option for playlists always begins with the first track.
|
||||
- #891: Dump config to log file when logging is enabled.
|
||||
- #854: Remove Videos menu option for servers which don't support it.
|
8
fastlane/metadata/android/en-US/changelogs/117.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/117.txt
Normal file
@ -0,0 +1,8 @@
|
||||
Bug fixes
|
||||
- Fix more exceptions
|
||||
|
||||
Changes since 4.2.0
|
||||
- #827: Make app full compliant Android Auto to publish in Play Store.
|
||||
- #878: "Play shuffled" option for playlists always begins with the first track.
|
||||
- #891: Dump config to log file when logging is enabled.
|
||||
- #854: Remove Videos menu option for servers which don't support it.
|
10
fastlane/metadata/android/en-US/changelogs/119.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/119.txt
Normal file
@ -0,0 +1,10 @@
|
||||
Features:
|
||||
- This releases focuses on shuffled playback. The view of the playlist will now present itself in the order it will actually play. You can toggle the shuffle mode to create a new order, while the past playback history will be preserved.
|
||||
- Use Coroutines for triggering the download or playback of music through the context menus
|
||||
- Enable Artists pictures by Default
|
||||
|
||||
Bug fixes:
|
||||
- Remove an unhelpful popup that "ID must be set"
|
||||
- Shuffle mode doesn't always play all tracks
|
||||
- Shuffle mode starts with the first track most of the time
|
||||
|
10
fastlane/metadata/android/en-US/changelogs/120.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/120.txt
Normal file
@ -0,0 +1,10 @@
|
||||
Features:
|
||||
- This releases focuses on shuffled playback. The view of the playlist will now present itself in the order it will actually play. You can toggle the shuffle mode to create a new order, while the past playback history will be preserved.
|
||||
- Use Coroutines for triggering the download or playback of music through the context menus
|
||||
- Enable Artists pictures by Default
|
||||
|
||||
Bug fixes:
|
||||
- Remove an unhelpful popup that "ID must be set"
|
||||
- Shuffle mode doesn't always play all tracks
|
||||
- Shuffle mode starts with the first track most of the time
|
||||
|
10
fastlane/metadata/android/en-US/changelogs/122.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/122.txt
Normal file
@ -0,0 +1,10 @@
|
||||
Features:
|
||||
- Revamp management of ratings. Tracks can be starred from the notification in Android 13, and the changes will show up everywhere immediately.
|
||||
- Add a setting to control the maximum bitrate when pinning music (can be used to avoid downloading lossless files like flac).
|
||||
- Modernize the Jukebox player.
|
||||
- The hardware keys can be used to set the Jukebox volume.
|
||||
- The current playlist shows a spinner when loading takes some time
|
||||
|
||||
Bug fixes:
|
||||
- Request correct bluetooth permission on Android 13 (needed to pause/play on connect)
|
||||
- Update dependencies (OkHttp, Material)
|
12
fastlane/metadata/android/en-US/changelogs/123.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/123.txt
Normal file
@ -0,0 +1,12 @@
|
||||
Features:
|
||||
- Search is accesible through a new icon on the main screen
|
||||
- Modernize Back Handling
|
||||
- Reenable R8 Code minification
|
||||
- Add a "Play Random Songs" shortcut
|
||||
|
||||
Bug fixes:
|
||||
- Tracks buttons flash a scrollbar sometimes in Android 13
|
||||
- Fix EndlessScrolling in genre listing
|
||||
- Couldn't delete a track when shuffle was active
|
||||
- Upgrade material to 1.9.0
|
||||
|
15
fastlane/metadata/android/en-US/changelogs/124.txt
Normal file
15
fastlane/metadata/android/en-US/changelogs/124.txt
Normal file
@ -0,0 +1,15 @@
|
||||
Features:
|
||||
- Search is accessible through a new icon on the main screen
|
||||
- Modernize Back Handling
|
||||
- Reenable R8 Code minification
|
||||
- Add a "Play Random Songs" shortcut
|
||||
|
||||
Bug fixes:
|
||||
- Readd the "Star" button to the Now Playing screen
|
||||
- Fix a rare crash when shuffling playlists with duplicate entries
|
||||
- Fix a crash when choosing "Play next" on an empty playlist.
|
||||
- Tracks buttons flash a scrollbar sometimes in Android 13
|
||||
- Fix EndlessScrolling in genre listing
|
||||
- Couldn't delete a track when shuffle was active
|
||||
- Upgrade material to 1.9.0
|
||||
|
15
fastlane/metadata/android/en-US/changelogs/125.txt
Normal file
15
fastlane/metadata/android/en-US/changelogs/125.txt
Normal file
@ -0,0 +1,15 @@
|
||||
Features:
|
||||
- Search is accessible through a new icon on the main screen
|
||||
- Modernize Back Handling
|
||||
- Reenable R8 Code minification
|
||||
- Add a "Play Random Songs" shortcut
|
||||
|
||||
Bug fixes:
|
||||
- Avoid triggering a bug in Supysonic
|
||||
- Readd the "Star" button to the Now Playing screen
|
||||
- Fix a rare crash when shuffling playlists with duplicate entries
|
||||
- Fix a crash when choosing "Play next" on an empty playlist.
|
||||
- Tracks buttons flash a scrollbar sometimes in Android 13
|
||||
- Fix EndlessScrolling in genre listing
|
||||
- Couldn't delete a track when shuffle was active
|
||||
|
15
fastlane/metadata/android/en-US/changelogs/126.txt
Normal file
15
fastlane/metadata/android/en-US/changelogs/126.txt
Normal file
@ -0,0 +1,15 @@
|
||||
Features:
|
||||
- Search is accessible through a new icon on the main screen
|
||||
- Modernize Back Handling
|
||||
- Reenable R8 Code minification
|
||||
- Add a "Play Random Songs" shortcut
|
||||
|
||||
Bug fixes:
|
||||
- Fix a few crashes
|
||||
- Avoid triggering a bug in Supysonic
|
||||
- Readd the "Star" button to the Now Playing screen
|
||||
- Fix a rare crash when shuffling playlists with duplicate entries
|
||||
- Fix a crash when choosing "Play next" on an empty playlist.
|
||||
- Tracks buttons flash a scrollbar sometimes in Android 13
|
||||
- Fix EndlessScrolling in genre listing
|
||||
- Couldn't delete a track when shuffle was active
|
9
fastlane/metadata/android/en-US/changelogs/128.txt
Normal file
9
fastlane/metadata/android/en-US/changelogs/128.txt
Normal file
@ -0,0 +1,9 @@
|
||||
### Features
|
||||
- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto)
|
||||
- Properly handling nested directory structures (Android Auto)
|
||||
- Add a toast when adding tracks to the playlist
|
||||
- Allow pinning when offline
|
||||
|
||||
### Dependencies
|
||||
- Update koin
|
||||
- Update media3 to v1.1.0
|
12
fastlane/metadata/android/en-US/changelogs/129.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/129.txt
Normal file
@ -0,0 +1,12 @@
|
||||
### Fixes
|
||||
- Fix a bug in 4.7.0 that repeat mode was activated by default.
|
||||
|
||||
### Features
|
||||
- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto)
|
||||
- Properly handling nested directory structures (Android Auto)
|
||||
- Add a toast when adding tracks to the playlist
|
||||
- Allow pinning when offline
|
||||
|
||||
### Dependencies
|
||||
- Update koin
|
||||
- Update media3 to v1.1.0
|
5
fastlane/metadata/android/en-US/changelogs/130.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/130.txt
Normal file
@ -0,0 +1,5 @@
|
||||
### Features
|
||||
- Improved display of rating stars
|
||||
- Completely modernize all older code parts
|
||||
- Updates for Android 14
|
||||
- Update dependencies
|
@ -1,17 +1,17 @@
|
||||
Ultrasonic is a Subsonic (and compatible servers) client to Android. You can use Ultrasonic to connect with your server and listen music.
|
||||
|
||||
Main features:
|
||||
* Thin
|
||||
* Fast
|
||||
* Dark and light theme
|
||||
* Small size & fast
|
||||
* Material You theme with dark and light variants
|
||||
* Multiple server support
|
||||
* Offline Mode
|
||||
* Download tracks for offline playback
|
||||
* Bookmarks
|
||||
* Playlists on server
|
||||
* Ramdom play
|
||||
* Shuffled playback
|
||||
* Jukebox mode
|
||||
* Server chat
|
||||
* And much more!!!
|
||||
* And much more!!
|
||||
|
||||
Note: Ultrasonic uses semantic release versions. Releases with a zero in the last digit introduce new features or significant changes, all other releases focus on fixing bugs.
|
||||
|
||||
The source code is available with GPL license in GitLab: https://gitlab.com/ultrasonic/ultrasonic
|
||||
If you have any issue, please post in: https://gitlab.com/ultrasonic/ultrasonic/issues
|
||||
|
7
fastlane/metadata/android/es-ES/changelogs/113.txt
Normal file
7
fastlane/metadata/android/es-ES/changelogs/113.txt
Normal file
@ -0,0 +1,7 @@
|
||||
Corrección de errores
|
||||
- #831: Versión 4.1.1 y desarrollo: 'jukebox on/off' ya no se muestra en 'Reproduciendo ahora'.
|
||||
|
||||
Mejoras
|
||||
- #827: Hacer aplicación completamente compatible con Android Auto para publicar en Play Store.
|
||||
- #878: "Reproducir aleatoriamente" ya no siempre comienza con la primera pista.
|
||||
- #891: Volcado de configuración al archivo de registro cuando el registro está habilitado.
|
4
fastlane/metadata/android/es-ES/changelogs/114.txt
Normal file
4
fastlane/metadata/android/es-ES/changelogs/114.txt
Normal file
@ -0,0 +1,4 @@
|
||||
Corrección de errores
|
||||
- Corrección de un fallo cuando la fecha de una etiqueta ID3 tiene un formato incorrecto.
|
||||
- Corrección de un fallo en la API 31 (versión de Android más reciente).
|
||||
- Corrección de resultados de búsqueda vacíos.
|
2
fastlane/metadata/android/es-ES/changelogs/115.txt
Normal file
2
fastlane/metadata/android/es-ES/changelogs/115.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Corrección de errores
|
||||
- Corrección de un fallo al descargar la carátula del álbum.
|
@ -4,10 +4,22 @@ org.gradle.configureondemand=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2g -XX:+UseParallelGC
|
||||
|
||||
|
||||
kotlin.incremental=true
|
||||
kotlin.caching.enabled=true
|
||||
kotlin.incremental.usePreciseJavaTracking=true
|
||||
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=false
|
||||
|
||||
# This properties enables transitive Resource classes, which decreases build time,
|
||||
# but could lead to problems referencing Resources. Set them to false if needed.
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=true
|
||||
|
||||
# This config was suggested by Android Studio to reduce build time
|
||||
# It can be removed if it makes problems
|
||||
org.gradle.unsafe.configuration-cache=true
|
||||
|
||||
# TODO Renable on day (check that Retrofit, Jackson, and Imageloader are working)
|
||||
android.enableR8.fullMode=true
|
||||
|
||||
|
@ -1,44 +1,46 @@
|
||||
[versions]
|
||||
# You need to run ./gradlew wrapper after updating the version
|
||||
gradle = "7.6"
|
||||
gradle = "8.13"
|
||||
|
||||
navigation = "2.5.3"
|
||||
gradlePlugin = "7.4.2"
|
||||
androidxcore = "1.9.0"
|
||||
ktlint = "0.43.2"
|
||||
ktlintGradle = "11.3.1"
|
||||
detekt = "1.22.0"
|
||||
preferences = "1.2.0"
|
||||
media3 = "1.0.0-rc02"
|
||||
navigation = "2.8.9"
|
||||
gradlePlugin = "8.9.1"
|
||||
androidxcar = "1.4.0"
|
||||
androidxcore = "1.16.0"
|
||||
ktlint = "1.0.1"
|
||||
ktlintGradle = "12.2.0"
|
||||
detekt = "1.23.8"
|
||||
preferences = "1.2.1"
|
||||
media3 = "1.6.1"
|
||||
|
||||
androidSupport = "1.6.0"
|
||||
materialDesign = "1.8.0"
|
||||
constraintLayout = "2.1.4"
|
||||
androidSupport = "1.9.1"
|
||||
materialDesign = "1.12.0"
|
||||
constraintLayout = "2.2.1"
|
||||
activity = "1.10.1"
|
||||
multidex = "2.0.1"
|
||||
room = "2.5.0"
|
||||
kotlin = "1.8.10"
|
||||
kotlinxCoroutines = "1.6.4"
|
||||
kotlinxGuava = "1.6.4"
|
||||
viewModelKtx = "2.6.0"
|
||||
room = "2.7.0"
|
||||
kotlin = "2.1.20"
|
||||
ksp = "2.1.20-2.0.0"
|
||||
kotlinxCoroutines = "1.10.2"
|
||||
viewModelKtx = "2.8.7"
|
||||
swipeRefresh = "1.1.0"
|
||||
|
||||
retrofit = "2.9.0"
|
||||
jackson = "2.14.2"
|
||||
okhttp = "4.10.0"
|
||||
koin = "3.3.2"
|
||||
retrofit = "2.11.0"
|
||||
jackson = "2.18.3"
|
||||
okhttp = "4.12.0"
|
||||
koin = "4.0.4"
|
||||
picasso = "2.8"
|
||||
|
||||
junit4 = "4.13.2"
|
||||
junit5 = "5.9.2"
|
||||
mockito = "5.2.0"
|
||||
mockitoKotlin = "4.1.0"
|
||||
kluent = "1.72"
|
||||
apacheCodecs = "1.15"
|
||||
robolectric = "4.9.2"
|
||||
junit5 = "5.12.2"
|
||||
mockito = "5.17.0"
|
||||
mockitoKotlin = "5.4.0"
|
||||
kluent = "1.73"
|
||||
apacheCodecs = "1.18.0"
|
||||
robolectric = "4.14.1"
|
||||
timber = "5.0.1"
|
||||
fastScroll = "2.0.1"
|
||||
colorPicker = "2.2.4"
|
||||
rxJava = "3.1.6"
|
||||
colorPicker = "2.3.0"
|
||||
rxJava = "3.1.10"
|
||||
rxAndroid = "3.0.2"
|
||||
multiType = "4.3.0"
|
||||
|
||||
@ -48,6 +50,7 @@ kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin"
|
||||
ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" }
|
||||
detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
|
||||
|
||||
car = { module = "androidx.car.app:app", version.ref = "androidxcar" }
|
||||
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
|
||||
design = { module = "com.google.android.material:material", version.ref = "materialDesign" }
|
||||
annotations = { module = "androidx.annotation:annotation", version.ref = "androidSupport" }
|
||||
@ -63,6 +66,7 @@ navigationFragmentKtx = { module = "androidx.navigation:navigation-fragment-kt
|
||||
navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
|
||||
navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" }
|
||||
navigationSafeArgs = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation"}
|
||||
activity = { module = "androidx.activity:activity-ktx", version.ref = "activity" }
|
||||
preferences = { module = "androidx.preference:preference", version.ref = "preferences" }
|
||||
media3common = { module = "androidx.media3:media3-common", version.ref = "media3" }
|
||||
media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
|
||||
@ -73,7 +77,7 @@ swipeRefresh = { module = "androidx.swiperefreshlayout:swiperefreshla
|
||||
kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||
kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
||||
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
|
||||
kotlinxGuava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxGuava"}
|
||||
kotlinxGuava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutines"}
|
||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
gsonConverter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
|
||||
jacksonConverter = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" }
|
||||
@ -95,9 +99,11 @@ junitVintage = { module = "org.junit.vintage:junit-vintage-engine", v
|
||||
kotlinJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||
mockitoKotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
|
||||
mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" }
|
||||
mockitoInline = { module = "org.mockito:mockito-inline", version.ref = "mockito" }
|
||||
kluent = { module = "org.amshove.kluent:kluent", version.ref = "kluent" }
|
||||
kluentAndroid = { module = "org.amshove.kluent:kluent-android", version.ref = "kluent" }
|
||||
mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
||||
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
|
||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||
|
||||
[plugins]
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
|
@ -1,5 +1,5 @@
|
||||
ext.versions = [
|
||||
minSdk : 21,
|
||||
minSdk : 26,
|
||||
targetSdk : 33,
|
||||
compileSdk : 33,
|
||||
]
|
||||
compileSdk : 35,
|
||||
]
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -2,9 +2,9 @@
|
||||
* This module provides a base for for submodules which depend on the Android runtime
|
||||
*/
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
@ -16,8 +16,8 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
@ -25,11 +25,8 @@ if (isCodeQualityEnabled) {
|
||||
// Builds the AST in parallel. Rules are always executed in parallel.
|
||||
// Can lead to speedups in larger projects.
|
||||
parallel = true
|
||||
baseline = file("${rootProject.projectDir}/detekt-baseline.xml")
|
||||
config = files("${rootProject.projectDir}/detekt-config.yml")
|
||||
}
|
||||
}
|
||||
tasks.detekt.jvmTarget = "11"
|
||||
tasks.detekt.jvmTarget = "17"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
* This module provides a base for for pure kotlin modules
|
||||
*/
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||
|
||||
sourceSets {
|
||||
@ -12,7 +12,6 @@ sourceSets {
|
||||
test.resources.srcDirs += "${projectDir}/src/integrationTest/resources"
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
api libs.kotlinStdlib
|
||||
|
||||
|
33
gradlew
vendored
33
gradlew
vendored
@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@ -83,10 +85,8 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@ -133,10 +133,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
@ -144,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@ -152,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@ -197,11 +200,15 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
22
gradlew.bat
vendored
22
gradlew.bat
vendored
@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
plugins {
|
||||
alias libs.plugins.ksp
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||
apply from: "../gradle_scripts/code_quality.gradle"
|
||||
|
||||
@ -9,12 +12,12 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.moire.ultrasonic"
|
||||
versionCode 112
|
||||
versionName "4.2.2"
|
||||
versionCode 130
|
||||
versionName "4.8.0"
|
||||
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.targetSdk
|
||||
resConfigs 'cs', 'de', 'en', 'es', 'fr', 'hu', 'it', 'nl', 'pl', 'pt', 'pt-rBR', 'ru', 'zh-rCN', 'zh-rTW'
|
||||
resourceConfigurations += ['cs', 'de', 'en', 'es', 'fr', 'gl', 'hu', 'it', 'ja', 'nb-rNO', 'nl', 'pl', 'pt', 'pt-rBR', 'ru', 'zh-rCN', 'zh-rTW']
|
||||
}
|
||||
|
||||
bundle.language.enableSplit = false
|
||||
@ -31,10 +34,10 @@ android {
|
||||
'minify/proguard-kotlin.pro'
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
multiDexEnabled true
|
||||
testCoverageEnabled true
|
||||
applicationIdSuffix ".debug"
|
||||
minifyEnabled = false
|
||||
multiDexEnabled = true
|
||||
testCoverageEnabled = true
|
||||
applicationIdSuffix = '.debug'
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,41 +53,43 @@ android {
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
jvmTarget = "21"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
dataBinding true
|
||||
viewBinding = true
|
||||
dataBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas".toString())
|
||||
}
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas")
|
||||
}
|
||||
|
||||
lint {
|
||||
baseline = file("lint-baseline.xml")
|
||||
abortOnError true
|
||||
warningsAsErrors true
|
||||
disable 'IconMissingDensityFolder', 'VectorPath'
|
||||
ignore 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
||||
abortOnError = true
|
||||
warningsAsErrors = true
|
||||
warning 'ImpliedQuantity'
|
||||
disable 'IconMissingDensityFolder', 'VectorPath'
|
||||
disable 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
||||
disable 'ObsoleteLintCustomCheck'
|
||||
textReport true
|
||||
checkDependencies true
|
||||
// We manage dependencies on Gitlab with RenovateBot
|
||||
disable 'GradleDependency'
|
||||
disable 'AndroidGradlePluginVersion'
|
||||
textReport = true
|
||||
checkDependencies = true
|
||||
}
|
||||
namespace 'org.moire.ultrasonic'
|
||||
namespace = 'org.moire.ultrasonic'
|
||||
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
tasks.withType(Test).configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
@ -96,6 +101,7 @@ dependencies {
|
||||
exclude group: "com.android.support"
|
||||
}
|
||||
|
||||
implementation libs.car
|
||||
implementation libs.core
|
||||
implementation libs.design
|
||||
implementation libs.multidex
|
||||
@ -128,7 +134,7 @@ dependencies {
|
||||
implementation libs.rxAndroid
|
||||
implementation libs.multiType
|
||||
|
||||
kapt libs.room
|
||||
ksp libs.room
|
||||
|
||||
testImplementation libs.kotlinReflect
|
||||
testImplementation libs.junit
|
||||
@ -136,11 +142,9 @@ dependencies {
|
||||
testImplementation libs.kotlinJunit
|
||||
testImplementation libs.kluent
|
||||
testImplementation libs.mockito
|
||||
testImplementation libs.mockitoInline
|
||||
testImplementation libs.mockitoKotlin
|
||||
testImplementation libs.robolectric
|
||||
|
||||
implementation libs.timber
|
||||
|
||||
}
|
||||
|
||||
|
19
ultrasonic/detekt-baseline.xml
Normal file
19
ultrasonic/detekt-baseline.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues></ManuallySuppressedIssues>
|
||||
<CurrentIssues>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name)</ID>
|
||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:PlaylistsFragment.kt$PlaylistsFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>LongMethod:SharesFragment.kt$SharesFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
|
||||
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</ID>
|
||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
||||
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable</ID>
|
||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
@ -1,27 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 7.4.0" type="baseline" client="gradle" dependencies="true" name="AGP (7.4.0)" variant="all" version="7.4.0">
|
||||
|
||||
<issue
|
||||
id="MissingPermission"
|
||||
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
|
||||
errorLine1=" manager.notify(NOTIFICATION_ID, notification)"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt"
|
||||
line="260"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="MissingPermission"
|
||||
message="Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`"
|
||||
errorLine1=" notificationManagerCompat.notify(notification.notificationId, notification.notification)"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt"
|
||||
line="194"
|
||||
column="9"/>
|
||||
</issue>
|
||||
<issues format="6" by="lint 8.0.1" type="baseline" client="gradle" dependencies="true" name="AGP (8.0.1)" variant="all" version="8.0.1">
|
||||
|
||||
<issue
|
||||
id="PluralsCandidate"
|
||||
@ -30,7 +8,7 @@
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/values/strings.xml"
|
||||
line="152"
|
||||
line="151"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
@ -48,50 +26,6 @@
|
||||
file="../core/subsonic-api/build/libs/subsonic-api.jar"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedContentProvider"
|
||||
message="Exported content providers can provide access to potentially sensitive data"
|
||||
errorLine1=" <provider"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="128"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedContentProvider"
|
||||
message="Exported content providers can provide access to potentially sensitive data"
|
||||
errorLine1=" <provider"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="133"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedReceiver"
|
||||
message="Exported receiver does not require permission"
|
||||
errorLine1=" <receiver android:name=".receiver.UltrasonicIntentReceiver""
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="88"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ExportedService"
|
||||
message="Exported service does not require permission"
|
||||
errorLine1=" <service android:name=".playback.PlaybackService""
|
||||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/AndroidManifest.xml"
|
||||
line="77"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.drawable.media3_notification_pause` appears to be unused"
|
||||
@ -136,61 +70,6 @@
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UnusedResources"
|
||||
message="The resource `R.drawable.media3_notification_small_icon` appears to be unused"
|
||||
errorLine1="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
||||
errorLine2="^">
|
||||
<location
|
||||
file="src/main/res/drawable/media3_notification_small_icon.xml"
|
||||
line="1"
|
||||
column="1"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Autofill"
|
||||
message="Missing `autofillHints` attribute"
|
||||
errorLine1=" <EditText"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/chat.xml"
|
||||
line="33"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Autofill"
|
||||
message="Missing `autofillHints` attribute"
|
||||
errorLine1=" <EditText"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/save_playlist.xml"
|
||||
line="9"
|
||||
column="6"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Autofill"
|
||||
message="Missing `autofillHints` attribute"
|
||||
errorLine1=" <EditText"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/share_details.xml"
|
||||
line="29"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Autofill"
|
||||
message="Missing `autofillHints` attribute"
|
||||
errorLine1=" <EditText"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/time_span_dialog.xml"
|
||||
line="28"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="LabelFor"
|
||||
message="Missing accessibility label: provide either a view with an `android:labelFor` that references this view or provide an `android:hint`"
|
||||
|
@ -1,5 +1,4 @@
|
||||
#### From Jackson
|
||||
|
||||
-keepattributes *Annotation*,EnclosingMethod,Signature
|
||||
-keepnames class com.fasterxml.jackson.** {
|
||||
*;
|
||||
|
@ -1,8 +1,14 @@
|
||||
-dontobfuscate
|
||||
|
||||
### Don't remove subsonic api serializers/entities
|
||||
-keep class org.moire.ultrasonic.api.subsonic.response.** { *; }
|
||||
-keep class org.moire.ultrasonic.api.subsonic.models.** { *; }
|
||||
-keep class org.moire.ultrasonic.api.subsonic.** { *; }
|
||||
|
||||
## Don't remove the domain models
|
||||
-keep class org.moire.ultrasonic.domain.** { *; }
|
||||
|
||||
## Don't remove the imageloader
|
||||
-keep class org.moire.ultrasonic.imageloader.** { *; }
|
||||
-keep class org.moire.ultrasonic.provider.AlbumArtContentProvider { *; }
|
||||
|
||||
## Don't remove NowPlayingFragment
|
||||
-keep class org.moire.ultrasonic.fragment.NowPlayingFragment { *; }
|
||||
|
@ -1,10 +1,41 @@
|
||||
#### From retrofit
|
||||
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
|
||||
# EnclosingMethod is required to use InnerClasses.
|
||||
-keepattributes Signature, InnerClasses, EnclosingMethod
|
||||
|
||||
# Retain generic type information for use by reflection by converters and adapters.
|
||||
-keepattributes Signature
|
||||
# Retain service method parameters.
|
||||
-keepclassmembernames,allowobfuscation interface * {
|
||||
# Retrofit does reflection on method and parameter annotations.
|
||||
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
|
||||
|
||||
# Keep annotation default values (e.g., retrofit2.http.Field.encoded).
|
||||
-keepattributes AnnotationDefault
|
||||
|
||||
# Retain service method parameters when optimizing.
|
||||
-keepclassmembers,allowshrinking,allowobfuscation interface * {
|
||||
@retrofit2.http.* <methods>;
|
||||
}
|
||||
|
||||
# Ignore annotation used for build tooling.
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
|
||||
# Ignore JSR 305 annotations for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
|
||||
-dontwarn kotlin.Unit
|
||||
|
||||
# Top-level functions that can only be used by Kotlin.
|
||||
-dontwarn retrofit2.KotlinExtensions
|
||||
-dontwarn retrofit2.KotlinExtensions$*
|
||||
|
||||
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
|
||||
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
|
||||
-if interface * { @retrofit2.http.* <methods>; }
|
||||
-keep,allowobfuscation interface <1>
|
||||
|
||||
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
|
||||
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||
|
||||
# With R8 full mode generic signatures are stripped for classes that are not
|
||||
# kept. Suspend functions are wrapped in continuations where the type argument
|
||||
# is used.
|
||||
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
@ -3,15 +3,17 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<uses-sdk tools:overrideLibrary="androidx.car.app" />
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
@ -20,18 +22,23 @@
|
||||
android:xlargeScreens="true"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.Material3.DynamicColors.Dark"
|
||||
android:name=".app.UApp"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/common.appname"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:supportsRtl="false"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:preserveLegacyExternalStorage="true"
|
||||
tools:ignore="UnusedAttribute">
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="false"
|
||||
android:theme="@style/Theme.Material3.DynamicColors.Dark"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:targetApi="q">
|
||||
<!-- Add for API 34 android:enableOnBackInvokedCallBack="true" -->
|
||||
|
||||
<meta-data android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc"/>
|
||||
@ -66,18 +73,12 @@
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".service.JukeboxMediaPlayer"
|
||||
android:label="Ultrasonic Jukebox Media Player Service"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
|
||||
<service android:name=".playback.PlaybackService"
|
||||
<service android:name=".service.PlaybackService"
|
||||
android:label="@string/common.appname"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaLibraryService" />
|
||||
@ -86,7 +87,8 @@
|
||||
</service>
|
||||
|
||||
<receiver android:name=".receiver.UltrasonicIntentReceiver"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="org.moire.ultrasonic.CMD_TOGGLEPAUSE"/>
|
||||
<action android:name="org.moire.ultrasonic.CMD_PLAY"/>
|
||||
@ -107,6 +109,12 @@
|
||||
<action android:name="android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name="androidx.media3.session.MediaButtonReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".provider.UltrasonicAppWidgetProvider"
|
||||
android:label="Ultrasonic"
|
||||
@ -119,21 +127,16 @@
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/appwidget_info"/>
|
||||
</receiver>
|
||||
<receiver android:name=".receiver.MediaButtonIntentReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter android:priority="2147483647">
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<provider
|
||||
android:name=".provider.SearchSuggestionProvider"
|
||||
android:authorities="${applicationId}.provider.SearchSuggestionProvider"
|
||||
android:exported="true" />
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedContentProvider" />
|
||||
|
||||
<provider
|
||||
android:name=".provider.AlbumArtContentProvider"
|
||||
android:authorities="${applicationId}.provider.AlbumArtContentProvider"
|
||||
android:exported="true" />
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedContentProvider" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -1,297 +0,0 @@
|
||||
package org.moire.ultrasonic.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||
import org.moire.ultrasonic.domain.ChatMessage;
|
||||
import org.moire.ultrasonic.service.MusicService;
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||
import org.moire.ultrasonic.util.BackgroundTask;
|
||||
import org.moire.ultrasonic.util.CancellationToken;
|
||||
import org.moire.ultrasonic.util.FragmentBackgroundTask;
|
||||
import org.moire.ultrasonic.util.Settings;
|
||||
import org.moire.ultrasonic.util.Util;
|
||||
import org.moire.ultrasonic.view.ChatAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
|
||||
/**
|
||||
* Provides online chat functionality
|
||||
*/
|
||||
public class ChatFragment extends Fragment {
|
||||
|
||||
private ListView chatListView;
|
||||
private EditText messageEditText;
|
||||
private MaterialButton sendButton;
|
||||
private Timer timer;
|
||||
private volatile static Long lastChatMessageTime = (long) 0;
|
||||
private static final ArrayList<ChatMessage> messageList = new ArrayList<>();
|
||||
private CancellationToken cancellationToken;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
|
||||
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
Util.applyTheme(this.getContext());
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.chat, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
swipeRefresh = view.findViewById(R.id.chat_refresh);
|
||||
swipeRefresh.setEnabled(false);
|
||||
|
||||
cancellationToken = new CancellationToken();
|
||||
messageEditText = view.findViewById(R.id.chat_edittext);
|
||||
sendButton = view.findViewById(R.id.chat_send);
|
||||
|
||||
sendButton.setOnClickListener(view1 -> sendMessage());
|
||||
|
||||
chatListView = view.findViewById(R.id.chat_entries_list);
|
||||
chatListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
|
||||
chatListView.setStackFromBottom(true);
|
||||
|
||||
String serverName = activeServerProvider.getValue().getActiveServer().getName();
|
||||
String userName = activeServerProvider.getValue().getActiveServer().getUserName();
|
||||
String title = String.format("%s [%s@%s]", getResources().getString(R.string.button_bar_chat), userName, serverName);
|
||||
|
||||
FragmentTitle.Companion.setTitle(this, title);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
messageEditText.setImeActionLabel("Send", KeyEvent.KEYCODE_ENTER);
|
||||
|
||||
messageEditText.addTextChangedListener(new TextWatcher()
|
||||
{
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable)
|
||||
{
|
||||
sendButton.setEnabled(!Util.isNullOrWhiteSpace(editable.toString()));
|
||||
}
|
||||
});
|
||||
|
||||
messageEditText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE || (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
{
|
||||
sendMessage();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
load();
|
||||
timerMethod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.chat, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
/*
|
||||
* Listen for option item selections so that we receive a notification
|
||||
* when the user requests a refresh by selecting the refresh action bar item.
|
||||
*/
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
// Check if user triggered a refresh:
|
||||
if (item.getItemId() == R.id.menu_refresh) {
|
||||
// Start the refresh background task.
|
||||
load();
|
||||
return true;
|
||||
}
|
||||
// User didn't trigger a refresh, let the superclass handle this action
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume()
|
||||
{
|
||||
super.onResume();
|
||||
|
||||
if (!messageList.isEmpty())
|
||||
{
|
||||
ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList);
|
||||
chatListView.setAdapter(chatAdapter);
|
||||
}
|
||||
|
||||
if (timer == null)
|
||||
{
|
||||
timerMethod();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause()
|
||||
{
|
||||
super.onPause();
|
||||
|
||||
if (timer != null)
|
||||
{
|
||||
timer.cancel();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
cancellationToken.cancel();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void timerMethod()
|
||||
{
|
||||
int refreshInterval = Settings.getChatRefreshInterval();
|
||||
|
||||
if (refreshInterval > 0)
|
||||
{
|
||||
timer = new Timer();
|
||||
|
||||
timer.schedule(new TimerTask()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
getActivity().runOnUiThread(() -> load());
|
||||
}
|
||||
}, refreshInterval, refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMessage()
|
||||
{
|
||||
if (messageEditText != null)
|
||||
{
|
||||
final String message;
|
||||
Editable text = messageEditText.getText();
|
||||
|
||||
if (text == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
message = text.toString();
|
||||
|
||||
if (!Util.isNullOrWhiteSpace(message))
|
||||
{
|
||||
messageEditText.setText("");
|
||||
|
||||
BackgroundTask<Void> task = new FragmentBackgroundTask<Void>(getActivity(), false, swipeRefresh, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground() throws Throwable
|
||||
{
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
musicService.addChatMessage(message);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(Void result)
|
||||
{
|
||||
load();
|
||||
}
|
||||
};
|
||||
|
||||
task.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void load()
|
||||
{
|
||||
BackgroundTask<List<ChatMessage>> task = new FragmentBackgroundTask<List<ChatMessage>>(getActivity(), false, swipeRefresh, cancellationToken)
|
||||
{
|
||||
@Override
|
||||
protected List<ChatMessage> doInBackground() throws Throwable
|
||||
{
|
||||
MusicService musicService = MusicServiceFactory.getMusicService();
|
||||
return musicService.getChatMessages(lastChatMessageTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done(List<ChatMessage> result)
|
||||
{
|
||||
if (result != null && !result.isEmpty())
|
||||
{
|
||||
// Reset lastChatMessageTime if we have a newer message
|
||||
for (ChatMessage message : result)
|
||||
{
|
||||
if (message.getTime() > lastChatMessageTime)
|
||||
{
|
||||
lastChatMessageTime = message.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse results to show them on the bottom
|
||||
Collections.reverse(result);
|
||||
messageList.addAll(result);
|
||||
|
||||
ListAdapter chatAdapter = new ChatAdapter(getContext(), messageList);
|
||||
chatListView.setAdapter(chatAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void error(Throwable error) {
|
||||
// Stop the timer in case of an error, otherwise it may repeat the error message forever
|
||||
if (timer != null)
|
||||
{
|
||||
timer.cancel();
|
||||
timer = null;
|
||||
}
|
||||
super.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
task.execute();
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Subsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2010 (C) Sindre Mehus
|
||||
*/
|
||||
package org.moire.ultrasonic.provider;
|
||||
|
||||
import android.content.SearchRecentSuggestionsProvider;
|
||||
|
||||
/**
|
||||
* Provides search suggestions based on recent searches.
|
||||
*
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public class SearchSuggestionProvider extends SearchRecentSuggestionsProvider
|
||||
{
|
||||
public static final String AUTHORITY = SearchSuggestionProvider.class.getName();
|
||||
public static final int MODE = DATABASE_MODE_QUERIES;
|
||||
|
||||
public SearchSuggestionProvider()
|
||||
{
|
||||
setupSuggestions(AUTHORITY, MODE);
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Subsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2010 (C) Sindre Mehus
|
||||
*/
|
||||
package org.moire.ultrasonic.receiver;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.moire.ultrasonic.util.Constants;
|
||||
import org.moire.ultrasonic.util.Settings;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Resume or pause playback on Bluetooth A2DP connect/disconnect.
|
||||
*
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
public class BluetoothIntentReceiver extends BroadcastReceiver
|
||||
{
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent)
|
||||
{
|
||||
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
String action = intent.getAction();
|
||||
String name = device != null ? device.getName() : "Unknown";
|
||||
String address = device != null ? device.getAddress() : "Unknown";
|
||||
|
||||
Timber.d("A2DP State: %d; Action: %s; Device: %s; Address: %s", state, action, name, address);
|
||||
|
||||
boolean actionBluetoothDeviceConnected = false;
|
||||
boolean actionBluetoothDeviceDisconnected = false;
|
||||
boolean actionA2dpConnected = false;
|
||||
boolean actionA2dpDisconnected = false;
|
||||
|
||||
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action))
|
||||
{
|
||||
actionBluetoothDeviceConnected = true;
|
||||
}
|
||||
else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action) || BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED.equals(action))
|
||||
{
|
||||
actionBluetoothDeviceDisconnected = true;
|
||||
}
|
||||
|
||||
if (state == android.bluetooth.BluetoothA2dp.STATE_CONNECTED) actionA2dpConnected = true;
|
||||
else if (state == android.bluetooth.BluetoothA2dp.STATE_DISCONNECTED) actionA2dpDisconnected = true;
|
||||
|
||||
boolean resume = false;
|
||||
boolean pause = false;
|
||||
|
||||
switch (Settings.getResumeOnBluetoothDevice())
|
||||
{
|
||||
case Constants.PREFERENCE_VALUE_ALL: resume = actionA2dpConnected || actionBluetoothDeviceConnected;
|
||||
break;
|
||||
case Constants.PREFERENCE_VALUE_A2DP: resume = actionA2dpConnected;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (Settings.getPauseOnBluetoothDevice())
|
||||
{
|
||||
case Constants.PREFERENCE_VALUE_ALL: pause = actionA2dpDisconnected || actionBluetoothDeviceDisconnected;
|
||||
break;
|
||||
case Constants.PREFERENCE_VALUE_A2DP: pause = actionA2dpDisconnected;
|
||||
break;
|
||||
}
|
||||
|
||||
if (resume)
|
||||
{
|
||||
Timber.i("Connected to Bluetooth device %s address %s, resuming playback.", name, address);
|
||||
context.sendBroadcast(new Intent(Constants.CMD_RESUME_OR_PLAY).setPackage(context.getPackageName()));
|
||||
}
|
||||
|
||||
if (pause)
|
||||
{
|
||||
Timber.i("Disconnected from Bluetooth device %s address %s, requesting pause.", name, address);
|
||||
context.sendBroadcast(new Intent(Constants.CMD_PAUSE).setPackage(context.getPackageName()));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package org.moire.ultrasonic.receiver;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import timber.log.Timber;
|
||||
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
public class UltrasonicIntentReceiver extends BroadcastReceiver
|
||||
{
|
||||
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent)
|
||||
{
|
||||
String intentAction = intent.getAction();
|
||||
Timber.i("Received Ultrasonic Intent: %s", intentAction);
|
||||
|
||||
try
|
||||
{
|
||||
lifecycleSupport.getValue().receiveIntent(intent);
|
||||
|
||||
if (isOrderedBroadcast())
|
||||
{
|
||||
abortBroadcast();
|
||||
}
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
// Ignored.
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package org.moire.ultrasonic.service;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import org.moire.ultrasonic.app.UApp;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Monitors the state of the mobile's external storage
|
||||
*/
|
||||
public class ExternalStorageMonitor
|
||||
{
|
||||
private BroadcastReceiver ejectEventReceiver;
|
||||
private boolean externalStorageAvailable = true;
|
||||
|
||||
public void onCreate(final Runnable ejectedCallback)
|
||||
{
|
||||
// Stop when SD card is ejected.
|
||||
ejectEventReceiver = new BroadcastReceiver()
|
||||
{
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent)
|
||||
{
|
||||
externalStorageAvailable = Intent.ACTION_MEDIA_MOUNTED.equals(intent.getAction());
|
||||
if (!externalStorageAvailable)
|
||||
{
|
||||
Timber.i("External media is ejecting. Stopping playback.");
|
||||
ejectedCallback.run();
|
||||
}
|
||||
else
|
||||
{
|
||||
Timber.i("External media is available.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IntentFilter ejectFilter = new IntentFilter(Intent.ACTION_MEDIA_EJECT);
|
||||
ejectFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
|
||||
ejectFilter.addDataScheme("file");
|
||||
UApp.Companion.applicationContext().registerReceiver(ejectEventReceiver, ejectFilter);
|
||||
}
|
||||
|
||||
public void onDestroy()
|
||||
{
|
||||
UApp.Companion.applicationContext().unregisterReceiver(ejectEventReceiver);
|
||||
}
|
||||
|
||||
public boolean isExternalStorageAvailable() { return externalStorageAvailable; }
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package org.moire.ultrasonic.service;
|
||||
|
||||
import timber.log.Timber;
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||
import org.moire.ultrasonic.domain.Track;
|
||||
|
||||
/**
|
||||
* Scrobbles played songs to Last.fm.
|
||||
*
|
||||
* @author Sindre Mehus
|
||||
* @version $Id$
|
||||
*/
|
||||
public class Scrobbler
|
||||
{
|
||||
private String lastSubmission;
|
||||
private String lastNowPlaying;
|
||||
|
||||
public void scrobble(final Track song, final boolean submission)
|
||||
{
|
||||
if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled()) return;
|
||||
|
||||
final String id = song.getId();
|
||||
|
||||
// Avoid duplicate registrations.
|
||||
if (submission && id.equals(lastSubmission)) return;
|
||||
|
||||
if (!submission && id.equals(lastNowPlaying)) return;
|
||||
|
||||
if (submission) lastSubmission = id;
|
||||
else lastNowPlaying = id;
|
||||
|
||||
new Thread(String.format("Scrobble %s", song))
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
MusicService service = MusicServiceFactory.getMusicService();
|
||||
try
|
||||
{
|
||||
service.scrobble(id, submission);
|
||||
Timber.i("Scrobbled '%s' for %s", submission ? "submission" : "now playing", song);
|
||||
}
|
||||
catch (Exception x)
|
||||
{
|
||||
Timber.i(x, "Failed to scrobble'%s' for %s", submission ? "submission" : "now playing", song);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package org.moire.ultrasonic.service;
|
||||
|
||||
/**
|
||||
* Abstract class for supplying items to a consumer
|
||||
* @param <T> The type of the item supplied
|
||||
*/
|
||||
public abstract class Supplier<T>
|
||||
{
|
||||
public abstract T get();
|
||||
}
|
||||
|
||||
|
@ -1,72 +0,0 @@
|
||||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Subsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Handler;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public abstract class BackgroundTask<T> implements ProgressListener
|
||||
{
|
||||
private final Activity activity;
|
||||
private final Handler handler;
|
||||
|
||||
public BackgroundTask(Activity activity)
|
||||
{
|
||||
this.activity = activity;
|
||||
handler = new Handler();
|
||||
}
|
||||
|
||||
protected Activity getActivity()
|
||||
{
|
||||
return activity;
|
||||
}
|
||||
|
||||
protected Handler getHandler()
|
||||
{
|
||||
return handler;
|
||||
}
|
||||
|
||||
public abstract void execute();
|
||||
|
||||
protected abstract T doInBackground() throws Throwable;
|
||||
|
||||
protected abstract void done(T result);
|
||||
|
||||
protected void error(Throwable error)
|
||||
{
|
||||
CommunicationError.handleError(error, activity);
|
||||
}
|
||||
|
||||
protected String getErrorMessage(Throwable error)
|
||||
{
|
||||
return CommunicationError.getErrorMessage(error, activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract void updateProgress(final String message);
|
||||
|
||||
@Override
|
||||
public void updateProgress(int messageId)
|
||||
{
|
||||
updateProgress(activity.getResources().getString(messageId));
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
* @version $Id$
|
||||
*/
|
||||
public abstract class FragmentBackgroundTask<T> extends BackgroundTask<T>
|
||||
{
|
||||
private final boolean changeProgress;
|
||||
private final SwipeRefreshLayout swipe;
|
||||
private final CancellationToken cancel;
|
||||
|
||||
public FragmentBackgroundTask(Activity activity, boolean changeProgress,
|
||||
SwipeRefreshLayout swipe, CancellationToken cancel)
|
||||
{
|
||||
super(activity);
|
||||
this.changeProgress = changeProgress;
|
||||
this.swipe = swipe;
|
||||
this.cancel = cancel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute()
|
||||
{
|
||||
if (changeProgress)
|
||||
{
|
||||
if (swipe != null) swipe.setRefreshing(true);
|
||||
}
|
||||
|
||||
new Thread()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
final T result = doInBackground();
|
||||
if (cancel.isCancellationRequested())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
getHandler().post(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if (changeProgress)
|
||||
{
|
||||
if (swipe != null) swipe.setRefreshing(false);
|
||||
}
|
||||
|
||||
done(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (final Throwable t)
|
||||
{
|
||||
if (cancel.isCancellationRequested())
|
||||
{
|
||||
return;
|
||||
}
|
||||
getHandler().post(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if (changeProgress)
|
||||
{
|
||||
if (swipe != null) swipe.setRefreshing(false);
|
||||
}
|
||||
|
||||
error(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgress(final String message)
|
||||
{
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
* @version $Id$
|
||||
*/
|
||||
public abstract class LoadingTask<T> extends BackgroundTask<T>
|
||||
{
|
||||
private final SwipeRefreshLayout swipe;
|
||||
private final CancellationToken cancel;
|
||||
|
||||
public LoadingTask(Activity activity, SwipeRefreshLayout swipe, CancellationToken cancel)
|
||||
{
|
||||
super(activity);
|
||||
this.swipe = swipe;
|
||||
this.cancel = cancel;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void execute()
|
||||
{
|
||||
swipe.setRefreshing(true);
|
||||
|
||||
new Thread()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
final T result = doInBackground();
|
||||
if (cancel.isCancellationRequested())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
getHandler().post(() -> {
|
||||
swipe.setRefreshing(false);
|
||||
done(result);
|
||||
});
|
||||
}
|
||||
catch (final Throwable t)
|
||||
{
|
||||
if (cancel.isCancellationRequested())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
getHandler().post(() -> {
|
||||
swipe.setRefreshing(false);
|
||||
error(t);
|
||||
});
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgress(final String message)
|
||||
{
|
||||
}
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Subsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import org.moire.ultrasonic.R;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public abstract class ModalBackgroundTask<T> extends BackgroundTask<T>
|
||||
{
|
||||
private final AlertDialog progressDialog;
|
||||
private Thread thread;
|
||||
private final boolean finishActivityOnCancel;
|
||||
private boolean cancelled;
|
||||
|
||||
public ModalBackgroundTask(Activity activity, boolean finishActivityOnCancel)
|
||||
{
|
||||
super(activity);
|
||||
this.finishActivityOnCancel = finishActivityOnCancel;
|
||||
progressDialog = createProgressDialog();
|
||||
}
|
||||
|
||||
public ModalBackgroundTask(Activity activity)
|
||||
{
|
||||
this(activity, true);
|
||||
}
|
||||
|
||||
private androidx.appcompat.app.AlertDialog createProgressDialog()
|
||||
{
|
||||
InfoDialog.Builder builder = new InfoDialog.Builder(getActivity().getApplicationContext());
|
||||
builder.setTitle(R.string.background_task_wait);
|
||||
builder.setMessage(R.string.background_task_loading);
|
||||
builder.setOnCancelListener(dialogInterface -> cancel());
|
||||
builder.setPositiveButton(R.string.common_cancel, (dialogInterface, i) -> cancel());
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute()
|
||||
{
|
||||
cancelled = false;
|
||||
progressDialog.show();
|
||||
|
||||
thread = new Thread()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
final T result = doInBackground();
|
||||
if (cancelled)
|
||||
{
|
||||
progressDialog.dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
getHandler().post(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// nothing
|
||||
}
|
||||
|
||||
done(result);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
catch (final Throwable t)
|
||||
{
|
||||
if (cancelled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
getHandler().post(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// nothing
|
||||
}
|
||||
|
||||
error(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
thread.start();
|
||||
}
|
||||
|
||||
protected void cancel()
|
||||
{
|
||||
cancelled = true;
|
||||
if (thread != null)
|
||||
{
|
||||
thread.interrupt();
|
||||
}
|
||||
|
||||
if (finishActivityOnCancel)
|
||||
{
|
||||
getActivity().finish();
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isCancelled()
|
||||
{
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void error(Throwable error)
|
||||
{
|
||||
Timber.w(error);
|
||||
new ErrorDialog(getActivity(), getErrorMessage(error), getActivity(), finishActivityOnCancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProgress(final String message)
|
||||
{
|
||||
getHandler().post(() -> progressDialog.setMessage(message));
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Subsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public interface ProgressListener
|
||||
{
|
||||
void updateProgress(String message);
|
||||
|
||||
void updateProgress(int messageId);
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
import org.moire.ultrasonic.domain.Track;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Josh on 12/17/13.
|
||||
*/
|
||||
public class ShareDetails
|
||||
{
|
||||
public String Description;
|
||||
public boolean ShareOnServer;
|
||||
public long Expiration;
|
||||
public List<Track> Entries;
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Subsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
import android.os.Binder;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public class SimpleServiceBinder<S> extends Binder
|
||||
{
|
||||
private final S service;
|
||||
|
||||
public SimpleServiceBinder(S service)
|
||||
{
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
public S getService()
|
||||
{
|
||||
return service;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package org.moire.ultrasonic.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import androidx.preference.DialogPreference;
|
||||
import org.moire.ultrasonic.R;
|
||||
|
||||
/**
|
||||
* Created by Joshua Bahnsen on 12/22/13.
|
||||
*/
|
||||
public class TimeSpanPreference extends DialogPreference
|
||||
{
|
||||
Context context;
|
||||
|
||||
public TimeSpanPreference(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
this.context = context;
|
||||
|
||||
setPositiveButtonText(android.R.string.ok);
|
||||
setNegativeButtonText(android.R.string.cancel);
|
||||
|
||||
setDialogIcon(null);
|
||||
|
||||
}
|
||||
|
||||
public String getText()
|
||||
{
|
||||
String persisted = getPersistedString("");
|
||||
|
||||
if (!"".equals(persisted))
|
||||
{
|
||||
return persisted.replace(':', ' ');
|
||||
}
|
||||
|
||||
return this.context.getResources().getString(R.string.time_span_disabled);
|
||||
}
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
package org.moire.ultrasonic.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
||||
import org.moire.ultrasonic.domain.ChatMessage;
|
||||
import org.moire.ultrasonic.imageloader.ImageLoader;
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import kotlin.Lazy;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.inject;
|
||||
|
||||
public class ChatAdapter extends ArrayAdapter<ChatMessage>
|
||||
{
|
||||
private final Context context;
|
||||
private final List<ChatMessage> messages;
|
||||
|
||||
private static final String phoneRegex = "1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})";
|
||||
private static final Pattern phoneMatcher = Pattern.compile(phoneRegex);
|
||||
|
||||
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
|
||||
private final Lazy<ImageLoaderProvider> imageLoaderProvider = inject(ImageLoaderProvider.class);
|
||||
|
||||
public ChatAdapter(Context context, List<ChatMessage> messages)
|
||||
{
|
||||
super(context, R.layout.chat_item, messages);
|
||||
this.context = context;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areAllItemsEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(int position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount()
|
||||
{
|
||||
return messages.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent)
|
||||
{
|
||||
ChatMessage message = this.getItem(position);
|
||||
|
||||
ViewHolder holder;
|
||||
int layout;
|
||||
|
||||
String messageUser = message.getUsername();
|
||||
Date messageTime = new java.util.Date(message.getTime());
|
||||
String messageText = message.getMessage();
|
||||
|
||||
String me = activeServerProvider.getValue().getActiveServer().getUserName();
|
||||
|
||||
layout = messageUser.equals(me) ? R.layout.chat_item_reverse : R.layout.chat_item;
|
||||
|
||||
if (convertView == null)
|
||||
{
|
||||
convertView = inflateView(layout, parent);
|
||||
holder = createViewHolder(layout, convertView);
|
||||
}
|
||||
else
|
||||
{
|
||||
holder = (ViewHolder) convertView.getTag();
|
||||
|
||||
if (!holder.chatMessage.equals(message))
|
||||
{
|
||||
convertView = inflateView(layout, parent);
|
||||
holder = createViewHolder(layout, convertView);
|
||||
}
|
||||
}
|
||||
|
||||
holder.chatMessage = message;
|
||||
|
||||
DateFormat timeFormat = android.text.format.DateFormat.getTimeFormat(context);
|
||||
String messageTimeFormatted = String.format("[%s]", timeFormat.format(messageTime));
|
||||
|
||||
ImageLoader imageLoader = imageLoaderProvider.getValue().getImageLoader();
|
||||
|
||||
if (holder.avatar != null && !TextUtils.isEmpty(messageUser))
|
||||
{
|
||||
imageLoader.loadAvatarImage(holder.avatar, messageUser);
|
||||
}
|
||||
|
||||
holder.username.setText(messageUser);
|
||||
holder.message.setText(messageText);
|
||||
holder.time.setText(messageTimeFormatted);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private View inflateView(int layout, ViewGroup parent)
|
||||
{
|
||||
return LayoutInflater.from(context).inflate(layout, parent, false);
|
||||
}
|
||||
|
||||
private static ViewHolder createViewHolder(int layout, View convertView)
|
||||
{
|
||||
ViewHolder holder = new ViewHolder();
|
||||
holder.layout = layout;
|
||||
|
||||
TextView usernameView;
|
||||
TextView timeView;
|
||||
TextView messageView;
|
||||
ImageView imageView;
|
||||
|
||||
if (convertView != null)
|
||||
{
|
||||
usernameView = (TextView) convertView.findViewById(R.id.chat_username);
|
||||
timeView = (TextView) convertView.findViewById(R.id.chat_time);
|
||||
messageView = (TextView) convertView.findViewById(R.id.chat_message);
|
||||
imageView = (ImageView) convertView.findViewById(R.id.chat_avatar);
|
||||
|
||||
messageView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
Linkify.addLinks(messageView, Linkify.ALL);
|
||||
Linkify.addLinks(messageView, phoneMatcher, "tel:");
|
||||
|
||||
holder.avatar = imageView;
|
||||
holder.message = messageView;
|
||||
holder.username = usernameView;
|
||||
holder.time = timeView;
|
||||
|
||||
convertView.setTag(holder);
|
||||
}
|
||||
|
||||
return holder;
|
||||
}
|
||||
|
||||
private static class ViewHolder
|
||||
{
|
||||
int layout;
|
||||
ImageView avatar;
|
||||
TextView message;
|
||||
TextView username;
|
||||
TextView time;
|
||||
ChatMessage chatMessage;
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Subsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2010 (C) Sindre Mehus
|
||||
*/
|
||||
package org.moire.ultrasonic.view;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.SectionIndexer;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.domain.Genre;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public class GenreAdapter extends ArrayAdapter<Genre> implements SectionIndexer
|
||||
{
|
||||
private final LayoutInflater layoutInflater;
|
||||
// Both arrays are indexed by section ID.
|
||||
private final Object[] sections;
|
||||
private final Integer[] positions;
|
||||
|
||||
public GenreAdapter(Context context, List<Genre> genres)
|
||||
{
|
||||
super(context, R.layout.list_item_generic, genres);
|
||||
|
||||
layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
Collection<String> sectionSet = new LinkedHashSet<String>(30);
|
||||
List<Integer> positionList = new ArrayList<Integer>(30);
|
||||
|
||||
for (int i = 0; i < genres.size(); i++)
|
||||
{
|
||||
Genre genre = genres.get(i);
|
||||
String index = genre.getIndex();
|
||||
if (!sectionSet.contains(index))
|
||||
{
|
||||
sectionSet.add(index);
|
||||
positionList.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
sections = sectionSet.toArray(new Object[0]);
|
||||
positions = positionList.toArray(new Integer[0]);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View rowView = convertView;
|
||||
if (rowView == null) {
|
||||
rowView = layoutInflater.inflate(R.layout.list_item_generic, parent, false);
|
||||
}
|
||||
|
||||
((TextView) rowView).setText(getItem(position).getName());
|
||||
|
||||
return rowView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getSections()
|
||||
{
|
||||
return sections;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPositionForSection(int section)
|
||||
{
|
||||
return positions[section];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionForPosition(int pos)
|
||||
{
|
||||
for (int i = 0; i < sections.length - 1; i++)
|
||||
{
|
||||
if (pos < positions[i + 1])
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return sections.length - 1;
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package org.moire.ultrasonic.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.domain.Share;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
public class ShareAdapter extends ArrayAdapter<Share>
|
||||
{
|
||||
private final Context context;
|
||||
|
||||
public ShareAdapter(Context context, List<Share> Shares)
|
||||
{
|
||||
super(context, R.layout.share_list_item, Shares);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent)
|
||||
{
|
||||
Share entry = getItem(position);
|
||||
ShareView view;
|
||||
|
||||
if (convertView instanceof ShareView)
|
||||
{
|
||||
ShareView currentView = (ShareView) convertView;
|
||||
|
||||
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
|
||||
view = currentView;
|
||||
view.setViewHolder(viewHolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
view = new ShareView(context);
|
||||
view.setLayout();
|
||||
}
|
||||
|
||||
view.setShare(entry);
|
||||
return view;
|
||||
}
|
||||
|
||||
static class ViewHolder
|
||||
{
|
||||
TextView url;
|
||||
TextView description;
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
/*
|
||||
This file is part of Subsonic.
|
||||
|
||||
Subsonic is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Subsonic is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2009 (C) Sindre Mehus
|
||||
*/
|
||||
package org.moire.ultrasonic.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.moire.ultrasonic.R;
|
||||
import org.moire.ultrasonic.domain.Share;
|
||||
|
||||
/**
|
||||
* Used to display playlists in a {@code ListView}.
|
||||
*
|
||||
* @author Joshua Bahnsen
|
||||
*/
|
||||
public class ShareView extends LinearLayout
|
||||
{
|
||||
private final Context context;
|
||||
private ShareAdapter.ViewHolder viewHolder;
|
||||
|
||||
public ShareView(Context context)
|
||||
{
|
||||
super(context);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setLayout()
|
||||
{
|
||||
LayoutInflater.from(context).inflate(R.layout.share_list_item, this, true);
|
||||
viewHolder = new ShareAdapter.ViewHolder();
|
||||
viewHolder.url = findViewById(R.id.share_url);
|
||||
viewHolder.description = findViewById(R.id.share_description);
|
||||
setTag(viewHolder);
|
||||
}
|
||||
|
||||
public void setViewHolder(ShareAdapter.ViewHolder viewHolder)
|
||||
{
|
||||
this.viewHolder = viewHolder;
|
||||
setTag(this.viewHolder);
|
||||
}
|
||||
|
||||
public void setShare(Share share)
|
||||
{
|
||||
viewHolder.url.setText(share.getName());
|
||||
viewHolder.description.setText(share.getDescription());
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* NavigationActivity.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
@ -13,21 +13,23 @@ import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.provider.SearchRecentSuggestions
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player.STATE_BUFFERING
|
||||
@ -35,6 +37,7 @@ import androidx.media3.common.Player.STATE_READY
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.navigateUp
|
||||
import androidx.navigation.ui.onNavDestinationSelected
|
||||
@ -46,17 +49,18 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.scope.ScopeActivity
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.NavigationGraphDirections
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.ServerSettingDao
|
||||
import org.moire.ultrasonic.fragment.OnBackPressedHandler
|
||||
import org.moire.ultrasonic.model.ServerSettingsModel
|
||||
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
@ -64,6 +68,7 @@ import org.moire.ultrasonic.util.InfoDialog
|
||||
import org.moire.ultrasonic.util.LocaleHelper
|
||||
import org.moire.ultrasonic.util.ServerColor
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.ShortcutUtil
|
||||
import org.moire.ultrasonic.util.Storage
|
||||
import org.moire.ultrasonic.util.UncaughtExceptionHandler
|
||||
import org.moire.ultrasonic.util.Util
|
||||
@ -75,7 +80,7 @@ import timber.log.Timber
|
||||
* onCreate/onResume/onDestroy methods...
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class NavigationActivity : AppCompatActivity() {
|
||||
class NavigationActivity : ScopeActivity() {
|
||||
private var videoMenuItem: MenuItem? = null
|
||||
private var chatMenuItem: MenuItem? = null
|
||||
private var bookmarksMenuItem: MenuItem? = null
|
||||
@ -90,15 +95,20 @@ class NavigationActivity : AppCompatActivity() {
|
||||
private var drawerLayout: DrawerLayout? = null
|
||||
private var host: NavHostFragment? = null
|
||||
private var selectServerButton: MaterialButton? = null
|
||||
private var selectServerDropdownImage: ImageView? = null
|
||||
private var headerBackgroundImage: ImageView? = null
|
||||
|
||||
// We store the last search string in this variable.
|
||||
// Seems a bit like a hack, is there a better way?
|
||||
var searchQuery: String? = null
|
||||
|
||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||
|
||||
private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
private val serverSettingsModel: ServerSettingsModel by viewModel()
|
||||
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
|
||||
private val mediaPlayerController: MediaPlayerController by inject()
|
||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
private val serverRepository: ServerSettingDao by inject()
|
||||
|
||||
@ -127,6 +137,8 @@ class NavigationActivity : AppCompatActivity() {
|
||||
navigationView = findViewById(R.id.nav_view)
|
||||
drawerLayout = findViewById(R.id.drawer_layout)
|
||||
|
||||
setupDrawerLayout(drawerLayout!!)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
@ -153,7 +165,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||
drawerLayout
|
||||
)
|
||||
|
||||
setupActionBar(navController, appBarConfiguration)
|
||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||
|
||||
setupNavigationMenu(navController)
|
||||
|
||||
@ -192,10 +204,11 @@ class NavigationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
rxBusSubscription += RxBus.playerStateObservable.subscribe {
|
||||
if (it.state == STATE_READY)
|
||||
if (it.state == STATE_READY) {
|
||||
showNowPlaying()
|
||||
else
|
||||
} else {
|
||||
hideNowPlaying()
|
||||
}
|
||||
}
|
||||
|
||||
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
|
||||
@ -211,6 +224,80 @@ class NavigationActivity : AppCompatActivity() {
|
||||
cachedServerCount = count ?: 0
|
||||
updateNavigationHeaderForServer()
|
||||
}
|
||||
|
||||
// Setup app shortcuts on supported devices, but not on first start, when the server
|
||||
// is not configured yet.
|
||||
if (!UApp.instance!!.isFirstRun) {
|
||||
ShortcutUtil.registerShortcuts(this)
|
||||
}
|
||||
|
||||
// Register our options menu
|
||||
addMenuProvider(
|
||||
searchMenuProvider,
|
||||
this,
|
||||
Lifecycle.State.RESUMED
|
||||
)
|
||||
}
|
||||
|
||||
private val searchMenuProvider: MenuProvider = object : MenuProvider {
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
setupSearchField(menu)
|
||||
}
|
||||
|
||||
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.search_view_menu, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun setupSearchField(menu: Menu) {
|
||||
Timber.i("Recreating search field")
|
||||
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
val searchableInfo = searchManager.getSearchableInfo(this.componentName)
|
||||
searchView.setSearchableInfo(searchableInfo)
|
||||
searchView.setIconifiedByDefault(false)
|
||||
|
||||
if (searchQuery != null) {
|
||||
Timber.e("Found existing search query")
|
||||
searchItem.expandActionView()
|
||||
searchView.isIconified = false
|
||||
searchView.setQuery(searchQuery, false)
|
||||
searchView.clearFocus()
|
||||
// Restore search text only once!
|
||||
searchQuery = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDrawerLayout(drawerLayout: DrawerLayout) {
|
||||
// Set initial state passed on drawer state
|
||||
closeNavigationDrawerOnBack.isEnabled = drawerLayout.isOpen
|
||||
|
||||
// Add the back press listener
|
||||
onBackPressedDispatcher.addCallback(this, closeNavigationDrawerOnBack)
|
||||
|
||||
// Listen to changes in the drawer state and enable the back press listener accordingly.
|
||||
drawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {
|
||||
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
override fun onDrawerOpened(drawerView: View) {
|
||||
closeNavigationDrawerOnBack.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onDrawerClosed(drawerView: View) {
|
||||
closeNavigationDrawerOnBack.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onDrawerStateChanged(newState: Int) {
|
||||
// Nothing
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@ -220,7 +307,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||
Storage.reset()
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
Storage.ensureRootIsAvailable()
|
||||
Storage.checkForErrorsWithCustomRoot()
|
||||
}
|
||||
|
||||
setMenuForServerCapabilities()
|
||||
@ -228,8 +315,11 @@ class NavigationActivity : AppCompatActivity() {
|
||||
// Lifecycle support's constructor registers some event receivers so it should be created early
|
||||
lifecycleSupport.onCreate()
|
||||
|
||||
if (!nowPlayingHidden) showNowPlaying()
|
||||
else hideNowPlaying()
|
||||
if (!nowPlayingHidden) {
|
||||
showNowPlaying()
|
||||
} else {
|
||||
hideNowPlaying()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -243,47 +333,31 @@ class NavigationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun updateNavigationHeaderForServer() {
|
||||
// Only show the vector graphic on Android 11 or earlier
|
||||
val showVectorBackground = (Build.VERSION.SDK_INT < Build.VERSION_CODES.S)
|
||||
|
||||
val activeServer = activeServerProvider.getActiveServer()
|
||||
|
||||
if (cachedServerCount == 0)
|
||||
if (cachedServerCount == 0) {
|
||||
selectServerButton?.text = getString(R.string.main_setup_server, activeServer.name)
|
||||
else selectServerButton?.text = activeServer.name
|
||||
} else {
|
||||
selectServerButton?.text = activeServer.name
|
||||
}
|
||||
|
||||
val foregroundColor =
|
||||
ServerColor.getForegroundColor(this, activeServer.color, showVectorBackground)
|
||||
ServerColor.getForegroundColor(this, activeServer.color)
|
||||
val backgroundColor =
|
||||
ServerColor.getBackgroundColor(this, activeServer.color)
|
||||
|
||||
if (activeServer.index == 0)
|
||||
if (activeServer.index == 0) {
|
||||
selectServerButton?.icon =
|
||||
ContextCompat.getDrawable(this, R.drawable.ic_menu_screen_on_off)
|
||||
else
|
||||
} else {
|
||||
selectServerButton?.icon =
|
||||
ContextCompat.getDrawable(this, R.drawable.ic_menu_select_server)
|
||||
}
|
||||
|
||||
selectServerButton?.iconTint = ColorStateList.valueOf(foregroundColor)
|
||||
selectServerButton?.setTextColor(foregroundColor)
|
||||
selectServerDropdownImage?.imageTintList = ColorStateList.valueOf(foregroundColor)
|
||||
headerBackgroundImage?.setBackgroundColor(backgroundColor)
|
||||
|
||||
// Hide the vector graphic on Android 12 or later
|
||||
if (!showVectorBackground) {
|
||||
headerBackgroundImage?.setImageDrawable(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
val isVolumeDown = keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
val isVolumeUp = keyCode == KeyEvent.KEYCODE_VOLUME_UP
|
||||
val isVolumeAdjust = isVolumeDown || isVolumeUp
|
||||
val isJukebox = mediaPlayerController.isJukeboxEnabled
|
||||
if (isVolumeAdjust && isJukebox) {
|
||||
mediaPlayerController.adjustVolume(isVolumeUp)
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
private fun setupNavigationMenu(navController: NavController) {
|
||||
@ -308,7 +382,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||
}
|
||||
R.id.menu_exit -> {
|
||||
setResult(Constants.RESULT_CLOSE_ALL)
|
||||
mediaPlayerController.onDestroy()
|
||||
mediaPlayerManager.onDestroy()
|
||||
finish()
|
||||
exit()
|
||||
}
|
||||
@ -328,26 +402,26 @@ class NavigationActivity : AppCompatActivity() {
|
||||
|
||||
selectServerButton =
|
||||
navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
|
||||
selectServerButton?.setOnClickListener {
|
||||
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true)
|
||||
selectServerDropdownImage =
|
||||
navigationView?.getHeaderView(0)?.findViewById(R.id.edit_server_button)
|
||||
|
||||
val onClick: (View) -> Unit = {
|
||||
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true) {
|
||||
this.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||
}
|
||||
navController.navigate(R.id.serverSelectorFragment)
|
||||
}
|
||||
|
||||
selectServerButton?.setOnClickListener(onClick)
|
||||
selectServerDropdownImage?.setOnClickListener(onClick)
|
||||
|
||||
headerBackgroundImage =
|
||||
navigationView?.getHeaderView(0)?.findViewById(R.id.img_header_bg)
|
||||
}
|
||||
|
||||
private fun setupActionBar(navController: NavController, appBarConfig: AppBarConfiguration) {
|
||||
setupActionBarWithNavController(navController, appBarConfig)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true) {
|
||||
this.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
val currentFragment = host!!.childFragmentManager.fragments.last()
|
||||
if (currentFragment is OnBackPressedHandler) currentFragment.onBackPressed()
|
||||
else super.onBackPressed()
|
||||
private val closeNavigationDrawerOnBack = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
drawerLayout?.closeDrawer(GravityCompat.START)
|
||||
}
|
||||
}
|
||||
|
||||
@ -361,44 +435,79 @@ class NavigationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return item.onNavDestinationSelected(findNavController(R.id.nav_host_fragment)) ||
|
||||
val navController = findNavController(R.id.nav_host_fragment)
|
||||
// Check if this item ID exists in the nav graph
|
||||
val destinationExists = navController.graph.findNode(item.itemId) != null
|
||||
return if (destinationExists) {
|
||||
item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
|
||||
} else {
|
||||
// Let the fragments handle their own menu items
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
val currentFragment = host!!.childFragmentManager.fragments.last()
|
||||
return if (currentFragment is OnBackPressedHandler) {
|
||||
currentFragment.onBackPressed()
|
||||
true
|
||||
} else {
|
||||
findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration)
|
||||
// This override is required by design when using setupActionBarWithNavController()
|
||||
// with an AppBarConfiguration. It ensures that the Up button behavior is correctly
|
||||
// delegated — either navigating "up" in the back stack, or opening the drawer if
|
||||
// we're at a top-level destination.
|
||||
return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration) ||
|
||||
super.onSupportNavigateUp()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
when (intent.action) {
|
||||
Constants.INTENT_PLAY_RANDOM_SONGS -> {
|
||||
playRandomSongs()
|
||||
}
|
||||
Intent.ACTION_MAIN -> {
|
||||
if (intent.getBooleanExtra(Constants.INTENT_SHOW_PLAYER, false)) {
|
||||
findNavController(R.id.nav_host_fragment).navigate(R.id.playerFragment)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SEARCH -> {
|
||||
searchQuery = intent.getStringExtra(SearchManager.QUERY)
|
||||
handleSearchIntent(searchQuery, false)
|
||||
}
|
||||
MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH -> {
|
||||
searchQuery = intent.getStringExtra(SearchManager.QUERY)
|
||||
handleSearchIntent(searchQuery, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Test if this works with external Intents
|
||||
// android.intent.action.SEARCH and android.media.action.MEDIA_PLAY_FROM_SEARCH calls here
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
if (intent == null) return
|
||||
private fun handleSearchIntent(query: String?, autoPlay: Boolean) {
|
||||
val suggestions = SearchRecentSuggestions(
|
||||
this,
|
||||
SearchSuggestionProvider.AUTHORITY,
|
||||
SearchSuggestionProvider.MODE
|
||||
)
|
||||
suggestions.saveRecentQuery(query, null)
|
||||
|
||||
if (intent.getBooleanExtra(Constants.INTENT_SHOW_PLAYER, false)) {
|
||||
findNavController(R.id.nav_host_fragment).navigate(R.id.playerFragment)
|
||||
return
|
||||
val action = NavigationGraphDirections.toSearchFragment(query, autoPlay)
|
||||
findNavController(R.id.nav_host_fragment).navigate(action)
|
||||
}
|
||||
|
||||
private fun playRandomSongs() {
|
||||
val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
|
||||
val service = MusicServiceFactory.getMusicService()
|
||||
val musicDirectory = service.getRandomSongs(Settings.maxSongs)
|
||||
|
||||
mediaPlayerManager.addToPlaylist(
|
||||
songs = musicDirectory.getTracks(),
|
||||
autoPlay = true,
|
||||
shuffle = false,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR
|
||||
)
|
||||
|
||||
if (Settings.shouldTransitionOnPlayback) {
|
||||
currentFragment.findNavController().popBackStack(R.id.playerFragment, true)
|
||||
currentFragment.findNavController().navigate(R.id.playerFragment)
|
||||
}
|
||||
|
||||
val query = intent.getStringExtra(SearchManager.QUERY)
|
||||
|
||||
if (query != null) {
|
||||
val autoPlay = intent.action == MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
|
||||
val suggestions = SearchRecentSuggestions(
|
||||
this,
|
||||
SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
|
||||
)
|
||||
suggestions.saveRecentQuery(query, null)
|
||||
|
||||
val action = NavigationGraphDirections.toSearchFragment(query, autoPlay)
|
||||
findNavController(R.id.nav_host_fragment).navigate(action)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
@ -428,7 +537,6 @@ class NavigationActivity : AppCompatActivity() {
|
||||
|
||||
private fun showWelcomeDialog() {
|
||||
if (!UApp.instance!!.setupDialogDisplayed) {
|
||||
|
||||
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
||||
|
||||
InfoDialog.Builder(this)
|
||||
@ -475,9 +583,9 @@ class NavigationActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (nowPlayingView != null) {
|
||||
val playerState: Int = mediaPlayerController.playbackState
|
||||
val playerState: Int = mediaPlayerManager.playbackState
|
||||
if (playerState == STATE_BUFFERING || playerState == STATE_READY) {
|
||||
val item: MediaItem? = mediaPlayerController.currentMediaItem
|
||||
val item: MediaItem? = mediaPlayerManager.currentMediaItem
|
||||
if (item != null) {
|
||||
nowPlayingView?.visibility = View.VISIBLE
|
||||
}
|
||||
|
@ -15,28 +15,28 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.drakeet.multitype.ItemViewDelegate
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.RatingUpdate
|
||||
import org.moire.ultrasonic.domain.Album
|
||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.LayoutType
|
||||
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Creates a Row in a RecyclerView which contains the details of an Album
|
||||
*/
|
||||
open class AlbumRowDelegate(
|
||||
open val onItemClick: (Album) -> Unit,
|
||||
open val onContextMenuClick: (MenuItem, Album) -> Boolean,
|
||||
private val imageLoader: ImageLoader
|
||||
open val onContextMenuClick: (MenuItem, Album) -> Boolean
|
||||
) : ItemViewDelegate<Album, AlbumRowDelegate.ListViewHolder>(), KoinComponent {
|
||||
|
||||
private val starDrawable: Int = R.drawable.ic_star_full
|
||||
private val starHollowDrawable: Int = R.drawable.ic_star_hollow
|
||||
private val starDrawable: Int = R.drawable.rating_star_full
|
||||
private val starHollowDrawable: Int = R.drawable.rating_star_hollow
|
||||
|
||||
open var layoutType = LayoutType.LIST
|
||||
|
||||
@ -58,10 +58,16 @@ open class AlbumRowDelegate(
|
||||
holder.star.setImageResource(if (item.starred) starDrawable else starHollowDrawable)
|
||||
holder.star.setOnClickListener { onStarClick(item, holder.star) }
|
||||
|
||||
imageLoader.loadImage(
|
||||
holder.coverArt, item,
|
||||
false, 0, R.drawable.unknown_album
|
||||
)
|
||||
val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
imageLoaderProvider.executeOn {
|
||||
it.loadImage(
|
||||
holder.coverArt,
|
||||
item,
|
||||
false,
|
||||
0,
|
||||
R.drawable.unknown_album
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -109,27 +115,13 @@ open class AlbumRowDelegate(
|
||||
private fun onStarClick(entry: Album, star: ImageView) {
|
||||
entry.starred = !entry.starred
|
||||
star.setImageResource(if (entry.starred) starDrawable else starHollowDrawable)
|
||||
val musicService = getMusicService()
|
||||
Thread {
|
||||
val useId3 = shouldUseId3Tags
|
||||
try {
|
||||
if (entry.starred) {
|
||||
musicService.star(
|
||||
if (!useId3) entry.id else null,
|
||||
if (useId3) entry.id else null,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
musicService.unstar(
|
||||
if (!useId3) entry.id else null,
|
||||
if (useId3) entry.id else null,
|
||||
null
|
||||
)
|
||||
}
|
||||
} catch (all: Exception) {
|
||||
Timber.e(all)
|
||||
}
|
||||
}.start()
|
||||
|
||||
RxBus.ratingSubmitter.onNext(
|
||||
RatingUpdate(
|
||||
entry.id,
|
||||
HeartRating(entry.starred)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(context: Context, parent: ViewGroup): ListViewHolder {
|
||||
@ -148,8 +140,7 @@ open class AlbumRowDelegate(
|
||||
|
||||
class AlbumGridDelegate(
|
||||
onItemClick: (Album) -> Unit,
|
||||
onContextMenuClick: (MenuItem, Album) -> Boolean,
|
||||
imageLoader: ImageLoader
|
||||
) : AlbumRowDelegate(onItemClick, onContextMenuClick, imageLoader) {
|
||||
onContextMenuClick: (MenuItem, Album) -> Boolean
|
||||
) : AlbumRowDelegate(onItemClick, onContextMenuClick) {
|
||||
override var layoutType = LayoutType.COVER
|
||||
}
|
||||
|
@ -17,12 +17,17 @@ import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.drakeet.multitype.ItemViewBinder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
@ -33,7 +38,6 @@ import org.moire.ultrasonic.util.Util
|
||||
class ArtistRowBinder(
|
||||
val onItemClick: (ArtistOrIndex) -> Unit,
|
||||
val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val enableSections: Boolean = true
|
||||
) : ItemViewBinder<ArtistOrIndex, ArtistRowBinder.ViewHolder>(),
|
||||
KoinComponent,
|
||||
@ -59,17 +63,26 @@ class ArtistRowBinder(
|
||||
|
||||
holder.coverArtId = item.coverArt
|
||||
|
||||
val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
|
||||
if (showArtistPicture()) {
|
||||
holder.coverArt.visibility = View.VISIBLE
|
||||
val key = FileUtil.getArtistArtKey(item.name, false)
|
||||
imageLoader.loadImage(
|
||||
view = holder.coverArt,
|
||||
id = holder.coverArtId,
|
||||
key = key,
|
||||
large = false,
|
||||
size = 0,
|
||||
defaultResourceId = R.drawable.ic_contact_picture
|
||||
)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val key = FileUtil.getArtistArtKey(item.name, false)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
imageLoaderProvider.executeOn {
|
||||
it.loadImage(
|
||||
view = holder.coverArt,
|
||||
id = holder.coverArtId,
|
||||
key = key,
|
||||
large = false,
|
||||
size = 0,
|
||||
defaultResourceId = R.drawable.ic_contact_picture
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
holder.coverArt.visibility = View.GONE
|
||||
}
|
||||
@ -111,7 +124,7 @@ class ArtistRowBinder(
|
||||
}
|
||||
|
||||
private fun showArtistPicture(): Boolean {
|
||||
return ActiveServerProvider.isID3Enabled() && Settings.shouldShowArtistPicture
|
||||
return ActiveServerProvider.shouldUseId3Tags() && Settings.shouldShowArtistPicture
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,7 +26,8 @@ import timber.log.Timber
|
||||
* It should be kept generic enough that it can be used a Base for all lists in the app.
|
||||
*/
|
||||
@Suppress("unused", "UNUSED_PARAMETER")
|
||||
class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView.SectionedAdapter {
|
||||
class BaseAdapter<T : Identifiable>(allowDuplicateEntries: Boolean = false) :
|
||||
MultiTypeAdapter(), FastScrollRecyclerView.SectionedAdapter {
|
||||
|
||||
// Update the BoundedTreeSet if selection type is changed
|
||||
internal var selectionType: SelectionType = SelectionType.MULTIPLE
|
||||
@ -41,7 +42,7 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
|
||||
private val diffCallback = GenericDiffCallback<T>()
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
setHasStableIds(!allowDuplicateEntries)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user