Compare commits

...

229 Commits

Author SHA1 Message Date
Óscar García Amor 90205fe0fb
Update README.md 2022-07-08 18:49:07 +02:00
Óscar García Amor 2165ce75b3
Merge branch 'develop' of github.com:ultrasonic/ultrasonic into develop 2022-07-08 12:42:04 +02:00
Óscar García Amor 8b3ee0a8d6
Migrate to GitLab 2022-07-08 12:41:53 +02:00
birdbird 695b2df63f
Merge pull request #750 from ultrasonic/offline/trackDao
Finish offline ID3 support
2022-07-08 09:20:40 +02:00
tzugen 798d795e81
Add Album list support in Offline 2022-07-07 19:20:40 +02:00
tzugen ecfce59e0f
Add clearer warning to ID3 offline setting 2022-07-07 18:53:36 +02:00
tzugen de0cb7713b
Improve offline support for Compilations 2022-07-06 15:35:34 +02:00
tzugen 78bfab3753
Conditionally hide offline Id3 Setting 2022-07-06 12:26:48 +02:00
tzugen b955d77152
Make Id3 offline dependent on Id3 2022-07-06 11:16:53 +02:00
tzugen b11694d6a2
Fix logic whether to showArtistPicture 2022-07-06 11:16:52 +02:00
tzugen 31a1fdace1
Formatting 2022-07-06 11:16:52 +02:00
tzugen 5b03b632fd
Fix three tests. 2022-07-06 11:16:52 +02:00
tzugen 152b1d261a
Fix two tests. 2022-07-06 11:16:52 +02:00
tzugen 53a1a5545a
Formatting.
Disable line length check in detekt. It's being guarded by KtLint already.
2022-07-06 11:16:52 +02:00
tzugen ad54db5bcb
Make Ids composite of Item Id + Server Id 2022-07-06 11:16:52 +02:00
tzugen 177329abcf
Add Migration 2022-07-06 11:16:52 +02:00
tzugen 241e51015f
Clean & formatting
Update room 2.4.0 -> 2.4.2
2022-07-06 11:16:52 +02:00
tzugen 60dbe70ca5
Add code to Downloader 2022-07-06 11:16:52 +02:00
tzugen 8490f7115d
Add Offline support for tracks 2022-07-06 11:16:52 +02:00
tzugen ee67f4c744
Add track Dao 2022-07-06 11:16:52 +02:00
tzugen 3a3bd10fdb
Add AlbumDao, rename getArtist to getAlbumsOfArtist 2022-07-06 11:16:52 +02:00
birdbird 3445576dc9
Merge pull request #775 from ultrasonic/seekBar
Add more values to preload count
2022-07-06 10:46:56 +02:00
tzugen 8c40f662a1
Add more values to preload count 2022-07-06 08:49:29 +02:00
birdbird 6c6227ce41
Merge pull request #770 from Maxmystere/notification/add-rating
Add song rating to notification
2022-07-05 19:31:18 +02:00
tzugen 240a2fa8f6
Fix a bug that the musicService was incorrectly cached... 2022-07-05 19:21:07 +02:00
tzugen 7de775dc26
Formatting 2022-07-05 18:38:26 +02:00
birdbird d034fc9c71
Update AutoMediaBrowserCallback.kt 2022-07-05 18:34:24 +02:00
birdbird 05ada9297d
Update MediaNotificationProvider.kt 2022-07-05 18:18:02 +02:00
Maxence G aa6c037b20
Remove useless request 2022-07-04 18:55:07 +02:00
Maxence G b8c924be27
MainThreadExecutor to Kotlin 2022-07-04 18:42:14 +02:00
Maxence G 0929a6a1bd
Lint 2022-07-03 18:40:58 +02:00
Maxence G fefee74a66
Use MainThreadExecutor 2022-07-03 18:38:29 +02:00
Maxence G 37e3ce09c1
Update notification on Rating change 2022-07-03 18:23:22 +02:00
Maxence G 16b3fcad32
Final lint 2022-07-02 21:58:45 +02:00
Maxence G d6aebd9989
Lint 2022-07-02 21:51:28 +02:00
Maxence G 3f408600cb
Add const for custom SessionCommands
Improve rating call
2022-07-02 21:47:32 +02:00
Maxence G 9014b47b74
Add song rating to notification 2022-07-02 01:27:12 +02:00
tzugen ac489ae8b9
Merge pull request #764 from ultrasonic/hmpr
Merge update build tools #755 by Holger Müller
2022-06-22 13:40:15 +02:00
tzugen e7f8fa21cb
Merge update build tools #755 by Holger Müller
Squashed commit of the following:

commit 4491c65b1b
Merge: 51ff716f 77865a14
Author: tzugen <67737443+tzugen@users.noreply.github.com>
Date:   Tue Jun 21 20:50:05 2022 +0200

    Merge branch 'develop' into gradle-update

commit 51ff716ff5
Author: Holger Müller <github@euhm.de>
Date:   Tue Jun 21 20:38:52 2022 +0200

    fixed lint warning

commit 18c31a5704
Author: Holger Müller <github@euhm.de>
Date:   Tue Jun 21 20:38:35 2022 +0200

    fixed lint warning

commit 603654c262
Author: Holger Müller <github@euhm.de>
Date:   Tue Jun 21 20:37:51 2022 +0200

    API is > lollipop ... target removed

commit b38a7211de
Author: Holger Müller <github@euhm.de>
Date:   Tue Jun 21 20:37:07 2022 +0200

    new created after fixes

commit 4929a526f7
Author: tzugen <tzugen@riseup.net>
Date:   Tue Jun 21 10:43:16 2022 +0200

    Disable ObsoleteLintCustomCheck

commit d0c30f0b6b
Author: tzugen <tzugen@riseup.net>
Date:   Tue Jun 21 10:14:06 2022 +0200

    Update more libs

commit e2fa447bbf
Merge: d4ead495 ff9c7b24
Author: tzugen <67737443+tzugen@users.noreply.github.com>
Date:   Tue Jun 21 09:47:03 2022 +0200

    Merge branch 'develop' into gradle-update

commit d4ead49548
Merge: 2dac6a7e 9a73d72f
Author: Holger Müller <github@euhm.de>
Date:   Tue Jun 21 08:50:42 2022 +0200

    merged with develop branch

commit 2dac6a7e01
Author: Holger Müller <github@euhm.de>
Date:   Mon Jun 20 21:45:22 2022 +0200

    update to android image tag 2022.06.1

commit f3dc259c39
Author: Holger Müller <github@euhm.de>
Date:   Mon Jun 20 20:56:37 2022 +0200

    rebuild lint-baseline.xml

commit c71bc1212a
Author: Holger Müller <github@euhm.de>
Date:   Mon Jun 20 20:55:00 2022 +0200

    removed unneeded cast

commit eca136dabe
Author: Holger Müller <github@euhm.de>
Date:   Fri Jun 17 23:58:37 2022 +0200

    commit signed

commit 540f476334
Author: Holger Müller <github@euhm.de>
Date:   Fri Jun 17 23:40:59 2022 +0200

    commit signed

    Signed-off-by: Holger Müller <github@euhm.de>

commit 986bd013a4
Author: Holger Müller <github@euhm.de>
Date:   Fri Jun 17 23:27:20 2022 +0200

    push to latest gradle version, set targetSdk to 33

