Compare commits

...

286 Commits

Author SHA1 Message Date
Nite
1f9de0be7e Merge branch 'feature/cache-fix' into 'develop'
Fixed cache space cleanup

Closes 

See merge request 
2025-04-14 16:47:12 +00:00
Nite
550c486077 Fixed cache space cleanup 2025-04-14 16:47:11 +00:00
Nite
006c554456 Merge branch 'feature/bunch-of-fixes' into 'develop'
Fixed DI to make the widget work

See merge request 
2025-04-11 17:48:08 +00:00
Nite
6443881193 Fixed DI to make the widget work 2025-04-11 17:48:08 +00:00
Nite
a98693bbdb Merge branch 'feature/dependency-update' into 'develop'
Updated libraries to latest version

See merge request 
2025-04-11 08:07:16 +00:00
Nite
03e555eb27
Updated libraries to latest version 2025-04-11 09:58:16 +02:00
Nite
ef73d89491 Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request 
2025-04-08 16:05:55 +00:00
josé m
4ad789d20b
Translated using Weblate (Galician)
Currently translated at 100.0% (425 of 425 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2025-04-04 15:47:10 +02:00
Nite
7c163c237a Merge branch 'feature/min-sdk-26' into 'develop'
Updated Min SDK to 26

Closes 

See merge request 
2025-04-04 12:55:13 +00:00
Nite
38432a3cdc Updated Min SDK to 26 2025-04-04 12:55:13 +00:00
Nite
53aee22794 Merge branch 'renovate/jackson-monorepo' into 'develop'
Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.18.3

See merge request 
2025-04-04 10:11:01 +00:00
Nite
9880f15017 Merge branch 'renovate/major-koin' into 'develop'
Update koin to v4 (major)

See merge request 
2025-04-04 09:36:19 +00:00
Renovate Bot
0fc836cf83 Update koin to v4 (major) 2025-04-04 09:36:19 +00:00
Nite
6eb20d1c0b Merge branch 'renovate/gradle-8.x' into 'develop'
Update dependency gradle to v8.13

See merge request 
2025-04-04 09:16:51 +00:00
Renovate Bot
1f9c9505d7 Update dependency gradle to v8.13 2025-04-04 09:16:50 +00:00
Nite
2487d24371 Merge branch 'renovate/apachecodecs' into 'develop'
Update dependency commons-codec:commons-codec to v1.18.0

See merge request 
2025-04-04 08:48:20 +00:00
Nite
c27b3759b1 Merge branch 'renovate/junit5-monorepo' into 'develop'
Update dependency org.junit.vintage:junit-vintage-engine to v5.12.1

See merge request 
2025-04-04 08:45:58 +00:00
Nite
481f4e0347 Merge branch 'renovate/mockitokotlin' into 'develop'
Update dependency org.mockito.kotlin:mockito-kotlin to v5.4.0

See merge request 
2025-04-04 08:31:59 +00:00
Renovate Bot
a9b47f5aef Update dependency org.mockito.kotlin:mockito-kotlin to v5.4.0 2025-04-04 08:31:59 +00:00
Renovate Bot
b53bb38631 Update dependency org.junit.vintage:junit-vintage-engine to v5.12.1 2025-04-04 08:31:27 +00:00
Renovate Bot
a2a42e06b0 Update dependency commons-codec:commons-codec to v1.18.0 2025-04-04 08:31:24 +00:00
Nite
adf5b95243 Merge branch 'renovate/mockito-monorepo' into 'develop'
Update dependency org.mockito:mockito-core to v5.16.1

See merge request 
2025-04-04 08:18:46 +00:00
Nite
3d82b3935d Merge branch 'renovate/robolectric' into 'develop'
Update dependency org.robolectric:robolectric to v4.14.1

See merge request 
2025-04-04 08:14:40 +00:00
Nite
7ee66e51e8 Merge branch 'renovate/kotlinx-coroutines-monorepo' into 'develop'
Update kotlinx-coroutines monorepo to v1.10.1

See merge request 
2025-04-04 08:00:49 +00:00
Nite
b126cd594a Merge branch 'renovate/retrofit-monorepo' into 'develop'
Update retrofit monorepo to v2.11.0

See merge request 
2025-04-04 07:52:58 +00:00
Renovate Bot
31f654ee2f Update retrofit monorepo to v2.11.0 2025-04-03 19:31:45 +00:00
Renovate Bot
42c8edc6e7 Update kotlinx-coroutines monorepo to v1.10.1 2025-04-03 19:31:41 +00:00
Renovate Bot
8f7c7c33ff Update dependency org.robolectric:robolectric to v4.14.1 2025-04-03 18:31:59 +00:00
Renovate Bot
c105c0d02d Update dependency org.mockito:mockito-core to v5.16.1 2025-04-03 18:31:51 +00:00
Nite
e74fb3795a Merge branch 'renovate/rxjava' into 'develop'
Update dependency io.reactivex.rxjava3:rxjava to v3.1.10

See merge request 
2025-04-03 17:50:54 +00:00
Nite
80923ca8ac Merge branch 'renovate/detekt' into 'develop'
Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.8

See merge request 
2025-04-03 17:44:18 +00:00
Nite
f2fa530047 Merge branch 'renovate/koin' into 'develop'
Update koin to v3.5.6

See merge request 
2025-04-03 17:36:58 +00:00
Óscar García Amor
4cb6ab031b
Fixes translations linting 2025-04-03 09:25:00 +02:00
Óscar García Amor
2d224e5f84
Merge remote-tracking branch 'weblate/develop' into develop 2025-04-03 09:08:13 +02:00
Adolfo Jayme Barrientos
5a653bebb2
Translated using Weblate (Portuguese)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 09:06:40 +02:00
ssantos
5566cb05ab
Translated using Weblate (Portuguese)
Currently translated at 97.8% (413 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 09:02:00 +02:00
Adolfo Jayme Barrientos
7ee7aa23be
Translated using Weblate (Japanese)
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/ja/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
5a2ae50c74
Translated using Weblate (Galician)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
62d7e6bb6d
Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.1% (351 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/nb_NO/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
5266fc0b0f
Translated using Weblate (Spanish)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
612925bc7d
Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 68.0% (287 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hant/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
81499a1b37
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hans/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
27fc8462d1
Translated using Weblate (Russian)
Currently translated at 82.2% (347 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/ru/
2025-04-03 08:58:28 +02:00
Adolfo Jayme Barrientos
f3e205a452
Translated using Weblate (Portuguese)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 08:58:04 +02:00
Adolfo Jayme Barrientos
84374f8c13
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt_BR/
2025-04-03 08:58:04 +02:00
Adolfo Jayme Barrientos
5e8bf249f8
Translated using Weblate (Polish)
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pl/
2025-04-03 08:58:04 +02:00
Adolfo Jayme Barrientos
3913e676b7
Translated using Weblate (Dutch)
Currently translated at 95.0% (401 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/nl/
2025-04-03 08:58:02 +02:00
Adolfo Jayme Barrientos
a70ee39403
Translated using Weblate (Italian)
Currently translated at 63.0% (266 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/it/
2025-04-03 08:57:21 +02:00
Adolfo Jayme Barrientos
05bd9f0d2c
Translated using Weblate (Hungarian)
Currently translated at 77.9% (329 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/hu/
2025-04-03 08:57:21 +02:00
Adolfo Jayme Barrientos
4d9388f8fe
Translated using Weblate (French)
Currently translated at 96.4% (407 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/fr/
2025-04-03 08:57:21 +02:00
Adolfo Jayme Barrientos
8acd7da959
Translated using Weblate (German)
Currently translated at 99.0% (418 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/de/
2025-04-03 08:57:21 +02:00
Adolfo Jayme Barrientos
c909848ed5
Translated using Weblate (Czech)
Currently translated at 75.5% (319 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/cs/
2025-04-03 08:57:18 +02:00
Adolfo Jayme Barrientos
e5c8e874e3
Translated using Weblate (English)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/en/
2025-04-03 08:56:27 +02:00
Paulo Schopf
9d5caed73d
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt_BR/
2025-04-03 08:56:27 +02:00
Walton Henry (WaltonH)
292086e9d6
Translated using Weblate (Hungarian)
Currently translated at 77.9% (329 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/hu/
2025-04-03 08:56:20 +02:00
josé m
5639fcfb8b
Translated using Weblate (Galician)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2025-04-03 08:54:39 +02:00
Paulo Schopf
ce39da4b79
Translated using Weblate (Portuguese)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 08:54:39 +02:00
ssantos
4e89672d15
Translated using Weblate (Portuguese)
Currently translated at 99.5% (420 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 08:54:39 +02:00
ssantos
6725ef5a2c
Translated using Weblate (Portuguese)
Currently translated at 97.8% (413 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 08:54:39 +02:00
Renovate Bot
680cc90871 Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.8 2025-04-02 20:31:32 +00:00
Nite
c19dc125a9 Merge branch 'renovate/ktlintgradle' into 'develop'
Update dependency org.jlleitschuh.gradle:ktlint-gradle to v12.1.1

See merge request 
2025-04-02 19:32:20 +00:00
Renovate Bot
be8fa3c0d1 Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.18.3 2025-04-02 19:31:22 +00:00
Renovate Bot
312a97d664 Update dependency io.reactivex.rxjava3:rxjava to v3.1.10 2025-04-02 19:31:16 +00:00
Renovate Bot
fca954102a Update dependency org.jlleitschuh.gradle:ktlint-gradle to v12.1.1 2025-04-02 19:25:01 +00:00
Nite
9e078b4879 Merge branch 'feature/unified-rating' into 'develop'
Updated rating to be able to use the 5 star and heart rating together

Closes , , and 

See merge request 
2025-04-02 19:02:33 +00:00
Nite
46e85c27a2 Updated rating to be able to use the 5 star and heart rating together 2025-04-02 19:02:30 +00:00
Adolfo Jayme Barrientos
1124cef382
Translated using Weblate (Japanese)
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/ja/
2025-03-22 18:44:12 +01:00
Adolfo Jayme Barrientos
01ebf6e1aa
Translated using Weblate (Galician)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
b65050ae85
Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.1% (351 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/nb_NO/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
ddec1d7bdc
Translated using Weblate (Spanish)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
888d34aeb9
Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 68.0% (287 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hant/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
0fb3d8aeca
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hans/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
c56913733f
Translated using Weblate (Russian)
Currently translated at 82.2% (347 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/ru/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
76c61e1866
Translated using Weblate (Portuguese)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
810bb9ddd1
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt_BR/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
68a2123293
Translated using Weblate (Polish)
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pl/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
4221b18e09
Translated using Weblate (Dutch)
Currently translated at 95.0% (401 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/nl/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
e7ed066eed
Translated using Weblate (Italian)
Currently translated at 63.0% (266 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/it/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
1301f7e116
Translated using Weblate (Hungarian)
Currently translated at 77.9% (329 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/hu/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
63b064b021
Translated using Weblate (French)
Currently translated at 96.4% (407 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/fr/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
a9be77aa75
Translated using Weblate (German)
Currently translated at 99.0% (418 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/de/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
fc7dcc9f77
Translated using Weblate (Czech)
Currently translated at 75.5% (319 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/cs/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
48461f82a8
Translated using Weblate (English)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/en/
2025-03-22 18:44:09 +01:00
Paulo Schopf
81ea86fd01
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt_BR/
2024-10-31 15:24:51 +01:00
Walton Henry (WaltonH)
8803f4444e
Translated using Weblate (Hungarian)
Currently translated at 77.9% (329 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/hu/
2024-04-23 21:07:11 +02:00
Renovate Bot
38a97b2b91 Update koin to v3.5.6 2024-04-12 07:32:05 +00:00
josé m
6e7bcc4362
Translated using Weblate (Galician)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2024-02-20 19:02:07 +01:00
Paulo Schopf
c12224e811
Translated using Weblate (Portuguese)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2024-01-09 13:06:11 +01:00
ssantos
b8ef3cd177
Translated using Weblate (Portuguese)
Currently translated at 99.5% (420 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2024-01-07 17:06:12 +00:00
ssantos
2de773e5de
Translated using Weblate (Portuguese)
Currently translated at 97.8% (413 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2023-12-19 17:09:57 +00:00
birdbird
fe5b63ad1f Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request 
2023-12-06 10:52:51 +00:00
Paulo Schopf
4aff5857fd
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt_BR/
2023-12-05 22:06:55 +00:00
birdbird
a0d26cb3e7 Merge branch 'renovate/media3' into 'develop'
Update media3 to v1.2.0

See merge request 
2023-12-04 16:40:18 +00:00
tzugen
639ef03bce
Adapt to media 1.2.0 2023-12-04 17:33:18 +01:00
Renovate Bot
b855e4bbe7
Update media3 to v1.2.0 2023-12-04 17:32:37 +01:00
birdbird
b83e349f5c Merge branch 'refactor2' into 'develop'
Update copyright in recently edited files

See merge request 
2023-12-04 15:19:09 +00:00
birdbird
abad0438e3 Update copyright in recently edited files 2023-12-04 15:19:09 +00:00
birdbird
30f02c7eac Merge branch 'room2' into 'develop'
Adapt to room changes

See merge request 
2023-12-04 10:01:42 +00:00
tzugen
64f1c3e172
Adapt to room changes 2023-12-04 10:49:35 +01:00
birdbird
f7f1f40668 Merge branch 'renovate/mockito-monorepo' into 'develop'
Update dependency org.mockito:mockito-core to v5.8.0

See merge request 
2023-12-03 19:45:41 +00:00
birdbird
6519945c7b Merge branch 'renovate/major-ktlintgradle' into 'develop'
Update dependency org.jlleitschuh.gradle:ktlint-gradle to v12

See merge request 
2023-12-03 19:45:20 +00:00
Renovate Bot
94979aeaab Update dependency org.jlleitschuh.gradle:ktlint-gradle to v12 2023-12-03 19:45:20 +00:00
Renovate Bot
37e43b73a7 Update dependency org.mockito:mockito-core to v5.8.0 2023-12-03 14:31:46 +00:00
birdbird
2cf2cf31c4 Merge branch 'exep' into 'develop'
Avoid two exceptions

See merge request 
2023-12-03 14:12:11 +00:00
birdbird
9736ae451a Avoid two exceptions 2023-12-03 14:12:10 +00:00
birdbird
26331c1a07 Merge branch 'renovate/ksp' into 'develop'
Update dependency com.google.devtools.ksp to v1.9.21-1.0.15

See merge request 
2023-12-03 14:06:06 +00:00
birdbird
35ffe9ef10 Merge branch 'renovate/room' into 'develop'
Update room to v2.6.1

See merge request 
2023-12-03 14:06:01 +00:00
birdbird
976400d0e1 Merge branch 'renovate/mockitokotlin' into 'develop'
Update dependency org.mockito.kotlin:mockito-kotlin to v5.2.1

See merge request 
2023-12-03 14:05:41 +00:00
Renovate Bot
d2ef76a2c5 Update dependency org.mockito.kotlin:mockito-kotlin to v5.2.1 2023-12-03 10:32:00 +00:00
Renovate Bot
c0926b1e13 Update room to v2.6.1 2023-12-03 10:31:56 +00:00
Renovate Bot
bf5d41ab30 Update dependency com.google.devtools.ksp to v1.9.21-1.0.15 2023-12-03 10:31:52 +00:00
birdbird
e2716a5965 Merge branch 'renovate/junit5-monorepo' into 'develop'
Update dependency org.junit.vintage:junit-vintage-engine to v5.10.1

See merge request 
2023-12-03 09:31:41 +00:00
birdbird
8351f1dc0a Merge branch 'renovate/gradleplugin' into 'develop'
Update dependency com.android.tools.build:gradle to v8.2.0

See merge request 
2023-12-03 09:31:40 +00:00
birdbird
90997d5f4c Merge branch 'renovate/activity' into 'develop'
Update dependency androidx.activity:activity-ktx to v1.8.1

See merge request 
2023-12-03 09:31:31 +00:00
birdbird
747f071f2f Merge branch 'renovate/jackson' into 'develop'
Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.16.0

See merge request 
2023-12-03 09:31:20 +00:00
birdbird
9e76308cf2 Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request 
2023-12-03 09:30:45 +00:00
josé m
c452030fe1
Translated using Weblate (Galician)
Currently translated at 27.9% (119 of 426 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2023-12-03 10:30:03 +01:00
birdbird
dbbeac6084 Merge branch 'renovate/gradle-8.x' into 'develop'
Update dependency gradle to v8.5

See merge request 
2023-12-03 09:29:52 +00:00
birdbird
d56ec198a1 Merge branch 'renovate/detekt' into 'develop'
Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.4

See merge request 
2023-12-03 09:29:47 +00:00
birdbird
13238dfdc0 Merge branch 'renovate/kotlin-monorepo' into 'develop'
Update kotlin monorepo to v1.9.21

See merge request 
2023-12-03 09:29:40 +00:00
Renovate Bot
8063814bdc Update dependency com.android.tools.build:gradle to v8.2.0 2023-11-30 18:32:19 +00:00
Renovate Bot
3d8abdc65b Update dependency gradle to v8.5 2023-11-29 14:33:08 +00:00
Renovate Bot
4832876e54 Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.4 2023-11-26 12:31:21 +00:00
Renovate Bot
a0b0409930 Update kotlin monorepo to v1.9.21 2023-11-23 12:31:20 +00:00
Renovate Bot
8e6e9d4e8e Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.16.0 2023-11-16 01:31:37 +00:00
Renovate Bot
ee6d03db35 Update dependency androidx.activity:activity-ktx to v1.8.1 2023-11-15 18:31:49 +00:00
Renovate Bot
364270d338 Update dependency org.junit.vintage:junit-vintage-engine to v5.10.1 2023-11-05 17:31:25 +00:00
birdbird
a4dc06fa8a Merge branch 'renovate/detekt' into 'develop'
Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.3

See merge request 
2023-11-03 10:35:44 +00:00
birdbird
5c94d995d4 Merge branch 'renovate/robolectric' into 'develop'
Update dependency org.robolectric:robolectric to v4.11.1

See merge request 
2023-11-03 10:35:41 +00:00
birdbird
366da1c30c Merge branch 'renovate/kotlin-monorepo' into 'develop'
Update kotlin monorepo to v1.9.20

See merge request 
2023-11-03 10:35:40 +00:00
birdbird
e893510e79 Merge branch 'renovate/navigation' into 'develop'
Update navigation to v2.7.5

See merge request 
2023-11-03 10:35:34 +00:00
birdbird
67b359999e Merge branch 'renovate/mockito-monorepo' into 'develop'
Update dependency org.mockito:mockito-core to v5.7.0

See merge request 
2023-11-03 10:35:29 +00:00
Renovate Bot
01569647f7 Update dependency org.mockito:mockito-core to v5.7.0 2023-11-02 20:31:40 +00:00
Renovate Bot
3ee20113ae Update navigation to v2.7.5 2023-11-01 17:31:36 +00:00
Renovate Bot
6edff7e053 Update dependency org.robolectric:robolectric to v4.11.1 2023-10-31 19:35:46 +00:00
Renovate Bot
70cc124818 Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.3 2023-10-31 16:31:16 +00:00
Renovate Bot
98bf943a86 Update kotlin monorepo to v1.9.20 2023-10-30 20:31:18 +00:00
birdbird
58944bb0fd Merge branch 'renovate/room' into 'develop'
Update room to v2.6.0

See merge request 
2023-10-27 14:21:50 +00:00
Renovate Bot
397e1b6ecc Update room to v2.6.0 2023-10-27 14:21:50 +00:00
birdbird
71336b3c9f Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request 
2023-10-27 14:01:51 +00:00
Óscar García Amor
cd47bcf082
Translated using Weblate (Spanish)
Currently translated at 100.0% (426 of 426 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/
2023-10-27 11:12:57 +00:00
birdbird
4fb5d2a437 Merge branch 'wifiPerf' into 'develop'
Address wifi deprecation

See merge request 
2023-10-27 11:12:46 +00:00
tzugen
e8bf5a38b7
Address wifi deprecation 2023-10-27 13:06:27 +02:00
birdbird
ddfaf520e5 Merge branch 'wifiPerf' into 'develop'
Add a ClearJukebox method

See merge request 
2023-10-27 11:00:01 +00:00
tzugen
4d1c7464b9
Add a ClearJukebox method 2023-10-27 12:32:22 +02:00
birdbird
42c6eac97f Merge branch 'renovate/okhttp-monorepo' into 'develop'
Update okhttp monorepo to v4.12.0

See merge request 
2023-10-20 09:23:43 +00:00
Renovate Bot
2ae0a27588 Update okhttp monorepo to v4.12.0 2023-10-19 23:31:28 +00:00
birdbird
f2e8c0c331 Merge branch 'renovate/ktlintgradle' into 'develop'
Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.6.1

See merge request 
2023-10-18 11:57:24 +00:00
birdbird
7ce522fd15 Merge branch 'renovate/jackson' into 'develop'
Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.15.3

See merge request 
2023-10-18 11:57:10 +00:00
birdbird
170c61ef84 Merge branch 'renovate/mockito-monorepo' into 'develop'
Update dependency org.mockito:mockito-core to v5.6.0

See merge request 
2023-10-18 11:56:51 +00:00
birdbird
87fdf94e61 Merge branch 'renovate/colorpicker' into 'develop'
Update dependency com.github.skydoves:colorpickerview to v2.3.0

See merge request 
2023-10-18 11:56:00 +00:00
birdbird
fffe245df5 Merge branch 'renovate/materialdesign' into 'develop'
Update dependency com.google.android.material:material to v1.10.0

See merge request 
2023-10-18 11:55:19 +00:00
birdbird
95194bed3e Merge branch 'renovate/gradle-8.x' into 'develop'
Update dependency gradle to v8.4

See merge request 
2023-10-18 11:53:45 +00:00
birdbird
a9467e2fd8 Merge branch 'renovate/navigation' into 'develop'
Update navigation to v2.7.4

See merge request 
2023-10-18 11:51:49 +00:00
birdbird
4b99fdb788 Merge branch 'master' into 'develop'
Merge back from master

See merge request 
2023-10-18 11:51:16 +00:00
birdbird
727e53e096 Merge branch '480' into 'master'
Release 4.8.0

See merge request 
2023-10-18 10:56:51 +00:00
tzugen
69fc9b955a
RC 4.8.0 2023-10-18 12:48:32 +02:00
tzugen
601d0ccdaa
Merge remote-tracking branch 'origin/develop' into 480 2023-10-18 12:44:01 +02:00
birdbird
b19b1fb65a Merge branch 'coroutineLegacy' into 'develop'
Migrate remaining Java Code and modernize it

See merge request 
2023-10-18 10:19:12 +00:00
birdbird
442f622b35 Migrate remaining Java Code and modernize it 2023-10-18 10:19:10 +00:00
birdbird
de523a6451 Merge branch 'refactorScopes' into 'develop'
Refactor Koin, Scopes & Lifecycles

See merge request 
2023-10-14 19:09:26 +00:00
birdbird
17260878ac Refactor Koin, Scopes & Lifecycles 2023-10-14 19:09:26 +00:00
Renovate Bot
c6d26cdd67 Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.15.3 2023-10-13 02:31:21 +00:00
Renovate Bot
458fe5c36e Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.6.1 2023-10-10 15:31:32 +00:00
Renovate Bot
f388aaf4d8 Update dependency org.mockito:mockito-core to v5.6.0 2023-10-06 15:31:34 +00:00
Renovate Bot
6ddff58afb Update dependency com.google.android.material:material to v1.10.0 2023-10-05 18:31:28 +00:00
Renovate Bot
1e176f995a Update dependency gradle to v8.4 2023-10-04 21:32:34 +00:00
Renovate Bot
22c61258cc Update navigation to v2.7.4 2023-10-04 17:31:13 +00:00
Renovate Bot
288a1ad1c2 Update dependency com.github.skydoves:colorpickerview to v2.3.0 2023-10-02 12:31:29 +00:00
birdbird
823465d6fd Merge branch 'enhancement/star-outline' into 'develop'
Created outline for rating star images

Closes 

See merge request 
2023-10-01 16:56:39 +00:00
tzugen
0786b634e5
Merge remote-tracking branch 'origin/enhancement/star-outline' into 4.8.0 2023-10-01 18:49:19 +02:00
tzugen
7f4f944d79
Merge remote-tracking branch 'origin/develop' into 4.8.0 2023-09-30 14:12:59 +02:00
birdbird
264c84d540 Merge branch 'renovate/gradleplugin' into 'develop'
Update dependency com.android.tools.build:gradle to v8.1.2

See merge request 
2023-09-30 08:47:21 +00:00
Renovate Bot
e48c823de0 Update dependency com.android.tools.build:gradle to v8.1.2 2023-09-30 08:31:34 +00:00
birdbird
79f503beb3 Merge branch 'renovate/koin' into 'develop'
Update koin to v3.5.0

See merge request 
2023-09-30 08:07:10 +00:00
birdbird
34da90ad26 Merge branch 'renovate/ksp' into 'develop'
Update dependency com.google.devtools.ksp to v1.9.10-1.0.13

See merge request 
2023-09-30 08:03:49 +00:00
birdbird
566f780621 Merge branch 'renovate/viewmodelktx' into 'develop'
Update dependency androidx.lifecycle:lifecycle-viewmodel-ktx to v2.6.2

See merge request 
2023-09-30 08:03:33 +00:00
birdbird
c76bce5e94 Merge branch 'renovate/rxjava' into 'develop'
Update dependency io.reactivex.rxjava3:rxjava to v3.1.8

See merge request 
2023-09-30 08:03:09 +00:00
birdbird
27f0ee3a03 Merge branch 'renovate/ktlintgradle' into 'develop'
Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.6.0

See merge request 
2023-09-30 08:03:01 +00:00
birdbird
a1c21da7a6 Merge branch 'renovate/androidxcore' into 'develop'
Update dependency androidx.core:core-ktx to v1.12.0

See merge request 
2023-09-30 08:02:58 +00:00
birdbird
401ea0fcf1 Merge branch 'renovate/androidsupport' into 'develop'
Update dependency androidx.annotation:annotation to v1.7.0

See merge request 
2023-09-30 08:02:55 +00:00
birdbird
c062345f5e Merge branch 'renovate/navigation' into 'develop'
Update navigation to v2.7.3

See merge request 
2023-09-30 08:02:47 +00:00
birdbird
516cc94772 Merge branch 'mergeBack' into 'develop'
Merge master to dev

See merge request 
2023-09-30 08:01:57 +00:00
birdbird
104df418cc Merge master to dev 2023-09-30 08:01:56 +00:00
Renovate Bot
5755363f2e Update dependency io.reactivex.rxjava3:rxjava to v3.1.8 2023-09-29 09:31:24 +00:00
Nite
5167f9e45e
Created outline for rating star images 2023-09-25 17:38:22 +02:00
Renovate Bot
7a24be4b98 Update navigation to v2.7.3 2023-09-20 17:31:48 +00:00
Renovate Bot
f9bafa93da Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.6.0 2023-09-18 20:31:51 +00:00
Renovate Bot
8c7e8a8ae0 Update koin to v3.5.0 2023-09-13 08:31:46 +00:00
Renovate Bot
76f16cc9f1 Update dependency androidx.core:core-ktx to v1.12.0 2023-09-06 18:31:57 +00:00
Renovate Bot
907c94096b Update dependency androidx.annotation:annotation to v1.7.0 2023-09-06 18:31:55 +00:00
Renovate Bot
f6ad5be3e0 Update dependency androidx.lifecycle:lifecycle-viewmodel-ktx to v2.6.2 2023-09-06 17:31:37 +00:00
Renovate Bot
db0f3b21e1 Update dependency com.google.devtools.ksp to v1.9.10-1.0.13 2023-08-24 09:31:42 +00:00
birdbird
709dff1a81 Merge branch 'back' into 'develop'
Sync master and develop

See merge request 
2023-08-24 06:32:44 +00:00
birdbird
d2ed058d31 Sync master and develop 2023-08-24 06:32:43 +00:00
birdbird
7ad16ad92c Merge branch 'renovate/navigation' into 'develop'
Update navigation to v2.7.1

See merge request 
2023-08-24 06:27:42 +00:00
Renovate Bot
8ab3b2634d Update navigation to v2.7.1 2023-08-23 17:08:15 +00:00
birdbird
3349da6809 Merge branch 'renovate/rxjava' into 'develop'
Update dependency io.reactivex.rxjava3:rxjava to v3.1.7

See merge request 
2023-08-23 15:03:37 +00:00
Renovate Bot
e581776e43 Update dependency io.reactivex.rxjava3:rxjava to v3.1.7 2023-08-23 12:31:51 +00:00
birdbird
f292dc667e Merge branch 'renovate/kotlin-monorepo' into 'develop'
Update kotlin monorepo to v1.9.10

See merge request 
2023-08-23 11:02:43 +00:00
birdbird
7b8d1dec9b Merge branch '471' into 'master'
Release canditate 4.7.1

See merge request 
2023-08-23 11:00:30 +00:00
Renovate Bot
2d943edd61 Update kotlin monorepo to v1.9.10 2023-08-23 10:31:43 +00:00
birdbird
86464ba137 Merge branch 'renovate/mockito-monorepo' into 'develop'
Update dependency org.mockito:mockito-core to v5.5.0

See merge request 
2023-08-23 09:47:51 +00:00
tzugen
f2b47a257d
Release 4.7.1 2023-08-23 11:47:21 +02:00
birdbird
09e87ce8a0
Disable a lint 2023-08-23 11:47:21 +02:00
birdbird
490461f840 Merge branch 'renovate/ksp' into 'develop'
Update dependency com.google.devtools.ksp to v1.9.0-1.0.13

See merge request 
2023-08-23 09:45:59 +00:00
Renovate Bot
fb123d926d Update dependency com.google.devtools.ksp to v1.9.0-1.0.13 2023-08-23 09:45:59 +00:00
Alex Katlein
d2a98a3022
Utilize CarConnection to determine whether to set repeat mode to ALL 2023-08-23 10:57:26 +02:00
Renovate Bot
896c946a5a Update dependency org.mockito:mockito-core to v5.5.0 2023-08-22 13:31:52 +00:00
birdbird
560b593fcb Merge branch 'renovate/mockitokotlin' into 'develop'
Update dependency org.mockito.kotlin:mockito-kotlin to v5.1.0

See merge request 
2023-08-22 12:46:00 +00:00
birdbird
fe9bce334a Merge branch 'renovate/gradle-8.x' into 'develop'
Update dependency gradle to v8.3

See merge request 
2023-08-22 12:45:47 +00:00
birdbird
6af4d31aa3 Merge branch 'renovate/gradleplugin' into 'develop'
Update dependency com.android.tools.build:gradle to v8.1.1

See merge request 
2023-08-22 12:45:20 +00:00
birdbird
0ec2bb4fce Merge branch 'renovate/media3' into 'develop'
Update media3 to v1.1.1

See merge request 
2023-08-22 12:45:03 +00:00
birdbird
7f1dc14a4f Merge branch 'renovate/ktlintgradle' into 'develop'
Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.5.1

See merge request 
2023-08-22 12:44:45 +00:00
birdbird
8b2843410f Merge branch 'disableALint' into 'develop'
Disable a lint

See merge request 
2023-08-22 10:08:53 +00:00
birdbird
deef540ddc Disable a lint 2023-08-22 10:08:53 +00:00
Renovate Bot
cd101aef2a Update dependency com.android.tools.build:gradle to v8.1.1 2023-08-22 09:31:49 +00:00
birdbird
72bf0085b4 Merge branch 'fix/1263-no-repeat-all-outside-Auto' into 'develop'
Utilize CarConnection to determine whether to set repeat mode to ALL

Closes 

See merge request 
2023-08-22 08:49:04 +00:00
birdbird
bfb7c85eac Merge branch 'master' into 'develop'
Merge back from release

See merge request 
2023-08-22 08:14:06 +00:00
birdbird
0a3717f448 Merge back from release 2023-08-22 08:14:06 +00:00
Renovate Bot
fa3ca57a4e Update dependency gradle to v8.3 2023-08-17 07:32:56 +00:00
Renovate Bot
002a3250e4 Update media3 to v1.1.1 2023-08-16 12:32:00 +00:00
Renovate Bot
ab10d39f75 Update dependency org.mockito.kotlin:mockito-kotlin to v5.1.0 2023-08-09 20:31:51 +00:00
Alex Katlein
82f6758649
Utilize CarConnection to determine whether to set repeat mode to ALL 2023-08-09 21:17:41 +02:00
Renovate Bot
979fb116e8 Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.5.1 2023-08-07 19:31:40 +00:00
birdbird
ae97ded344 Merge branch 'kotlin' into 'develop'
Migrate more files to Kotlin

See merge request 
2023-08-05 15:10:25 +00:00
birdbird
71d45c89fb Migrate more files to Kotlin 2023-08-05 15:10:25 +00:00
birdbird
76a5705b2b Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request 
2023-08-05 14:34:06 +00:00
birdbird
6b000bc90f
Translated using Weblate (German)
Currently translated at 99.3% (426 of 429 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/de/
2023-08-04 14:03:57 +02:00
birdbird
b87203bfe4 Merge branch '470' into 'master'
4.7.0 Release canditate

See merge request 
2023-08-03 16:31:12 +00:00
tzugen
818cb96eb5
Release 4.7.0 2023-08-03 18:25:02 +02:00
tzugen
e9fdbd924b
Don't skip around when doing "Play Next" while music is playing 2023-08-03 14:28:22 +02:00
Alex Katlein
a0b9e738a5
Using android.R.color.transparent instead of empty drawable for the placeholder
- For some reason Android Auto displays a square with a white fill color where the empty space should be
- Got rid of empty drawable
2023-08-02 19:43:55 +02:00
birdbird
0dcdd94149 Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request 
2023-08-02 08:29:01 +00:00
gallegonovato
410e4d9d96
Translated using Weblate (Spanish)
Currently translated at 100.0% (429 of 429 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/
2023-08-01 19:07:35 +02:00
birdbird
68567731ad Merge branch 'renovate/gradleplugin' into 'develop'
Update dependency com.android.tools.build:gradle to v8.1.0

See merge request 
2023-07-31 14:37:48 +00:00
birdbird
75ecfce1df Merge branch 'toast' into 'develop'
Add a toast when adding tracks to the playlist, allow pinning when offline

See merge request 
2023-07-31 14:37:33 +00:00
tzugen
ddf8ce7029
Migrate from KAPT to KSP 2023-07-31 14:47:35 +02:00
Renovate Bot
f99fb1c92a
Update dependency com.android.tools.build:gradle to v8.1.0 2023-07-31 14:46:58 +02:00
tzugen
7fcb58963c
Show Pin button also when offline 2023-07-31 12:47:49 +02:00
tzugen
f89b2da30d
More more functions to InsertionMode pattern,
show a Toast when adding tracks to the playlist.
2023-07-31 12:40:37 +02:00
tzugen
691bfbe594
Merge remote-tracking branch 'origin/master' into mergeBack 2023-07-31 11:32:10 +02:00
birdbird
7061b7a324 Merge branch 'renovate/kotlinxcoroutines' into 'develop'
Update kotlinxCoroutines to v1.7.3

See merge request 
2023-07-31 09:14:45 +00:00
birdbird
6bc520d551 Merge branch 'renovate/preferences' into 'develop'
Update dependency androidx.preference:preference to v1.2.1

See merge request 
2023-07-31 09:14:34 +00:00
birdbird
ff20c671a2 Merge branch 'renovate/koin' into 'develop'
Update koin to v3.4.3

See merge request 
2023-07-31 09:14:23 +00:00
birdbird
37935a5f86 Merge branch '463' into 'master'
Release candidate 4.6.3

See merge request 
2023-07-30 14:02:12 +00:00
Renovate Bot
1f6df202b9 Update koin to v3.4.3 2023-07-28 08:31:49 +00:00
Renovate Bot
06496ddf37 Update dependency androidx.preference:preference to v1.2.1 2023-07-26 17:31:46 +00:00
Renovate Bot
509e33ac20 Update kotlinxCoroutines to v1.7.3 2023-07-25 16:31:46 +00:00
tzugen
6bc09854ac
Fix wrong thread in throttled observers 2023-07-25 12:11:26 +02:00
tzugen
fc637166bb
Fix inject 2023-07-24 23:20:44 +02:00
birdbird
6eb7c9d25c Merge branch 'modernize' into 'develop'
Modernize code after media3 1.1.0 update

See merge request 
2023-07-24 21:08:01 +00:00
birdbird
0492b0fa6f Modernize code after media3 1.1.0 update 2023-07-24 21:08:01 +00:00
birdbird
acbaae9f14 Merge branch 'feature/android-auto-shuffle-repeat' into 'develop'
Added custom buttons for shuffling the current queue and setting repeat mode

Closes 

See merge request 
2023-07-24 19:20:21 +00:00
Alex Katlein
77c1329be5 Added custom buttons for shuffling the current queue and setting repeat mode 2023-07-24 19:20:21 +00:00
birdbird
aefdadbbd5 Merge branch 'renovate/gradle-8.x' into 'develop'
Update dependency gradle to v8.2.1

See merge request 
2023-07-24 19:20:05 +00:00
birdbird
29a1527d24 Merge branch 'renovate/junit5-monorepo' into 'develop'
Update dependency org.junit.vintage:junit-vintage-engine to v5.10.0

See merge request 
2023-07-24 19:19:51 +00:00
tzugen
18fd8f64c6
Release 4.6.3
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
2023-07-24 21:16:26 +02:00
birdbird
4f55a2a4a5
Fix an exception when removeIncompleteTracksFromPlaylist() could be called on the wrong thread. 2023-07-24 21:09:39 +02:00
birdbird
1fa5f4c2f8
Fix unpin 2023-07-24 21:07:19 +02:00
birdbird
2a9bf9dd29 Merge branch 'fixException' into 'develop'
Fix an exception when removeIncompleteTracksFromPlaylist() could be called on the wrong thread.

See merge request 
2023-07-24 18:57:29 +00:00
birdbird
a04517de90 Fix an exception when removeIncompleteTracksFromPlaylist() could be called on the wrong thread. 2023-07-24 18:57:29 +00:00
birdbird
21e4293adb Merge branch 'root' into 'develop'
Avoid rare NPE

See merge request 

(cherry picked from commit 7209779b64820572910059fdd24342bb73734b54)

ecee57e1 Avoid rare NPE
2023-07-24 18:55:27 +00:00
birdbird
358af365d2 Merge branch 'fixUnpin' into 'develop'
Fix unpin

Closes 

See merge request 
2023-07-24 18:38:09 +00:00
birdbird
62ba16eedd Fix unpin 2023-07-24 18:38:08 +00:00
Renovate Bot
e7825fc90c Update dependency org.junit.vintage:junit-vintage-engine to v5.10.0 2023-07-23 13:31:45 +00:00
Renovate Bot
1fba65ed3a Update dependency gradle to v8.2.1 2023-07-10 12:32:41 +00:00
birdbird
7209779b64 Merge branch 'root' into 'develop'
Avoid rare NPE

See merge request 
2023-07-06 20:49:29 +00:00
tzugen
ecee57e166
Avoid rare NPE 2023-07-06 18:05:16 +02:00
birdbird
288f72b972 Merge branch 'renovate/koin' into 'develop'
Update koin to v3.4.2

See merge request 
2023-07-06 15:46:47 +00:00
Renovate Bot
6be96ee8c9 [ROBOTEST] Update koin to v3.4.2 2023-07-06 15:46:47 +00:00
birdbird
65dd30eaa8 Merge branch 'supy' into 'develop'
Fix a bug introduced in 725d9281

See merge request 
2023-07-06 15:42:15 +00:00
tzugen
698360b77a
Release 4.6.2
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
2023-07-06 17:36:37 +02:00
tzugen
b57b799feb
Fix a bug introduced in 725d9281 2023-07-06 17:33:48 +02:00
tzugen
2436537609
Fix a bug introduced in 725d9281 2023-07-06 17:26:33 +02:00
birdbird
96ac6fcac7 Merge branch 'fix/android-auto-nested-directories' into 'develop'
Properly handling nested directory structures in Android Auto

Closes 

See merge request 
2023-07-06 15:11:02 +00:00
Alex Katlein
9fa80d206b Properly handling nested directory structures in Android Auto 2023-07-06 15:11:02 +00:00
birdbird
8990b4d622 Merge branch 'renovate/media3' into 'develop'
Update media3 to v1.1.0

See merge request 
2023-07-06 15:06:49 +00:00
Renovate Bot
211af57e8b Update media3 to v1.1.0 2023-07-06 15:06:49 +00:00
birdbird
9fd2a91f15 Merge branch 'renovate/kotlinxcoroutines' into 'develop'
Update kotlinxCoroutines to v1.7.2

See merge request 
2023-07-06 14:32:40 +00:00
birdbird
c85e2ef3f4 Merge branch 'renovate/gradle-8.x' into 'develop'
Update dependency gradle to v8.2

See merge request 
2023-07-06 14:32:33 +00:00
birdbird
8ab840023a Merge branch 'renovate/ktlintgradle' into 'develop'
Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.5.0

See merge request 
2023-07-06 14:32:28 +00:00
Renovate Bot
db278afe4a Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.5.0 2023-07-03 22:32:22 +00:00
Renovate Bot
d08711eb0c Update dependency gradle to v8.2 2023-06-30 18:33:00 +00:00
Renovate Bot
500ffa8009 Update kotlinxCoroutines to v1.7.2 2023-06-29 13:31:51 +00:00
birdbird
7124939467 [ROBOTEST] Update .gitlab-ci.yml file 2023-06-29 13:09:51 +00:00
270 changed files with 5461 additions and 5074 deletions
.editorconfig.gitignore.gitlab-ci.yml
.idea
build.gradle
core
fastlane/metadata/android/en-US/changelogs
gradle
gradle_scripts
gradlewgradlew.bat
ultrasonic

2
.editorconfig Normal file

@ -0,0 +1,2 @@
[*.{kt,kts}]
ktlint_code_style = android_studio

1
.gitignore vendored

@ -18,6 +18,7 @@ out/
# Gradle files
.gradle/
.kotlin/
build/
# Local configuration file (sdk path, etc)

@ -1,5 +1,5 @@
default:
image: registry.gitlab.com/ultrasonic/ci-android:1.1.0
image: registry.gitlab.com/ultrasonic/ci-android:1.2.0
cache: &global_cache
key:
files:
@ -138,7 +138,7 @@ RoboTest:
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 --device model=Pixel5,version=30,locale=zh,orientation=portrait
- 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

@ -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

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
<bytecodeTargetLevel target="21" />
</component>
</project>

@ -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,7 +12,8 @@ buildscript {
repositories {
google()
mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
gradlePluginPortal()
maven { url = "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath libs.gradle
@ -26,24 +29,32 @@ allprojects {
buildscript {
repositories {
mavenCentral()
gradlePluginPortal()
google()
}
}
repositories {
mavenCentral()
gradlePluginPortal()
google()
}
// 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 = "17"
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"
}

@ -1,13 +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

@ -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)

@ -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

@ -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

@ -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

@ -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

@ -0,0 +1,5 @@
### Features
- Improved display of rating stars
- Completely modernize all older code parts
- Updates for Android 14
- Update dependencies

@ -1,44 +1,46 @@
[versions]
# You need to run ./gradlew wrapper after updating the version
gradle = "8.1.1"
gradle = "8.13"
navigation = "2.6.0"
gradlePlugin = "8.0.2"
androidxcore = "1.10.1"
ktlint = "0.43.2"
ktlintGradle = "11.4.2"
detekt = "1.23.0"
preferences = "1.2.0"
media3 = "1.0.2"
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.9.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.2"
kotlin = "1.8.22"
kotlinxCoroutines = "1.7.1"
viewModelKtx = "2.6.1"
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"
## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24
jackson = "2.13.5"
okhttp = "4.11.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.3"
mockito = "5.4.0"
mockitoKotlin = "5.0.0"
junit5 = "5.12.2"
mockito = "5.17.0"
mockitoKotlin = "5.4.0"
kluent = "1.73"
apacheCodecs = "1.16.0"
robolectric = "4.10.3"
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" }
@ -100,3 +104,6 @@ kluentAndroid = { module = "org.amshove.kluent:kluent-android", versio
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,
]

Binary file not shown.

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

@ -4,7 +4,7 @@
apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
apply plugin: 'org.jetbrains.kotlin.kapt'
apply plugin: 'com.google.devtools.ksp'
android {
compileSdkVersion versions.compileSdk

@ -2,7 +2,7 @@
* This module provides a base for for pure kotlin modules
*/
apply plugin: 'kotlin'
apply plugin: 'org.jetbrains.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

26
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,7 +85,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# 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
@ -130,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.
@ -141,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
@ -149,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
@ -198,11 +204,11 @@ fi
# 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 $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.
# 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

@ -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: 'org.jetbrains.kotlin.android'
apply plugin: 'org.jetbrains.kotlin.kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"
apply from: "../gradle_scripts/code_quality.gradle"
@ -9,8 +12,8 @@ android {
defaultConfig {
applicationId "org.moire.ultrasonic"
versionCode 124
versionName "4.6.1"
versionCode 130
versionName "4.8.0"
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
@ -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,42 +53,43 @@ android {
}
kotlinOptions {
jvmTarget = "17"
jvmTarget = "21"
}
buildFeatures {
viewBinding true
dataBinding true
buildConfig true
viewBinding = true
dataBinding = true
buildConfig = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
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()
}
@ -97,6 +101,7 @@ dependencies {
exclude group: "com.android.support"
}
implementation libs.car
implementation libs.core
implementation libs.design
implementation libs.multidex
@ -129,7 +134,7 @@ dependencies {
implementation libs.rxAndroid
implementation libs.multiType
kapt libs.room
ksp libs.room
testImplementation libs.kotlinReflect
testImplementation libs.junit
@ -141,6 +146,5 @@ dependencies {
testImplementation libs.robolectric
implementation libs.timber
}

@ -1,9 +1,7 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues>
<ID>TooManyFunctions:PlaybackService.kt$PlaybackService : MediaLibraryServiceKoinComponentCoroutineScope</ID>
<ID>UnusedPrivateMember:UApp.kt$private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder</ID>
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.&lt;no name provided&gt;$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID>
<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>
@ -11,19 +9,11 @@
<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&lt;ServerSetting&gt;, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -&gt; Unit), private val serverEditRequestedCallback: ((Int) -&gt; 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>
</ManuallySuppressedIssues>
<CurrentIssues/>
</CurrentIssues>
</SmellBaseline>

@ -70,50 +70,6 @@
column="1"/>
</issue>
<issue
id="Autofill"
message="Missing `autofillHints` attribute"
errorLine1=" &lt;EditText"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/chat.xml"
line="33"
column="10"/>
</issue>
<issue
id="Autofill"
message="Missing `autofillHints` attribute"
errorLine1=" &lt;EditText"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/save_playlist.xml"
line="9"
column="6"/>
</issue>
<issue
id="Autofill"
message="Missing `autofillHints` attribute"
errorLine1=" &lt;EditText"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/share_details.xml"
line="29"
column="10"/>
</issue>
<issue
id="Autofill"
message="Missing `autofillHints` attribute"
errorLine1=" &lt;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`"

@ -12,6 +12,8 @@
<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,20 +22,22 @@
android:xlargeScreens="true"/>
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:hasFragileUserData="true" tools:targetApi="q"
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:preserveLegacyExternalStorage="true"
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="UnusedAttribute">
android:preserveLegacyExternalStorage="true"
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"
@ -70,7 +74,7 @@
</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"
@ -105,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"
@ -117,12 +127,6 @@
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"
@ -135,5 +139,4 @@
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,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);
}
@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,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,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(@NonNull 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,7 +13,6 @@ 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
@ -23,7 +22,6 @@ import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
@ -39,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
@ -50,6 +49,7 @@ 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
@ -63,7 +63,6 @@ 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.subsonic.DownloadHandler
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.InfoDialog
import org.moire.ultrasonic.util.LocaleHelper
@ -81,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
@ -96,6 +95,7 @@ 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.
@ -165,7 +165,7 @@ class NavigationActivity : AppCompatActivity() {
drawerLayout
)
setupActionBar(navController, appBarConfiguration)
setupActionBarWithNavController(navController, appBarConfiguration)
setupNavigationMenu(navController)
@ -204,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 {
@ -226,7 +227,7 @@ class NavigationActivity : AppCompatActivity() {
// Setup app shortcuts on supported devices, but not on first start, when the server
// is not configured yet.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !UApp.instance!!.isFirstRun) {
if (!UApp.instance!!.isFirstRun) {
ShortcutUtil.registerShortcuts(this)
}
@ -306,7 +307,7 @@ class NavigationActivity : AppCompatActivity() {
Storage.reset()
lifecycleScope.launch(Dispatchers.IO) {
Storage.ensureRootIsAvailable()
Storage.checkForErrorsWithCustomRoot()
}
setMenuForServerCapabilities()
@ -314,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()
}
}
/*
@ -329,35 +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)
}
}
private fun setupNavigationMenu(navController: NavController) {
@ -402,26 +402,23 @@ class NavigationActivity : AppCompatActivity() {
selectServerButton =
navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
val dropDownButton: ImageView? =
selectServerDropdownImage =
navigationView?.getHeaderView(0)?.findViewById(R.id.edit_server_button)
val onClick: (View) -> Unit = {
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true)
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true) {
this.drawerLayout?.closeDrawer(GravityCompat.START)
}
navController.navigate(R.id.serverSelectorFragment)
}
selectServerButton?.setOnClickListener(onClick)
dropDownButton?.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)
}
private val closeNavigationDrawerOnBack = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
drawerLayout?.closeDrawer(GravityCompat.START)
@ -438,19 +435,30 @@ 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)
}
}
// TODO: Why is this needed? Shouldn't it just work by default?
override fun onSupportNavigateUp(): Boolean {
return 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?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
when (intent?.action) {
when (intent.action) {
Constants.INTENT_PLAY_RANDOM_SONGS -> {
playRandomSongs()
}
@ -473,7 +481,8 @@ class NavigationActivity : AppCompatActivity() {
private fun handleSearchIntent(query: String?, autoPlay: Boolean) {
val suggestions = SearchRecentSuggestions(
this,
SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
SearchSuggestionProvider.AUTHORITY,
SearchSuggestionProvider.MODE
)
suggestions.saveRecentQuery(query, null)
@ -485,16 +494,19 @@ class NavigationActivity : AppCompatActivity() {
val currentFragment = host?.childFragmentManager?.fragments?.last() ?: return
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getRandomSongs(Settings.maxSongs)
val downloadHandler: DownloadHandler by inject()
downloadHandler.addTracksToMediaController(
mediaPlayerManager.addToPlaylist(
songs = musicDirectory.getTracks(),
append = false,
playNext = false,
autoPlay = true,
shuffle = false,
fragment = currentFragment,
playlistName = null
insertionMode = MediaPlayerManager.InsertionMode.CLEAR
)
if (Settings.shouldTransitionOnPlayback) {
currentFragment.findNavController().popBackStack(R.id.playerFragment, true)
currentFragment.findNavController().navigate(R.id.playerFragment)
}
return
}
@ -525,7 +537,6 @@ class NavigationActivity : AppCompatActivity() {
private fun showWelcomeDialog() {
if (!UApp.instance!!.setupDialogDisplayed) {
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
InfoDialog.Builder(this)

@ -32,11 +32,11 @@ import org.moire.ultrasonic.util.LayoutType
*/
open class AlbumRowDelegate(
open val onItemClick: (Album) -> Unit,
open val onContextMenuClick: (MenuItem, Album) -> Boolean,
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
@ -61,8 +61,11 @@ open class AlbumRowDelegate(
val imageLoaderProvider: ImageLoaderProvider by inject()
imageLoaderProvider.executeOn {
it.loadImage(
holder.coverArt, item,
false, 0, R.drawable.unknown_album
holder.coverArt,
item,
false,
0,
R.drawable.unknown_album
)
}
}

@ -17,6 +17,10 @@ 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
@ -63,16 +67,21 @@ class ArtistRowBinder(
if (showArtistPicture()) {
holder.coverArt.visibility = View.VISIBLE
val key = FileUtil.getArtistArtKey(item.name, false)
imageLoaderProvider.executeOn {
it.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

@ -23,10 +23,7 @@ class DividerBinder : ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHo
holder.textView.setText(item.stringId)
}
override fun onCreateViewHolder(
inflater: LayoutInflater,
parent: ViewGroup
): ViewHolder {
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
return ViewHolder(inflater.inflate(layout, parent, false))
}

@ -78,7 +78,10 @@ class FolderSelectorBinder(context: Context) :
val popup = PopupMenu(weakContext.get()!!, layout)
var menuItem = popup.menu.add(
MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
MENU_GROUP_MUSIC_FOLDER,
-1,
0,
R.string.select_artist_all_folders
)
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {

@ -46,7 +46,6 @@ class HeaderViewBinder(
}
override fun onBindViewHolder(holder: ViewHolder, item: AlbumHeader) {
val context = weakContext.get() ?: return
val resources = context.resources
@ -98,7 +97,8 @@ class HeaderViewBinder(
holder.yearView.text = year
val songs = resources.getQuantityString(
R.plurals.select_album_n_songs, item.childCount,
R.plurals.n_songs,
item.childCount,
item.childCount
)
holder.songCountView.text = songs

@ -77,7 +77,6 @@ internal class ServerRowAdapter(
*/
@Suppress("LongMethod")
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
var vi: View? = convertView
if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false)

@ -1,13 +1,18 @@
package org.moire.ultrasonic.adapters
import android.graphics.Color
import android.graphics.drawable.LayerDrawable
import android.os.Build
import android.view.MenuInflater
import android.view.View
import android.widget.Checkable
import android.widget.CheckedTextView
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.MutableLiveData
@ -31,7 +36,7 @@ import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import timber.log.Timber
import org.moire.ultrasonic.util.Util.themeColor
const val INDICATOR_THICKNESS_INDEFINITE = 5
const val INDICATOR_THICKNESS_DEFINITE = 10
@ -51,17 +56,12 @@ class TrackViewHolder(val view: View) :
var entry: Track? = null
private set
var songLayout: LinearLayout = view.findViewById(R.id.song_layout)
private var songLayout: LinearLayout = view.findViewById(R.id.song_layout)
var check: CheckedTextView = view.findViewById(R.id.song_check)
var drag: ImageView = view.findViewById(R.id.song_drag)
var observableChecked = MutableLiveData(false)
private var rating: LinearLayout = view.findViewById(R.id.song_rating)
private var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
private var fiveStar2: ImageView = view.findViewById(R.id.song_five_star_2)
private var fiveStar3: ImageView = view.findViewById(R.id.song_five_star_3)
private var fiveStar4: ImageView = view.findViewById(R.id.song_five_star_4)
private var fiveStar5: ImageView = view.findViewById(R.id.song_five_star_5)
private var star: ImageView = view.findViewById(R.id.song_star)
private var track: TextView = view.findViewById(R.id.song_track)
private var title: TextView = view.findViewById(R.id.song_title)
@ -79,20 +79,35 @@ class TrackViewHolder(val view: View) :
private var rxBusSubscription: CompositeDisposable? = null
init {
Timber.v("New ViewHolder created")
}
@Suppress("ComplexMethod")
fun setSong(
song: Track,
checkable: Boolean,
draggable: Boolean,
isSelected: Boolean = false
) {
val useFiveStarRating = Settings.useFiveStarRating
fun setSong(song: Track, checkable: Boolean, draggable: Boolean, isSelected: Boolean = false) {
entry = song
// Create new Disposable for the new Subscriptions
rxBusSubscription = CompositeDisposable()
rxBusSubscription!! += RxBus.playerStateObservable.subscribe {
setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition)
}
rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe {
if (it.id != song.id) return@subscribe
updateStatus(it.state, it.progress)
}
// Listen for rating updates
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
launch(Dispatchers.Main) {
// Ignore updates which are not for the current song
if (it.id != song.id) return@launch
if (it.rating is HeartRating) {
updateRatingDisplay(song.userRating, it.rating.isHeart)
} else if (it.rating is StarRating) {
updateRatingDisplay(it.rating.starRating.toInt(), song.starred)
}
}
}
val entryDescription = Util.readableEntryDescription(song)
artist.text = entryDescription.artist
@ -113,7 +128,7 @@ class TrackViewHolder(val view: View) :
if (ActiveServerProvider.isOffline()) {
star.isGone = true
} else {
setupStarButtons(song, useFiveStarRating)
setupRating(song)
}
// Instead of blocking the UI thread while looking up the current state,
@ -125,41 +140,12 @@ class TrackViewHolder(val view: View) :
)
}
if (useFiveStarRating) {
updateFiveStars(entry?.userRating ?: 0)
} else {
updateSingleStar(entry!!.starred)
}
updateRatingDisplay(entry!!.userRating, entry!!.starred)
if (song.isVideo) {
artist.isGone = true
progressIndicator.isGone = true
}
// Create new Disposable for the new Subscriptions
rxBusSubscription = CompositeDisposable()
rxBusSubscription!! += RxBus.playerStateObservable.subscribe {
setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition)
}
rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe {
if (it.id != song.id) return@subscribe
updateStatus(it.state, it.progress)
}
// Listen for rating updates
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
launch(Dispatchers.Main) {
// Ignore updates which are not for the current song
if (it.id != song.id) return@launch
if (it.rating is HeartRating) {
updateSingleStar(it.rating.isHeart)
} else if (it.rating is StarRating) {
updateFiveStars(it.rating.starRating.toInt())
}
}
}
}
// This is called when the Holder is recycled and receives a new Song
@ -176,7 +162,10 @@ class TrackViewHolder(val view: View) :
if (isPlaying && !isPlayingCached) {
isPlayingCached = true
title.setCompoundDrawablesWithIntrinsicBounds(
playingIcon, null, null, null
playingIcon,
null,
null,
null
)
val color = MaterialColors.getColor(view, COLOR_HIGHLIGHT)
songLayout.setBackgroundColor(color)
@ -184,62 +173,98 @@ class TrackViewHolder(val view: View) :
} else if (!isPlaying && isPlayingCached) {
isPlayingCached = false
title.setCompoundDrawablesWithIntrinsicBounds(
0, 0, 0, 0
0,
0,
0,
0
)
songLayout.setBackgroundColor(Color.TRANSPARENT)
songLayout.elevation = 0F
}
}
private fun setupStarButtons(track: Track, useFiveStarRating: Boolean) {
if (useFiveStarRating) {
// Hide single star
star.isGone = true
rating.isVisible = true
val rating = if (track.userRating == null) 0 else track.userRating!!
updateFiveStars(rating)
private fun setupRating(track: Track) {
star.isVisible = true
updateRatingDisplay(track.userRating, track.starred)
// Five star rating has no click handler because in the
// track view theres not enough space
} else {
star.isVisible = true
rating.isGone = true
updateSingleStar(track.starred)
star.setOnClickListener {
track.starred = !track.starred
updateSingleStar(track.starred)
RxBus.ratingSubmitter.onNext(
RatingUpdate(track.id, HeartRating(track.starred))
)
}
}
star.setOnClickListener { toggleHeart(track) }
star.setOnLongClickListener { view -> showRatingPopup(view, track) }
}
private fun toggleHeart(track: Track) {
track.starred = !track.starred
updateRatingDisplay(track.userRating, track.starred)
RxBus.ratingSubmitter.onNext(
RatingUpdate(track.id, HeartRating(track.starred))
)
}
@Suppress("MagicNumber")
private fun updateFiveStars(rating: Int) {
fiveStar1.setImageResource(
if (rating > 0) R.drawable.ic_star_full else R.drawable.ic_star_hollow
)
fiveStar2.setImageResource(
if (rating > 1) R.drawable.ic_star_full else R.drawable.ic_star_hollow
)
fiveStar3.setImageResource(
if (rating > 2) R.drawable.ic_star_full else R.drawable.ic_star_hollow
)
fiveStar4.setImageResource(
if (rating > 3) R.drawable.ic_star_full else R.drawable.ic_star_hollow
)
fiveStar5.setImageResource(
if (rating > 4) R.drawable.ic_star_full else R.drawable.ic_star_hollow
)
private fun showRatingPopup(view: View, track: Track): Boolean {
val popup = PopupMenu(view.context, view)
val inflater: MenuInflater = popup.menuInflater
inflater.inflate(R.menu.rating, popup.menu)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) popup.setForceShowIcon(true)
popup.setOnMenuItemClickListener {
val rating = when (it.itemId) {
R.id.popup_rate_1 -> 1
R.id.popup_rate_2 -> 2
R.id.popup_rate_3 -> 3
R.id.popup_rate_4 -> 4
R.id.popup_rate_5 -> 5
else -> 0
}
track.userRating = rating
updateRatingDisplay(track.userRating, track.starred)
RxBus.ratingSubmitter.onNext(
RatingUpdate(track.id, StarRating(5, rating.toFloat()))
)
true
}
popup.show()
return true
}
private fun updateSingleStar(starred: Boolean) {
if (starred) {
star.setImageResource(R.drawable.ic_star_full)
} else {
star.setImageResource(R.drawable.ic_star_hollow)
@Suppress("MagicNumber")
private fun updateRatingDisplay(rating: Int?, starred: Boolean) {
val ratingDrawable = when (rating) {
1 -> R.drawable.rating_star_1
2 -> R.drawable.rating_star_2
3 -> R.drawable.rating_star_3
4 -> R.drawable.rating_star_4
5 -> R.drawable.rating_star_5
else -> {
R.drawable.rating_star_0
}
}
val layers = if (starred) {
arrayOf(
ResourcesCompat.getDrawable(view.resources, ratingDrawable, null)!!,
ResourcesCompat.getDrawable(
view.resources,
R.drawable.rating_heart_mini_overlay,
null
)!!
)
} else {
arrayOf(
ResourcesCompat.getDrawable(view.resources, ratingDrawable, null)!!
)
}
val ratingDisplay = LayerDrawable(layers)
ratingDisplay.getDrawable(0).setTint(
view.context.themeColor(com.google.android.material.R.attr.colorOnBackground)
)
if (starred) {
ratingDisplay.getDrawable(1).setTint(
view.context.themeColor(com.google.android.material.R.attr.colorTertiary)
)
}
star.setImageDrawable(ratingDisplay)
}
private fun updateStatus(status: DownloadState, progress: Int?) {
@ -262,7 +287,8 @@ class TrackViewHolder(val view: View) :
showProgress()
}
DownloadState.RETRYING,
DownloadState.QUEUED -> {
DownloadState.QUEUED
-> {
showIndefiniteProgress()
}
else -> {

@ -114,10 +114,8 @@ private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder {
detectLeakedClosableObjects()
detectLeakedRegistrationObjects()
detectFileUriExposure()
detectContentUriWithoutPermission()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
detectContentUriWithoutPermission()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
detectCredentialProtectedWhileLocked()
}

@ -50,7 +50,8 @@ class EqualizerController : CoroutineScope by CoroutineScope(Dispatchers.IO) {
launch {
try {
val settings = deserialize<EqualizerSettings>(
UApp.applicationContext(), "equalizer.dat"
UApp.applicationContext(),
"equalizer.dat"
)
settings?.apply(equalizer!!)
} catch (all: Throwable) {

@ -7,8 +7,6 @@
package org.moire.ultrasonic.data
import android.os.Handler
import android.os.Looper
import androidx.room.Room
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -53,7 +51,8 @@ class ActiveServerProvider(
}
Timber.d(
"getActiveServer retrieved from DataBase, id: %s cachedServer: %s",
serverId, cachedServer
serverId,
cachedServer
)
}
@ -106,6 +105,30 @@ class ActiveServerProvider(
}
}
/**
* Sets the Active Server by its unique id
* @param serverId: The id of the desired server
*/
fun setActiveServerById(serverId: Int) {
val oldServerId = Settings.activeServer
if (oldServerId == serverId) return
// Notify components about the change before actually resetting the MusicService
// so they can react by e.g. stopping playback on the old server
RxBus.activeServerChangingPublisher.onNext(oldServerId)
// Use a coroutine to post the server change to the end of the message queue
launch {
withContext(Dispatchers.Main) {
resetMusicService()
Settings.activeServer = serverId
RxBus.activeServerChangedPublisher.onNext(getActiveServer(serverId))
Timber.i("setActiveServerById done, new id: %s", serverId)
}
}
}
@Synchronized
fun getActiveMetaDatabase(): MetaDatabase {
val activeServer = getActiveServerId()
@ -136,7 +159,7 @@ class ActiveServerProvider(
METADATA_DB + serverId
)
.addMigrations(META_MIGRATION_2_3)
.fallbackToDestructiveMigrationOnDowngrade()
.fallbackToDestructiveMigrationOnDowngrade(true)
.build()
}
@ -234,29 +257,6 @@ class ActiveServerProvider(
return Settings.activeServer
}
/**
* Sets the Active Server by its unique id
* @param serverId: The id of the desired server
*/
fun setActiveServerById(serverId: Int) {
val oldServerId = Settings.activeServer
if (oldServerId == serverId) return
// Notify components about the change before actually resetting the MusicService
// so they can react by e.g. stopping playback on the old server
RxBus.activeServerChangingPublisher.onNext(oldServerId)
// Post the server change to the end of the message queue,
// so the cleanup have time to finish
Handler(Looper.getMainLooper()).post {
resetMusicService()
Settings.activeServer = serverId
RxBus.activeServerChangedPublisher.onNext(serverId)
Timber.i("setActiveServerById done, new id: %s", serverId)
}
}
/**
* Queries if Scrobbling is enabled
*/

@ -1,3 +1,12 @@
/*
* AppDatabase.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@file:Suppress("ktlint:standard:max-line-length")
package org.moire.ultrasonic.data
import androidx.room.Database
@ -31,8 +40,8 @@ val MIGRATION_1_2: Migration = object : Migration(1, 2) {
}
val MIGRATION_2_1: Migration = object : Migration(2, 1) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
id INTEGER NOT NULL PRIMARY KEY,
@ -48,7 +57,7 @@ val MIGRATION_2_1: Migration = object : Migration(2, 1) {
)
""".trimIndent()
)
database.execSQL(
db.execSQL(
"""
INSERT INTO ServerSettingMigration (
id, [index], name, url, userName, password, jukeboxByDefault,
@ -60,10 +69,10 @@ val MIGRATION_2_1: Migration = object : Migration(2, 1) {
FROM ServerSetting
""".trimIndent()
)
database.execSQL(
db.execSQL(
"DROP TABLE ServerSetting"
)
database.execSQL(
db.execSQL(
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
)
}
@ -87,8 +96,8 @@ val MIGRATION_2_3: Migration = object : Migration(2, 3) {
}
val MIGRATION_3_2: Migration = object : Migration(3, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
id INTEGER NOT NULL PRIMARY KEY,
@ -105,7 +114,7 @@ val MIGRATION_3_2: Migration = object : Migration(3, 2) {
)
""".trimIndent()
)
database.execSQL(
db.execSQL(
"""
INSERT INTO ServerSettingMigration (
id, [index], name, url, userName, password, jukeboxByDefault,
@ -117,26 +126,26 @@ val MIGRATION_3_2: Migration = object : Migration(3, 2) {
FROM ServerSetting
""".trimIndent()
)
database.execSQL(
db.execSQL(
"DROP TABLE ServerSetting"
)
database.execSQL(
db.execSQL(
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
)
}
}
val MIGRATION_3_4: Migration = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE ServerSetting ADD COLUMN color INTEGER"
)
}
}
val MIGRATION_4_3: Migration = object : Migration(4, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
id INTEGER NOT NULL PRIMARY KEY,
@ -157,7 +166,7 @@ val MIGRATION_4_3: Migration = object : Migration(4, 3) {
)
""".trimIndent()
)
database.execSQL(
db.execSQL(
"""
INSERT INTO ServerSettingMigration (
id, [index], name, url, userName, password, jukeboxByDefault,
@ -171,18 +180,18 @@ val MIGRATION_4_3: Migration = object : Migration(4, 3) {
FROM ServerSetting
""".trimIndent()
)
database.execSQL(
db.execSQL(
"DROP TABLE ServerSetting"
)
database.execSQL(
db.execSQL(
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
)
}
}
val MIGRATION_4_5: Migration = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
@ -204,7 +213,7 @@ val MIGRATION_4_5: Migration = object : Migration(4, 5) {
)
""".trimIndent()
)
database.execSQL(
db.execSQL(
"""
INSERT INTO `_new_ServerSetting` (
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
@ -218,14 +227,14 @@ val MIGRATION_4_5: Migration = object : Migration(4, 5) {
FROM `ServerSetting`
""".trimIndent()
)
database.execSQL("DROP TABLE `ServerSetting`")
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
db.execSQL("DROP TABLE `ServerSetting`")
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
}
}
val MIGRATION_5_4: Migration = object : Migration(5, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
`id` INTEGER PRIMARY KEY NOT NULL,
@ -247,7 +256,7 @@ val MIGRATION_5_4: Migration = object : Migration(5, 4) {
)
""".trimIndent()
)
database.execSQL(
db.execSQL(
"""
INSERT INTO `_new_ServerSetting` (
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
@ -261,25 +270,31 @@ val MIGRATION_5_4: Migration = object : Migration(5, 4) {
FROM `ServerSetting`
""".trimIndent()
)
database.execSQL("DROP TABLE `ServerSetting`")
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
db.execSQL("DROP TABLE `ServerSetting`")
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
}
}
/* ktlint-disable max-line-length */
val MIGRATION_5_6: Migration = object : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `forcePlainTextPassword` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER, `jukeboxSupport` INTEGER, `videoSupport` INTEGER)")
database.execSQL("INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`")
database.execSQL("DROP TABLE `ServerSetting`")
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `forcePlainTextPassword` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER, `jukeboxSupport` INTEGER, `videoSupport` INTEGER)"
)
db.execSQL(
"INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`"
)
db.execSQL("DROP TABLE `ServerSetting`")
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
}
}
val MIGRATION_6_5: Migration = object : Migration(6, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `ldapSupport` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER)")
database.execSQL("INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`")
database.execSQL("DROP TABLE `ServerSetting`")
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `ldapSupport` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER)"
)
db.execSQL(
"INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`"
)
db.execSQL("DROP TABLE `ServerSetting`")
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
}
}
/* ktlint-enable max-line-length */

@ -1,3 +1,10 @@
/*
* BasicDaos.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.data
import androidx.room.Dao
@ -91,7 +98,7 @@ interface GenericDao<T> {
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
@JvmSuppressWildcards
fun insertIgnoring(obj: List<T>?): List<Long>
fun insertIgnoring(obj: List<T>): List<Long>
/**
* Update an object from the database.
@ -109,7 +116,7 @@ interface GenericDao<T> {
*/
@Update
@JvmSuppressWildcards
fun update(obj: List<T>?)
fun update(obj: List<T>)
/**
* Delete an object from the database

@ -1,11 +1,11 @@
/*
* CachedDataSource.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.playback
package org.moire.ultrasonic.data
import android.net.Uri
import androidx.core.net.toUri
@ -39,9 +39,7 @@ class CachedDataSource(
)
}
private fun createDataSourceInternal(
upstreamDataSource: DataSource
): CachedDataSource {
private fun createDataSourceInternal(upstreamDataSource: DataSource): CachedDataSource {
return CachedDataSource(
upstreamDataSource
)
@ -93,7 +91,9 @@ class CachedDataSource(
readInternal(buffer, offset, length)
} catch (e: IOException) {
throw HttpDataSourceException.createForIOException(
e, Util.castNonNull(dataSpec), HttpDataSourceException.TYPE_READ
e,
Util.castNonNull(dataSpec),
HttpDataSourceException.TYPE_READ
)
}
} else {

@ -1,10 +1,12 @@
/*
* MetaDatabase.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@file:Suppress("ktlint:standard:max-line-length")
package org.moire.ultrasonic.data
import androidx.room.AutoMigration
@ -37,7 +39,7 @@ import org.moire.ultrasonic.domain.Track
AutoMigration(
from = 1,
to = 2
),
)
],
exportSchema = true,
version = 3
@ -67,19 +69,27 @@ class Converters {
}
}
/* ktlint-disable max-line-length */
val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE `albums`")
database.execSQL("DROP TABLE `indexes`")
database.execSQL("DROP TABLE `artists`")
database.execSQL("DROP TABLE `tracks`")
database.execSQL("DROP TABLE `music_folders`")
database.execSQL("CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("DROP TABLE `albums`")
db.execSQL("DROP TABLE `indexes`")
db.execSQL("DROP TABLE `artists`")
db.execSQL("DROP TABLE `tracks`")
db.execSQL("DROP TABLE `music_folders`")
db.execSQL(
"CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))"
)
}
}
/* ktlint-enable max-line-length */

@ -2,7 +2,7 @@ package org.moire.ultrasonic.di
import androidx.room.Room
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.module.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.moire.ultrasonic.data.AppDatabase

@ -1,14 +1,15 @@
package org.moire.ultrasonic.di
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.CacheCleaner
/**
* This Koin module contains the registration of general classes needed for Ultrasonic
*/
val applicationModule = module {
single { ActiveServerProvider(get()) }
single { ImageLoaderProvider(androidContext()) }
single { ImageLoaderProvider() }
single { CacheCleaner() }
}

@ -5,15 +5,21 @@ import org.moire.ultrasonic.service.ExternalStorageMonitor
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.PlaybackStateSerializer
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
/**
* This Koin module contains the registration of classes related to the media player
*/
val mediaPlayerModule = module {
single { MediaPlayerLifecycleSupport() }
// These are dependency-free
single { PlaybackStateSerializer() }
single { ExternalStorageMonitor() }
single { NetworkAndStorageChecker() }
single { ShareHandler() }
// TODO Ideally this can be cleaned up when all circular references are removed.
single { MediaPlayerManager(get(), get(), get()) }
// These MUST be singletons, for the media playback must work headless (without an activity)
single { MediaPlayerManager(get(), get()) }
single { MediaPlayerLifecycleSupport(get(), get(), get(), get()) }
}

@ -1,9 +1,9 @@
@file:JvmName("MusicServiceModule")
package org.moire.ultrasonic.di
import kotlin.math.abs
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.moire.ultrasonic.BuildConfig
@ -16,9 +16,6 @@ import org.moire.ultrasonic.service.CachedMusicService
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.OfflineMusicService
import org.moire.ultrasonic.service.RESTMusicService
import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.util.Constants
/**
@ -68,8 +65,4 @@ val musicServiceModule = module {
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
OfflineMusicService()
}
single { DownloadHandler(get(), get()) }
single { NetworkAndStorageChecker(androidContext()) }
single { ShareHandler(androidContext()) }
}

@ -8,6 +8,7 @@
// Converts Album entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIAlbumConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Album

@ -8,6 +8,7 @@
// Converts Artist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIArtistConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist

@ -1,5 +1,6 @@
// Contains helper functions to convert from api ChatMessage entity to domain entity
@file:JvmName("APIChatMessageConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.ChatMessage as ApiChatMessage

@ -1,5 +1,6 @@
// Collection of functions to convert api Genre entity to domain entity
@file:JvmName("ApiGenreConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Genre as APIGenre

@ -1,5 +1,6 @@
// Collection of function to convert subsonic api jukebox responses to app entities
@file:JvmName("APIJukeboxConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.JukeboxStatus as ApiJukeboxStatus

@ -1,6 +1,7 @@
// Converts Lyrics entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APILyricsConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Lyrics as APILyrics

@ -6,6 +6,7 @@
*/
@file:JvmName("APIMusicDirectoryConverter")
package org.moire.ultrasonic.domain
import java.text.DateFormat
@ -35,10 +36,7 @@ fun MusicDirectoryChild.toAlbumEntity(serverId: Int): Album = Album(id, serverId
populateCommonProps(this, this@toAlbumEntity)
}
private fun populateCommonProps(
entry: MusicDirectory.Child,
source: MusicDirectoryChild
) {
private fun populateCommonProps(entry: MusicDirectory.Child, source: MusicDirectoryChild) {
entry.parent = source.parent
entry.isDirectory = source.isDir
entry.title = source.title
@ -63,10 +61,7 @@ private fun populateCommonProps(
}
}
private fun populateTrackProps(
track: Track,
source: MusicDirectoryChild
) {
private fun populateTrackProps(track: Track, source: MusicDirectoryChild) {
track.size = source.size
track.contentType = source.contentType
track.suffix = source.suffix
@ -84,10 +79,11 @@ fun List<MusicDirectoryChild>.toDomainEntityList(serverId: Int): List<MusicDirec
val newList: MutableList<MusicDirectory.Child> = mutableListOf()
forEach {
if (it.isDir)
if (it.isDir) {
newList.add(it.toAlbumEntity(serverId))
else
} else {
newList.add(it.toTrackEntity(serverId))
}
}
return newList

@ -8,6 +8,7 @@
// Converts MusicFolder entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIMusicFolderConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
@ -18,9 +19,8 @@ fun APIMusicFolder.toDomainEntity(serverId: Int): MusicFolder = MusicFolder(
name = this.name
)
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> =
this.map {
val item = it.toDomainEntity(serverId)
item.serverId = serverId
item
}
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> = this.map {
val item = it.toDomainEntity(serverId)
item.serverId = serverId
item
}

@ -31,8 +31,11 @@ fun APIPlaylist.toMusicDirectoryDomainEntity(serverId: Int): MusicDirectory =
}
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
this.id, this.name, this.owner,
this.comment, this.songCount.toString(),
this.id,
this.name,
this.owner,
this.comment,
this.songCount.toString(),
this.created.ifNotNull { playlistDateFormat.format(it.time) } ?: "",
public
)

@ -1,12 +1,17 @@
// Converts podcasts entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIPodcastConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.PodcastChannel
fun PodcastChannel.toDomainEntity(): PodcastsChannel = PodcastsChannel(
this.id, this.title, this.url, this.description, this.status
this.id,
this.title,
this.url,
this.description,
this.status
)
fun List<PodcastChannel>.toDomainEntitiesList(): List<PodcastsChannel> = this

@ -8,6 +8,7 @@
// Converts SearchResult entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APISearchConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult
@ -15,7 +16,8 @@ import org.moire.ultrasonic.api.subsonic.models.SearchThreeResult
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
fun APISearchResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
emptyList(), emptyList(),
emptyList(),
emptyList(),
this.matchList.map { it.toTrackEntity(serverId) }
)

@ -7,6 +7,7 @@
// Contains helper method to convert subsonic api share to domain model
@file:JvmName("APIShareConverter")
package org.moire.ultrasonic.domain
import java.text.SimpleDateFormat

@ -1,5 +1,6 @@
// Helper functions to convert User entity to domain entity
@file:JvmName("APIUserConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.User

@ -1,6 +1,6 @@
/*
* AboutFragment.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -8,17 +8,16 @@
package org.moire.ultrasonic.fragment
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import java.util.Locale
import org.moire.ultrasonic.R
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.util.Util.applyTheme
import org.moire.ultrasonic.util.Util.getVersionName
@ -56,18 +55,18 @@ class AboutFragment : Fragment() {
versionName
)
setTitle(this@AboutFragment, getString(R.string.menu_about))
FragmentTitle.setTitle(this@AboutFragment, getString(R.string.menu_about))
titleText?.text = title
webPageButton?.setOnClickListener {
startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.about_webpage_url)))
Intent(Intent.ACTION_VIEW, getString(R.string.about_webpage_url).toUri())
)
}
reportBugButton?.setOnClickListener {
startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.about_report_url)))
Intent(Intent.ACTION_VIEW, getString(R.string.about_report_url).toUri())
)
}
}

@ -1,12 +1,10 @@
/*
* AlbumListFragment.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@file:Suppress("NAME_SHADOWING")
package org.moire.ultrasonic.fragment
import android.os.Bundle
@ -32,6 +30,7 @@ import org.moire.ultrasonic.domain.Album
import org.moire.ultrasonic.model.AlbumListModel
import org.moire.ultrasonic.util.LayoutType
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.toastingExceptionHandler
import org.moire.ultrasonic.view.FilterButtonBar
import org.moire.ultrasonic.view.SortOrder
import org.moire.ultrasonic.view.ViewCapabilities
@ -66,19 +65,17 @@ class AlbumListFragment(
/**
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(
refresh: Boolean,
append: Boolean
): LiveData<List<Album>> {
override fun getLiveData(refresh: Boolean, append: Boolean): LiveData<List<Album>> {
fetchAlbums(refresh)
return listModel.list
}
private fun fetchAlbums(refresh: Boolean = navArgs.refresh, append: Boolean = navArgs.append) {
listModel.viewModelScope.launch(handler) {
refreshListView?.isRefreshing = true
listModel.viewModelScope.launch(
toastingExceptionHandler()
) {
swipeRefresh?.isRefreshing = true
if (navArgs.byArtist) {
listModel.getAlbumsOfArtist(
@ -95,7 +92,7 @@ class AlbumListFragment(
refresh = refresh or append
)
}
refreshListView?.isRefreshing = false
swipeRefresh?.isRefreshing = false
}
}
@ -185,8 +182,8 @@ class AlbumListFragment(
super.onViewCreated(view, savedInstanceState)
// Setup refresh handler
refreshListView = view.findViewById(refreshListId)
refreshListView?.setOnRefreshListener {
swipeRefresh = view.findViewById(refreshListId)
swipeRefresh?.setOnRefreshListener {
fetchAlbums(refresh = true)
}

@ -17,7 +17,6 @@ import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.model.ArtistListModel
@ -43,7 +42,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(refresh: Boolean, append: Boolean): LiveData<List<ArtistOrIndex>> {
return listModel.getItems(navArgs.refresh || refresh, refreshListView!!)
return listModel.getItems(navArgs.refresh || refresh, swipeRefresh!!)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -70,7 +69,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
id = item.id,
name = item.name,
parentId = item.id,
isArtist = (item is Artist)
isArtist = false
)
} else {
NavigationGraphDirections.toAlbumList(

Some files were not shown because too many files have changed in this diff Show More