Signed-off-by: tzugen <tzugen@riseup.net>
2022-06-22 13:35:19 +02:00
tzugen b1c3cabfef
Update README.md (#766) 2022-06-21 22:23:10 +02:00
tzugen 77865a143d
Enable schema export (needed for AutoMigration generator) (#757)
* Enable schema export (needed for AutoMigration generator)
* Add Schema folder to GIT
2022-06-21 10:07:03 +02:00
Óscar García Amor ff9c7b2435
Merge pull request #761 from ultrasonic/fix_#759
Fix #759 , a crash when the artist name was empty.
2022-06-21 08:52:54 +02:00
Óscar García Amor 737563bf6b
Merge branch 'develop' into fix_#759 2022-06-21 08:24:52 +02:00
tzugen 9a73d72fa4
Merge pull request #762 from ultrasonic/tzugen-patch-1
Update CONTRIBUTING.md
2022-06-21 06:38:35 +02:00
tzugen 98ce519014
Merge branch 'develop' into tzugen-patch-1 2022-06-20 23:05:21 +02:00
tzugen 83fc54d332
Merge pull request #758 from Maxmystere/media3-beta01
Upgrade to media3-beta01
2022-06-20 23:04:02 +02:00
Maxence G a2b9c6b9a3
Final final version good v3.2 2022-06-20 19:54:26 +02:00
Maxence G 5ae56d26c5
Merge branch 'media3-beta01' of github.com:Maxmystere/ultrasonic into media3-beta01 2022-06-20 19:50:15 +02:00
Maxence G 4efb6dcb58
Up lint 2022-06-20 19:49:17 +02:00
tzugen 8a90e98989
Update CONTRIBUTING.md 2022-06-20 19:35:46 +02:00
tzugen 46a8f4640d
Fix #759 , a crash when the artist name was empty. 2022-06-20 10:12:45 +02:00
tzugen ab41966943
Readd comment 2022-06-20 09:49:49 +02:00
tzugen 00d7ce326c
Rm comment 2022-06-20 09:48:28 +02:00
Maxence G bc4b0aa832
final fix checkstyle v2 2022-06-19 23:51:18 +02:00
Maxence G 23fd336ffd
Fix lint 2022-06-19 23:46:01 +02:00
Maxence G b57a973510
Fix static analysis 2022-06-19 23:29:07 +02:00
Maxence G 8796006ced
Fix checkstyle 2022-06-19 23:24:23 +02:00
Maxence G 545b65921e
Put Previous/Play/Next in compact notification 2022-06-19 19:43:55 +02:00
Maxence G cf367ead92
Add back notification icon 2022-06-19 18:40:04 +02:00
Maxence G 9961213f09
Upgrade to media3-beta01 2022-06-19 18:21:33 +02:00
tzugen 5deb7d4d58
Merge pull request #749 from ultrasonic/ready/removeJacoco
Remove Jacoco, closes #751
2022-06-18 22:37:12 +02:00
tzugen 5f31eaaffe
Merge branch 'develop' into ready/removeJacoco 2022-06-18 15:36:46 +02:00
tzugen cad6477cd9
Merge pull request #754 from hmueller01/target-sdk-31
Target sdk 31 into develop
2022-06-17 18:14:22 +02:00
tzugen b440821ea8
Tweak backup rules 2022-06-17 13:36:41 +02:00
Holger Müller 8663b9d50e
Target SDK 31 2022-06-17 13:27:30 +02:00
Óscar García Amor 2bae243be0
Merge branch 'develop' into ready/removeJacoco 2022-06-17 10:29:46 +02:00
Óscar García Amor 139e810186
Include beta tags on builds 2022-06-17 09:46:37 +02:00
tzugen 66443ba018
Remove Jacoco 2022-06-16 16:40:59 +02:00
tzugen f8b78a47d2
Merge pull request #748 from ultrasonic/tzugen-patch-1
Typo
2022-06-16 16:31:04 +02:00
tzugen 4cda114f4c
Typo 2022-06-16 16:22:37 +02:00
tzugen d8b5b774ee
Merge pull request #726 from ultrasonic/media3-flat
Implement Media3
2022-06-16 13:47:36 +02:00
tzugen b6730f5a93
Reset widget on manual exit as well. 2022-06-08 10:02:04 +02:00
tzugen 87c160610f
Clear widget when service is being shutdown 2022-06-08 09:17:14 +02:00
tzugen 70f8b75019
Completely stop and release the player on dismissing the app. 2022-06-07 16:18:26 +02:00
tzugen 147d7cd46e
Fix reappearing notification 2022-06-07 16:00:51 +02:00
tzugen 59e37e62a6
Call stopForeground(yes) 2022-06-07 13:30:33 +02:00
tzugen 1e571e165c
Don't ecit the process, and reinit Koin on resume 2022-06-06 22:12:16 +02:00
tzugen 5e0dd14c4f
Shutdown service on exit button press 2022-05-31 20:53:58 +02:00
tzugen 53ae0cd232
Update copyright 2022-05-31 10:18:23 +02:00
tzugen 608f86ac5f
Always call prepare before starting playback,
otherwise resuming playback is difficult when the session was dismissed...
2022-05-30 16:24:24 +02:00
tzugen 669b51c0d2
Merge remote-tracking branch 'origin/develop' into media3-flat 2022-05-30 16:11:59 +02:00
tzugen 6e1478d896
Pause playback when swiping away... 2022-05-30 16:10:54 +02:00
Nite d9e4b8b3d3
Merge pull request #737 from i-do-cpp/develop
Support HTTP basic authentication
2022-05-17 17:49:07 +02:00
Nite f790e29add
Fixed AndroidManifest to receive Bluetooth events
Set parallel downloads to 2 so the third thread can stream for playback
2022-05-10 18:15:37 +02:00
Nite faf07f2887
Fixed not closed connection in CachedDataSource 2022-05-06 18:51:12 +02:00
i-do-cpp 057644f592
Support HTTP basic authentication 2022-05-05 20:41:04 +02:00
tzugen 926081f84c
Add exported attributes, fix lint 2022-05-04 11:59:48 +02:00
tzugen 4a00494647
Readd Headphone plug and Bluetooth listeners 2022-05-04 11:52:45 +02:00
Nite 34e0178db3
Fixed Lint errors 2022-05-02 20:47:38 +02:00
Nite cbe3992b01
Fixed Notification Ids to be different for Downloader and Player
Fixed multiple start of periodic CheckDownloads
2022-05-02 20:46:01 +02:00
Nite 46846bd5c9
Incremented max playlist size
Fixed DownloadService start
Minor fixes
2022-04-24 08:44:36 +02:00
tzugen 707339b88b
Try to fix the mess :) 2022-04-22 21:03:57 +02:00
tzugen 827654c0c1
Create lock before launching the coroutine,
return always a copy of the list and not the real one.
2022-04-21 20:24:43 +02:00
tzugen 1d236aa6e3
RM unused PlayerState.kt 2022-04-21 14:52:11 +02:00
tzugen 9cdba9a27a
Ensure main thread usage in Rx subscription. 2022-04-21 14:42:24 +02:00
tzugen 7ba599f58c
Remove throttle again, causes calls from wrong thread... 2022-04-21 14:39:16 +02:00
tzugen 2e1e627b7a
- Set Wake mode flag according to Offline status
- Use Rx to trigger CheckDownloads()
- Fix #680 by listening to PositionDiscontinuity
- Throttle RxEvents
2022-04-21 10:45:37 +02:00
tzugen d550eabf88
Always call prepare on adding items 2022-04-21 10:12:05 +02:00
tzugen dda86b42c7
Fix prefs 2022-04-20 22:54:59 +02:00
tzugen b6e890b26c
Cleanup 2022-04-20 22:07:50 +02:00
tzugen c2ac1d436f
Use previous values for channel id 2022-04-20 21:56:30 +02:00
tzugen 2aaa3c2119
Fix de(serilization) 2022-04-20 21:40:22 +02:00
tzugen 5d4aff1f21
Cleanup restore 2022-04-20 20:58:10 +02:00
tzugen 6115ac995f
Remove legacyPlayerState 2022-04-20 20:58:10 +02:00
tzugen 647435fe55
launch restore on Main Thread 2022-04-20 20:57:51 +02:00
tzugen 81d24f6cbb
Cleanup MediaPlayerLifecycleSupport.kt 2022-04-20 17:06:30 +02:00
tzugen 69c78f4c37
Enable StrictMode logging, rm unused prefs 2022-04-20 13:15:22 +02:00
tzugen 3691428a68
Squashed commit of the following:
commit 7a8f20ade7
Author: Holger Müller <github@euhm.de>
Date:   Fri Apr 8 23:21:15 2022 +0200

    make build working with target sdk 31

commit 5890c32b7e
Author: Holger Müller <github@euhm.de>
Date:   Fri Apr 8 22:57:23 2022 +0200

    make build working with target sdk 31

commit 1c7c4839b3
Merge: 48b0cdea 92ef78a3
Author: Holger Müller <github@euhm.de>
Date:   Fri Apr 8 21:55:14 2022 +0200

    Merge remote-tracking branch 'upstream/media3-flat' into media3-flat

commit 48b0cdea83
Merge: 9101980c 3ca25ed1
Author: Holger Müller <github@euhm.de>
Date:   Fri Apr 8 19:58:55 2022 +0200

    Merge remote-tracking branch 'upstream/media3-flat' into media3-flat

commit 9101980cb6
Merge: 515690ab 97798446
Author: Holger Müller <github@euhm.de>
Date:   Fri Apr 8 17:07:54 2022 +0200

    Merge remote-tracking branch 'upstream/media3-flat' into media3-flat

commit 515690abac
Author: Holger Müller <github@euhm.de>
Date:   Thu Apr 7 08:55:42 2022 +0200

    made button off mode better visible
2022-04-17 06:58:39 +02:00
tzugen 788538ee6a
Squashed commit of the following:
commit c7d9af4529
Author: Holger Müller <github@euhm.de>
Date:   Sat Apr 16 17:56:41 2022 +0200

    fixed CI failure

commit 59e18bc5c0
Author: Holger Müller <github@euhm.de>
Date:   Sat Apr 16 17:42:49 2022 +0200

    removed unused dataSourceFactory, commented debug output
2022-04-17 06:55:24 +02:00
tzugen 762aeec5d3
Cleanup 2022-04-16 21:20:10 +02:00
tzugen a3a0c7f41d
Minor 2022-04-16 11:23:05 +02:00
tzugen 1564379bd1
Fix a bunch of problems with the DataSources 2022-04-16 00:04:52 +02:00
tzugen 7d33770fd6
Add some more logging calls 2022-04-12 17:12:17 +02:00
tzugen 728afad00c
Merge remote-tracking branch 'origin/develop' into media3-flat 2022-04-12 17:04:35 +02:00
Óscar García Amor f121e297df
Fix publish github signed apk CI step (again) 2022-04-11 11:58:57 +02:00
Óscar García Amor 3f2cfb131a
Fix publish github signed apk CI step 2022-04-11 11:40:01 +02:00
Óscar García Amor b8b4b81726
Fix sign release apk CI step 2022-04-11 11:27:02 +02:00
Óscar García Amor 383089a409
Improve CI pipeline 2022-04-11 11:07:29 +02:00
Óscar García Amor 8d8a5f05ea
Bump version to 3.2.0 2022-04-11 10:12:52 +02:00
Óscar García Amor 46a2e5d67b
Update translations 2022-04-11 09:50:17 +02:00
tzugen 92ef78a36a
Move alpha value to const val. 2022-04-08 21:28:14 +02:00
tzugen e5021959c3
Fix a small bug in togglePlayPause() 2022-04-08 21:24:04 +02:00
tzugen 3ca25ed1c6
Rework ActiveServer handling.
Remove blocking call on setting the server.
Implement offline server display more cleanly.
Reconfigure the SourceFactory when the active server has changed
2022-04-08 18:08:56 +02:00
tzugen 6da83db9df
Set memory to 3.2g 2022-04-08 18:07:13 +02:00
tzugen 9779844620
Solve more simple... 2022-04-07 15:29:24 +02:00
tzugen f936ad690c
Remove GC logging 2022-04-07 15:27:52 +02:00
tzugen 5230ce011d
Make full body logging an optional debug setting. 2022-04-07 15:15:08 +02:00
Holger Müller a98c9e2ffd
fixed copy paste error with repeat button 2022-04-06 21:05:44 +02:00
tzugen 0128a8b29d
Fix memory settings 2022-04-06 21:00:27 +02:00
tzugen 41f5520f1f
Remove unused resources & update lint baseline 2022-04-05 22:52:02 +02:00
tzugen fd34199c27
Remove unused resources 2022-04-05 22:45:51 +02:00
tzugen bb77216eff
Format 2022-04-05 22:33:04 +02:00
tzugen e1f4ee15d5
Fix Widget 2022-04-05 22:24:06 +02:00
tzugen d0959ffcb5
AppWidget to Kotlin 2022-04-05 21:56:25 +02:00
tzugen 4c22c8b41b
Formating / detekt 2022-04-05 21:56:13 +02:00
tzugen ba1a1c5538
Use grayed out shuffle and repeat buttons.
Fix removing and resorting playlist.
2022-04-05 21:41:27 +02:00
tzugen 7742f67796
Add pref switch for HW offload 2022-04-05 20:58:11 +02:00
tzugen 1a69507e34
Remove a bunch of now unused prefs 2022-04-05 10:21:46 +02:00
tzugen 46fb7664c3
Fix insertAfterCurrent, Fix getUri 2022-04-05 10:10:24 +02:00
tzugen dd65a12b53
Migrate AutoMediaBrowser 2022-04-05 00:05:52 +02:00
tzugen 2f7f47783a
Enable HW playback for better performance 2022-04-05 00:05:52 +02:00
tzugen b1c2d020b5
Fix a bug in MetadataParser (unrelated to media3) 2022-04-04 18:27:59 +02:00
tzugen 5dc9fda7a4
Detekt fixes 2022-04-04 18:18:52 +02:00
tzugen 1313fb6c0c
Split Cache into dedicated datasource 2022-04-04 17:59:12 +02:00
tzugen 5966dd7299
Remove gapless setting 2022-04-04 17:43:02 +02:00
tzugen 1703f02aad
Remove unused file 2022-04-04 00:01:26 +02:00
tzugen 922022ab03
Switch to Media3 2022-04-03 23:59:18 +02:00
tzugen bfc11f9924
Don't manage ID manually, but use autoGenerate to ensure uniqueness
(across the lifetime of the db)
2022-04-03 23:54:52 +02:00
tzugen e77b5abd3e
Merge pull request #719 from ultrasonic/AlbumTrackCachePrepare
Add migration path for coming DB change
2022-03-31 12:20:27 +02:00
tzugen 988bf62acf
try was missing a catch block. 2022-03-30 16:16:59 +02:00
tzugen 1a46f7e2c6
Make it clear which ID the OfflineServer has (-1)
Also provide migration path to future version
2022-03-28 23:57:37 +02:00
tzugen 1d88c585c4
Remove migration function from 2020.
All regular users will have run the migration by now.
2022-03-28 23:57:37 +02:00
tzugen 287169649a
Merge pull request #722 from ultrasonic/ready/OnlyRename
Cleanup terminology surrounding entry vs track vs song.
2022-03-28 23:56:22 +02:00
tzugen 020f67d5e6
Merge branch 'develop' into ready/OnlyRename 2022-03-28 23:06:21 +02:00
Óscar García Amor fcc57ae316
[skip ci] Replace Airsonic with Airsonic-Advanced 2022-03-28 15:39:52 +02:00
tzugen 0fd17bfe8c
Merge pull request #721 from ultrasonic/ready/cleanupPref
Cleanup an unused preference
2022-03-28 13:20:20 +02:00
tzugen 0c016bff41
Remove unused strings 2022-03-27 20:14:18 +02:00
tzugen 12435ed9ec
Cleanup an unused preference 2022-03-27 20:09:49 +02:00
tzugen c2226ba202
Fix older detekt issues in DownloadFile and regenerate Baselinefile 2022-03-27 16:40:09 +02:00
tzugen 892b441c0d
Move Track and Album to their own files 2022-03-27 16:39:35 +02:00
tzugen e53da92dac
Unify terminology also by renaming DownloadFile.song -> DownloadFile.track 2022-03-27 16:39:35 +02:00
tzugen 2de59b2206
Rename MusicDirectory.Entry to Track to make more clear what it is 2022-03-27 16:39:35 +02:00
tzugen 34c13d7908
Merge pull request #711 from ultrasonic/shareStyles
Don't exclude the whole .idea folder
2022-03-27 15:41:14 +02:00
tzugen 88918bd839
Keep the whole .idea directory ignored. 2022-03-27 15:24:33 +02:00
tzugen f49063664b
Add weak warning for code which is not formatted according to the official style guide.
This only run in newly added/modified files.
2022-03-27 15:22:43 +02:00
tzugen 126efd35c6
Include official kotlin code style 2022-03-27 15:22:43 +02:00
tzugen a6a052781d
Don't exclude the whole .idea folder
https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
2022-03-27 15:22:43 +02:00
tzugen 107b01fd91
Merge pull request #710 from hmueller01/detailed-media-info
Add detailed song information to Now Playing
2022-03-23 13:06:22 +01:00
tzugen d05ac1489e
Merge branch 'develop' into detailed-media-info 2022-03-23 11:48:04 +01:00
Holger Müller fc94d28862
satisfy CI checker
Signed-off-by: Holger Müller <github@euhm.de>
2022-03-22 08:50:35 +01:00
Holger Müller 8bec74e66a
replaced visibility by isVisible thanks to @tzugen
Signed-off-by: Holger Müller <github@euhm.de>
2022-03-22 08:26:24 +01:00
Holger Müller acf6c5a681
removed untranslatable string from all languages
Signed-off-by: Holger Müller <github@euhm.de>
2022-03-22 08:09:53 +01:00
tzugen 81a21ce8b7
Merge pull request #708 from hmueller01/config-text-update
updated settings.download_transition
2022-03-21 23:40:43 +01:00
Holger Müller cf86101de2
updated settings.download_transition 2022-03-21 22:54:41 +01:00
Nite 5a44fcfe29
Merge pull request #713 from ultrasonic/update_circleci_img
Update CircleCi Image, it started to throw a warning:
2022-03-21 19:27:40 +01:00
Nite 2aa5174fbd
Merge branch 'develop' into update_circleci_img 2022-03-21 19:09:25 +01:00
Nite cf7cef9831
Merge pull request #653 from ultrasonic/draft/okhttp
Update OkHttp to 4.9.1
2022-03-21 19:07:50 +01:00
Nite 423957d954
Merge branch 'develop' into draft/okhttp 2022-03-21 18:51:00 +01:00
Holger Müller 0944bd2217
removed unused import to satisfy CI checker
Signed-off-by: Holger Müller <github@euhm.de>
2022-03-21 18:43:10 +01:00
Holger Müller 7b750c692c
shorten onCurrentChanged() to 60 lines to not fail CI LongMethod
Signed-off-by: Holger Müller <github@euhm.de>
2022-03-21 18:38:27 +01:00
Holger Müller c247e930c4
reworks requested by @ogarcia
Signed-off-by: Holger Müller <github@euhm.de>
2022-03-21 18:04:55 +01:00
tzugen 44d68a71da
Merge pull request #712 from ultrasonic/addLocaleChooser
Add locale chooser
2022-03-21 11:23:10 +01:00
tzugen 7a51c271ba
Merge branch 'develop' into addLocaleChooser 2022-03-21 10:00:28 +01:00
Óscar García Amor 4d91068535
Merge pull request #714 from ultrasonic/wordingAlert
Reword alert for better help
2022-03-21 09:56:25 +01:00
tzugen fe3b713241
Tweak wording 2022-03-21 09:47:46 +01:00
Holger Müller 2f5704548c
implemented @tzugen way to show the song details
Signed-off-by: Holger Müller <github@euhm.de>
2022-03-20 22:05:59 +01:00
tzugen 17850980e1
Remove traditional german variant 2022-03-20 13:35:11 +01:00
tzugen 34d2b45d71
Add Res Config 2022-03-20 13:12:47 +01:00
tzugen 555ef5b7ff
Differentiate chinese variants 2022-03-20 13:10:40 +01:00
tzugen c269243a0d
Complete traditional strings 2022-03-20 09:14:47 +01:00
tzugen 2d8b93301f
Add traditional german variant 2022-03-20 09:09:14 +01:00
tzugen e4a41de3ef
Reword alert for better help 2022-03-20 07:57:50 +01:00
tzugen f0447105d2
Fix methods in integration tests 2022-03-19 18:39:13 +01:00
tzugen 65c4f2b100
Update CircleCi Image, it started to throw a warning:
https://discuss.circleci.com/t/legacy-convenience-image-deprecation/41034
2022-03-19 17:44:12 +01:00
tzugen f6f9683a9c
Migrate some old calls 2022-03-19 17:39:59 +01:00
tzugen c2d62e8688
Disable language split in bundle 2022-03-19 17:36:46 +01:00
tzugen e153565086
Update Okhttp, Jackson and retrofit 2022-03-19 17:07:16 +01:00
tzugen f30a582c7b
Add locale chooser 2022-03-19 17:00:16 +01:00
Holger Müller cc5f29ca98
fixed failed ultrasonic:ktlintMainSourceSetCheck
Signed-off-by: Holger Müller <github@euhm.de>
2022-03-19 14:51:39 +01:00
Holger Müller 8f18192c36
fixed failed ultrasonic:ktlintMainSourceSetCheck
Signed-off-by: Holger Müller <github@euhm.de>
2022-03-19 14:38:10 +01:00
Holger Müller ae2055e324
add detailed song information to Now Playing
Signed-off-by: Holger Müller <github@euhm.de>
2022-03-18 20:46:38 +01:00
Nite dee4675715
Merge pull request #706 from hmueller01/fix-debug-file-size2
fixed output of debug file size (in MB)
2022-03-15 12:26:46 +01:00
Nite e8c31db90f
Merge branch 'develop' into fix-debug-file-size2 2022-03-15 11:33:44 +01:00
Nite 34fb63c783
Merge pull request #707 from nitehu/fix/search2-result-artist
Fixed Search result to return Artist correctly when ID3 tags are turned off
2022-03-15 11:32:55 +01:00
Nite 9ee03aae2f
Merge branch 'develop' into fix/search2-result-artist 2022-03-15 11:08:24 +01:00
Nite ebfc06c423
Fixed tests 2022-03-15 10:52:22 +01:00
Nite 0587f4d837
Merge pull request #705 from hmueller01/strings-update
updated english language
2022-03-15 10:39:41 +01:00
Nite 6bfd06c6a0
Updated Search2 Artist result to use Index instead of Artist
(As needed for listing the Artist's albums afterwards)
2022-03-15 10:31:38 +01:00
Óscar García Amor 6442bae882
Merge branch 'develop' into strings-update 2022-03-15 09:15:40 +01:00
Nite c81c685800
Merge pull request #699 from omgcem/fix-context-menu-track
Updated references to context_menu_track to correctly identify events
2022-03-15 09:11:27 +01:00
Holger Müller 5941e5ab87
fixed output of debug file size (in MB) 2022-03-12 15:40:21 +01:00
Holger Müller cf52d76698
updated english language 2022-03-12 15:35:45 +01:00
Cem Eren 273ac8f9b8
Reused existing functions for track actions
Updated existing functions with additional parameter so they can be used
by context menu but added default value function so that existing calls
on buttons can remain without parameters being passed in.
2022-03-11 19:35:18 +01:00
Nite 465c211017
Merge branch 'develop' into fix-context-menu-track 2022-03-09 16:57:31 +01:00
Nite 4fbedc3d2b
Merge pull request #695 from ultrasonic/improve-template
Improve issue template adding server info
2022-03-09 16:56:55 +01:00
Cem Eren 0961f56a7d
Corrected style violation 2022-03-06 00:19:25 +01:00
Cem Eren eb0fa67431
Updated references to context_menu_track to correctly identify events
Additionally updated event handler to correctly play, queue, pin and
download songs now that events are triggering correctly.
2022-03-05 00:10:20 +01:00
Óscar García Amor 3d65c0a90c
Improve issue template adding server info 2022-02-25 08:44:05 +01:00
Nite 7dd479c0d2
Merge pull request #691 from blastrock/fix-play-all
TrackCollectionFragment: fix play all button
2022-02-20 13:46:32 +01:00
Nite b5bfd87fcc
Merge branch 'develop' into fix-play-all 2022-02-19 21:30:36 +01:00
Philippe Daouadi d03b633eeb
TrackCollectionFragment: fix play all button
The button would not work because the entries are actually of the Album
type instead of Entry.

Signed-off-by: Philippe Daouadi <philippe@ud2.org>
2022-02-19 18:30:59 +01:00
Nite 4ee4b70e09
Merge pull request #692 from nitehu/hide-keyboard
Fixed Search and Edit Server to hide the keyboard when navigating away
2022-02-19 14:32:47 +01:00
Nite 8675f25668
Fixed Search and Edit Server to hide the keyboard when navigating away 2022-02-19 13:54:25 +01:00
Nite 00dc87d5df
Merge pull request #690 from nitehu/fix-about
Made About text scrollable, fixed title
2022-02-19 11:45:43 +01:00
Nite b7d1e4acf6
Made About text scrollable, fixed title 2022-02-19 11:26:13 +01:00
Óscar García Amor baa0c92c7a
Bump version to 3.1.1 2022-02-17 08:34:15 +01:00
Nite 7c9d51f758
Merge pull request #689 from nitehu/update-gradle
Updated Gradle to fix R8 bug with Kotlin 1.6
2022-02-16 19:20:11 +01:00
Nite 9bf7e99abd
Fixed Lint 2022-02-16 19:04:27 +01:00
Nite b5e606455e
Updated Gradle to fix R8 bug with Kotlin 1.6 2022-02-16 18:33:07 +01:00
216 changed files with 7407 additions and 7883 deletions

View File

@ -1,11 +1,20 @@
version: 3
version: 2.1
parameters:
memory-config:
type: string
default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g"
memory-config-debug:
type: string
default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g -verbose:gc -Xlog:gc*"
jobs:
build:
docker:
- image: circleci/android:api-30
- image: cimg/android:2022.06.1
working_directory: ~/ultrasonic
environment:
JVM_OPTS: -Xmx3200m
JVM_OPTS: << pipeline.parameters.memory-config >>
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
GRADLE_OPTS: << pipeline.parameters.memory-config >>
steps:
- checkout
- restore_cache:
@ -18,6 +27,7 @@ jobs:
command: |
sed -i '/^org.gradle.jvmargs/d' gradle.properties
sed -i 's/^org.gradle.daemon=true/org.gradle.daemon=false/g' gradle.properties
cat gradle.properties
- run:
name: checkstyle
command: ./gradlew -Pqc ktlintCheck
@ -31,7 +41,6 @@ jobs:
name: unit-tests
command: |
./gradlew ciTest testDebugUnitTest
./gradlew jacocoFullReport
- run:
name: lint
command: ./gradlew :ultrasonic:lintRelease
@ -44,18 +53,16 @@ jobs:
- save_cache:
paths:
- ~/.gradle
key: v1-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
key: v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
- store_artifacts:
path: ultrasonic/build/reports
destination: reports
- store_artifacts:
path: subsonic-api/build/reports
destination: reports
- store_artifacts:
path: build/reports/jacoco/jacocoFullReport/
push_translations:
docker:
- image: circleci/python:3.6
- image: cimg/python:3.6
working_directory: ~/ultrasonic
steps:
- checkout
@ -75,8 +82,12 @@ jobs:
tx push -s
generate_signed_apk:
docker:
- image: circleci/android:api-30
- image: cimg/android:2022.06.1
working_directory: ~/ultrasonic
environment:
JVM_OPTS: << pipeline.parameters.memory-config >>
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
GRADLE_OPTS: << pipeline.parameters.memory-config >>
steps:
- checkout
- restore_cache:
@ -95,22 +106,22 @@ jobs:
command: |
export PATH="${JAVA_HOME}/bin:${PATH}"
mkdir -p /tmp/ultrasonic-release
${ANDROID_HOME}/build-tools/30.0.0/zipalign -v 4 ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
${ANDROID_HOME}/build-tools/30.0.0/apksigner sign --verbose --ks ~/ultrasonic/ultrasonic-keystore --ks-pass pass:${ULTRASONIC_KEYSTORE_STOREPASS} --key-pass pass:${ULTRASONIC_KEYSTORE_KEYPASS} /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
${ANDROID_HOME}/build-tools/30.0.0/apksigner verify --verbose /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
${ANDROID_HOME}/build-tools/32.0.0/zipalign -v 4 ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
${ANDROID_HOME}/build-tools/32.0.0/apksigner sign --verbose --ks ~/ultrasonic/ultrasonic-keystore --ks-pass pass:${ULTRASONIC_KEYSTORE_STOREPASS} --key-pass pass:${ULTRASONIC_KEYSTORE_KEYPASS} /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
${ANDROID_HOME}/build-tools/32.0.0/apksigner verify --verbose /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
- persist_to_workspace:
root: /tmp/ultrasonic-release
paths:
- ultrasonic-*.apk*
publish_github_signed_apk:
docker:
- image: circleci/golang
- image: cimg/go:1.18
steps:
- attach_workspace:
at: /tmp/ultrasonic-release
- run:
name: install ghr
command: go get -v github.com/tcnksm/ghr
command: go install -v github.com/tcnksm/ghr@latest
- run:
name: publish release on github tag
command: ghr -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} ${CIRCLE_TAG} /tmp/ultrasonic-release
@ -129,7 +140,7 @@ workflows:
- generate_signed_apk:
filters:
tags:
only: /^[0-9]+(\.[0-9]+)*/
only: /^[0-9]+(\.[0-9]+)*(-beta\.[0-9]+)?/
branches:
ignore: /.*/
- publish_github_signed_apk:
@ -137,7 +148,7 @@ workflows:
- generate_signed_apk
filters:
tags:
only: /^[0-9]+(\.[0-9]+)*/
only: /^[0-9]+(\.[0-9]+)*(-beta\.[0-9]+)?/
branches:
ignore: /.*/

1
.gitignore vendored
View File

@ -39,6 +39,7 @@ captures/
*.iml
.idea/
# Keystore files
*.jks

View File

@ -0,0 +1,128 @@
<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">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
.idea/compiler.xml Normal file
View File

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

View File

@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="&amp;#36;file.fileName&#10;Copyright (C) 2009-&amp;#36;today.year Ultrasonic developers&#10;&#10;Distributed under terms of the GNU GPLv3 license." />
<option name="myName" value="Default" />
</copyright>
</component>

View File

@ -0,0 +1,7 @@
<component name="CopyrightManager">
<settings default="Default">
<LanguageOptions name="Kotlin">
<option name="fileTypeOverride" value="3" />
</LanguageOptions>
</settings>
</component>

View File

@ -0,0 +1,8 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Reformat" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="processChangedFilesOnly" value="true" />
</inspection_tool>
</profile>
</component>

4
.idea/misc.xml Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -18,18 +18,46 @@ By default Pull Request should be opened against **develop** branch, PR against
### Here are a few guidelines you should follow before submitting:
1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted.
Use `git commit --signoff` to acknowledge this.
2. **App is migrating to [Kotlin](https://kotlinlang.org/) programming language:** new Pull Requests
should be written in this programming language.
3. **No Breakage:** New features or changes to existing ones must not degrade the user experience.
4. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms.
Use `git commit --signoff` to acknowledge this.
2. **No Breakage:** New features or changes to existing ones must not degrade the user experience.
3. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms.
Refactoring existing messes is great, but watch out for breakage.
5. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review
4. **No large PR:** Try to limit the scope of PR only to the related issue, so it will be easier to review
and test.
### Pull Request Process
On each Pull Request Github runs a number of checks to make sure there are no problems.
#### Signed commits
Commits must be signed. [See here how to set it up](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)
#### KtLint
This programm checks if the source code is formatted correctly.
You can run it yourself locally with
`./gradlew -Pqc ktlintFormat`
Running this command will fix common problems and will notify you of problems it couldn't fix automatically.
#### Detekt
Detekt is a static analyser. It helps to find potential bugs in our code.
You can run it yourself locally with
`./gradlew -Pqc detekt`
There is a "baseline" file, in which errors which have been in the code base before are noted.
Sometimes it is necessary to regenerate this file by running:
`./gradlew -Pqc detektBaseline`
#### Lint
Lint looks for general problems in the code or unused resources etc.
You can run it with
`./gradlew -Pqc lintRelease`
If there is a need to regenerate the baseline, remove `ultrasonic/lint-baseline.xml` and rerun the command.
1. Ensure [all commits are signed-off](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification).
2. Check tests for the new code are added.
3. Check code style is passing.
4. Check code static analysis is passing.

View File

@ -1,20 +1,28 @@
## Problem description
Describe your problem here. Describe what you want to happen, and what happens
if you try to do it. If you have a stack trace or any logs, please format them using
github triple backquote notation
Describe your problem here. Describe what you want to happen, and what
happens if you try to do it. If you have a stack trace or any logs, please
format them using GitHub triple backquote notation.
### Steps to reproduce
Describe how somebody else could observe the same behavior you do. Don't share here any logins and
passwords!
Describe how somebody else could observe the same behavior you do. Don't
share here any logins and passwords!
## System information
### Ultrasonic client
* **Ultrasonic version**: *version of the app*
* **Android version**: *Version of Android OS on the device*
* **Device info**: *Device manufacturer, model*
### Server
* **Server name**: *Airsonic, Ampache, Supysonic...*
* **Server version**: *version of server software*
* **Protocol used**: *http or https (self certificate, letsencrypt...)*
## Additional notes
Include any extra notes here. Otherwise you may remove this section.

View File

@ -1,14 +1,25 @@
# Ultrasonic
[![Build Status](https://circleci.com/gh/ultrasonic/ultrasonic/tree/develop.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/ultrasonic)
[![Codecov branch](https://img.shields.io/codecov/c/github/ultrasonic/ultrasonic/develop.svg)]()
[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)
# WE HAVE MOVED
Ultrasonic is free and open-source music streaming Android client for [Subsonic](http://www.subsonic.org/) [API](http://www.subsonic.org/pages/api.jsp) (version 1.7.0 or higher) compatible servers.
Ultrasonic code is now hosted in [GitLab][ultrasonic].
- New Web: https://ultrasonic.gitlab.io
- New Git: https://gitlab.com/ultrasonic/ultrasonic
- New bugtracker: https://gitlab.com/ultrasonic/ultrasonic/-/issues
- New releases: https://gitlab.com/ultrasonic/ultrasonic/-/packages
[ultrasonic]: https://gitlab.com/ultrasonic/ultrasonic
# Ultrasonic
Ultrasonic is free and open-source music streaming Android client for
[Subsonic][subsonic] [API][subapi] (version 1.7.0 or higher) compatible
servers.
## Help wanted
We currently don't have that much time to spend developing Subsonic, so any
contributions or active developers are always welcomed.
Have a look at [CONTRIBUTING](CONTRIBUTING.md) to get started.
## Download
@ -16,24 +27,26 @@ App is available to download at following stores:
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="70">](https://play.google.com/store/apps/details?id=org.moire.ultrasonic)
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="70">](https://f-droid.org/packages/org.moire.ultrasonic/)
[<img src="https://ultrasonic.github.io/assets/img/get-it-on-github.png" alt="Get it on GitHub" height="70">](https://github.com/ultrasonic/ultrasonic/releases)
[<img src="https://ultrasonic.gitlab.io/assets/img/get-it-on-gitlab.png" alt="Get it on GitLab" height="70">](https://gitlab.com/ultrasonic/ultrasonic/-/releases)
**Warning**: All three versions (Google Play, F-Droid and the APKs) are not
compatible (not signed by the same key)! You must uninstall one to install
the other, which will delete all your data.
the other, which will delete all your data.
If you want to use the version downloaded from F-Droid or form Github with **Android Auto**, you must enable Unknown Sources as it is described in [this wiki page](https://github.com/ultrasonic/ultrasonic/wiki/Using-Ultrasonic-with-Android-Auto).
If you want to use the version downloaded from F-Droid or from GitLab with
**Android Auto**, you must enable Unknown Sources as it is described in
[this wiki page][wikiaa].
## Bugs and issues
First, see if your issue havent been yet reported [here](https://github.com/ultrasonic/ultrasonic/issues),
otherwise open [a new issue](https://github.com/ultrasonic/ultrasonic/issues/new).
First, see if your issue havent been yet reported [here][issues], otherwise
open [a new issue][newissue].
### Known (not our) bugs
If you are using *Madsonic 5.1.X* several sections of Ultrasonic will not
work. This is caused by bad implementation of Subsonic API by Madsonic. For
more info about this you can read [this bug](https://github.com/ultrasonic/ultrasonic/issues/129).
more info about this you can read [this bug][madbug].
## Contributing
@ -41,16 +54,29 @@ See [CONTRIBUTING](CONTRIBUTING.md).
## Supported (tested) Subsonic API implementations
- [Subsonic](http://www.subsonic.org/pages/index.jsp)
- [Airsonic](https://github.com/airsonic/airsonic)
- [Supysonic](https://github.com/spl0k/supysonic)
- [Ampache](https://ampache.org/)
- [Subsonic][subsonic]
- [Airsonic-Advanced][airsonic]
- [Supysonic][supysonic]
- [Ampache][ampache]
Other *Subsonic API* implementations should work as well as long as they follow API
[documentation](http://www.subsonic.org/pages/api.jsp).
Other *Subsonic API* implementations should work as well as long as they
follow API [documentation][subapi].
## License
This software is licensed under the terms of the GNU General Public License version 3 (GPLv3).
This software is licensed under the terms of the GNU General Public License
version 3 (GPLv3).
Full text of the license is available in the [LICENSE](LICENSE) file and [online](https://opensource.org/licenses/gpl-3.0.html).
Full text of the license is available in the [LICENSE](LICENSE) file and
[online][gpl3].
[wikiaa]: https://gitlab.com/ultrasonic/ultrasonic/-/wikis/Using-Ultrasonic-with-Android-Auto
[issues]: https://gitlab.com/ultrasonic/ultrasonic/-/issues
[newissue]: https://gitlab.com/ultrasonic/ultrasonic/-/issues/new
[madbug]: https://gitlab.com/ultrasonic/ultrasonic/-/issues/129
[subsonic]: http://www.subsonic.org/
[subapi]: http://www.subsonic.org/pages/api.jsp
[airsonic]: https://github.com/airsonic-advanced/airsonic-advanced
[supysonic]: https://github.com/spl0k/supysonic
[ampache]: https://ampache.org/
[gpl3]: https://opensource.org/licenses/gpl-3.0.html

View File

@ -17,7 +17,6 @@ buildscript {
classpath libs.kotlin
classpath libs.ktlintGradle
classpath libs.detekt
classpath libs.jacoco
}
}
@ -44,8 +43,6 @@ allprojects {
}
}
apply from: 'gradle_scripts/jacoco.gradle'
wrapper {
gradleVersion(libs.versions.gradle.get())
distributionType("all")

View File

@ -1,12 +1,6 @@
apply from: bootstrap.androidModule
apply plugin: 'kotlin-kapt'
ext {
jacocoExclude = [
'**/domain/**'
]
}
dependencies {
implementation libs.roomRuntime
implementation libs.roomKtx

View File

@ -0,0 +1,38 @@
/*
* Album.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import java.util.Date
@Entity(tableName = "albums", primaryKeys = ["id", "serverId"])
data class Album(
override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
override var parent: String? = null,
override var album: String? = null,
override var title: String? = null,
override val name: String? = null,
override var discNumber: Int? = 0,
override var coverArt: String? = null,
override var songCount: Long? = null,
override var created: Date? = null,
override var artist: String? = null,
override var artistId: String? = null,
override var duration: Int? = 0,
override var year: Int? = 0,
override var genre: String? = null,
override var starred: Boolean = false,
override var path: String? = null,
override var closeness: Int = 0,
) : MusicDirectory.Child() {
override var isDirectory = true
override var isVideo = false
}

View File

@ -1,14 +1,23 @@
/*
* Artist.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "artists")
@Entity(tableName = "artists", primaryKeys = ["id", "serverId"])
data class Artist(
@PrimaryKey override var id: String,
override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
override var name: String? = null,
override var index: String? = null,
override var coverArt: String? = null,
override var albumCount: Long? = null,
override var closeness: Int = 0
) : ArtistOrIndex(id)
) : ArtistOrIndex(id, serverId)

View File

@ -1,11 +1,21 @@
/*
* ArtistOrIndex.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.Ignore
@Suppress("LongParameterList")
abstract class ArtistOrIndex(
@Ignore
override var id: String,
@Ignore
open var serverId: Int,
@Ignore
override var name: String? = null,
@Ignore
open var index: String? = null,
@ -18,15 +28,15 @@ abstract class ArtistOrIndex(
) : GenericEntry() {
fun compareTo(other: ArtistOrIndex): Int {
when {
return when {
this.closeness == other.closeness -> {
return 0
0
}
this.closeness > other.closeness -> {
return -1
-1
}
else -> {
return 1
1
}
}
}

View File

@ -2,7 +2,6 @@ package org.moire.ultrasonic.domain
import java.io.Serializable
import java.util.Date
import org.moire.ultrasonic.domain.MusicDirectory.Entry
data class Bookmark(
val position: Int = 0,
@ -10,7 +9,7 @@ data class Bookmark(
val comment: String,
val created: Date? = null,
val changed: Date? = null,
val entry: Entry
val track: Track
) : Serializable {
companion object {
private const val serialVersionUID = 8988990025189807803L

View File

@ -1,15 +1,24 @@
/*
* Index.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "indexes")
@Entity(tableName = "indexes", primaryKeys = ["id", "serverId"])
data class Index(
@PrimaryKey override var id: String,
override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
override var name: String? = null,
override var index: String? = null,
override var coverArt: String? = null,
override var albumCount: Long? = null,
override var closeness: Int = 0,
var musicFolderId: String? = null
) : ArtistOrIndex(id)
) : ArtistOrIndex(id, serverId)

View File

@ -1,8 +1,12 @@
/*
* MusicDirectory.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
import java.util.Date
class MusicDirectory : ArrayList<MusicDirectory.Child>() {
@ -20,9 +24,9 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
return filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles }
}
fun getTracks(): List<Entry> {
fun getTracks(): List<Track> {
return mapNotNull {
it as? Entry
it as? Track
}
}
@ -34,6 +38,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
abstract class Child : GenericEntry() {
abstract override var id: String
abstract var serverId: Int
abstract var parent: String?
abstract var isDirectory: Boolean
abstract var album: String?
@ -53,87 +58,4 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
abstract var closeness: Int
abstract var isVideo: Boolean
}
// TODO: Rename to Track
@Entity
data class Entry(
@PrimaryKey override var id: String,
override var parent: String? = null,
override var isDirectory: Boolean = false,
override var title: String? = null,
override var album: String? = null,
var albumId: String? = null,
override var artist: String? = null,
override var artistId: String? = null,
var track: Int? = null,
override var year: Int? = null,
override var genre: String? = null,
var contentType: String? = null,
var suffix: String? = null,
var transcodedContentType: String? = null,
var transcodedSuffix: String? = null,
override var coverArt: String? = null,
var size: Long? = null,
override var songCount: Long? = null,
override var duration: Int? = null,
var bitRate: Int? = null,
override var path: String? = null,
override var isVideo: Boolean = false,
override var starred: Boolean = false,
override var discNumber: Int? = null,
var type: String? = null,
override var created: Date? = null,
override var closeness: Int = 0,
var bookmarkPosition: Int = 0,
var userRating: Int? = null,
var averageRating: Float? = null,
override var name: String? = null
) : Serializable, Child() {
fun setDuration(duration: Long) {
this.duration = duration.toInt()
}
companion object {
private const val serialVersionUID = -3339106650010798108L
}
fun compareTo(other: Entry): Int {
when {
this.closeness == other.closeness -> {
return 0
}
this.closeness > other.closeness -> {
return -1
}
else -> {
return 1
}
}
}
override fun compareTo(other: Identifiable) = compareTo(other as Entry)
}
data class Album(
@PrimaryKey override var id: String,
override var parent: String? = null,
override var album: String? = null,
override var title: String? = null,
override val name: String? = null,
override var discNumber: Int? = 0,
override var coverArt: String? = null,
override var songCount: Long? = null,
override var created: Date? = null,
override var artist: String? = null,
override var artistId: String? = null,
override var duration: Int? = 0,
override var year: Int? = 0,
override var genre: String? = null,
override var starred: Boolean = false,
override var path: String? = null,
override var closeness: Int = 0,
) : Child() {
override var isDirectory = true
override var isVideo = false
}
}

View File

@ -1,13 +1,22 @@
/*
* MusicFolder.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* Represents a top level directory in which music or other media is stored.
*/
@Entity(tableName = "music_folders")
@Entity(tableName = "music_folders", primaryKeys = ["id", "serverId"])
data class MusicFolder(
@PrimaryKey override val id: String,
override val name: String
override val id: String,
override val name: String,
@ColumnInfo(defaultValue = "-1")
var serverId: Int
) : GenericEntry()

View File

@ -1,12 +0,0 @@
package org.moire.ultrasonic.domain
enum class PlayerState {
IDLE,
DOWNLOADING,
PREPARING,
PREPARED,
STARTED,
STOPPED,
PAUSED,
COMPLETED
}

View File

@ -1,15 +0,0 @@
package org.moire.ultrasonic.domain
enum class RepeatMode {
OFF {
override operator fun next(): RepeatMode = ALL
},
ALL {
override operator fun next(): RepeatMode = SINGLE
},
SINGLE {
override operator fun next(): RepeatMode = OFF
};
abstract operator fun next(): RepeatMode
}

View File

@ -1,13 +1,10 @@
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.domain.MusicDirectory.Album
import org.moire.ultrasonic.domain.MusicDirectory.Entry
/**
* The result of a search. Contains matching artists, albums and songs.
*/
data class SearchResult(
val artists: List<ArtistOrIndex> = listOf(),
val albums: List<Album> = listOf(),
val songs: List<Entry> = listOf()
val songs: List<Track> = listOf()
)

View File

@ -1,7 +1,6 @@
package org.moire.ultrasonic.domain
import java.io.Serializable
import org.moire.ultrasonic.domain.MusicDirectory.Entry
data class Share(
override var id: String,
@ -12,7 +11,7 @@ data class Share(
var lastVisited: String? = null,
var expires: String? = null,
var visitCount: Long? = null,
private val entries: MutableList<Entry> = mutableListOf()
private val tracks: MutableList<Track> = mutableListOf()
) : Serializable, GenericEntry() {
override val name: String?
get() {
@ -22,12 +21,12 @@ data class Share(
return null
}
fun getEntries(): List<Entry> {
return entries.toList()
fun getEntries(): List<Track> {
return tracks.toList()
}
fun addEntry(entry: Entry) {
entries.add(entry)
fun addEntry(track: Track) {
tracks.add(track)
}
companion object {

View File

@ -0,0 +1,74 @@
/*
* Track.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import java.io.Serializable
import java.util.Date
@Entity(tableName = "tracks", primaryKeys = ["id", "serverId"])
data class Track(
override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
override var parent: String? = null,
override var isDirectory: Boolean = false,
override var title: String? = null,
override var album: String? = null,
var albumId: String? = null,
override var artist: String? = null,
override var artistId: String? = null,
var track: Int? = null,
override var year: Int? = null,
override var genre: String? = null,
var contentType: String? = null,
var suffix: String? = null,
var transcodedContentType: String? = null,
var transcodedSuffix: String? = null,
override var coverArt: String? = null,
var size: Long? = null,
override var songCount: Long? = null,
override var duration: Int? = null,
var bitRate: Int? = null,
override var path: String? = null,
override var isVideo: Boolean = false,
override var starred: Boolean = false,
override var discNumber: Int? = null,
var type: String? = null,
override var created: Date? = null,
override var closeness: Int = 0,
var bookmarkPosition: Int = 0,
var userRating: Int? = null,
var averageRating: Float? = null,
override var name: String? = null
) : Serializable, MusicDirectory.Child() {
fun setDuration(duration: Long) {
this.duration = duration.toInt()
}
companion object {
private const val serialVersionUID = -3339106650010798108L
}
fun compareTo(other: Track): Int {
when {
this.closeness == other.closeness -> {
return 0
}
this.closeness > other.closeness -> {
return -1
}
else -> {
return 1
}
}
}
override fun compareTo(other: Identifiable) = compareTo(other as Track)
}

View File

@ -20,11 +20,3 @@ dependencies {
testImplementation libs.mockWebServer
testImplementation libs.apacheCodecs
}
ext {
// Excluding data classes
jacocoExclude = [
'**/models/**',
'**/di/**'
]
}

View File

@ -8,7 +8,8 @@ import java.util.Locale
import java.util.TimeZone
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.Okio
import okio.buffer
import okio.source
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should contain`
import org.amshove.kluent.`should not be`
@ -40,12 +41,12 @@ fun MockWebServer.enqueueResponse(resourceName: String) {
}
fun Any.loadJsonResponse(name: String): String {
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
val source = javaClass.classLoader.getResourceAsStream(name)!!.source().buffer()
return source.readString(Charset.forName("UTF-8"))
}
fun Any.loadResourceStream(name: String): InputStream {
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
val source = javaClass.classLoader.getResourceAsStream(name)!!.source().buffer()
return source.inputStream()
}

View File

@ -17,8 +17,8 @@ fun Response<out ResponseBody>.toStreamResponse(): StreamResponse {
val contentType = responseBody?.contentType()
if (
contentType != null &&
contentType.type().equals("application", true) &&
contentType.subtype().equals("json", true)
contentType.type.equals("application", true) &&
contentType.subtype.equals("json", true)
) {
val error = SubsonicAPIClient.jacksonMapper.readValue<SubsonicResponse>(
responseBody.byteStream()
@ -40,11 +40,11 @@ fun Response<out ResponseBody>.toStreamResponse(): StreamResponse {
* It creates Exceptions from the results returned by the Subsonic API
*/
@Suppress("ThrowsCount")
fun <T : SubsonicResponse> Response<out T>.throwOnFailure(): Response<out T> {
fun <T : SubsonicResponse> Response<T>.throwOnFailure(): Response<T> {
val response = this
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
return this as Response<T>
return this
}
if (!response.isSuccessful) {
throw IOException("Server error, code: " + response.code())

View File

@ -8,6 +8,7 @@ import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import okhttp3.logging.HttpLoggingInterceptor
@ -68,12 +69,24 @@ class SubsonicAPIClient(
.addInterceptor { chain ->
// Adds default request params
val originalRequest = chain.request()
val newUrl = originalRequest.url().newBuilder()
val newUrl = originalRequest.url.newBuilder()
.addQueryParameter("u", config.username)
.addQueryParameter("c", config.clientID)
.addQueryParameter("f", "json")
.build()
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
val newRequestBuilder = originalRequest.newBuilder().url(newUrl)
if (originalRequest.url.username.isNotEmpty() &&
originalRequest.url.password.isNotEmpty()
) {
newRequestBuilder.addHeader(
"Authorization",
Credentials.basic(
originalRequest.url.username,
originalRequest.url.password
)
)
}
chain.proceed(newRequestBuilder.build())
}
.addInterceptor(versionInterceptor)
.addInterceptor(proxyPasswordInterceptor)
@ -109,7 +122,7 @@ class SubsonicAPIClient(
private fun OkHttpClient.Builder.addLogging() {
val loggingInterceptor = HttpLoggingInterceptor(okLogger)
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
loggingInterceptor.level = HttpLoggingInterceptor.Level.HEADERS
this.addInterceptor(loggingInterceptor)
}

View File

@ -18,7 +18,7 @@ class PasswordHexInterceptor(private val password: String) : Interceptor {
override fun intercept(chain: Chain): Response {
val originalRequest = chain.request()
val updatedUrl = originalRequest.url().newBuilder()
val updatedUrl = originalRequest.url.newBuilder()
.addEncodedQueryParameter("p", passwordHex).build()
return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build())
}

View File

@ -21,7 +21,7 @@ class PasswordMD5Interceptor(private val password: String) : Interceptor {
override fun intercept(chain: Chain): Response {
val originalRequest = chain.request()
val salt = getSalt()
val updatedUrl = originalRequest.url().newBuilder()
val updatedUrl = originalRequest.url.newBuilder()
.addQueryParameter("t", getPasswordMD5Hash(salt))
.addQueryParameter("s", salt)
.build()

View File

@ -19,7 +19,7 @@ internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
internal class RangeHeaderInterceptor : Interceptor {
override fun intercept(chain: Chain): Response {
val originalRequest = chain.request()
val headers = originalRequest.headers()
val headers = originalRequest.headers
return if (headers.names().contains("Range")) {
val offsetValue = headers["Range"] ?: "0"
val offset = "bytes=$offsetValue-"

View File

@ -18,7 +18,7 @@ internal class VersionInterceptor(
val newRequest = originalRequest.newBuilder()
.url(
originalRequest
.url()
.url
.newBuilder()
.addQueryParameter("v", protocolVersion.restApiVersion)
.build()

View File

@ -1,68 +1,26 @@
<?xml version="1.0" ?>
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$!append &amp;&amp; !playNext &amp;&amp; !unpin &amp;&amp; !background</ID>
<ID>ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"</ID>
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song)</ID>
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song)</ID>
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("DownloadTask (%s)", song)</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>
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.&lt;no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID>
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID>
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID>
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name)</ID>
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile)</ID>
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)</ID>
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s &gt; %s", suffix, transcodedSuffix)</ID>
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
<ID>LongMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateDisplay(refresh: Boolean)</ID>
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: 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>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, passedData: Array&lt;ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) )</ID>
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$60000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$3</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$0.05f</ID>
<ID>MagicNumber:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$50</ID>
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
<ID>MagicNumber:SongView.kt$SongView$3</ID>
<ID>MagicNumber:SongView.kt$SongView$4</ID>
<ID>MagicNumber:SongView.kt$SongView$60</ID>
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</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>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$ex: Exception</ID>
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$exception: Throwable</ID>
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
<ID>TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw Exception(String.format("Download of '%s' was cancelled", song))</ID>
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer$x: Throwable</ID>
<ID>TooGenericExceptionCaught:JukeboxMediaPlayer.kt$JukeboxMediaPlayer.TaskQueue$x: Throwable</ID>
<ID>TooGenericExceptionThrown:Downloader.kt$Downloader.DownloadTask$throw RuntimeException( String.format( Locale.ROOT, "Download of '%s' was cancelled", downloadFile.track ) )</ID>
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
</CurrentIssues>
</SmellBaseline>

View File

@ -64,13 +64,10 @@ style:
WildcardImport:
active: true
MaxLineLength:
active: true
maxLineLength: 120
excludePackageStatements: false
excludeImportStatements: false
active: false
MagicNumber:
# 100 common in percentage, 1000 in milliseconds
ignoreNumbers: ['-1', '0', '1', '2', '5', '10', '100', '256', '512', '1000', '1024']
ignoreNumbers: ['-1', '0', '1', '2', '5', '10', '100', '256', '512', '1000', '1024', '4096']
ignoreEnums: true
ignorePropertyDeclaration: true
UnnecessaryAbstractClass:

View File

@ -1,4 +1,3 @@
Others
- #671: Bump versions.mockito from 4.1.0 to 4.3.1.
- Update translations.

View File

@ -1,4 +1,3 @@
Enhancements
- #683: Rewrite the about and remove the webview.
- #685: Server coloring feature.

View File

@ -0,0 +1,2 @@
Bug fixes
- #688: Connection failure.

View File

@ -0,0 +1,14 @@
Bug fixes
- #673: Disabling "Browse Using ID3 Tags" causes artist search result content to mismatch.
- #679: Keyboard should be hidden when navigating away from Search.
- #686: In landscape mode, the scroll bar is missing in the about text.
- #691: TrackCollectionFragment: fix play all button.
- #698: Track based context menus do not function correctly in most fragments.
Features
- #669: Option to change language.
Enhancements
- #654: Update OkHttp.
- #694: Reword alert for better help.
- #702: Show Downloads on Play.

View File

@ -1 +0,0 @@
Refactor and redesign artist list

View File

@ -1 +0,0 @@
Fall backs to path when comparing tracks and fixes #369

View File

@ -1 +0,0 @@
Refactored the application main menu

View File

@ -1 +0,0 @@
Refactored the application main menu

View File

@ -9,7 +9,5 @@ Enhancements
- #568: Rework Downloader.
- #567: Use semantically correct API endpoint when streaming/downloading.
- #572: Moved drag handle to the left in the Now Playing list.
- #585: Added setting to disable Now Playing List sending for incompatible
bluetooth devices.
- #596: Added option whether to create a share on the server when sharing
songs.
- #585: Added setting to disable Now Playing List sending for incompatible bluetooth devices.
- #596: Added option whether to create a share on the server when sharing songs.

View File

@ -1,4 +1,3 @@
Otros
- #671: Actualizado versions.mockito de 4.1.0 a 4.3.1.
- Traducciones actualizadas.

View File

@ -1,4 +1,3 @@
Mejoras
- #683: Reescribir el acerca de y eliminar el webview.
- #685: Posibilidad de seleccionar el color del servidor.

View File

@ -0,0 +1,2 @@
Corrección de errores
- #688: Fallo de conexión.

View File

@ -0,0 +1,14 @@
Corrección de errores
- #673: La desactivación de la opción "Examinar mediante etiquetas ID3" hace que el contenido de los resultados de la búsqueda de artistas no coincida.
- #679: El teclado debería estar oculto cuando se navega fuera de la Búsqueda.
- #686: En modo apaisado, falta la barra de desplazamiento en el texto de acerca de.
- #691: TrackCollectionFragment: arreglar el botón de reproducir todo.
- #698: Los menús contextuales basados en pistas no funcionan correctamente en la mayoría de los fragmentos.
Características
- #669: Opción de cambio de idioma.
Mejoras
- #654: Actualización de OkHttp.
- #694: Reformular la alerta para mejorar la ayuda.
- #702: Mostrar descargas al reproducir.

View File

@ -1 +0,0 @@
Refactorizada y rediseñada la lista de artistas

View File

@ -1 +0,0 @@
Cuando un fichero no tiene etiquetas de número de pista lo ordena por orden alfabético. Además hemos arreglado el bug #369

View File

@ -1 +0,0 @@
Refactorizado el menú principal de la aplicación

View File

@ -1 +0,0 @@
Refactorizado el menú principal de la aplicación

View File

@ -1,6 +1,5 @@
Correción de errores
- #594: Agregado un intent de PlaybackComplete cuando se completa la
reproducción de una canción.
- #594: Agregado un intent de PlaybackComplete cuando se completa la reproducción de una canción.
- #593: Corregidas las listas de álbumes.
- #602: NPE corregido.
@ -8,11 +7,7 @@ Mejoras
- #558: La llamada a video puede ser estática.
- #559: Agregado un mejor soporte sin conexión.
- #568: Se ha reescrito el downloader.
- #567: Se utiliza el endpoint semánticamente correcto al realizar streaming
o descargar.
- #572: Se ha movido el botón de arrastre de canción hacia la izquierda en
la lista de reproducción.
- #585: Agregada una configuración para deshabilitar el envío de la Lista de
reproducción en curso para dispositivos Bluetooth incompatibles.
- #596: Se agregó la opción de crear un recurso compartido en el servidor al
compartir canciones.
- #567: Se utiliza el endpoint semánticamente correcto al realizar streaming o descargar.
- #572: Se ha movido el botón de arrastre de canción hacia la izquierda en la lista de reproducción.
- #585: Agregada una configuración para deshabilitar el envío de la Lista de reproducción en curso para dispositivos Bluetooth incompatibles.
- #596: Se agregó la opción de crear un recurso compartido en el servidor al compartir canciones.

View File

@ -2,16 +2,13 @@ Corrección de errores
- #609: Comportamiento extraño de scrobbling (offset).
Mejoras
- #599: Se ha movido el selector de servidor y la configuración al menú de
navegación.
- #600: Migración de la utilidad de permisos a Kotlin, aumento del SDK
mínimo a 17.
- #599: Se ha movido el selector de servidor y la configuración al menú de navegación.
- #600: Migración de la utilidad de permisos a Kotlin, aumento del SDK mínimo a 17.
- #604: Implementar una vista de Descarga.
- #613: targetSdkVersion debe ser 30 o superior.
- #622: Refactorización de eventos.
- #641: Eliminar el almacenamiento de funciones.
- #642: Eliminar MergeAdapter y SackOfViewsAdapter.
- #649: Unificar el manejo del diálogo de error.
- #652: Manejo de ubicación de caché personalizado actualizado para eliminar
isUri.
- #652: Manejo de ubicación de caché personalizado actualizado para eliminar isUri.
- #662: Mejorar las migraciones de bases de datos.

View File

@ -1,30 +1,31 @@
[versions]
# You need to run ./gradlew wrapper after updating the version
gradle = "7.3.2"
gradle = "7.3.3"
navigation = "2.3.5"
gradlePlugin = "7.0.4"
gradlePlugin = "7.2.1"
androidxcore = "1.6.0"
ktlint = "0.43.2"
ktlintGradle = "10.2.0"
detekt = "1.19.0"
jacoco = "0.8.7"
preferences = "1.1.1"
media = "1.3.1"
media3 = "1.0.0-beta01"
androidSupport = "28.0.0"
androidSupport = "1.4.0"
androidLegacySupport = "1.0.0"
androidSupportDesign = "1.4.0"
androidSupportDesign = "1.6.1"
constraintLayout = "2.1.1"
multidex = "2.0.1"
room = "2.4.0"
room = "2.4.2"
kotlin = "1.6.10"
kotlinxCoroutines = "1.6.0-native-mt"
viewModelKtx = "2.3.0"
kotlinxGuava = "1.6.0"
viewModelKtx = "2.4.1"
retrofit = "2.6.4"
jackson = "2.9.5"
okhttp = "3.12.13"
retrofit = "2.9.0"
jackson = "2.10.1"
okhttp = "4.9.1"
koin = "3.0.2"
picasso = "2.71828"
@ -47,12 +48,11 @@ gradle = { module = "com.android.tools.build:gradle", version.r
kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" }
detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
jacoco = { module = "org.jacoco:org.jacoco.core", version.ref = "jacoco" }
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
support = { module = "androidx.legacy:legacy-support-v4", version.ref = "androidLegacySupport" }
design = { module = "com.google.android.material:material", version.ref = "androidSupportDesign" }
annotations = { module = "com.android.support:support-annotations", version.ref = "androidSupport" }
annotations = { module = "androidx.annotation:annotation", version.ref = "androidSupport" }
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" }
room = { module = "androidx.room:room-compiler", version.ref = "room" }
@ -66,10 +66,14 @@ navigationUiKtx = { module = "androidx.navigation:navigation-ui-ktx", ve
navigationFeature = { module = "androidx.navigation:navigation-dynamic-features-fragment", version.ref = "navigation" }
preferences = { module = "androidx.preference:preference", version.ref = "preferences" }
media = { module = "androidx.media:media", version.ref = "media" }
media3exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
media3okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
media3session = { module = "androidx.media3:media3-session", version.ref = "media3" }
kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
kotlinxGuava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxGuava"}
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
gsonConverter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
jacksonConverter = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" }
@ -97,4 +101,3 @@ 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" }

View File

@ -1,5 +1,5 @@
ext.versions = [
minSdk : 21,
targetSdk : 30,
targetSdk : 33,
compileSdk : 31,
]

View File

@ -1,5 +1,6 @@
#Fri Jun 17 23:13:49 CEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View File

@ -3,7 +3,6 @@
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'jacoco'
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
android {
@ -48,10 +47,6 @@ android {
tasks.withType(Test) {
useJUnitPlatform()
jacoco {
includeNoLocationClasses = true
excludes += jacocoExclude
}
}
dependencies {
@ -61,11 +56,4 @@ dependencies {
testRuntimeOnly libs.junitVintage
}
jacoco {
toolVersion(libs.versions.jacoco.get())
}
ext {
jacocoExclude = ['jdk.internal.*']
}

View File

@ -1,92 +0,0 @@
apply plugin: 'jacoco'
jacoco {
toolVersion(libs.versions.jacoco.get())
}
def mergedJacocoExec = file("${project.buildDir}/jacoco/jacocoMerged.exec")
def merge = tasks.register('jacocoMergeReports', JacocoMerge) {
group = "Reporting"
description = "Merge all jacoco reports from projects into one."
ListProperty<File> jacocoFiles = project.objects.listProperty(File.class)
project.subprojects { subproject ->
subproject.plugins.withId("jacoco") {
project.logger.info("${subproject.name} has Jacoco plugin applied")
subproject.tasks.withType(Test) { task ->
File destFile = task.extensions.getByType(JacocoTaskExtension.class).destinationFile
if (destFile.exists() && !task.name.contains("Release")) {
jacocoFiles.add(destFile)
}
}
}
}
executionData(jacocoFiles)
destinationFile(mergedJacocoExec)
}
tasks.register('jacocoFullReport', JacocoReport) {
dependsOn merge
group = "Reporting"
description = "Generate full Jacoco coverage report including all modules."
getClassDirectories().setFrom(files())
getSourceDirectories().setFrom(files())
getExecutionData().setFrom(files())
reports {
xml.enabled = true
html.enabled = true
csv.enabled = false
}
// Always run merging, as all input calculation is done in doFirst {}
outputs.upToDateWhen { false }
// Task will run anyway even if initial inputs are empty
onlyIf = { true }
project.subprojects { subproject ->
subproject.plugins.withId("jacoco") {
project.logger.info("${subproject.name} has Jacoco plugin applied")
subproject.plugins.withId("kotlin-android") {
project.logger.info("${subproject.name} is android project")
def mainSources = subproject.extensions.findByName("android").sourceSets['main']
project.logger.info("Android sources: ${mainSources.java.srcDirs}")
mainSources.java.srcDirs.forEach {
additionalSourceDirs(it)
}
project.logger.info("Subproject exclude: ${subproject.jacocoExclude}")
additionalClassDirs(fileTree(
dir: "${subproject.buildDir}/tmp/kotlin-classes/debug",
excludes: subproject.jacocoExclude
))
}
subproject.plugins.withId("kotlin") { plugin ->
project.logger.info("${subproject.name} is common kotlin project")
SourceDirectorySet mainSources = subproject.extensions.getByName("kotlin")
.sourceSets[SourceSet.MAIN_SOURCE_SET_NAME]
.kotlin
mainSources.srcDirs.forEach {
project.logger.debug("Adding sources: $it")
additionalSourceDirs(it)
}
project.logger.info("Subproject exclude: ${subproject.jacocoExclude}")
additionalClassDirs(fileTree(
dir: "${subproject.buildDir}/classes/kotlin/main",
excludes: subproject.jacocoExclude
))
}
subproject.tasks.withType(Test) { task ->
File destFile = task.extensions.getByType(JacocoTaskExtension.class).destinationFile
if (destFile.exists() && !task.name.contains("Release")) {
project.logger.info("Adding execution data: $destFile")
executionData(destFile)
}
}
}
}
}

View File

@ -3,7 +3,6 @@
*/
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
sourceSets {
@ -21,36 +20,8 @@ dependencies {
testRuntimeOnly libs.junitVintage
}
jacoco {
toolVersion(libs.versions.jacoco.get())
}
ext {
// override it in the module
jacocoExclude = ['jdk.internal.*']
}
jacocoTestReport {
reports {
html.required = true
xml.required = false
csv.required = false
}
afterEvaluate {
getClassDirectories().setFrom(files(classDirectories.files.collect {
fileTree(dir: it, excludes: jacocoExclude)
}))
}
}
tasks.named("test").configure {
useJUnitPlatform()
jacoco {
excludes += jacocoExclude
includeNoLocationClasses = true
}
finalizedBy jacocoTestReport
}
tasks.register("ciTest") {

View File

@ -1,7 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply from: "../gradle_scripts/code_quality.gradle"
android {
@ -9,15 +8,16 @@ android {
defaultConfig {
applicationId "org.moire.ultrasonic"
versionCode 101
versionName "3.1.0"
versionCode 103
versionName "3.2.0"
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
resConfigs "cs", "de", "en", "es", "fr", "hu", "it", "nl", "pl", "pt", "pt-rBR", "ru", "zh-rCN", "zh-rTW"
resConfigs 'cs', 'de', 'en', 'es', 'fr', 'hu', 'it', 'nl', 'pl', 'pt', 'pt-rBR', 'ru', 'zh-rCN', 'zh-rTW'
}
bundle.language.enableSplit = false
buildTypes {
release {
minifyEnabled true
@ -40,20 +40,12 @@ android {
main.java.srcDirs += "${projectDir}/src/main/kotlin"
test.java.srcDirs += "${projectDir}/src/test/kotlin"
}
packagingOptions {
exclude 'META-INF/LICENSE'
resources {
excludes += ['META-INF/LICENSE']
}
}
lintOptions {
baselineFile file("lint-baseline.xml")
ignore 'MissingTranslation'
ignore 'UnusedQuantity'
warning 'ImpliedQuantity'
disable 'IconMissingDensityFolder', "VectorPath"
abortOnError true
warningsAsErrors true
}
kotlinOptions {
jvmTarget = "1.8"
@ -71,9 +63,18 @@ android {
kapt {
arguments {
arg("room.schemaLocation", "$buildDir/schemas".toString())
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas".toString())
}
}
lint {
baseline = file("lint-baseline.xml")
abortOnError true
warningsAsErrors true
disable 'IconMissingDensityFolder', 'VectorPath'
ignore 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
warning 'ImpliedQuantity'
disable 'ObsoleteLintCustomCheck'
}
}
@ -99,6 +100,9 @@ dependencies {
implementation libs.constraintLayout
implementation libs.preferences
implementation libs.media
implementation libs.media3exoplayer
implementation libs.media3session
implementation libs.media3okhttp
implementation libs.navigationFragment
implementation libs.navigationUi
@ -108,6 +112,7 @@ dependencies {
implementation libs.kotlinStdlib
implementation libs.kotlinxCoroutines
implementation libs.kotlinxGuava
implementation libs.koinAndroid
implementation libs.okhttpLogging
implementation libs.fastScroll
@ -130,36 +135,3 @@ dependencies {
implementation libs.timber
}
jacoco {
toolVersion(libs.versions.jacoco.get())
}
// Excluding all java classes and stuff that should not be covered
ext {
jacocoExclude = [
'**/activity/**',
'**/audiofx/**',
'**/fragment/**',
'**/provider/**',
'**/receiver/**',
'**/service/**',
'**/Test/**',
'**/util/**',
'**/view/**',
'**/R$*.class',
'**/R.class',
'**/BuildConfig.class',
'**/di/**',
'jdk.internal.*'
]
}
jacoco {
toolVersion(libs.versions.jacoco.get())
}
tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
jacoco.excludes += jacocoExclude
}

View File

@ -1,37 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 7.0.4" type="baseline" client="gradle" name="AGP (7.0.4)" variant="all" version="7.0.4">
<issues format="6" by="lint 7.2.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.2.1)" variant="all" version="7.2.1">
<issue
id="InflateParams"
message="Avoid passing `null` as the view root (needed to resolve layout parameters on the inflated layout&apos;s root element)"
errorLine1=" View view = inflater.inflate(R.layout.jukebox_volume, null);"
errorLine2=" ~~~~">
errorLine1=" val view = inflater.inflate(R.layout.jukebox_volume, null)"
errorLine2=" ~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/JukeboxMediaPlayer.java"
line="477"
column="58"/>
</issue>
<issue
id="Typos"
message="&quot;lizensiert&quot; is a common misspelling; did you mean &quot;lizenziert&quot; ?"
errorLine1=" &lt;string name=&quot;settings.testing_unlicensed&quot;>Verbindung OK, Server nicht lizensiert.&lt;/string>"
errorLine2=" ^">
<location
file="src/main/res/values-de/strings.xml"
line="289"
column="76"/>
</issue>
<issue
id="PluralsCandidate"
message="Formatting %d followed by words (&quot;Artists&quot;): This should probably be a plural rather than a string"
errorLine1=" &lt;string name=&quot;parser.artist_count&quot;>Got %d Artists.&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="134"
column="5"/>
file="src/main/kotlin/org/moire/ultrasonic/service/JukeboxMediaPlayer.kt"
line="331"
column="66"/>
</issue>
<issue
@ -41,7 +19,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="151"
line="154"
column="5"/>
</issue>
@ -56,17 +34,6 @@
column="73"/>
</issue>
<issue
id="SetJavaScriptEnabled"
message="Using `setJavaScriptEnabled` can introduce XSS vulnerabilities into your application, review carefully"
errorLine1=" webView.getSettings().setJavaScriptEnabled(true);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/fragment/AboutFragment.java"
line="51"
column="9"/>
</issue>
<issue
id="TrustAllX509TrustManager"
message="`checkClientTrusted` is empty, which could cause insecure network traffic due to trusting arbitrary TLS/SSL certificates presented by peers">
@ -88,197 +55,32 @@
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="146"
line="155"
column="10"/>
</issue>
<issue
id="ExportedReceiver"
message="Exported receiver does not require permission"
errorLine1=" &lt;receiver android:name=&quot;.receiver.UltrasonicIntentReceiver&quot;>"
errorLine1=" &lt;receiver android:name=&quot;.receiver.UltrasonicIntentReceiver&quot;"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="81"
line="79"
column="10"/>
</issue>
<issue
id="IntentFilterExportedReceiver"
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \&#xA;available to other apps, and `false` otherwise. For launcher activities, this should be set to `true`."
errorLine1=" &lt;activity android:name=&quot;.activity.NavigationActivity&quot;"
errorLine2=" ~~~~~~~~">
id="ExportedService"
message="Exported service does not require permission"
errorLine1=" &lt;service android:name=&quot;.playback.PlaybackService&quot;"
errorLine2=" ~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="41"
line="68"
column="10"/>
</issue>
<issue
id="IntentFilterExportedReceiver"
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \&#xA;available to other apps, and `false` otherwise."
errorLine1=" &lt;receiver android:name=&quot;.receiver.MediaButtonIntentReceiver&quot;>"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="76"
column="10"/>
</issue>
<issue
id="IntentFilterExportedReceiver"
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \&#xA;available to other apps, and `false` otherwise."
errorLine1=" &lt;receiver android:name=&quot;.receiver.UltrasonicIntentReceiver&quot;>"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="81"
column="10"/>
</issue>
<issue
id="IntentFilterExportedReceiver"
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \&#xA;available to other apps, and `false` otherwise."
errorLine1=" &lt;receiver android:name=&quot;.receiver.BluetoothIntentReceiver&quot;>"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="93"
column="10"/>
</issue>
<issue
id="IntentFilterExportedReceiver"
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \&#xA;available to other apps, and `false` otherwise."
errorLine1=" &lt;receiver"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="101"
column="10"/>
</issue>
<issue
id="IntentFilterExportedReceiver"
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \&#xA;available to other apps, and `false` otherwise."
errorLine1=" &lt;receiver"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="112"
column="10"/>
</issue>
<issue
id="IntentFilterExportedReceiver"
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \&#xA;available to other apps, and `false` otherwise."
errorLine1=" &lt;receiver"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="123"
column="10"/>
</issue>
<issue
id="IntentFilterExportedReceiver"
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \&#xA;available to other apps, and `false` otherwise."
errorLine1=" &lt;receiver"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="134"
column="10"/>
</issue>
<issue
id="UnspecifiedImmutableFlag"
message="Missing `PendingIntent` mutability flag"
errorLine1=" return PendingIntent.getActivity(this, 0, intent, flags)"
errorLine2=" ~~~~~">
<location
file="src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt"
line="708"
column="59"/>
</issue>
<issue
id="UnspecifiedImmutableFlag"
message="Missing `PendingIntent` mutability flag"
errorLine1=" PendingIntent.FLAG_CANCEL_CURRENT"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/kotlin/org/moire/ultrasonic/util/MediaSessionHandler.kt"
line="323"
column="13"/>
</issue>
<issue
id="UnspecifiedImmutableFlag"
message="Missing `PendingIntent` mutability flag"
errorLine1=" PendingIntent pendingIntent = PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java"
line="198"
column="80"/>
</issue>
<issue
id="UnspecifiedImmutableFlag"
message="Missing `PendingIntent` mutability flag"
errorLine1=" pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0);"
errorLine2=" ~">
<location
file="src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java"
line="206"
column="67"/>
</issue>
<issue
id="UnspecifiedImmutableFlag"
message="Missing `PendingIntent` mutability flag"
errorLine1=" pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0);"
errorLine2=" ~">
<location
file="src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java"
line="212"
column="67"/>
</issue>
<issue
id="UnspecifiedImmutableFlag"
message="Missing `PendingIntent` mutability flag"
errorLine1=" pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0);"
errorLine2=" ~">
<location
file="src/main/java/org/moire/ultrasonic/provider/UltrasonicAppWidgetProvider.java"
line="218"
column="67"/>
</issue>
<issue
id="UnspecifiedImmutableFlag"
message="Missing `PendingIntent` mutability flag"
errorLine1=" return PendingIntent.getBroadcast(context, requestCode, intent, flags)"
errorLine2=" ~~~~~">
<location
file="src/main/kotlin/org/moire/ultrasonic/util/Util.kt"
line="891"
column="73"/>
</issue>
<issue
id="NotifyDataSetChanged"
message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
errorLine1=" viewAdapter.notifyDataSetChanged()"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt"
line="908"
column="21"/>
</issue>
<issue
id="ObsoleteLayoutParam"
message="Invalid layout param in a `LinearLayout`: `layout_above`"
@ -345,6 +147,17 @@
column="5"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_baseline_close` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/ic_baseline_close.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.ic_menu_arrow` appears to be unused"
@ -358,189 +171,57 @@
<issue
id="UnusedResources"
message="The resource `R.drawable.menu_arrow` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
message="The resource `R.drawable.media3_notification_pause` appears to be unused"
errorLine1="&lt;vector android:height=&quot;48dp&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/menu_arrow.xml"
file="src/main/res/drawable/media3_notification_pause.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.main_shuffle` appears to be unused"
errorLine1=" &lt;string name=&quot;main.shuffle&quot;>Shuffle Play&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
message="The resource `R.drawable.media3_notification_play` appears to be unused"
errorLine1="&lt;vector android:height=&quot;48dp&quot;"
errorLine2="^">
<location
file="src/main/res/values/strings.xml"
line="114"
column="13"/>
file="src/main/res/drawable/media3_notification_play.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.menu_navigation` appears to be unused"
errorLine1=" &lt;string name=&quot;menu.navigation&quot;>Navigation&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
message="The resource `R.drawable.media3_notification_seek_to_next` appears to be unused"
errorLine1="&lt;vector android:height=&quot;32dp&quot;"
errorLine2="^">
<location
file="src/main/res/values/strings.xml"
line="128"
column="13"/>
file="src/main/res/drawable/media3_notification_seek_to_next.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.music_service_retry` appears to be unused"
errorLine1=" &lt;string name=&quot;music_service.retry&quot;>A network error occurred. Retrying %1$d of %2$d.&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
message="The resource `R.drawable.media3_notification_seek_to_previous` appears to be unused"
errorLine1="&lt;vector android:height=&quot;32dp&quot;"
errorLine2="^">
<location
file="src/main/res/values/strings.xml"
line="133"
column="13"/>
file="src/main/res/drawable/media3_notification_seek_to_previous.xml"
line="1"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.parser_artist_count` appears to be unused"
errorLine1=" &lt;string name=&quot;parser.artist_count&quot;>Got %d Artists.&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
message="The resource `R.drawable.media3_notification_small_icon` appears to be unused"
errorLine1="&lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/values/strings.xml"
line="134"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.parser_reading` appears to be unused"
errorLine1=" &lt;string name=&quot;parser.reading&quot;>Reading from server.&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="135"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.parser_reading_done` appears to be unused"
errorLine1=" &lt;string name=&quot;parser.reading_done&quot;>Reading from server. Done!&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="136"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.progress_wait` appears to be unused"
errorLine1=" &lt;string name=&quot;progress.wait&quot;>Please wait&amp;#8230;&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="141"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.search_search` appears to be unused"
errorLine1=" &lt;string name=&quot;search.search&quot;>Click to search&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="147"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.service_connecting` appears to be unused"
errorLine1=" &lt;string name=&quot;service.connecting&quot;>Contacting server, please wait.&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="159"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.settings_allow_self_signed_certificate` appears to be unused"
errorLine1=" &lt;string name=&quot;settings.allow_self_signed_certificate&quot; translatable=&quot;false&quot;>allowSelfSignedCertificate&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="160"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.settings_enable_ldap_user_support` appears to be unused"
errorLine1=" &lt;string name=&quot;settings.enable_ldap_user_support&quot; translatable=&quot;false&quot;>enableLdapUserSupport&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="161"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.settings_invalid_username` appears to be unused"
errorLine1=" &lt;string name=&quot;settings.invalid_username&quot;>Please specify a valid username (no trailing spaces).&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="230"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.settings_server_remove_server` appears to be unused"
errorLine1=" &lt;string name=&quot;settings.server_remove_server&quot;>Remove Server&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="299"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.settings_server_unused` appears to be unused"
errorLine1=" &lt;string name=&quot;settings.server_unused&quot;>Unused&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="302"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.string.settings_server_address_unset` appears to be unused"
errorLine1=" &lt;string name=&quot;settings.server_address_unset&quot; translatable=&quot;false&quot;>http://example.com&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="387"
column="13"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.plurals.select_album_donate_dialog_n_trial_days_left` appears to be unused"
errorLine1=" &lt;plurals name=&quot;select_album_donate_dialog_n_trial_days_left&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="447"
column="14"/>
file="src/main/res/drawable/media3_notification_small_icon.xml"
line="1"
column="1"/>
</issue>
<issue
@ -838,39 +519,6 @@
column="6"/>
</issue>
<issue
id="ContentDescription"
message="Missing `contentDescription` attribute on image"
errorLine1=" &lt;ImageView a:id=&quot;@+id/help_back&quot;"
errorLine2=" ~~~~~~~~~">
<location
file="src/main/res/layout/help.xml"
line="12"
column="10"/>
</issue>
<issue
id="ContentDescription"
message="Missing `contentDescription` attribute on image"
errorLine1=" &lt;ImageView a:id=&quot;@+id/help_stop&quot;"
errorLine2=" ~~~~~~~~~">
<location
file="src/main/res/layout/help.xml"
line="18"
column="10"/>
</issue>
<issue
id="ContentDescription"
message="Missing `contentDescription` attribute on image"
errorLine1=" &lt;ImageView a:id=&quot;@+id/help_forward&quot;"
errorLine2=" ~~~~~~~~~">
<location
file="src/main/res/layout/help.xml"
line="24"
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`"
@ -1025,17 +673,6 @@
column="13"/>
</issue>
<issue
id="RelativeOverlap"
message="`LinearLayout-3` can overlap `LinearLayout-1` if LinearLayout-1, LinearLayout-3 grow due to localized text expansion"
errorLine1=" &lt;LinearLayout"
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/main/res/layout/player_media_info.xml"
line="52"
column="6"/>
</issue>
<issue
id="RelativeOverlap"
message="`@id/current_playing_duration` can overlap `@id/current_playing_position` if @string/util.no_time, @string/util.no_time grow due to localized text expansion"

View File

@ -0,0 +1,124 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "4cea788a99b9bc28500948b1cd92e537",
"entities": [
{
"tableName": "ServerSetting",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "userName",
"columnName": "userName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "jukeboxByDefault",
"columnName": "jukeboxByDefault",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "allowSelfSignedCertificate",
"columnName": "allowSelfSignedCertificate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ldapSupport",
"columnName": "ldapSupport",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "musicFolderId",
"columnName": "musicFolderId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "minimumApiVersion",
"columnName": "minimumApiVersion",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "chatSupport",
"columnName": "chatSupport",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bookmarkSupport",
"columnName": "bookmarkSupport",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "shareSupport",
"columnName": "shareSupport",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "podcastSupport",
"columnName": "podcastSupport",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4cea788a99b9bc28500948b1cd92e537')"
]
}
}

View File

@ -0,0 +1,146 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "0580217b1e87b02d2edaf9b008891cbc",
"entities": [
{
"tableName": "artists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumCount",
"columnName": "albumCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "indexes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumCount",
"columnName": "albumCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "musicFolderId",
"columnName": "musicFolderId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "music_folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0580217b1e87b02d2edaf9b008891cbc')"
]
}
}

View File

@ -0,0 +1,474 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "b6ac795e7857eac4fed2dbbd01f80fb8",
"entities": [
{
"tableName": "artists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumCount",
"columnName": "albumCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "albums",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `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`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parent",
"columnName": "parent",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "discNumber",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "songCount",
"columnName": "songCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDirectory",
"columnName": "isDirectory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isVideo",
"columnName": "isVideo",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "tracks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `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`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parent",
"columnName": "parent",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDirectory",
"columnName": "isDirectory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "albumId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "contentType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcodedContentType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcodedSuffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "songCount",
"columnName": "songCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitRate",
"columnName": "bitRate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "isVideo",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "discNumber",
"columnName": "discNumber",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmarkPosition",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "userRating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "averageRating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "indexes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumCount",
"columnName": "albumCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "musicFolderId",
"columnName": "musicFolderId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "music_folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b6ac795e7857eac4fed2dbbd01f80fb8')"
]
}
}

View File

@ -0,0 +1,514 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "95e83d6663a862c03ac46f9567453ded",
"entities": [
{
"tableName": "artists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumCount",
"columnName": "albumCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id",
"serverId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "albums",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "parent",
"columnName": "parent",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "discNumber",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "songCount",
"columnName": "songCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDirectory",
"columnName": "isDirectory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isVideo",
"columnName": "isVideo",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id",
"serverId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "tracks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "parent",
"columnName": "parent",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDirectory",
"columnName": "isDirectory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "albumId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "contentType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcodedContentType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcodedSuffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "songCount",
"columnName": "songCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitRate",
"columnName": "bitRate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "isVideo",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "discNumber",
"columnName": "discNumber",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmarkPosition",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "userRating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "averageRating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"serverId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "indexes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumCount",
"columnName": "albumCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "musicFolderId",
"columnName": "musicFolderId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"serverId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "music_folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT NOT NULL, PRIMARY KEY(`id`, `serverId`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id",
"serverId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '95e83d6663a862c03ac46f9567453ded')"
]
}
}

View File

@ -21,6 +21,8 @@
<application
android:allowBackup="false"
android:fullBackupContent="@xml/backup_descriptor"
android:dataExtractionRules="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/NoActionBar"
@ -40,8 +42,8 @@
<activity android:name=".activity.NavigationActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/common.appname"
android:launchMode="singleTask">
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SEARCH"/>
@ -57,28 +59,25 @@
</activity>
<service
android:name=".service.MediaPlayerService"
android:name=".service.DownloadService"
android:label="Ultrasonic Media Player Service"
android:exported="false">
</service>
<service
tools:ignore="ExportedService"
android:name=".service.AutoMediaBrowserService"
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
<service android:name=".playback.PlaybackService"
android:label="@string/common.appname"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver android:name=".receiver.MediaButtonIntentReceiver">
<intent-filter android:priority="2147483647">
<action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</receiver>
<receiver android:name=".receiver.UltrasonicIntentReceiver">
<receiver android:name=".receiver.UltrasonicIntentReceiver"
android:exported="true">
<intent-filter>
<action android:name="org.moire.ultrasonic.CMD_TOGGLEPAUSE"/>
<action android:name="org.moire.ultrasonic.CMD_PLAY"/>
@ -90,7 +89,8 @@
<action android:name="org.moire.ultrasonic.CMD_PROCESS_KEYCODE"/>
</intent-filter>
</receiver>
<receiver android:name=".receiver.BluetoothIntentReceiver">
<receiver android:name=".receiver.BluetoothIntentReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.bluetooth.device.action.ACL_CONNECTED"/>
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED"/>
@ -100,7 +100,8 @@
</receiver>
<receiver
android:name=".provider.UltrasonicAppWidgetProvider4X1"
android:label="Ultrasonic (4x1)">
android:label="Ultrasonic (4x1)"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
@ -111,7 +112,8 @@
</receiver>
<receiver
android:name=".provider.UltrasonicAppWidgetProvider4X2"
android:label="Ultrasonic (4x2)">
android:label="Ultrasonic (4x2)"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
@ -122,7 +124,8 @@
</receiver>
<receiver
android:name=".provider.UltrasonicAppWidgetProvider4X3"
android:label="Ultrasonic (4x3)">
android:label="Ultrasonic (4x3)"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
@ -133,7 +136,8 @@
</receiver>
<receiver
android:name=".provider.UltrasonicAppWidgetProvider4X4"
android:label="Ultrasonic (4x4)">
android:label="Ultrasonic (4x4)"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
@ -142,18 +146,17 @@
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info_4x4"/>
</receiver>
<provider
android:name=".provider.SearchSuggestionProvider"
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/>
<receiver
android:name=".receiver.A2dpIntentReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.android.music.playstatusrequest"/>
<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="org.moire.ultrasonic.provider.SearchSuggestionProvider"
android:exported="true" />
</application>
</manifest>

View File

@ -1,221 +0,0 @@
package org.moire.ultrasonic.provider;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.os.Environment;
import android.view.KeyEvent;
import android.widget.RemoteViews;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.activity.NavigationActivity;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.imageloader.BitmapUtils;
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.service.MediaPlayerController;
import org.moire.ultrasonic.util.Constants;
import timber.log.Timber;
/**
* Widget Provider for the Ultrasonic Widgets
*/
public class UltrasonicAppWidgetProvider extends AppWidgetProvider
{
protected int layoutId;
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
{
defaultAppWidget(context, appWidgetIds);
}
/**
* Initialize given widgets to default state, where we launch Ultrasonic on default click
* and hide actions if service not running.
*/
private void defaultAppWidget(Context context, int[] appWidgetIds)
{
final Resources res = context.getResources();
final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId);
views.setTextViewText(R.id.title, null);
views.setTextViewText(R.id.album, null);
views.setTextViewText(R.id.artist, res.getText(R.string.widget_initial_text));
linkButtons(context, views, false);
pushUpdate(context, appWidgetIds, views);
}
private void pushUpdate(Context context, int[] appWidgetIds, RemoteViews views)
{
// Update specific list of appWidgetIds if given, otherwise default to all
final AppWidgetManager manager = AppWidgetManager.getInstance(context);
if (manager != null)
{
if (appWidgetIds != null)
{
manager.updateAppWidget(appWidgetIds, views);
}
else
{
manager.updateAppWidget(new ComponentName(context, this.getClass()), views);
}
}
}
/**
* Handle a change notification coming over from {@link MediaPlayerController}
*/
public void notifyChange(Context context, MusicDirectory.Entry currentSong, boolean playing, boolean setAlbum)
{
if (hasInstances(context))
{
performUpdate(context, currentSong, playing, setAlbum);
}
}
/**
* Check against {@link AppWidgetManager} if there are any instances of this widget.
*/
private boolean hasInstances(Context context)
{
AppWidgetManager manager = AppWidgetManager.getInstance(context);
if (manager != null)
{
int[] appWidgetIds = manager.getAppWidgetIds(new ComponentName(context, getClass()));
return (appWidgetIds.length > 0);
}
return false;
}
/**
* Update all active widget instances by pushing changes
*/
private void performUpdate(Context context, MusicDirectory.Entry currentSong, boolean playing, boolean setAlbum)
{
final Resources res = context.getResources();
final RemoteViews views = new RemoteViews(context.getPackageName(), this.layoutId);
String title = currentSong == null ? null : currentSong.getTitle();
String artist = currentSong == null ? null : currentSong.getArtist();
String album = currentSong == null ? null : currentSong.getAlbum();
CharSequence errorState = null;
// Show error message?
String status = Environment.getExternalStorageState();
if (status.equals(Environment.MEDIA_SHARED) || status.equals(Environment.MEDIA_UNMOUNTED))
{
errorState = res.getText(R.string.widget_sdcard_busy);
}
else if (status.equals(Environment.MEDIA_REMOVED))
{
errorState = res.getText(R.string.widget_sdcard_missing);
}
else if (currentSong == null)
{
errorState = res.getText(R.string.widget_initial_text);
}
if (errorState != null)
{
// Show error state to user
views.setTextViewText(R.id.title, null);
views.setTextViewText(R.id.artist, errorState);
if (setAlbum)
{
views.setTextViewText(R.id.album, null);
}
views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album);
}
else
{
// No error, so show normal titles
views.setTextViewText(R.id.title, title);
views.setTextViewText(R.id.artist, artist);
if (setAlbum)
{
views.setTextViewText(R.id.album, album);
}
}
// Set correct drawable for pause state
if (playing)
{
views.setImageViewResource(R.id.control_play, R.drawable.media_pause_normal_dark);
}
else
{
views.setImageViewResource(R.id.control_play, R.drawable.media_start_normal_dark);
}
// Set the cover art
try
{
Bitmap bitmap = currentSong == null ? null : BitmapUtils.Companion.getAlbumArtBitmapFromDisk(currentSong, 240);
if (bitmap == null)
{
// Set default cover art
views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album);
}
else
{
views.setImageViewBitmap(R.id.appwidget_coverart, bitmap);
}
}
catch (Exception x)
{
Timber.e(x, "Failed to load cover art");
views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album);
}
// Link actions buttons to intents
linkButtons(context, views, currentSong != null);
pushUpdate(context, null, views);
}
/**
* Link up various button actions using {@link PendingIntent}.
*/
private static void linkButtons(Context context, RemoteViews views, boolean playerActive)
{
Intent intent = new Intent(context, NavigationActivity.class).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
if (playerActive)
intent.putExtra(Constants.INTENT_SHOW_PLAYER, true);
intent.setAction("android.intent.action.MAIN");
intent.addCategory("android.intent.category.LAUNCHER");
PendingIntent pendingIntent = PendingIntent.getActivity(context, 10, intent, PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent);
views.setOnClickPendingIntent(R.id.appwidget_top, pendingIntent);
// Emulate media button clicks.
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, 0);
views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT));
pendingIntent = PendingIntent.getBroadcast(context, 12, intent, 0);
views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
intent = new Intent(Constants.CMD_PROCESS_KEYCODE);
intent.setComponent(new ComponentName(context, MediaButtonIntentReceiver.class));
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS));
pendingIntent = PendingIntent.getBroadcast(context, 13, intent, 0);
views.setOnClickPendingIntent(R.id.control_previous, pendingIntent);
}
}

View File

@ -1,42 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2010 (C) Sindre Mehus
*/
package org.moire.ultrasonic.provider;
import org.moire.ultrasonic.R;
public class UltrasonicAppWidgetProvider4X1 extends UltrasonicAppWidgetProvider
{
public UltrasonicAppWidgetProvider4X1()
{
super();
this.layoutId = R.layout.appwidget4x1;
}
private static UltrasonicAppWidgetProvider4X1 instance;
public static synchronized UltrasonicAppWidgetProvider4X1 getInstance()
{
if (instance == null)
{
instance = new UltrasonicAppWidgetProvider4X1();
}
return instance;
}
}

View File

@ -1,42 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2010 (C) Sindre Mehus
*/
package org.moire.ultrasonic.provider;
import org.moire.ultrasonic.R;
public class UltrasonicAppWidgetProvider4X2 extends UltrasonicAppWidgetProvider
{
public UltrasonicAppWidgetProvider4X2()
{
super();
this.layoutId = R.layout.appwidget4x2;
}
private static UltrasonicAppWidgetProvider4X2 instance;
public static synchronized UltrasonicAppWidgetProvider4X2 getInstance()
{
if (instance == null)
{
instance = new UltrasonicAppWidgetProvider4X2();
}
return instance;
}
}

View File

@ -1,42 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2010 (C) Sindre Mehus
*/
package org.moire.ultrasonic.provider;
import org.moire.ultrasonic.R;
public class UltrasonicAppWidgetProvider4X3 extends UltrasonicAppWidgetProvider
{
public UltrasonicAppWidgetProvider4X3()
{
super();
this.layoutId = R.layout.appwidget4x3;
}
private static UltrasonicAppWidgetProvider4X3 instance;
public static synchronized UltrasonicAppWidgetProvider4X3 getInstance()
{
if (instance == null)
{
instance = new UltrasonicAppWidgetProvider4X3();
}
return instance;
}
}

View File

@ -1,42 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2010 (C) Sindre Mehus
*/
package org.moire.ultrasonic.provider;
import org.moire.ultrasonic.R;
public class UltrasonicAppWidgetProvider4X4 extends UltrasonicAppWidgetProvider
{
public UltrasonicAppWidgetProvider4X4()
{
super();
this.layoutId = R.layout.appwidget4x4;
}
private static UltrasonicAppWidgetProvider4X4 instance;
public static synchronized UltrasonicAppWidgetProvider4X4 getInstance()
{
if (instance == null)
{
instance = new UltrasonicAppWidgetProvider4X4();
}
return instance;
}
}

View File

@ -1,57 +0,0 @@
package org.moire.ultrasonic.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.service.MediaPlayerController;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
public class A2dpIntentReceiver extends BroadcastReceiver
{
private static final String PLAYSTATUS_RESPONSE = "com.android.music.playstatusresponse";
private Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
@Override
public void onReceive(Context context, Intent intent)
{
if (mediaPlayerControllerLazy.getValue().getCurrentPlaying() == null) return;
Entry song = mediaPlayerControllerLazy.getValue().getCurrentPlaying().getSong();
if (song == null) return;
Intent avrcpIntent = new Intent(PLAYSTATUS_RESPONSE);
Integer duration = song.getDuration();
int playerPosition = mediaPlayerControllerLazy.getValue().getPlayerPosition();
int listSize = mediaPlayerControllerLazy.getValue().getPlaylistSize();
if (duration != null)
{
avrcpIntent.putExtra("duration", (long) duration);
}
avrcpIntent.putExtra("position", (long) playerPosition);
avrcpIntent.putExtra("ListSize", (long) listSize);
switch (mediaPlayerControllerLazy.getValue().getPlayerState())
{
case STARTED:
avrcpIntent.putExtra("playing", true);
break;
case STOPPED:
case PAUSED:
case COMPLETED:
avrcpIntent.putExtra("playing", false);
break;
default:
return;
}
context.sendBroadcast(avrcpIntent);
}
}

View File

@ -1,83 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2010 (C) Sindre Mehus
*/
package org.moire.ultrasonic.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import timber.log.Timber;
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Settings;
import org.moire.ultrasonic.util.Util;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* @author Sindre Mehus
*/
public class MediaButtonIntentReceiver extends BroadcastReceiver
{
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
@Override
public void onReceive(Context context, Intent intent)
{
String intentAction = intent.getAction();
// If media button are turned off and we received a media button, exit
if (!Settings.getMediaButtonsEnabled() && Intent.ACTION_MEDIA_BUTTON.equals(intentAction))
return;
// Only process media buttons and CMD_PROCESS_KEYCODE, which is received from the widgets
if (!Intent.ACTION_MEDIA_BUTTON.equals(intentAction) &&
!Constants.CMD_PROCESS_KEYCODE.equals(intentAction)) return;
Bundle extras = intent.getExtras();
if (extras == null)
{
return;
}
Parcelable event = (Parcelable) extras.get(Intent.EXTRA_KEY_EVENT);
Timber.i("Got MEDIA_BUTTON key event: %s", event);
try
{
Intent serviceIntent = new Intent(Constants.CMD_PROCESS_KEYCODE);
serviceIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
lifecycleSupport.getValue().receiveIntent(serviceIntent);
if (isOrderedBroadcast())
{
abortBroadcast();
}
}
catch (Exception x)
{
// Ignored.
}
}
}

View File

@ -1,489 +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.service;
import android.content.Context;
import android.os.Handler;
import timber.log.Timber;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.Toast;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException;
import org.moire.ultrasonic.app.UApp;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.JukeboxStatus;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.util.Util;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Provides an asynchronous interface to the remote jukebox on the Subsonic server.
*
* @author Sindre Mehus
* @version $Id$
*/
public class JukeboxMediaPlayer
{
private static final long STATUS_UPDATE_INTERVAL_SECONDS = 5L;
private final TaskQueue tasks = new TaskQueue();
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private ScheduledFuture<?> statusUpdateFuture;
private final AtomicLong timeOfLastUpdate = new AtomicLong();
private JukeboxStatus jukeboxStatus;
private float gain = 0.5f;
private VolumeToast volumeToast;
private final AtomicBoolean running = new AtomicBoolean();
private Thread serviceThread;
private boolean enabled = false;
// TODO: These create circular references, try to refactor
private final Lazy<MediaPlayerController> mediaPlayerControllerLazy = inject(MediaPlayerController.class);
private final Downloader downloader;
// TODO: Report warning if queue fills up.
// TODO: Create shutdown method?
// TODO: Disable repeat.
// TODO: Persist RC state?
// TODO: Minimize status updates.
public JukeboxMediaPlayer(Downloader downloader)
{
this.downloader = downloader;
}
public void startJukeboxService()
{
if (running.get())
{
return;
}
running.set(true);
startProcessTasks();
Timber.d("Started Jukebox Service");
}
public void stopJukeboxService()
{
running.set(false);
Util.sleepQuietly(1000);
if (serviceThread != null)
{
serviceThread.interrupt();
}
Timber.d("Stopped Jukebox Service");
}
private void startProcessTasks()
{
serviceThread = new Thread()
{
@Override
public void run()
{
processTasks();
}
};
serviceThread.start();
}
private synchronized void startStatusUpdate()
{
stopStatusUpdate();
Runnable updateTask = new Runnable()
{
@Override
public void run()
{
tasks.remove(GetStatus.class);
tasks.add(new GetStatus());
}
};
statusUpdateFuture = executorService.scheduleWithFixedDelay(updateTask, STATUS_UPDATE_INTERVAL_SECONDS, STATUS_UPDATE_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
private synchronized void stopStatusUpdate()
{
if (statusUpdateFuture != null)
{
statusUpdateFuture.cancel(false);
statusUpdateFuture = null;
}
}
private void processTasks()
{
while (running.get())
{
JukeboxTask task = null;
try
{
if (!ActiveServerProvider.Companion.isOffline())
{
task = tasks.take();
JukeboxStatus status = task.execute();
onStatusUpdate(status);
}
}
catch (InterruptedException ignored)
{
}
catch (Throwable x)
{
onError(task, x);
}
Util.sleepQuietly(1);
}
}
private void onStatusUpdate(JukeboxStatus jukeboxStatus)
{
timeOfLastUpdate.set(System.currentTimeMillis());
this.jukeboxStatus = jukeboxStatus;
// Track change?
Integer index = jukeboxStatus.getCurrentPlayingIndex();
if (index != null && index != -1 && index != downloader.getCurrentPlayingIndex())
{
mediaPlayerControllerLazy.getValue().setCurrentPlaying(index);
}
}
private void onError(JukeboxTask task, Throwable x)
{
if (x instanceof ApiNotSupportedException && !(task instanceof Stop))
{
disableJukeboxOnError(x, R.string.download_jukebox_server_too_old);
}
else if (x instanceof OfflineException && !(task instanceof Stop))
{
disableJukeboxOnError(x, R.string.download_jukebox_offline);
}
else if (x instanceof SubsonicRESTException && ((SubsonicRESTException) x).getCode() == 50 && !(task instanceof Stop))
{
disableJukeboxOnError(x, R.string.download_jukebox_not_authorized);
}
else
{
Timber.e(x, "Failed to process jukebox task");
}
}
private void disableJukeboxOnError(Throwable x, final int resourceId)
{
Timber.w(x.toString());
Context context = UApp.Companion.applicationContext();
new Handler().post(() -> Util.toast(context, resourceId, false));
mediaPlayerControllerLazy.getValue().setJukeboxEnabled(false);
}
public void updatePlaylist()
{
if (!enabled) return;
tasks.remove(Skip.class);
tasks.remove(Stop.class);
tasks.remove(Start.class);
List<String> ids = new ArrayList<>();
for (DownloadFile file : downloader.getAll())
{
ids.add(file.getSong().getId());
}
tasks.add(new SetPlaylist(ids));
}
public void skip(final int index, final int offsetSeconds)
{
tasks.remove(Skip.class);
tasks.remove(Stop.class);
tasks.remove(Start.class);
startStatusUpdate();
if (jukeboxStatus != null)
{
jukeboxStatus.setPositionSeconds(offsetSeconds);
}
tasks.add(new Skip(index, offsetSeconds));
mediaPlayerControllerLazy.getValue().setPlayerState(PlayerState.STARTED);
}
public void stop()
{
tasks.remove(Stop.class);
tasks.remove(Start.class);
stopStatusUpdate();
tasks.add(new Stop());
}
public void start()
{
tasks.remove(Stop.class);
tasks.remove(Start.class);
startStatusUpdate();
tasks.add(new Start());
}
public synchronized void adjustVolume(boolean up)
{
float delta = up ? 0.05f : -0.05f;
gain += delta;
gain = Math.max(gain, 0.0f);
gain = Math.min(gain, 1.0f);
tasks.remove(SetGain.class);
tasks.add(new SetGain(gain));
Context context = UApp.Companion.applicationContext();
if (volumeToast == null) volumeToast = new VolumeToast(context);
volumeToast.setVolume(gain);
}
private MusicService getMusicService()
{
return MusicServiceFactory.getMusicService();
}
public int getPositionSeconds()
{
if (jukeboxStatus == null || jukeboxStatus.getPositionSeconds() == null || timeOfLastUpdate.get() == 0)
{
return 0;
}
if (jukeboxStatus.isPlaying())
{
int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - timeOfLastUpdate.get()) / 1000L);
return jukeboxStatus.getPositionSeconds() + secondsSinceLastUpdate;
}
return jukeboxStatus.getPositionSeconds();
}
public void setEnabled(boolean enabled)
{
Timber.d("Jukebox Service setting enabled to %b", enabled);
this.enabled = enabled;
tasks.clear();
if (enabled)
{
updatePlaylist();
}
stop();
}
public boolean isEnabled()
{
return enabled;
}
private static class TaskQueue
{
private final LinkedBlockingQueue<JukeboxTask> queue = new LinkedBlockingQueue<>();
void add(JukeboxTask jukeboxTask)
{
queue.add(jukeboxTask);
}
JukeboxTask take() throws InterruptedException
{
return queue.take();
}
void remove(Class<? extends JukeboxTask> taskClass)
{
try
{
Iterator<JukeboxTask> iterator = queue.iterator();
while (iterator.hasNext())
{
JukeboxTask task = iterator.next();
if (taskClass.equals(task.getClass()))
{
iterator.remove();
}
}
}
catch (Throwable x)
{
Timber.w(x, "Failed to clean-up task queue.");
}
}
void clear()
{
queue.clear();
}
}
private abstract static class JukeboxTask
{
abstract JukeboxStatus execute() throws Exception;
@NotNull
@Override
public String toString()
{
return getClass().getSimpleName();
}
}
private class GetStatus extends JukeboxTask
{
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().getJukeboxStatus();
}
}
private class SetPlaylist extends JukeboxTask
{
private final List<String> ids;
SetPlaylist(List<String> ids)
{
this.ids = ids;
}
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().updateJukeboxPlaylist(ids);
}
}
private class Skip extends JukeboxTask
{
private final int index;
private final int offsetSeconds;
Skip(int index, int offsetSeconds)
{
this.index = index;
this.offsetSeconds = offsetSeconds;
}
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().skipJukebox(index, offsetSeconds);
}
}
private class Stop extends JukeboxTask
{
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().stopJukebox();
}
}
private class Start extends JukeboxTask
{
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().startJukebox();
}
}
private class SetGain extends JukeboxTask
{
private final float gain;
private SetGain(float gain)
{
this.gain = gain;
}
@Override
JukeboxStatus execute() throws Exception
{
return getMusicService().setJukeboxGain(gain);
}
}
private static class VolumeToast extends Toast
{
private final ProgressBar progressBar;
public VolumeToast(Context context)
{
super(context);
setDuration(Toast.LENGTH_SHORT);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.jukebox_volume, null);
progressBar = (ProgressBar) view.findViewById(R.id.jukebox_volume_progress_bar);
setView(view);
setGravity(Gravity.TOP, 0, 0);
}
public void setVolume(float volume)
{
progressBar.setProgress(Math.round(100 * volume));
show();
}
}
}

View File

@ -18,7 +18,7 @@ public class Scrobbler
{
if (song == null || !ActiveServerProvider.Companion.isScrobblingEnabled()) return;
final String id = song.getSong().getId();
final String id = song.getTrack().getId();
if (id == null) return;
// Avoid duplicate registrations.

View File

@ -1,6 +1,6 @@
package org.moire.ultrasonic.service;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.Track;
import java.io.Serializable;
import java.util.ArrayList;
@ -13,7 +13,7 @@ public class State implements Serializable
{
public static final long serialVersionUID = -6346438781062572270L;
public List<MusicDirectory.Entry> songs = new ArrayList<>();
public List<Track> songs = new ArrayList<>();
public int currentPlayingIndex;
public int currentPlayingPosition;
}

View File

@ -1,6 +1,6 @@
package org.moire.ultrasonic.util;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.Track;
import java.util.List;
@ -12,5 +12,5 @@ public class ShareDetails
public String Description;
public boolean ShareOnServer;
public long Expiration;
public List<MusicDirectory.Entry> Entries;
public List<Track> Entries;
}

View File

@ -1,124 +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 org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import timber.log.Timber;
/**
* @author Sindre Mehus
* @version $Id$
*/
public class ShufflePlayBuffer
{
private static final int CAPACITY = 50;
private static final int REFILL_THRESHOLD = 40;
private final List<MusicDirectory.Entry> buffer = new ArrayList<>();
private ScheduledExecutorService executorService;
private int currentServer;
public boolean isEnabled = false;
public ShufflePlayBuffer()
{
}
public void onCreate()
{
executorService = Executors.newSingleThreadScheduledExecutor();
Runnable runnable = this::refill;
executorService.scheduleWithFixedDelay(runnable, 1, 10, TimeUnit.SECONDS);
Timber.i("ShufflePlayBuffer created");
}
public void onDestroy()
{
executorService.shutdown();
Timber.i("ShufflePlayBuffer destroyed");
}
public List<MusicDirectory.Entry> get(int size)
{
clearBufferIfNecessary();
List<MusicDirectory.Entry> result = new ArrayList<>(size);
synchronized (buffer)
{
while (!buffer.isEmpty() && result.size() < size)
{
result.add(buffer.remove(buffer.size() - 1));
}
}
Timber.i("Taking %d songs from shuffle play buffer. %d remaining.", result.size(), buffer.size());
return result;
}
private void refill()
{
if (!isEnabled) return;
// Check if active server has changed.
clearBufferIfNecessary();
if (buffer.size() > REFILL_THRESHOLD || (!Util.isNetworkConnected() && !ActiveServerProvider.Companion.isOffline()))
{
return;
}
try
{
MusicService service = MusicServiceFactory.getMusicService();
int n = CAPACITY - buffer.size();
MusicDirectory songs = service.getRandomSongs(n);
synchronized (buffer)
{
buffer.addAll(songs.getTracks());
Timber.i("Refilled shuffle play buffer with %d songs.", songs.getTracks().size());
}
}
catch (Exception x)
{
Timber.w(x, "Failed to refill shuffle play buffer.");
}
}
private void clearBufferIfNecessary()
{
synchronized (buffer)
{
if (currentServer != ActiveServerProvider.Companion.getActiveServerId())
{
currentServer = ActiveServerProvider.Companion.getActiveServerId();
buffer.clear();
}
}
}
}

View File

@ -1,290 +0,0 @@
package org.moire.ultrasonic.util;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.Supplier;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.util.StringTokenizer;
import timber.log.Timber;
public class StreamProxy implements Runnable
{
private Thread thread;
private boolean isRunning;
private ServerSocket socket;
private int port;
private Supplier<DownloadFile> currentPlaying;
public StreamProxy(Supplier<DownloadFile> currentPlaying)
{
// Create listening socket
try
{
socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
socket.setSoTimeout(5000);
port = socket.getLocalPort();
this.currentPlaying = currentPlaying;
}
catch (UnknownHostException e)
{ // impossible
}
catch (IOException e)
{
Timber.e(e, "IOException initializing server");
}
}
public int getPort()
{
return port;
}
public void start()
{
thread = new Thread(this);
thread.start();
}
public void stop()
{
isRunning = false;
thread.interrupt();
}
@Override
public void run()
{
isRunning = true;
while (isRunning)
{
try
{
Socket client = socket.accept();
if (client == null)
{
continue;
}
Timber.i("Client connected");
StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client);
if (task.processRequest())
{
new Thread(task).start();
}
}
catch (SocketTimeoutException e)
{
// Do nothing
}
catch (IOException e)
{
Timber.e(e, "Error connecting to client");
}
}
Timber.i("Proxy interrupted. Shutting down.");
}
private class StreamToMediaPlayerTask implements Runnable {
String localPath;
Socket client;
int cbSkip;
StreamToMediaPlayerTask(Socket client) {
this.client = client;
}
private String readRequest() {
InputStream is;
String firstLine;
try {
is = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192);
firstLine = reader.readLine();
} catch (IOException e) {
Timber.e(e, "Error parsing request");
return null;
}
if (firstLine == null) {
Timber.i("Proxy client closed connection without a request.");
return null;
}
StringTokenizer st = new StringTokenizer(firstLine);
st.nextToken(); // method
String uri = st.nextToken();
String realUri = uri.substring(1);
Timber.i(realUri);
return realUri;
}
boolean processRequest() {
final String uri = readRequest();
if (uri == null || uri.isEmpty()) {
return false;
}
// Read HTTP headers
Timber.i("Processing request: %s", uri);
try {
localPath = URLDecoder.decode(uri, Constants.UTF_8);
} catch (UnsupportedEncodingException e) {
Timber.e(e, "Unsupported encoding");
return false;
}
Timber.i("Processing request for file %s", localPath);
if (Storage.INSTANCE.isPathExists(localPath)) return true;
// Usually the .partial file will be requested here, but sometimes it has already
// been renamed, so check if it is completed since
String saveFileName = FileUtil.INSTANCE.getSaveFile(localPath);
String completeFileName = FileUtil.INSTANCE.getCompleteFile(saveFileName);
if (Storage.INSTANCE.isPathExists(saveFileName)) {
localPath = saveFileName;
return true;
}
if (Storage.INSTANCE.isPathExists(completeFileName)) {
localPath = completeFileName;
return true;
}
Timber.e("File %s does not exist", localPath);
return false;
}
@Override
public void run()
{
Timber.i("Streaming song in background");
DownloadFile downloadFile = currentPlaying == null? null : currentPlaying.get();
MusicDirectory.Entry song = downloadFile.getSong();
long fileSize = downloadFile.getBitRate() * ((song.getDuration() != null) ? song.getDuration() : 0) * 1000 / 8;
Timber.i("Streaming fileSize: %d", fileSize);
// Create HTTP header
String headers = "HTTP/1.0 200 OK\r\n";
headers += "Content-Type: application/octet-stream\r\n";
headers += "Connection: close\r\n";
headers += "\r\n";
long cbToSend = fileSize - cbSkip;
OutputStream output = null;
byte[] buff = new byte[64 * 1024];
try
{
output = new BufferedOutputStream(client.getOutputStream(), 32 * 1024);
output.write(headers.getBytes());
if (!downloadFile.isWorkDone())
{
// Loop as long as there's stuff to send
while (isRunning && !client.isClosed())
{
// See if there's more to send
String file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteOrSaveFile() : downloadFile.getPartialFile();
int cbSentThisBatch = 0;
AbstractFile storageFile = Storage.INSTANCE.getFromPath(file);
if (storageFile != null)
{
InputStream input = storageFile.getFileInputStream();
try
{
long skip = input.skip(cbSkip);
int cbToSendThisBatch = input.available();
while (cbToSendThisBatch > 0)
{
int cbToRead = Math.min(cbToSendThisBatch, buff.length);
int cbRead = input.read(buff, 0, cbToRead);
if (cbRead == -1)
{
break;
}
cbToSendThisBatch -= cbRead;
cbToSend -= cbRead;
output.write(buff, 0, cbRead);
output.flush();
cbSkip += cbRead;
cbSentThisBatch += cbRead;
}
}
finally
{
input.close();
}
// Done regardless of whether or not it thinks it is
if (downloadFile.isWorkDone() && cbSkip >= file.length())
{
break;
}
}
// If we did nothing this batch, block for a second
if (cbSentThisBatch == 0)
{
Timber.d("Blocking until more data appears (%d)", cbToSend);
Util.sleepQuietly(1000L);
}
}
}
else
{
Timber.w("Requesting data for completely downloaded file");
}
}
catch (SocketException socketException)
{
Timber.e("SocketException() thrown, proxy client has probably closed. This can exit harmlessly");
}
catch (Exception e)
{
Timber.e("Exception thrown from streaming task:");
Timber.e("%s : %s", e.getClass().getName(), e.getLocalizedMessage());
}
// Cleanup
try
{
if (output != null)
{
output.close();
}
client.close();
}
catch (IOException e)
{
Timber.e("IOException while cleaning up streaming task:");
Timber.e("%s : %s", e.getClass().getName(), e.getLocalizedMessage());
}
}
}
}

View File

@ -18,6 +18,8 @@
*/
package org.moire.ultrasonic.view;
import static org.koin.java.KoinJavaComponent.inject;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
@ -29,14 +31,11 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;
import org.moire.ultrasonic.audiofx.VisualizerController;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.service.MediaPlayerController;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
/**
* A simple class that draws waveform data received from a
* {@link Visualizer.OnDataCaptureListener#onWaveFormDataCapture}
@ -130,7 +129,7 @@ public class VisualizerView extends View
return;
}
if (mediaPlayerControllerLazy.getValue().getPlayerState() != PlayerState.STARTED)
if (!mediaPlayerControllerLazy.getValue().isPlaying())
{
return;
}

View File

@ -1,12 +1,14 @@
/*
* NavigationActivity.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.activity
import android.app.SearchManager
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Resources
@ -25,6 +27,9 @@ import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentContainerView
import androidx.media3.common.MediaItem
import androidx.media3.common.Player.STATE_BUFFERING
import androidx.media3.common.Player.STATE_READY
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
@ -36,23 +41,24 @@ import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.google.android.material.button.MaterialButton
import com.google.android.material.navigation.NavigationView
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSettingDao
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.fragment.OnBackPressedHandler
import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.provider.SearchSuggestionProvider
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.InfoDialog
import org.moire.ultrasonic.util.LocaleHelper
import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Storage
@ -61,7 +67,7 @@ import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* The main Activity of Ultrasonic which loads all other screens as Fragments
* The main (and only) Activity of Ultrasonic which loads all other screens as Fragments
*/
@Suppress("TooManyFunctions")
class NavigationActivity : AppCompatActivity() {
@ -78,8 +84,8 @@ class NavigationActivity : AppCompatActivity() {
private var headerBackgroundImage: ImageView? = null
private lateinit var appBarConfiguration: AppBarConfiguration
private var themeChangedEventSubscription: Disposable? = null
private var playerStateSubscription: Disposable? = null
private var rxBusSubscription: CompositeDisposable = CompositeDisposable()
private val serverSettingsModel: ServerSettingsModel by viewModel()
private val lifecycleSupport: MediaPlayerLifecycleSupport by inject()
@ -93,6 +99,16 @@ class NavigationActivity : AppCompatActivity() {
private var cachedServerCount: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
Timber.d("onCreate called")
// First check if Koin has been started
if (UApp.instance != null && !UApp.instance!!.initiated) {
Timber.d("Starting Koin")
UApp.instance!!.startKoin()
} else {
Timber.d("No need to start Koin")
}
setUncaughtExceptionHandler()
Util.applyTheme(this)
@ -154,9 +170,8 @@ class NavigationActivity : AppCompatActivity() {
setMenuForServerCapabilities()
}
// Determine first run and migrate server settings to DB as early as possible
var showWelcomeScreen = Util.isFirstRun()
val areServersMigrated: Boolean = serverSettingsModel.migrateFromPreferences()
// Determine if this is a first run
val showWelcomeScreen = Util.isFirstRun()
// Migrate Feature storage if needed
// TODO: Remove in December 2022
@ -164,9 +179,6 @@ class NavigationActivity : AppCompatActivity() {
Settings.migrateFeatureStorage()
}
// If there are any servers in the DB, do not show the welcome screen
showWelcomeScreen = showWelcomeScreen and !areServersMigrated
loadSettings()
// This is a first run with only the demo entry inside the database
@ -180,25 +192,25 @@ class NavigationActivity : AppCompatActivity() {
hideNowPlaying()
}
playerStateSubscription = RxBus.playerStateObservable.subscribe {
if (it.state === PlayerState.STARTED || it.state === PlayerState.PAUSED)
rxBusSubscription += RxBus.playerStateObservable.subscribe {
if (it.state == STATE_READY)
showNowPlaying()
else
hideNowPlaying()
}
themeChangedEventSubscription = RxBus.themeChangedEventObservable.subscribe {
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
recreate()
}
serverRepository.liveServerCount().observe(
this,
{ count ->
cachedServerCount = count ?: 0
updateNavigationHeaderForServer()
}
)
ActiveServerProvider.liveActiveServerId.observe(this, { updateNavigationHeaderForServer() })
rxBusSubscription += RxBus.activeServerChangeObservable.subscribe {
updateNavigationHeaderForServer()
}
serverRepository.liveServerCount().observe(this) { count ->
cachedServerCount = count ?: 0
updateNavigationHeaderForServer()
}
}
private fun updateNavigationHeaderForServer() {
@ -224,6 +236,7 @@ class NavigationActivity : AppCompatActivity() {
}
override fun onResume() {
Timber.d("onResume called")
super.onResume()
Storage.reset()
@ -237,10 +250,11 @@ class NavigationActivity : AppCompatActivity() {
}
override fun onDestroy() {
super.onDestroy()
themeChangedEventSubscription?.dispose()
playerStateSubscription?.dispose()
Timber.d("onDestroy called")
rxBusSubscription.dispose()
imageLoaderProvider.clearImageLoader()
UApp.instance!!.shutdownKoin()
super.onDestroy()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
@ -351,19 +365,35 @@ class NavigationActivity : AppCompatActivity() {
}
}
/**
* Apply the customized language settings if needed
*/
override fun attachBaseContext(newBase: Context?) {
val locale = Settings.overrideLanguage
val localeUpdatedContext: ContextWrapper = LocaleHelper.wrap(newBase, locale)
super.attachBaseContext(localeUpdatedContext)
}
private fun loadSettings() {
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
}
private fun exit() {
Timber.d("User choose to exit the app")
// Broadcast that the service is being shutdown
RxBus.stopCommandPublisher.onNext(Unit)
lifecycleSupport.onDestroy()
finish()
finishAndRemoveTask()
}
private fun showWelcomeDialog() {
if (!infoDialogDisplayed) {
infoDialogDisplayed = true
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
InfoDialog.Builder(this)
.setTitle(R.string.main_welcome_title)
.setMessage(R.string.main_welcome_text_demo)
@ -406,10 +436,10 @@ class NavigationActivity : AppCompatActivity() {
}
if (nowPlayingView != null) {
val playerState: PlayerState = mediaPlayerController.playerState
if (playerState == PlayerState.PAUSED || playerState == PlayerState.STARTED) {
val file: DownloadFile? = mediaPlayerController.currentPlaying
if (file != null) {
val playerState: Int = mediaPlayerController.playbackState
if (playerState == STATE_BUFFERING || playerState == STATE_READY) {
val item: MediaItem? = mediaPlayerController.currentMediaItem
if (item != null) {
nowPlayingView?.visibility = View.VISIBLE
}
} else {

View File

@ -20,7 +20,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.ItemViewBinder
import org.koin.core.component.KoinComponent
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Album
import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
@ -31,11 +31,11 @@ import timber.log.Timber
* Creates a Row in a RecyclerView which contains the details of an Album
*/
class AlbumRowBinder(
val onItemClick: (MusicDirectory.Album) -> Unit,
val onContextMenuClick: (MenuItem, MusicDirectory.Album) -> Boolean,
val onItemClick: (Album) -> Unit,
val onContextMenuClick: (MenuItem, Album) -> Boolean,
private val imageLoader: ImageLoader,
context: Context
) : ItemViewBinder<MusicDirectory.Album, AlbumRowBinder.ViewHolder>(), KoinComponent {
) : ItemViewBinder<Album, AlbumRowBinder.ViewHolder>(), KoinComponent {
private val starDrawable: Drawable =
Util.getDrawableFromAttribute(context, R.attr.star_full)
@ -46,7 +46,7 @@ class AlbumRowBinder(
val layout = R.layout.list_item_album
val contextMenuLayout = R.menu.context_menu_artist
override fun onBindViewHolder(holder: ViewHolder, item: MusicDirectory.Album) {
override fun onBindViewHolder(holder: ViewHolder, item: Album) {
holder.album.text = item.title
holder.artist.text = item.artist
holder.details.setOnClickListener { onItemClick(item) }
@ -86,7 +86,7 @@ class AlbumRowBinder(
/**
* Handles the star / unstar action for an album
*/
private fun onStarClick(entry: MusicDirectory.Album, star: ImageView) {
private fun onStarClick(entry: Album, star: ImageView) {
entry.starred = !entry.starred
star.setImageDrawable(if (entry.starred) starDrawable else starHollowDrawable)
val musicService = getMusicService()

View File

@ -1,6 +1,6 @@
/*
* ArtistRowAdapter.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* ArtistRowBinder.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -19,6 +19,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.ItemViewBinder
import org.koin.core.component.KoinComponent
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.imageloader.ImageLoader
@ -57,7 +58,7 @@ class ArtistRowBinder(
holder.coverArtId = item.coverArt
if (Settings.shouldShowArtistPicture) {
if (showArtistPicture()) {
holder.coverArt.visibility = View.VISIBLE
val key = FileUtil.getArtistArtKey(item.name, false)
imageLoader.loadImage(
@ -102,11 +103,16 @@ class ArtistRowBinder(
}
private fun getSectionFromName(name: String): String {
var section = name.first().uppercaseChar()
if (!section.isLetter()) section = '#'
if (name.isEmpty()) return SECTION_KEY_DEFAULT
val section = name.first().uppercaseChar()
if (!section.isLetter()) return SECTION_KEY_DEFAULT
return section.toString()
}
private fun showArtistPicture(): Boolean {
return ActiveServerProvider.isID3Enabled() && Settings.shouldShowArtistPicture
}
/**
* Creates an instance of our ViewHolder class
*/
@ -123,4 +129,8 @@ class ArtistRowBinder(
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
return ViewHolder(inflater.inflate(layout, parent, false))
}
companion object {
const val SECTION_KEY_DEFAULT = "#"
}
}

View File

@ -59,7 +59,7 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
throw IllegalAccessException("You must use submitList() to add data to the Adapter")
}
var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
private var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
AdapterListUpdateCallback(this),
AsyncDifferConfig.Builder(diffCallback).build()
)
@ -182,12 +182,11 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
// Select them all
getCurrentList().mapNotNullTo(
selectedSet,
{ entry ->
// Exclude any -1 ids, eg. headers and other UI elements
entry.longId.takeIf { it != -1L }
}
)
selectedSet
) { entry ->
// Exclude any -1 ids, eg. headers and other UI elements
entry.longId.takeIf { it != -1L }
}
return selectedSet.count()
}

View File

@ -1,4 +1,4 @@
package org.moire.ultrasonic.fragment
package org.moire.ultrasonic.adapters
import android.content.Context
import android.graphics.drawable.Drawable
@ -30,7 +30,7 @@ import org.moire.ultrasonic.util.Util
*/
internal class ServerRowAdapter(
private var context: Context,
private var data: Array<ServerSetting>,
passedData: Array<ServerSetting>,
private val model: ServerSettingsModel,
private val activeServerProvider: ActiveServerProvider,
private val manageMode: Boolean,
@ -38,6 +38,12 @@ internal class ServerRowAdapter(
private val serverEditRequestedCallback: ((Int) -> Unit)
) : BaseAdapter() {
private var data: MutableList<ServerSetting> = mutableListOf()
init {
setData(passedData)
}
companion object {
private const val MENU_ID_EDIT = 1
private const val MENU_ID_DELETE = 2
@ -49,12 +55,19 @@ internal class ServerRowAdapter(
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
fun setData(data: Array<ServerSetting>) {
this.data = data
this.data.clear()
// In read mode show the offline server as well
if (!manageMode) {
this.data.add(ActiveServerProvider.OFFLINE_DB)
}
this.data.addAll(data)
notifyDataSetChanged()
}
override fun getCount(): Int {
return if (manageMode) data.size else data.size + 1
return data.size
}
override fun getItem(position: Int): Any {
@ -69,11 +82,11 @@ internal class ServerRowAdapter(
* Creates the Row representation of a Server Setting
*/
@Suppress("LongMethod")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? {
var index = position
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
var position = pos
// Skip "Offline" in manage mode
if (manageMode) index++
if (manageMode) position++
var vi: View? = convertView
if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false)
@ -83,22 +96,17 @@ internal class ServerRowAdapter(
val layout = vi?.findViewById<ConstraintLayout>(R.id.server_layout)
val image = vi?.findViewById<ImageView>(R.id.server_image)
val serverMenu = vi?.findViewById<ImageButton>(R.id.server_menu)
val setting = data.singleOrNull { t -> t.index == index }
val setting = data.singleOrNull { t -> t.index == position }
if (index == 0) {
text?.text = context.getString(R.string.main_offline)
description?.text = ""
} else {
text?.text = setting?.name ?: ""
description?.text = setting?.url ?: ""
if (setting == null) serverMenu?.visibility = View.INVISIBLE
}
text?.text = setting?.name ?: ""
description?.text = setting?.url ?: ""
if (setting == null) serverMenu?.visibility = View.INVISIBLE
val icon: Drawable?
val background: Drawable?
// Configure icons for the row
if (index == 0) {
if (setting?.id == ActiveServerProvider.OFFLINE_DB_ID) {
serverMenu?.visibility = View.INVISIBLE
icon = Util.getDrawableFromAttribute(context, R.attr.screen_on_off)
background = ContextCompat.getDrawable(context, R.drawable.circle)
@ -116,7 +124,7 @@ internal class ServerRowAdapter(
image?.background = background
// Highlight the Active Server's row by changing its background
if (index == activeServerProvider.getActiveServer().index) {
if (position == activeServerProvider.getActiveServer().index) {
layout?.background = ContextCompat.getDrawable(context, R.drawable.select_ripple)
} else {
layout?.background = ContextCompat.getDrawable(context, R.drawable.default_ripple)
@ -128,7 +136,7 @@ internal class ServerRowAdapter(
R.drawable.select_ripple_circle
)
serverMenu?.setOnClickListener { view -> serverMenuClick(view, index) }
serverMenu?.setOnClickListener { view -> serverMenuClick(view, position) }
return vi
}
@ -192,7 +200,8 @@ internal class ServerRowAdapter(
return true
}
MENU_ID_DELETE -> {
serverDeletedCallback.invoke(position)
val server = getItem(position) as ServerSetting
serverDeletedCallback.invoke(server.id)
return true
}
MENU_ID_UP -> {

View File

@ -12,12 +12,12 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader
class TrackViewBinder(
val onItemClick: (DownloadFile) -> Unit,
val onItemClick: (DownloadFile, Int) -> Unit,
val onContextMenuClick: ((MenuItem, DownloadFile) -> Boolean)? = null,
val checkable: Boolean,
val draggable: Boolean,
@ -29,7 +29,7 @@ class TrackViewBinder(
// Set our layout files
val layout = R.layout.list_item_track
val contextMenuLayout = R.menu.context_menu_track
private val contextMenuLayout = R.menu.context_menu_track
private val downloader: Downloader by inject()
private val imageHelper: Utils.ImageHelper = Utils.ImageHelper(context)
@ -41,15 +41,14 @@ class TrackViewBinder(
@SuppressLint("ClickableViewAccessibility")
@Suppress("LongMethod")
override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) {
val downloadFile: DownloadFile?
val diffAdapter = adapter as BaseAdapter<*>
when (item) {
is MusicDirectory.Entry -> {
downloadFile = downloader.getDownloadFileForSong(item)
val downloadFile: DownloadFile = when (item) {
is Track -> {
downloader.getDownloadFileForSong(item)
}
is DownloadFile -> {
downloadFile = item
item
}
else -> {
return
@ -77,7 +76,7 @@ class TrackViewBinder(
}
} else {
// Minimize or maximize the Text view (if song title is very long)
if (!downloadFile.song.isDirectory) {
if (!downloadFile.track.isDirectory) {
holder.maximizeOrMinimize()
}
}
@ -86,11 +85,11 @@ class TrackViewBinder(
}
holder.itemView.setOnClickListener {
if (checkable && !downloadFile.song.isVideo) {
if (checkable && !downloadFile.track.isVideo) {
val nowChecked = !holder.check.isChecked
holder.isChecked = nowChecked
} else {
onItemClick(downloadFile)
onItemClick(downloadFile, holder.bindingAdapterPosition)
}
}
@ -103,41 +102,37 @@ class TrackViewBinder(
// Notify the adapter of selection changes
holder.observableChecked.observe(
lifecycleOwner,
{ isCheckedNow ->
if (isCheckedNow) {
diffAdapter.notifySelected(holder.entry!!.longId)
} else {
diffAdapter.notifyUnselected(holder.entry!!.longId)
}
lifecycleOwner
) { isCheckedNow ->
if (isCheckedNow) {
diffAdapter.notifySelected(holder.entry!!.longId)
} else {
diffAdapter.notifyUnselected(holder.entry!!.longId)
}
)
}
// Listen to changes in selection status and update ourselves
diffAdapter.selectionRevision.observe(
lifecycleOwner,
{
val newStatus = diffAdapter.isSelected(item.longId)
lifecycleOwner
) {
val newStatus = diffAdapter.isSelected(item.longId)
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
}
)
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
}
// Observe download status
downloadFile.status.observe(
lifecycleOwner,
{
holder.updateStatus(it)
diffAdapter.notifyChanged()
}
)
lifecycleOwner
) {
holder.updateStatus(it)
diffAdapter.notifyChanged()
}
downloadFile.progress.observe(
lifecycleOwner,
{
holder.updateProgress(it)
}
)
lifecycleOwner
) {
holder.updateProgress(it)
}
}
override fun onViewRecycled(holder: TrackViewHolder) {

View File

@ -15,7 +15,7 @@ import io.reactivex.rxjava3.disposables.Disposable
import org.koin.core.component.KoinComponent
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.DownloadStatus
import org.moire.ultrasonic.service.MusicServiceFactory
@ -44,7 +44,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
var duration: TextView = view.findViewById(R.id.song_duration)
var progress: TextView = view.findViewById(R.id.song_status)
var entry: MusicDirectory.Entry? = null
var entry: Track? = null
private set
var downloadFile: DownloadFile? = null
private set
@ -67,7 +67,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
isSelected: Boolean = false
) {
val useFiveStarRating = Settings.useFiveStarRating
val song = file.song
val song = file.track
downloadFile = file
entry = song
@ -109,7 +109,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
}
rxSubscription = RxBus.playerStateObservable.subscribe {
setPlayIcon(it.track == downloadFile)
setPlayIcon(it.index == bindingAdapterPosition && it.track == downloadFile)
}
}
@ -131,7 +131,7 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
}
}
private fun setupStarButtons(song: MusicDirectory.Entry, useFiveStarRating: Boolean) {
private fun setupStarButtons(song: Track, useFiveStarRating: Boolean) {
if (useFiveStarRating) {
// Hide single star
star.isVisible = false
@ -153,6 +153,8 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
star.setImageDrawable(imageHelper.starHollowDrawable)
song.starred = false
}
// Should this be done here ?
Thread {
val musicService = MusicServiceFactory.getMusicService()
try {

View File

@ -2,8 +2,12 @@ package org.moire.ultrasonic.app
import android.content.Context
import androidx.multidex.MultiDexApplication
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.core.logger.Level
import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.di.appPermanentStorage
@ -23,22 +27,39 @@ import timber.log.Timber.DebugTree
class UApp : MultiDexApplication() {
private var ioScope = CoroutineScope(Dispatchers.IO)
init {
instance = this
// if (BuildConfig.DEBUG)
// StrictMode.enableDefaults()
}
var initiated = false
override fun onCreate() {
initiated = true
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(DebugTree())
}
if (Settings.debugLogToFile) {
FileLoggerTree.plantToTimberForest()
Timber.d("onCreate called")
// In general we should not access the settings from the main thread to avoid blocking...
ioScope.launch {
if (Settings.debugLogToFile) {
FileLoggerTree.plantToTimberForest()
}
}
startKoin()
}
internal fun startKoin() {
startKoin {
// TODO Currently there is a bug in Koin which makes necessary to set the loglevel to ERROR
// TODO Currently there is a bug in Koin which makes necessary to set the log level to ERROR
logger(TimberKoinLogger(Level.ERROR))
// logger(TimberKoinLogger(Level.INFO))
@ -55,8 +76,13 @@ class UApp : MultiDexApplication() {
}
}
internal fun shutdownKoin() {
stopKoin()
initiated = false
}
companion object {
private var instance: UApp? = null
var instance: UApp? = null
fun applicationContext(): Context {
return instance!!.applicationContext

View File

@ -1,6 +1,12 @@
/*
* ActiveServerProvider.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.data
import androidx.lifecycle.MutableLiveData
import androidx.room.Room
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -11,6 +17,7 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.di.DB_FILENAME
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
@ -19,8 +26,6 @@ import timber.log.Timber
/**
* This class can be used to retrieve the properties of the Active Server
* It caches the settings read up from the DB to improve performance.
*
* TODO: There seems to be some confusion whether offline id is 0 or -1. Clean this up (carefully!)
*/
class ActiveServerProvider(
private val repository: ServerSettingDao
@ -35,7 +40,7 @@ class ActiveServerProvider(
*/
@JvmOverloads
fun getActiveServer(serverId: Int = getActiveServerId()): ServerSetting {
if (serverId > 0) {
if (serverId > OFFLINE_DB_ID) {
if (cachedServer != null && cachedServer!!.id == serverId) return cachedServer!!
// Ideally this is the only call where we block the thread while using the repository
@ -53,22 +58,31 @@ class ActiveServerProvider(
return cachedServer!!
}
setActiveServerId(0)
// Fallback to Offline
setActiveServerById(OFFLINE_DB_ID)
}
return ServerSetting(
id = -1,
index = 0,
name = UApp.applicationContext().getString(R.string.main_offline),
url = "http://localhost",
userName = "",
password = "",
jukeboxByDefault = false,
allowSelfSignedCertificate = false,
ldapSupport = false,
musicFolderId = "",
minimumApiVersion = null
)
return OFFLINE_DB
}
/**
* Resolves the index (sort order) of a server to its id (unique)
* @param index: The index of the server in the server selector
* @return id: The unique id of the server
*/
fun getServerIdFromIndex(index: Int): Int {
if (index <= OFFLINE_DB_INDEX) {
// Offline mode is selected
return OFFLINE_DB_ID
}
var id: Int
runBlocking {
id = repository.findByIndex(index)?.id ?: 0
}
return id
}
/**
@ -77,15 +91,15 @@ class ActiveServerProvider(
*/
fun setActiveServerByIndex(index: Int) {
Timber.d("setActiveServerByIndex $index")
if (index < 1) {
if (index <= OFFLINE_DB_INDEX) {
// Offline mode is selected
setActiveServerId(0)
setActiveServerById(OFFLINE_DB_ID)
return
}
launch {
val serverId = repository.findByIndex(index)?.id ?: 0
setActiveServerId(serverId)
setActiveServerById(serverId)
}
}
@ -103,21 +117,22 @@ class ActiveServerProvider(
Timber.i("Switching to new database, id:$activeServer")
cachedServerId = activeServer
return Room.databaseBuilder(
UApp.applicationContext(),
MetaDatabase::class.java,
METADATA_DB + cachedServerId
)
.fallbackToDestructiveMigrationOnDowngrade()
.build()
cachedDatabase = initDatabase(activeServer)
return cachedDatabase!!
}
val offlineMetaDatabase: MetaDatabase by lazy {
Room.databaseBuilder(
initDatabase(0)
}
private fun initDatabase(serverId: Int): MetaDatabase {
return Room.databaseBuilder(
UApp.applicationContext(),
MetaDatabase::class.java,
METADATA_DB + 0
METADATA_DB + serverId
)
.addMigrations(META_MIGRATION_2_3)
.fallbackToDestructiveMigrationOnDowngrade()
.build()
}
@ -177,16 +192,30 @@ class ActiveServerProvider(
}
companion object {
const val METADATA_DB = "$DB_FILENAME-meta-"
val liveActiveServerId: MutableLiveData<Int> = MutableLiveData(getActiveServerId())
const val OFFLINE_DB_ID = -1
const val OFFLINE_DB_INDEX = 0
val OFFLINE_DB = ServerSetting(
id = OFFLINE_DB_ID,
index = OFFLINE_DB_INDEX,
name = UApp.applicationContext().getString(R.string.main_offline),
url = "http://localhost",
userName = "",
password = "",
jukeboxByDefault = false,
allowSelfSignedCertificate = false,
ldapSupport = false,
musicFolderId = "",
minimumApiVersion = null
)
/**
* Queries if the Active Server is the "Offline" mode of Ultrasonic
* @return True, if the "Offline" mode is selected
*/
fun isOffline(): Boolean {
return getActiveServerId() < 1
return getActiveServerId() == OFFLINE_DB_ID
}
/**
@ -197,13 +226,16 @@ class ActiveServerProvider(
}
/**
* Sets the Id of the Active Server
* Sets the Active Server by its unique id
* @param serverId: The id of the desired server
*/
fun setActiveServerId(serverId: Int) {
fun setActiveServerById(serverId: Int) {
resetMusicService()
Settings.activeServer = serverId
liveActiveServerId.postValue(serverId)
Timber.i("setActiveServerById done, new id: %s", serverId)
RxBus.activeServerChangePublisher.onNext(serverId)
}
/**
@ -217,6 +249,13 @@ class ActiveServerProvider(
return preferences.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false)
}
/**
* Queries if ID3 tags should be used
*/
fun isID3Enabled(): Boolean {
return Settings.shouldUseId3Tags && (!isOffline() || Settings.useId3TagsOffline)
}
/**
* Queries if Server Scaling is enabled
*/

View File

@ -0,0 +1,80 @@
package org.moire.ultrasonic.data
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import org.moire.ultrasonic.domain.Album
@Dao
interface AlbumDao : GenericDao<Album> {
/**
* Clear the whole database
*/
@Query("DELETE FROM albums")
fun clear()
/**
* Get all albums
*/
@Query("SELECT * FROM albums")
fun get(): List<Album>
/**
* Get all albums in a specific range
*/
@Query("SELECT * FROM albums LIMIT :offset,:size")
fun get(size: Int, offset: Int = 0): List<Album>
/**
* Get album by id
*/
@Query("SELECT * FROM albums where id LIKE :albumId LIMIT 1")
fun get(albumId: String): Album
/**
* Get albums by artist
*/
@Query("SELECT * FROM albums WHERE artistId LIKE :id")
fun byArtist(id: String): List<Album>
/**
* Clear albums by artist
*/
@Query("DELETE FROM albums WHERE artistId LIKE :id")
fun clearByArtist(id: String)
/**
* TODO: Make generic
* Upserts (insert or update) an object to the database
*
* @param obj the object to upsert
*/
@Transaction
@JvmSuppressWildcards
fun upsert(obj: Album) {
val id = insertIgnoring(obj)
if (id == -1L) {
update(obj)
}
}
/**
* Upserts (insert or update) a list of objects
*
* @param objList the object to be upserted
*/
@Transaction
@JvmSuppressWildcards
fun upsert(objList: List<Album>) {
val insertResult = insertIgnoring(objList)
val updateList: MutableList<Album> = ArrayList()
for (i in insertResult.indices) {
if (insertResult[i] == -1L) {
updateList.add(objList[i])
}
}
if (updateList.isNotEmpty()) {
update(updateList)
}
}
}

View File

@ -9,7 +9,11 @@ import androidx.sqlite.db.SupportSQLiteDatabase
* Room Database to be used to store global data for the whole app.
* This could be settings or data that are not specific to any remote music database
*/
@Database(entities = [ServerSetting::class], version = 4)
@Database(
entities = [ServerSetting::class],
version = 5,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
/**
@ -175,3 +179,89 @@ val MIGRATION_4_3: Migration = object : Migration(4, 3) {
)
}
}
val MIGRATION_4_5: Migration = object : Migration(4, 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
)
""".trimIndent()
)
database.execSQL(
"""
INSERT INTO `_new_ServerSetting` (
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,
`podcastSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`
)
SELECT `ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,
`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,
`bookmarkSupport`,`name`,`podcastSupport`,`id`,`allowSelfSignedCertificate`,
`chatSupport`
FROM `ServerSetting`
""".trimIndent()
)
database.execSQL("DROP TABLE `ServerSetting`")
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
}
}
val MIGRATION_5_4: Migration = object : Migration(5, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
`id` INTEGER PRIMARY KEY 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
)
""".trimIndent()
)
database.execSQL(
"""
INSERT INTO `_new_ServerSetting` (
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,
`podcastSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`
)
SELECT `ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,
`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,
`bookmarkSupport`,`name`,`podcastSupport`,`id`,`allowSelfSignedCertificate`,
`chatSupport`
FROM `ServerSetting`
""".trimIndent()
)
database.execSQL("DROP TABLE `ServerSetting`")
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
}
}

View File

@ -7,7 +7,7 @@ import androidx.room.Query
import org.moire.ultrasonic.domain.Artist
@Dao
interface ArtistsDao {
interface ArtistDao {
/**
* Insert a list in the database. If the item already exists, replace it.
*
@ -43,5 +43,5 @@ interface ArtistsDao {
* Get artist by id
*/
@Query("SELECT * FROM artists WHERE id LIKE :id")
fun get(id: String): Artist
fun get(id: String): Artist?
}

View File

@ -53,6 +53,7 @@ interface IndexDao : GenericDao<Index> {
fun get(musicFolderId: String): List<Index>
/**
* TODO: Make generic
* Upserts (insert or update) an object to the database
*
* @param obj the object to upsert

View File

@ -1,23 +1,85 @@
/*
* MetaDatabase.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.data
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import java.util.Date
import org.moire.ultrasonic.domain.Album
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.domain.Track
/**
* This database is used to store and cache the ID3 metadata
*/
@Database(
entities = [Artist::class, Index::class, MusicFolder::class],
version = 1
entities = [
Artist::class,
Album::class,
Track::class,
Index::class,
MusicFolder::class
],
autoMigrations = [
AutoMigration(
from = 1,
to = 2
),
],
exportSchema = true,
version = 3
)
@TypeConverters(Converters::class)
abstract class MetaDatabase : RoomDatabase() {
abstract fun artistsDao(): ArtistsDao
abstract fun artistDao(): ArtistDao
abstract fun albumDao(): AlbumDao
abstract fun trackDao(): TrackDao
abstract fun musicFoldersDao(): MusicFoldersDao
abstract fun indexDao(): IndexDao
}
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}
/* 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`))")
}
}
/* ktlint-enable max-line-length */

View File

@ -19,7 +19,8 @@ import androidx.room.PrimaryKey
*/
@Entity
data class ServerSetting(
@PrimaryKey var id: Int,
// Default ID is 0, which will trigger SQLite to generate a unique ID.
@PrimaryKey(autoGenerate = true) var id: Int = 0,
@ColumnInfo(name = "index") var index: Int,
@ColumnInfo(name = "name") var name: String,
@ColumnInfo(name = "url") var url: String,
@ -37,9 +38,6 @@ data class ServerSetting(
@ColumnInfo(name = "podcastSupport") var podcastSupport: Boolean? = null
) {
constructor() : this (
-1, 0, "", "", null, "", "", false, false, false, null, null
)
constructor(name: String, url: String) : this(
-1, 0, name, url, null, "", "", false, false, false, null, null
0, 0, "", "", null, "", "", false, false, false, null, null
)
}

View File

@ -69,12 +69,6 @@ interface ServerSettingDao {
@Query("SELECT COUNT(*) FROM serverSetting")
fun liveServerCount(): LiveData<Int?>
/**
* Retrieves the greatest value of the Id column in the table
*/
@Query("SELECT MAX([id]) FROM serverSetting")
suspend fun getMaxId(): Int?
/**
* Retrieves the greatest value of the Index column in the table
*/

View File

@ -0,0 +1,34 @@
package org.moire.ultrasonic.data
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Query
import org.moire.ultrasonic.domain.Track
@Dao
@Entity(tableName = "tracks")
interface TrackDao : GenericDao<Track> {
/**
* Clear the whole database
*/
@Query("DELETE FROM tracks")
fun clear()
/**
* Get all albums
*/
@Query("SELECT * FROM tracks")
fun get(): List<Track>
/**
* Get albums by artist
*/
@Query("SELECT * FROM tracks WHERE albumId LIKE :id")
fun byAlbum(id: String): List<Track>
/**
* Get albums by artist
*/
@Query("SELECT * FROM tracks WHERE artistId LIKE :id")
fun byArtist(id: String): List<Track>
}

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