diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2694a36 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,253 @@ +# Changelog + +## Pending release... +* chore: Update russian strings.xml by @Sevinfolds in https://github.com/eddyizm/tempus/pull/249 +* fix: disallow duplicate songs in queue by @eddyizm in https://github.com/eddyizm/tempus/pull/252 +* fix:github release check by @eddyizm in https://github.com/eddyizm/tempus/pull/253 +* fix: Fixed crash when viewing share by @drakeerv in https://github.com/eddyizm/tempus/pull/255 +* chore: Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/257 +* fix: add podcast/radio channel visible when empty podcasts/radio by @eddyizm in https://github.com/eddyizm/tempus/pull/260 + +## New Contributors +* @Sevinfolds made their first contribution in https://github.com/eddyizm/tempus/pull/249 +* @drakeerv made their first contribution in https://github.com/eddyizm/tempus/pull/255 + +## [4.2.0](https://github.com/eddyizm/tempo/releases/tag/v4.2.0) (2025-11-09) +## What's Changed +* fix: Equalizer fix in main build variant by @jaime-grj in https://github.com/eddyizm/tempus/pull/239 +* fix: Images not filling holder by @eddyizm in https://github.com/eddyizm/tempus/pull/244 +* feat: Make artist and album clickable by @eddyizm in https://github.com/eddyizm/tempus/pull/243 +* feat: implement scroll to currently playing feature by @shrapnelnet in https://github.com/eddyizm/tempus/pull/247 +* fix: shuffling genres only queuing 25 songs by @shrapnelnet in https://github.com/eddyizm/tempus/pull/246 + +## New Contributors +* @shrapnelnet made their first contribution in https://github.com/eddyizm/tempus/pull/247 + +**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.1.3...v4.2.0 + +## [4.1.3](https://github.com/eddyizm/tempo/releases/tag/v4.1.3) (2025-11-06) +## What's Changed +* [fix: equalizer missing referenced value](https://github.com/eddyizm/tempus/commit/923cfd5bc97ed7db28c90348e3619d0a784fc434) +* Fix: Album track list bug by @eddyizm in https://github.com/eddyizm/tempus/pull/237 +* fix: Add listener to enable equalizer when audioSessionId changes by @jaime-grj in https://github.com/eddyizm/tempus/pull/235 + +**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.1.0...v4.1.3 + +## [4.1.0](https://github.com/eddyizm/tempo/releases/tag/v4.1.0) (2025-11-05) +## What's Changed +* chore(i18n): Update Spanish (es-ES) translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/205 +* shuffle for artists without using `getTopSongs` by @pca006132 in https://github.com/eddyizm/tempus/pull/207 +* Update USAGE.md with instant mix details by @zc-devs in https://github.com/eddyizm/tempus/pull/220 +* feat: sort artists by album count by @pca006132 in https://github.com/eddyizm/tempus/pull/206 +* Fix downloaded tab performance by @pca006132 in https://github.com/eddyizm/tempus/pull/210 +* fix: remove NestedScrollViews for fragment_album_page by @pca006132 in https://github.com/eddyizm/tempus/pull/216 +* fix: playlist page should not snap by @pca006132 in https://github.com/eddyizm/tempus/pull/218 +* fix: do not override getItemViewType and getItemId by @pca006132 in https://github.com/eddyizm/tempus/pull/221 +* chore: update media3 dependencies by @pca006132 in https://github.com/eddyizm/tempus/pull/217 +* fix: update MediaItems after network change by @pca006132 in https://github.com/eddyizm/tempus/pull/222 +* fix: skip mapping downloaded item by @pca006132 in https://github.com/eddyizm/tempus/pull/228 + +## New Contributors +* @pca006132 made their first contribution in https://github.com/eddyizm/tempus/pull/207 + +**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.0.7...v4.1.0 + +## [4.0.7](https://github.com/eddyizm/tempo/releases/tag/v4.0.7) (2025-10-28) +## What's Changed +* chore: updated tempo references to tempus including github check by @eddyizm in https://github.com/eddyizm/tempus/pull/197 +* fix: Crash on share no expiration date or field returned from api by @eddyizm in https://github.com/eddyizm/tempus/pull/199 + +**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.0.6...v4.0.7 + +## [4.0.6](https://github.com/eddyizm/tempo/releases/tag/v4.0.6) (2025-10-26) +## Attention +This release will not update previous installs as it is considered a new app, no longer `Tempo`, new icon, new app id, and new app name. Hoping it will not be a huge inconvenience but was necessary in order to publish to app stores like IzzyDroid and FDroid. + +**Android Auto** +Support should be the same as before, however, I was not able to test any of the icons/visuals, so please let me know if there are any remnants of the tempo logo/icon as I believe I removed them all and replaced them successfully. + +## What's Changed +* Check also underlying transport by @zc-devs in https://github.com/eddyizm/tempus/pull/90 +* fix: updated workflow for 32/64 bit apks by @eddyizm in https://github.com/eddyizm/tempus/pull/176 +* Unhide genre from album details view by @sebaFlame in https://github.com/eddyizm/tempus/pull/161 +* fix: persist album sorting on resume by @eddyizm in https://github.com/eddyizm/tempus/pull/181 +* chore: update readme and usage references to tempus. added new banner… by @eddyizm in https://github.com/eddyizm/tempus/pull/182 +* Tempus rebrand by @eddyizm in https://github.com/eddyizm/tempus/pull/183 +* Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/188 + +## New Contributors +* @zc-devs made their first contribution in https://github.com/eddyizm/tempus/pull/90 +* @sebaFlame made their first contribution in https://github.com/eddyizm/tempus/pull/161 + +**Full Changelog**: https://github.com/eddyizm/tempus/compare/v3.17.14...v4.0.1 + +## [3.17.14](https://github.com/eddyizm/tempo/releases/tag/v3.17.14) (2025-10-16) +## What's Changed +* fix: General build warning and playback issues by @le-firehawk in https://github.com/eddyizm/tempo/pull/167 +* fix: persist album sort preference by @eddyizm in https://github.com/eddyizm/tempo/pull/168 +* Fix album parse empty date field by @eddyizm in https://github.com/eddyizm/tempo/pull/171 +* fix: Include shuffle/repeat controls in f-droid build's media notific… by @le-firehawk in https://github.com/eddyizm/tempo/pull/174 +* fix: limits image size to prevent widget crash #172 by @eddyizm in https://github.com/eddyizm/tempo/pull/175 + +**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.17.0...v3.17.14 + +## [3.17.0](https://github.com/eddyizm/tempo/releases/tag/v3.17.0) (2025-10-10) +## What's Changed +* chore: adding screenshot and docs for 4 icons/buttons in player control by @eddyizm in https://github.com/eddyizm/tempo/pull/162 +* Update Polish translation by @skajmer in https://github.com/eddyizm/tempo/pull/160 +* feat: Make all objects in Tempo references for quick access by @le-firehawk in https://github.com/eddyizm/tempo/pull/158 +* fix: Glide module incorrectly encoding IPv6 addresses by @le-firehawk in https://github.com/eddyizm/tempo/pull/159 + +**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.16.6...v3.17.0 + +## [3.16.6](https://github.com/eddyizm/tempo/releases/tag/v3.16.6) (2025-10-08) +## What's Changed +* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempo/pull/151 +* fix: Re-add new equalizer settings that got lost by @jaime-grj in https://github.com/eddyizm/tempo/pull/153 +* chore: removed play variant by @eddyizm in https://github.com/eddyizm/tempo/pull/155 +* fix: updating release workflow to account for the 32/64 bit builds an… by @eddyizm in https://github.com/eddyizm/tempo/pull/156 +* feat: Show sampling rate and bit depth in downloads by @jaime-grj in https://github.com/eddyizm/tempo/pull/154 +* fix: Replace hardcoded strings in SettingsFragment by @jaime-grj in https://github.com/eddyizm/tempo/pull/152 + + +**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.16.0...v3.16.6 + +## [3.16.0](https://github.com/eddyizm/tempo/releases/tag/v3.16.0) (2025-10-07) +## What's Changed +* chore: add sha256 fingerprint for validation by @eddyizm in https://github.com/eddyizm/tempo/commit/3c58e6fbb2157a804853259dfadbbffe3b6793b5 +* fix: Prevent crash when getting artist radio and song list is null by @jaime-grj in https://github.com/eddyizm/tempo/pull/117 +* chore: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/125 +* fix: Update search query validation to require at least 2 characters instead of 3 by @jaime-grj in https://github.com/eddyizm/tempo/pull/124 +* feat: download starred artists. by @eddyizm in https://github.com/eddyizm/tempo/pull/137 +* feat: Enable downloading of song lyrics for offline viewing by @le-firehawk in https://github.com/eddyizm/tempo/pull/99 +* fix: Lag during startup when local url is not available by @SinTan1729 in https://github.com/eddyizm/tempo/pull/110 +* chore: add link to discussion page in settings by @eddyizm in https://github.com/eddyizm/tempo/pull/143 +* feat: Notification heart rating by @eddyizm in https://github.com/eddyizm/tempo/pull/140 +* chore: Unify and update polish translation by @skajmer in https://github.com/eddyizm/tempo/pull/146 +* chore: added sha256 signing key for verification by @eddyizm in https://github.com/eddyizm/tempo/pull/147 +* feat: Support user-defined download directory for media by @le-firehawk in https://github.com/eddyizm/tempo/pull/21 +* feat: Added support for skipping duplicates by @SinTan1729 in https://github.com/eddyizm/tempo/pull/135 +* feat: Add home screen music playback widget and some updates in Turkish localization by @mucahit-kaya in https://github.com/eddyizm/tempo/pull/98 + +## New Contributors +* @SinTan1729 made their first contribution in https://github.com/eddyizm/tempo/pull/110 + +**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.15.0...v3.16.0 + +## [3.15.0](https://github.com/eddyizm/tempo/releases/tag/v3.15.0) (2025-09-23) +## What's Changed +* chore: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/84 +* chore: Update RU locale by @ArchiDevil in https://github.com/eddyizm/tempo/pull/87 +* chore: Update Korean translations by @kongwoojin in https://github.com/eddyizm/tempo/pull/97 +* fix: only plays the first song on an album by @eddyizm in https://github.com/eddyizm/tempo/pull/81 +* fix: handle null and not crash when disconnecting chromecast by @eddyizm in https://github.com/eddyizm/tempo/pull/81 +* feat: Built-in audio equalizer by @jaime-grj in https://github.com/eddyizm/tempo/pull/94 +* fix: Resolve playback issues with live radio MPEG & HLS streams by @jaime-grj in https://github.com/eddyizm/tempo/pull/89 +* chore: Updates to polish translation by @skajmer in https://github.com/eddyizm/tempo/pull/105 +* feat: added 32bit build and debug build for testing. Removed unused f… by @eddyizm in https://github.com/eddyizm/tempo/pull/108 +* feat: Mark currently playing song with play/pause button by @jaime-grj in https://github.com/eddyizm/tempo/pull/107 +* fix: add listener to track playlist click/change by @eddyizm in https://github.com/eddyizm/tempo/pull/113 +* feat: Tap anywhere on the song item to toggle playback by @jaime-grj in https://github.com/eddyizm/tempo/pull/112 + +## New Contributors +* @ArchiDevil made their first contribution in https://github.com/eddyizm/tempo/pull/87 +* @kongwoojin made their first contribution in https://github.com/eddyizm/tempo/pull/97 + +**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.14.8...v3.15.0 + + +## [3.14.8](https://github.com/eddyizm/tempo/releases/tag/v3.14.8) (2025-08-30) +## What's Changed +* fix: Use correct SearchView widget to avoid crash in AlbumListPageFragment by @jaime-grj in https://github.com/eddyizm/tempo/pull/76 +* chore(i18n): Update Spanish (es-ES) and English translations by @jaime-grj in https://github.com/eddyizm/tempo/pull/77 +* style: Center subtitle text in empty_download_layout in fragment_download.xml when there is more than one line by @jaime-grj in https://github.com/eddyizm/tempo/pull/78 +* fix: Disable "sync starred tracks/albums" switches when Cancel is clicked in warning dialog, use proper view for "Sync starred albums" dialog by @jaime-grj in https://github.com/eddyizm/tempo/pull/79 +* bug fixes, chores, docs v3.14.8 by @eddyizm in https://github.com/eddyizm/tempo/pull/80 + + +**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.14.1...v3.14.8 + +## [3.14.1](https://github.com/eddyizm/tempo/releases/tag/v3.14.1) (2025-08-30) +## What's Changed +* feat: rating dialog added to album page by @eddyizm in https://github.com/eddyizm/tempo/pull/52 +* style: Add song rating bar in landscape player controller layout by @jaime-grj in https://github.com/eddyizm/tempo/pull/57 +* feat: setting to show/hide 5 star rating on playerview by @eddyizm in https://github.com/eddyizm/tempo/pull/59 +* chore: setting-to-hide-song-rating by @eddyizm in https://github.com/eddyizm/tempo/pull/60 +* fix: catches null value and prepares bundle appropriately adding sing… by @eddyizm in https://github.com/eddyizm/tempo/pull/64 +* fix: artist filtering in library view browse artist resolves #45 by @eddyizm in https://github.com/eddyizm/tempo/pull/69 +* chore: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/70 +* feat: adds sync starred albums functionality #66 by @eddyizm in https://github.com/eddyizm/tempo/pull/73 + + +**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.13.0...v3.14.1 + +## [3.13.0](https://github.com/eddyizm/tempo/releases/tag/v3.13.0) (2025-08-23) +## What's Changed +* style: Change position and size of rating container by @jaime-grj in https://github.com/eddyizm/tempo/pull/44 +* feat: Add Turkish localization (values-tr) by @mucahit-kaya in https://github.com/eddyizm/tempo/pull/50 +* chore: adding a note/not fully baked label to the sync user play queue setting by @eddyizm in https://github.com/eddyizm/tempo/commit/8ed0a4642bd0cd637c65e3115142596331fa7ef7 +* fix: moved hardcoded italian save text to string template, updated with english and italian language xmls by @eddyizm in https://github.com/eddyizm/tempo/commit/26a5fb029a07752c9c0db0d08a89afd638772579 + + +## New Contributors +* @mucahit-kaya made their first contribution in https://github.com/eddyizm/tempo/pull/50 + +**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.12.0...v3.13.0 + +## [3.12.0](https://github.com/eddyizm/tempo/releases/tag/v3.12.0) (2025-08-15) +### What's Changed +* [chore]: add German translations for track info and home section strings (#29) by @BreadWare92 in https://github.com/eddyizm/tempo/pull/31 +* [chore]: increased "Offline mode" text size, changed its color in dark theme by @jaime-grj in https://github.com/eddyizm/tempo/pull/33 +* [chore]: Translations for sections by @skajmer in https://github.com/eddyizm/tempo/pull/30 +* [chore]: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/36 +* [fix]: Show placeholder string in TrackInfoDialog fields when there is no data by @jaime-grj in https://github.com/eddyizm/tempo/pull/37 +* [feat]: added transcoding codec and bitrate info to PlayerControllerFragment, replace hardcoded strings by @jaime-grj in https://github.com/eddyizm/tempo/pull/38 +* [chore]: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/39 +* [feat]: show rating on song view by @eddyizm in https://github.com/eddyizm/tempo/pull/40 + +### New Contributors +* @BreadWare92 made their first contribution in https://github.com/eddyizm/tempo/pull/31 +* @skajmer made their first contribution in https://github.com/eddyizm/tempo/pull/30 +* @benoit-smith made their first contribution in https://github.com/eddyizm/tempo/pull/36 + +**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.11.2...v3.12.0 + +## [3.11.2](https://github.com/eddyizm/tempo/releases/tag/v3.11.2) (2025-08-09) + + +([Full Changelog](https://github.com/eddyizm/tempo/compare/v3.10.0...eddyizm:tempo:v3.11.2?expand=1)) + +**Housekeeping:** + +- [Chore] Added change log. + +**Merged pull requests:** + +- [Fix] make hardcoded strings in home fragment dynamic [\#27](https://github.com/eddyizm/tempo/pull/22) ([jaime-grj](https://github.com/jaime-grj)) + +- [Fix] show "System default" language option, sort languages alphabetically, include country when showing language in settings [\#26](https://github.com/eddyizm/tempo/pull/26) ([jaime-grj ](https://github.com/jaime-grj)) + +- [Fix] check for IP connectivity instead of Internet access [\#25](https://github.com/eddyizm/tempo/pull/25) ([jaime-grj](https://github.com/jaime-grj)) + +- [Fix] hide unnecessary TextViews in AlbumPageFragment when there is no data, fixed incorrect album release date [\#24](https://github.com/eddyizm/tempo/pull/24) ([jaime-grj](https://github.com/jaime-grj)) + +- [Feat] show sampling rate and bit depth if available [\#22](https://github.com/eddyizm/tempo/pull/22) ([jaime-grj](https://github.com/jaime-grj)) + +- [Feat] Fix lyric scrolling during playback, keep screen on while viewing [\#20](https://github.com/eddyizm/tempo/pull/20) ([le-firehawk](https://github.com/le-firehawk)) + +## [3.10.0](https://github.com/eddyizm/tempo/releases/tag/v3.10.0) (2025-08-04) + +**Merged pull requests:** + +- [Fix] redirection to artist fragment on artist label click [\#379](https://github.com/CappielloAntonio/tempo/pull/379) +- [Fix] Player queue lag, limits [\#385](https://github.com/CappielloAntonio/tempo/pull/385) +- [Fix] crash when sorting albums with a null artist [\#389](https://github.com/CappielloAntonio/tempo/pull/389) +- [Feat] Display toast message after adding a song to a playlist [\#371](https://github.com/CappielloAntonio/tempo/pull/371) +- [Feat] Album add to playlist context menu item [\#367](https://github.com/CappielloAntonio/tempo/pull/367) +- [Feat] Store and retrieve replay and shuffle states in preferences [\#397](https://github.com/CappielloAntonio/tempo/pull/397) +- [Feat] Enhance Android media player notification window #400 + [\#400](https://github.com/CappielloAntonio/tempo/pull/400) +- [Chore] Spanish translation [\#374](https://github.com/CappielloAntonio/tempo/pull/374) +- [Chore] Polish translation [\#378](https://github.com/CappielloAntonio/tempo/pull/378) + +***This log is for this fork to detail updates since 3.9.0 from the main repo.*** \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program 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. + + This program 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 this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 0ebc5a1..874a718 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,126 @@ -# tempus +

+ Tempus +

-Subsonic Audio Streaming Android \ No newline at end of file +--- + +

+ Access your music library on all your android devices +

+ +
+ + + + +
+ +

+ + +

+ + + +**Tempus** is an open-source and lightweight music client for Subsonic, designed and built natively for Android. It provides a seamless and intuitive music streaming experience, allowing you to access and play your Subsonic music library directly from your Android device. + +Tempus does not rely on magic algorithms to decide what you should listen to. Instead, the interface is built around your listening history, randomness, and optionally integrates with services like Last.fm to personalize your music experience. + +The project is a fork of [Tempo](#credits). + +**If you find Tempus useful, please consider starring the project on GitHub. It would mean a lot to me and help promote the app to a wider audience.** + +**Use the Github version of the app for full Android Auto and Chromecast support.** + +sha256 signing key fingerprint +`B7:85:01:B9:34:D0:4E:0A:CA:8D:94:AF:D6:72:6A:4D:1D:CE:65:79:7F:1D:41:71:0F:64:3C:29:00:EB:1D:1D` + +### Releases + +Please note the two variants in the release assets include release/debug and 32/64 bit flavors. + +`app-tempus` <- The github release with all the android auto/chromecast features + +`app-degoogled*` <- The izzyOnDroid release that goes without any of the google stuff. It is now available on izzyOnDroid (64bit) I am releasing the both 32/64bit apk's here on github for those who need a 32bit version. + +[CHANGELOG.md](CHANGELOG.md) + +## Usage + +[Documentation](USAGE.md) (work in progress) + +## Features +- **Subsonic Integration**: Tempus seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go. +- **Sleek and Intuitive UI**: Enjoy a clean and user-friendly interface designed to enhance your music listening experience, tailored to your preferences and listening history. +- **Browse and Search**: Easily navigate through your music library using various browsing and searching options, including artists, albums, genres, playlists, decades and more. +- **Streaming and Offline Mode**: Stream music directly from your Subsonic server. Offline mode is currently under active development and may have limitations when using multiple servers. +- **Playlist Management**: Create, edit, and manage playlists to curate your perfect music collection. +- **Gapless Playback**: Experience uninterrupted playback with gapless listening mode. +- **Chromecast Support**: Stream your music to Chromecast devices. The support is currently in a rudimentary state. +- **Scrobbling Integration**: Optionally integrate Tempus with Last.fm or Listenbrainz.org to scrobble your played tracks, gather music insights, and further personalize your music recommendations, if supported by your Subsonic server. +- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempus, expanding your audio entertainment options. +- **Transcoding Support**: Activate transcoding of tracks on your Subsonic server, allowing you to set a transcoding profile for optimized streaming directly from the app. This feature requires support from your Subsonic server. +- **Android Auto Support**: Enjoy your favorite music on the go with full Android Auto integration, allowing you to seamlessly control and listen to your tracks directly from your mobile device while driving. +- **Multiple Libraries**: Tempus handles multi-library setups gracefully. They are displayed as Library folders. +- **Equalizer**: Option to use in app equalizer. +- **Widget**: New widget to keeping the basic controls on your screen at all times. +- **Available in 11 languages**: Currently in Chinese, French, German, Italian, Korean, Polish, Portuguese, Russion, Spanish and Turkish + +## Screenshot + +

+ Light theme +

+ +

+ + + + + + + +

+ +
+ +

+ Dark theme +

+ +

+ + + + + + + + +

+ +## Contributing + +Please fork and open PR's against the development branch. Make sure your PR builds successfully. + +If there is an UI change, please include a before/after screenshot and a short video/gif if that helps elaborating the fix/feature in the PR. + +Currently there are no tests but I would love to start on some unit tests. + +Not a hard requirement but any new feature/change should ideally include an update to the nacent documention. + +## Support + +[**Buy me a coffee**](https://ko-fi.com/eddyizm) +bitcoin: `3QVHSSCJvn6yXEcJ3A3cxYLMmbvFsrnUs5` + +## License + +Tempus is released under the [GNU General Public License v3.0](LICENSE). Feel free to modify, distribute, and use the app in accordance with the terms of the license. Contributions to the project are also welcome. + +## Credits +Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0) + +[Opensvg.org](https://opensvg.org) for the new turntable logo. \ No newline at end of file diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..05abee9 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,183 @@ +# Tempus Usage Guide +[<- back home](README.md) + +## Table of Contents +- [Prerequisites](#prerequisites) +- [Getting Started](#getting-started) +- [Server Configuration](#server-configuration) +- [Main Features](#main-features) + +- [Navigation](#navigation) +- [Playback Controls](#playback-controls) +- [Favorites](#favorites) +- [Playlist Management](#playlist-management) +- [Android Auto](#android-auto) +- [Settings](#settings) +- [Troubleshooting](#troubleshooting) + +## Prerequisites + +**Important Notice**: This app is a Subsonic-compatible client and does not provide any music content itself. To use this application, you must have: + +- An active Subsonic API server (or compatible service) already set up +- Valid login credentials for your Subsonic server +- Music content uploaded and organized on your server + +### Verified backends +This app works with any service that implements the Subsonic API, including: +- [LMS - Lightweight Music Server](https://github.com/epoupon/lms) - *personal fave and my backend* +- [Navidrome](https://www.navidrome.org/) +- [Gonic](https://github.com/sentriz/gonic) +- [Ampache](https://github.com/ampache/ampache) +- [NextCloud Music](https://apps.nextcloud.com/apps/music) + + + + +## Getting Started + +### Installation +1. Download the APK from the [Releases](https://github.com/eddyizm/tempus/releases) section +2. Enable "Install from unknown sources" in your Android settings +3. Install the application + +### First Launch +1. Open the application +2. You will be prompted to configure your server connection +3. Grant necessary permissions for media playback and background operation + +## Server Configuration + +### Initial Setup +**IN PROGRESS** +1. Enter your server URL (e.g., `https://your-subsonic-server.com`) +2. Provide your username and password +3. Test the connection to ensure proper configuration + +### Advanced Settings +**TODO** + +## Main Features + +### Library View + +**Multi-library** + +Tempus handles multi-library setups gracefully. They are displayed as Library folders. + +However, if you want to limit or change libraries you could use a workaround, if your server supports it. + +You can create multiple users , one for each library, and save each of them in Tempus app. + +### Now Playing Screen + +On the main player control screen, tapping on the artwork will reveal a small collection of 4 buttons/icons. +

+ +

+ +*marked the icons with numbers for clarity* + +1. Downloads the track (there is a notification if the android screen but not a pop toast currently ) +2. Adds track to playlist - pops up playlist dialog. +3. Adds tracks to the queue via instant mix function + * TBD: what is the _instant mix function_? + * Uses [getSimilarSongs](https://opensubsonic.netlify.app/docs/endpoints/getsimilarsongs/) of OpenSubsonic API. + Which tracks to be mixed depends on the server implementation. For example, Navidrome gets 15 similar artists from LastFM, then 20 top songs from each. +4. Saves play queue (if the feature is enabled in the settings) + * if the setting is not enabled, it toggles a view of the lyrics if available (slides to the right) + +### Podcasts +If your server supports it - add a podcast rss feed +

+ +

+ +### Radio Stations +If your server supports it - add a internet radio station feed +

+ +

+ +## Navigation + +### Bottom Navigation Bar +**IN PROGRESS** +- **Home**: Recently played and server recommendations +- **Library**: Your server's complete music collection +- **Download**: Locally downloaded files from server + +## Playback Controls + +### Streaming Controls +**TODO** + +### Advanced Controls +**TODO** + +## Favorites + +### Favorites (aka heart aka star) to albums and artists +- Long pressing on an album gives you access to heart/unheart an album + +

+ +

+ +- Long pressing on an artist cover gets you the same access to to heart/unheart an album + +

+ +

+ + +## Playlist Management + +### Server Playlists +**TODO** + +### Creating Playlists +**TODO** + +## Settings + + +## Android Auto + +### Enabling on your head unit +- You have to enable Android Auto developer options, which are different from actual Android dev options. Then you have to enable "Unknown sources" in Android Auto, otherwise the app won't appear as it isn't downloaded from Play Store. (screenshots needed) + + +### Server Settings +**IN PROGRESS** +- Manage multiple server connections +- Configure sync intervals +- Set data usage limits for streaming + +### Audio Settings +**IN PROGRESS** +- Streaming quality settings +- Offline caching preferences + +### Appearance +**TODO** + +## Troubleshooting + +### Connection Issues + +**TODO** + +### Common Issues + +**TODO** + +### Support +For additional help: +- Question? Start a [Discussion](https://github.com/eddyizm/tempus/discussions) +- Open an [issue](https://github.com/eddyizm/tempus/issues) if you don't find a discussion solving your issue. +- Consult your Subsonic server's documentation + +--- + +*Note: This app requires a pre-existing Subsonic-compatible server with music content.* diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..3f3f577 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,133 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-parcelize' + +android { + compileSdk 35 + buildToolsVersion = '35.0.0' + + defaultConfig { + minSdkVersion 24 + targetSdk 35 + + versionCode 6 + versionName '4.2.4' + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + + javaCompileOptions { + annotationProcessorOptions { + arguments += [ + "room.schemaLocation": "$projectDir/schemas".toString(), + "room.incremental" : "true" + ] + } + } + + } + + splits { + abi { + enable true + reset() + //noinspection ChromeOsAbiSupport + include 'armeabi-v7a', 'arm64-v8a' + universalApk false + } + } + + dependenciesInfo { + // Disables dependency metadata when building APKs (for IzzyOnDroid/F-Droid) + includeInApk = false + // Disables dependency metadata when building Android App Bundles (for Google Play) + includeInBundle = false + } + + flavorDimensions += "default" + + productFlavors { + tempus { + dimension = "default" + applicationId 'com.eddyizm.tempus' + } + + degoogled { + dimension = "default" + applicationId "com.eddyizm.degoogled.tempus" + } + + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + debuggable false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + + debug { + applicationIdSuffix ".debug" + debuggable true + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + + buildFeatures { + viewBinding true + buildConfig true + } + + namespace 'com.cappielloantonio.tempo' +} + +dependencies { + implementation files('../libs/lib-decoder-ffmpeg-release.aar') + + // AndroidX + implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' + implementation 'androidx.preference:preference-ktx:1.2.1' + implementation 'androidx.navigation:navigation-fragment-ktx:2.8.6' + implementation 'androidx.navigation:navigation-ui-ktx:2.8.6' + implementation 'androidx.recyclerview:recyclerview:1.4.0' + implementation 'androidx.room:room-runtime:2.6.1' + implementation 'androidx.core:core-splashscreen:1.0.1' + implementation 'androidx.appcompat:appcompat:1.7.0' + + // Android Material + implementation 'com.google.android.material:material:1.10.0' + + // Glide + implementation 'com.github.bumptech.glide:glide:4.16.0' + implementation 'com.github.bumptech.glide:annotations:4.16.0' + + // Media3 + implementation 'androidx.media3:media3-session:1.8.0' + implementation 'androidx.media3:media3-common:1.8.0' + implementation 'androidx.media3:media3-exoplayer:1.8.0' + implementation 'androidx.media3:media3-ui:1.8.0' + implementation 'androidx.media3:media3-exoplayer-hls:1.8.0' + tempusImplementation 'androidx.media3:media3-cast:1.8.0' + + + annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' + annotationProcessor 'androidx.room:room-compiler:2.6.1' + + // Retrofit + implementation 'com.squareup.retrofit2:retrofit:2.11.0' + implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14' + implementation 'com.squareup.retrofit2:converter-gson:2.11.0' +} +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..48b1f06 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,28 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-keepattributes SourceFile, LineNumberTable +-keep public class * extends java.lang.Exception +-keep class retrofit2.** { *; } + +-keep class **.reflect.TypeToken { *; } +-keep class * extends **.reflect.TypeToken \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/1.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/1.json new file mode 100644 index 0000000..0ff270b --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/1.json @@ -0,0 +1,746 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "1f4e50f90f58fb9cb53c89747d142fd9", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "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, '1f4e50f90f58fb9cb53c89747d142fd9')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/10.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/10.json new file mode 100644 index 0000000..e039aed --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/10.json @@ -0,0 +1,1065 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "58cb958cdb09f054c27673d1de7f26d0", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `local_address` TEXT, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localAddress", + "columnName": "local_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, `stream_id` TEXT, `stream_url` TEXT, `timestamp` INTEGER)", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamUrl", + "columnName": "stream_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "index" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `duration` INTEGER NOT NULL, `coverArt` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "coverArtId", + "columnName": "coverArt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "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, '58cb958cdb09f054c27673d1de7f26d0')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/11.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/11.json new file mode 100644 index 0000000..9febeba --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/11.json @@ -0,0 +1,1101 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "cceefd0896d9f0e949a30b53dd682bee", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `sampling_rate` INTEGER, `bit_depth` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "samplingRate", + "columnName": "sampling_rate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitDepth", + "columnName": "bit_depth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `local_address` TEXT, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localAddress", + "columnName": "local_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `sampling_rate` INTEGER, `bit_depth` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "samplingRate", + "columnName": "sampling_rate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitDepth", + "columnName": "bit_depth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `sampling_rate` INTEGER, `bit_depth` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "samplingRate", + "columnName": "sampling_rate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitDepth", + "columnName": "bit_depth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, `stream_id` TEXT, `stream_url` TEXT, `timestamp` INTEGER)", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamUrl", + "columnName": "stream_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "index" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `duration` INTEGER NOT NULL, `coverArt` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "coverArtId", + "columnName": "coverArt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "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, 'cceefd0896d9f0e949a30b53dd682bee')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json new file mode 100644 index 0000000..7797459 --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json @@ -0,0 +1,1151 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "2d26471ae15a1cdaf996261b72f81613", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `sampling_rate` INTEGER, `bit_depth` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "samplingRate", + "columnName": "sampling_rate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitDepth", + "columnName": "bit_depth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `local_address` TEXT, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localAddress", + "columnName": "local_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `sampling_rate` INTEGER, `bit_depth` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "samplingRate", + "columnName": "sampling_rate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitDepth", + "columnName": "bit_depth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `sampling_rate` INTEGER, `bit_depth` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "samplingRate", + "columnName": "sampling_rate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bitDepth", + "columnName": "bit_depth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isVideo", + "columnName": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, `stream_id` TEXT, `stream_url` TEXT, `timestamp` INTEGER)", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamUrl", + "columnName": "stream_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "index" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `duration` INTEGER NOT NULL, `coverArt` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "coverArtId", + "columnName": "coverArt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "lyrics_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_id` TEXT NOT NULL, `artist` TEXT, `title` TEXT, `lyrics` TEXT, `structured_lyrics` TEXT, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`song_id`))", + "fields": [ + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "structuredLyrics", + "columnName": "structured_lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "song_id" + ] + }, + "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, '2d26471ae15a1cdaf996261b72f81613')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/2.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/2.json new file mode 100644 index 0000000..773c693 --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/2.json @@ -0,0 +1,790 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "ff99e331b4c34a82c560588c4dd5735f", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "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, 'ff99e331b4c34a82c560588c4dd5735f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/3.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/3.json new file mode 100644 index 0000000..ef76d57 --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/3.json @@ -0,0 +1,797 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "6ea111217793c58d54eabb1190dd92ec", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "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, '6ea111217793c58d54eabb1190dd92ec')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/4.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/4.json new file mode 100644 index 0000000..5356ab9 --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/4.json @@ -0,0 +1,997 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "528d037bee0f0575f8e0670ae1b04e00", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "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, '528d037bee0f0575f8e0670ae1b04e00')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/5.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/5.json new file mode 100644 index 0000000..e8e7e86 --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/5.json @@ -0,0 +1,1004 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "0e65e1c3fb44d9dc04c9c6cf35b7ea58", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL DEFAULT 0, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "index" + ] + }, + "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, '0e65e1c3fb44d9dc04c9c6cf35b7ea58')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/6.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/6.json new file mode 100644 index 0000000..c6ccf4b --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/6.json @@ -0,0 +1,1016 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "dff788fb3b6ff922a1f566a9752c2029", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER NOT NULL DEFAULT 0, `id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, `stream_id` TEXT, `stream_url` TEXT, PRIMARY KEY(`index`))", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamUrl", + "columnName": "stream_url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "index" + ] + }, + "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, 'dff788fb3b6ff922a1f566a9752c2029')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/7.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/7.json new file mode 100644 index 0000000..a3286de --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/7.json @@ -0,0 +1,1021 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "cca7b016c047d8fdc86dd6373f2fb173", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, `stream_id` TEXT, `stream_url` TEXT, `timestamp` INTEGER)", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamUrl", + "columnName": "stream_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "index" + ] + }, + "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, 'cca7b016c047d8fdc86dd6373f2fb173')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/8.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/8.json new file mode 100644 index 0000000..26ab81b --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/8.json @@ -0,0 +1,1021 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "cca7b016c047d8fdc86dd6373f2fb173", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, `stream_id` TEXT, `stream_url` TEXT, `timestamp` INTEGER)", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamUrl", + "columnName": "stream_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "index" + ] + }, + "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, 'cca7b016c047d8fdc86dd6373f2fb173')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/9.json b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/9.json new file mode 100644 index 0000000..0bdcae2 --- /dev/null +++ b/app/schemas/com.cappielloantonio.tempo.database.AppDatabase/9.json @@ -0,0 +1,1027 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "237a704eed556782438a6493deadaed7", + "entities": [ + { + "tableName": "queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackOrder", + "columnName": "track_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastPlay", + "columnName": "last_play", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playingChanged", + "columnName": "playing_changed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "track_order" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "server", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `local_address` TEXT, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localAddress", + "columnName": "local_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLowSecurity", + "columnName": "low_security", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_search", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))", + "fields": [ + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "search" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "playlistName", + "columnName": "playlist_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadState", + "columnName": "download_state", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "downloadUri", + "columnName": "download_uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "chronology", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "server", + "columnName": "server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "favorite", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "toStar", + "columnName": "toStar", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "session_media_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, `stream_id` TEXT, `stream_url` TEXT, `timestamp` INTEGER)", + "fields": [ + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDir", + "columnName": "is_dir", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artist", + "columnName": "artist", + "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": "coverArtId", + "columnName": "cover_art_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedContentType", + "columnName": "transcoding_content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "transcodedSuffix", + "columnName": "transcoded_suffix", + "affinity": "TEXT", + "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": "is_video", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userRating", + "columnName": "user_rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "averageRating", + "columnName": "average_rating", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "playCount", + "columnName": "play_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "discNumber", + "columnName": "disc_number", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "albumId", + "columnName": "album_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "artistId", + "columnName": "artist_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookmarkPosition", + "columnName": "bookmark_position", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalWidth", + "columnName": "original_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "originalHeight", + "columnName": "original_height", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "streamUrl", + "columnName": "stream_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "index" + ] + }, + "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, '237a704eed556782438a6493deadaed7')" + ] + } +} \ No newline at end of file diff --git a/app/src/degoogled/ic_launcher-playstore.png b/app/src/degoogled/ic_launcher-playstore.png new file mode 100644 index 0000000..8709829 Binary files /dev/null and b/app/src/degoogled/ic_launcher-playstore.png differ diff --git a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt new file mode 100644 index 0000000..5c4e939 --- /dev/null +++ b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -0,0 +1,561 @@ +package com.cappielloantonio.tempo.service + +import android.annotation.SuppressLint +import android.app.PendingIntent.FLAG_IMMUTABLE +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.app.TaskStackBuilder +import android.content.Intent +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.os.Binder +import android.os.Bundle +import android.os.IBinder +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.media3.common.* +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.session.* +import androidx.media3.session.MediaSession.ControllerInfo +import com.cappielloantonio.tempo.R +import com.cappielloantonio.tempo.repository.QueueRepository +import com.cappielloantonio.tempo.ui.activity.MainActivity +import com.cappielloantonio.tempo.util.AssetLinkUtil +import com.cappielloantonio.tempo.util.Constants +import com.cappielloantonio.tempo.util.DownloadUtil +import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory +import com.cappielloantonio.tempo.util.MappingUtil +import com.cappielloantonio.tempo.util.Preferences +import com.cappielloantonio.tempo.util.ReplayGainUtil +import com.cappielloantonio.tempo.widget.WidgetUpdateManager +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture + + +@UnstableApi +class MediaService : MediaLibraryService() { + private val librarySessionCallback = CustomMediaLibrarySessionCallback() + + private lateinit var player: ExoPlayer + private lateinit var mediaLibrarySession: MediaLibrarySession + private lateinit var shuffleCommands: List + private lateinit var repeatCommands: List + private lateinit var networkCallback: CustomNetworkCallback + lateinit var equalizerManager: EqualizerManager + + private var customLayout = ImmutableList.of() + private val widgetUpdateHandler = Handler(Looper.getMainLooper()) + private var widgetUpdateScheduled = false + private val widgetUpdateRunnable = object : Runnable { + override fun run() { + if (!player.isPlaying) { + widgetUpdateScheduled = false + return + } + updateWidget() + widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS) + } + } + + inner class LocalBinder : Binder() { + fun getEqualizerManager(): EqualizerManager { + return this@MediaService.equalizerManager + } + } + + private val binder = LocalBinder() + + companion object { + private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON = + "android.media3.session.demo.SHUFFLE_ON" + private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = + "android.media3.session.demo.SHUFFLE_OFF" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = + "android.media3.session.demo.REPEAT_OFF" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = + "android.media3.session.demo.REPEAT_ONE" + private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = + "android.media3.session.demo.REPEAT_ALL" + const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER" + const val ACTION_EQUALIZER_UPDATED = "com.cappielloantonio.tempo.service.EQUALIZER_UPDATED" + } + + fun updateMediaItems() { + Log.d("MediaService", "update items"); + val n = player.mediaItemCount + val k = player.currentMediaItemIndex + val current = player.currentPosition + val items = (0 .. n-1).map{i -> MappingUtil.mapMediaItem(player.getMediaItemAt(i))} + player.clearMediaItems() + player.setMediaItems(items, k, current) + } + + inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() { + var wasWifi = false + + init { + val manager = getSystemService(ConnectivityManager::class.java) + val network = manager.activeNetwork + val capabilities = manager.getNetworkCapabilities(network) + if (capabilities != null) + wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + } + + override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) { + val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + if (isWifi != wasWifi) { + wasWifi = isWifi + widgetUpdateHandler.post(Runnable { + updateMediaItems() + }) + } + } + } + + override fun onCreate() { + super.onCreate() + + initializeCustomCommands() + initializePlayer() + initializeMediaLibrarySession() + restorePlayerFromQueue() + initializePlayerListener() + initializeEqualizerManager() + initializeNetworkListener() + + setPlayer(player) + } + + override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession { + return mediaLibrarySession + } + + override fun onDestroy() { + releaseNetworkCallback() + equalizerManager.release() + stopWidgetUpdates() + releasePlayer() + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? { + // Check if the intent is for our custom equalizer binder + if (intent?.action == ACTION_BIND_EQUALIZER) { + return binder + } + // Otherwise, handle it as a normal MediaLibraryService connection + return super.onBind(intent) + } + + private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback { + + override fun onConnect( + session: MediaSession, + controller: ControllerInfo + ): MediaSession.ConnectionResult { + val connectionResult = super.onConnect(session, controller) + val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() + + (shuffleCommands + repeatCommands).forEach { commandButton -> + commandButton.sessionCommand?.let { availableSessionCommands.add(it) } + } + + customLayout = buildCustomLayout(session.player) + + return MediaSession.ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands(availableSessionCommands.build()) + .setAvailablePlayerCommands(connectionResult.availablePlayerCommands) + .setCustomLayout(customLayout) + .build() + } + + override fun onPostConnect(session: MediaSession, controller: ControllerInfo) { + if (!customLayout.isEmpty() && controller.controllerVersion != 0) { + ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout)) + } + } + + fun buildCustomLayout(player: Player): ImmutableList { + val shuffle = shuffleCommands[if (player.shuffleModeEnabled) 1 else 0] + val repeat = when (player.repeatMode) { + Player.REPEAT_MODE_ONE -> repeatCommands[1] + Player.REPEAT_MODE_ALL -> repeatCommands[2] + else -> repeatCommands[0] + } + return ImmutableList.of(shuffle, repeat) + } + + override fun onCustomCommand( + session: MediaSession, + controller: ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + when (customCommand.customAction) { + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> player.shuffleModeEnabled = true + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> player.shuffleModeEnabled = false + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> { + val nextMode = when (player.repeatMode) { + Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL + Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE + else -> Player.REPEAT_MODE_OFF + } + player.repeatMode = nextMode + } + } + + customLayout = librarySessionCallback.buildCustomLayout(player) + session.setCustomLayout(customLayout) + + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + + override fun onAddMediaItems( + mediaSession: MediaSession, + controller: ControllerInfo, + mediaItems: List + ): ListenableFuture> { + val updatedMediaItems = mediaItems.map { mediaItem -> + val mediaMetadata = mediaItem.mediaMetadata + + val newMetadata = mediaMetadata.buildUpon() + .setArtist( + if (mediaMetadata.artist != null) mediaMetadata.artist + else mediaMetadata.extras?.getString("uri") ?: "" + ) + .build() + + mediaItem.buildUpon() + .setUri(mediaItem.requestMetadata.mediaUri) + .setMediaMetadata(newMetadata) + .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .build() + } + return Futures.immediateFuture(updatedMediaItems) + } + } + + private fun initializeCustomCommands() { + shuffleCommands = listOf( + getShuffleCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY) + ), + getShuffleCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY) + ) + ) + + repeatCommands = listOf( + getRepeatCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY) + ), + getRepeatCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY) + ), + getRepeatCommandButton( + SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY) + ) + ) + + customLayout = ImmutableList.of(shuffleCommands[0], repeatCommands[0]) + } + + private fun initializePlayer() { + player = ExoPlayer.Builder(this) + .setRenderersFactory(getRenderersFactory()) + .setMediaSourceFactory(getMediaSourceFactory()) + .setAudioAttributes(AudioAttributes.DEFAULT, true) + .setHandleAudioBecomingNoisy(true) + .setWakeMode(C.WAKE_MODE_NETWORK) + .setLoadControl(initializeLoadControl()) + .build() + + player.shuffleModeEnabled = Preferences.isShuffleModeEnabled() + player.repeatMode = Preferences.getRepeatMode() + } + + private fun initializeEqualizerManager() { + equalizerManager = EqualizerManager() + val audioSessionId = player.audioSessionId + attachEqualizerIfPossible(audioSessionId) + } + + private fun initializeMediaLibrarySession() { + val sessionActivityPendingIntent = + TaskStackBuilder.create(this).run { + addNextIntent(Intent(this@MediaService, MainActivity::class.java)) + getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT) + } + + mediaLibrarySession = + MediaLibrarySession.Builder(this, player, librarySessionCallback) + .setSessionActivity(sessionActivityPendingIntent) + .build() + + if (!customLayout.isEmpty()) { + mediaLibrarySession.setCustomLayout(customLayout) + } + } + + private fun initializeNetworkListener() { + networkCallback = CustomNetworkCallback() + getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback) + updateMediaItems() + } + + private fun restorePlayerFromQueue() { + if (player.mediaItemCount > 0) return + + val queueRepository = QueueRepository() + val storedQueue = queueRepository.media + if (storedQueue.isNullOrEmpty()) return + + val mediaItems = MappingUtil.mapMediaItems(storedQueue) + if (mediaItems.isEmpty()) return + + val lastIndex = try { + queueRepository.lastPlayedMediaIndex + } catch (_: Exception) { + 0 + }.coerceIn(0, mediaItems.size - 1) + + val lastPosition = try { + queueRepository.lastPlayedMediaTimestamp + } catch (_: Exception) { + 0L + }.let { if (it < 0L) 0L else it } + + player.setMediaItems(mediaItems, lastIndex, lastPosition) + player.prepare() + updateWidget() + } + + private fun initializePlayerListener() { + player.addListener(object : Player.Listener { + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + if (mediaItem == null) return + + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { + MediaManager.setLastPlayedTimestamp(mediaItem) + } + updateWidget() + } + + override fun onTracksChanged(tracks: Tracks) { + ReplayGainUtil.setReplayGain(player, tracks) + val currentMediaItem = player.currentMediaItem + if (currentMediaItem != null && currentMediaItem.mediaMetadata.extras != null) { + MediaManager.scrobble(currentMediaItem, false) + } + + if (player.currentMediaItemIndex + 1 == player.mediaItemCount) + MediaManager.continuousPlay(player.currentMediaItem) + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + if (!isPlaying) { + MediaManager.setPlayingPausedTimestamp( + player.currentMediaItem, + player.currentPosition + ) + } else { + MediaManager.scrobble(player.currentMediaItem, false) + } + if (isPlaying) { + scheduleWidgetUpdates() + } else { + stopWidgetUpdates() + } + updateWidget() + } + + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + if (!player.hasNextMediaItem() && + playbackState == Player.STATE_ENDED && + player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC + ) { + MediaManager.scrobble(player.currentMediaItem, true) + MediaManager.saveChronology(player.currentMediaItem) + } + updateWidget() + } + + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + super.onPositionDiscontinuity(oldPosition, newPosition, reason) + + if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { + if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { + MediaManager.scrobble(oldPosition.mediaItem, true) + MediaManager.saveChronology(oldPosition.mediaItem) + } + + if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { + MediaManager.setLastPlayedTimestamp(newPosition.mediaItem) + } + } + } + + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + Preferences.setShuffleModeEnabled(shuffleModeEnabled) + customLayout = librarySessionCallback.buildCustomLayout(player) + mediaLibrarySession.setCustomLayout(customLayout) + } + + override fun onRepeatModeChanged(repeatMode: Int) { + Preferences.setRepeatMode(repeatMode) + customLayout = librarySessionCallback.buildCustomLayout(player) + mediaLibrarySession.setCustomLayout(customLayout) + } + + override fun onAudioSessionIdChanged(audioSessionId: Int) { + attachEqualizerIfPossible(audioSessionId) + } + }) + if (player.isPlaying) { + scheduleWidgetUpdates() + } + } + + private fun setPlayer(player: Player) { + mediaLibrarySession.player = player + } + + private fun releasePlayer() { + player.release() + mediaLibrarySession.release() + } + + private fun releaseNetworkCallback() { + getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback) + } + + @SuppressLint("PrivateResource") + private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton { + val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON + return CommandButton.Builder() + .setDisplayName( + getString( + if (isOn) R.string.exo_controls_shuffle_on_description + else R.string.exo_controls_shuffle_off_description + ) + ) + .setSessionCommand(sessionCommand) + .setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on) + .build() + } + + @SuppressLint("PrivateResource") + private fun getRepeatCommandButton(sessionCommand: SessionCommand): CommandButton { + val icon = when (sessionCommand.customAction) { + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> R.drawable.exo_icon_repeat_one + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> R.drawable.exo_icon_repeat_all + else -> R.drawable.exo_icon_repeat_off + } + val description = when (sessionCommand.customAction) { + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> R.string.exo_controls_repeat_one_description + CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> R.string.exo_controls_repeat_all_description + else -> R.string.exo_controls_repeat_off_description + } + return CommandButton.Builder() + .setDisplayName(getString(description)) + .setSessionCommand(sessionCommand) + .setIconResId(icon) + .build() + } + + private fun ignoreFuture(@Suppress("UNUSED_PARAMETER") customLayout: ListenableFuture) { + /* Do nothing. */ + } + + private fun initializeLoadControl(): DefaultLoadControl { + return DefaultLoadControl.Builder() + .setBufferDurationsMs( + (DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(), + (DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(), + DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, + DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS + ) + .build() + } + + private fun updateWidget() { + val mi = player.currentMediaItem + val title = mi?.mediaMetadata?.title?.toString() + ?: mi?.mediaMetadata?.extras?.getString("title") + val artist = mi?.mediaMetadata?.artist?.toString() + ?: mi?.mediaMetadata?.extras?.getString("artist") + val album = mi?.mediaMetadata?.albumTitle?.toString() + ?: mi?.mediaMetadata?.extras?.getString("album") + val extras = mi?.mediaMetadata?.extras + val coverId = extras?.getString("coverArtId") + val songLink = extras?.getString("assetLinkSong") + ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id")) + val albumLink = extras?.getString("assetLinkAlbum") + ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId")) + val artistLink = extras?.getString("assetLinkArtist") + ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId")) + val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L + val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L + WidgetUpdateManager.updateFromState( + this, + title ?: "", + artist ?: "", + album ?: "", + coverId, + player.isPlaying, + player.shuffleModeEnabled, + player.repeatMode, + position, + duration, + songLink, + albumLink, + artistLink + ) + } + + private fun scheduleWidgetUpdates() { + if (widgetUpdateScheduled) return + widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS) + widgetUpdateScheduled = true + } + + private fun stopWidgetUpdates() { + if (!widgetUpdateScheduled) return + widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable) + widgetUpdateScheduled = false + } + + private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean { + if (audioSessionId == 0 || audioSessionId == -1) return false + val attached = equalizerManager.attachToSession(audioSessionId) + if (attached) { + val enabled = Preferences.isEqualizerEnabled() + equalizerManager.setEnabled(enabled) + val bands = equalizerManager.getNumberOfBands() + val savedLevels = Preferences.getEqualizerBandLevels(bands) + for (i in 0 until bands) { + equalizerManager.setBandLevel(i.toShort(), savedLevels[i]) + } + sendBroadcast(Intent(ACTION_EQUALIZER_UPDATED)) + } + return attached + } + + private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) + + private fun getMediaSourceFactory(): MediaSource.Factory = DynamicMediaSourceFactory(this) +} + +private const val WIDGET_UPDATE_INTERVAL_MS = 1000L diff --git a/app/src/degoogled/java/com/cappielloantonio/tempo/ui/fragment/ToolbarFragment.java b/app/src/degoogled/java/com/cappielloantonio/tempo/ui/fragment/ToolbarFragment.java new file mode 100644 index 0000000..2ad74c8 --- /dev/null +++ b/app/src/degoogled/java/com/cappielloantonio/tempo/ui/fragment/ToolbarFragment.java @@ -0,0 +1,65 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentToolbarBinding; +import com.cappielloantonio.tempo.ui.activity.MainActivity; + +@UnstableApi +public class ToolbarFragment extends Fragment { + private static final String TAG = "ToolbarFragment"; + + private FragmentToolbarBinding bind; + private MainActivity activity; + + public ToolbarFragment() { + // Required empty public constructor + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.main_page_menu, menu); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentToolbarBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + + return view; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_search) { + activity.navController.navigate(R.id.searchFragment); + return true; + } else if (item.getItemId() == R.id.action_settings) { + activity.navController.navigate(R.id.settingsFragment); + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/app/src/degoogled/java/com/cappielloantonio/tempo/util/Flavors.java b/app/src/degoogled/java/com/cappielloantonio/tempo/util/Flavors.java new file mode 100644 index 0000000..68d2350 --- /dev/null +++ b/app/src/degoogled/java/com/cappielloantonio/tempo/util/Flavors.java @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.util; + +import android.content.Context; + +public class Flavors { + public static void initializeCastContext(Context context) { + + } +} diff --git a/app/src/degoogled/res/drawable/ic_launcher_foreground.xml b/app/src/degoogled/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2f629d4 --- /dev/null +++ b/app/src/degoogled/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/degoogled/res/drawable/ic_splash_logo.xml b/app/src/degoogled/res/drawable/ic_splash_logo.xml new file mode 100644 index 0000000..cc4ea53 --- /dev/null +++ b/app/src/degoogled/res/drawable/ic_splash_logo.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/degoogled/res/menu/main_page_menu.xml b/app/src/degoogled/res/menu/main_page_menu.xml new file mode 100644 index 0000000..4016fef --- /dev/null +++ b/app/src/degoogled/res/menu/main_page_menu.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/degoogled/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/degoogled/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..7353dbd --- /dev/null +++ b/app/src/degoogled/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/degoogled/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/degoogled/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..7353dbd --- /dev/null +++ b/app/src/degoogled/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/degoogled/res/mipmap-hdpi/ic_launcher.webp b/app/src/degoogled/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..411c361 Binary files /dev/null and b/app/src/degoogled/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/degoogled/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/degoogled/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..fbbe2e1 Binary files /dev/null and b/app/src/degoogled/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/degoogled/res/mipmap-mdpi/ic_launcher.webp b/app/src/degoogled/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..940d52f Binary files /dev/null and b/app/src/degoogled/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/degoogled/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/degoogled/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..dd79b42 Binary files /dev/null and b/app/src/degoogled/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/degoogled/res/mipmap-xhdpi/ic_launcher.webp b/app/src/degoogled/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..f502e42 Binary files /dev/null and b/app/src/degoogled/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/degoogled/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/degoogled/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..40a9c5a Binary files /dev/null and b/app/src/degoogled/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/degoogled/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/degoogled/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..7454bb0 Binary files /dev/null and b/app/src/degoogled/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/degoogled/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/degoogled/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..0b81536 Binary files /dev/null and b/app/src/degoogled/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/degoogled/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/degoogled/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..d8ed149 Binary files /dev/null and b/app/src/degoogled/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/degoogled/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/degoogled/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1a6a93a Binary files /dev/null and b/app/src/degoogled/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/degoogled/res/values/ic_launcher_background.xml b/app/src/degoogled/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..dacc1d2 --- /dev/null +++ b/app/src/degoogled/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #626A75 + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b8d72d8 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..b086779 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/cappielloantonio/tempo/App.java b/app/src/main/java/com/cappielloantonio/tempo/App.java new file mode 100644 index 0000000..40105ee --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/App.java @@ -0,0 +1,109 @@ +package com.cappielloantonio.tempo; + +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import com.cappielloantonio.tempo.github.Github; +import com.cappielloantonio.tempo.helper.ThemeHelper; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.SubsonicPreferences; +import com.cappielloantonio.tempo.util.Preferences; + +public class App extends Application { + private static App instance; + private static Context context; + private static Subsonic subsonic; + private static Github github; + private static SharedPreferences preferences; + + @Override + public void onCreate() { + super.onCreate(); + + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + String themePref = sharedPreferences.getString(Preferences.THEME, ThemeHelper.DEFAULT_MODE); + ThemeHelper.applyTheme(themePref); + + instance = new App(); + context = getApplicationContext(); + preferences = PreferenceManager.getDefaultSharedPreferences(context); + } + + public static App getInstance() { + if (instance == null) { + instance = new App(); + } + + return instance; + } + + public static Context getContext() { + if (context == null) { + context = getInstance(); + } + + return context; + } + + public static Subsonic getSubsonicClientInstance(boolean override) { + if (subsonic == null || override) { + subsonic = getSubsonicClient(); + } + return subsonic; + } + + public static Github getGithubClientInstance() { + if (github == null) { + github = new Github(); + } + return github; + } + + public SharedPreferences getPreferences() { + if (preferences == null) { + preferences = PreferenceManager.getDefaultSharedPreferences(context); + } + + return preferences; + } + + public static void refreshSubsonicClient() { + subsonic = getSubsonicClient(); + } + + private static Subsonic getSubsonicClient() { + SubsonicPreferences preferences = getSubsonicPreferences(); + + if (preferences.getAuthentication() != null) { + if (preferences.getAuthentication().getPassword() != null) + Preferences.setPassword(preferences.getAuthentication().getPassword()); + if (preferences.getAuthentication().getToken() != null) + Preferences.setToken(preferences.getAuthentication().getToken()); + if (preferences.getAuthentication().getSalt() != null) + Preferences.setSalt(preferences.getAuthentication().getSalt()); + } + + return new Subsonic(preferences); + } + + @NonNull + private static SubsonicPreferences getSubsonicPreferences() { + String server = Preferences.getInUseServerAddress(); + String username = Preferences.getUser(); + String password = Preferences.getPassword(); + String token = Preferences.getToken(); + String salt = Preferences.getSalt(); + boolean isLowSecurity = Preferences.isLowScurity(); + + SubsonicPreferences preferences = new SubsonicPreferences(); + preferences.setServerUrl(server); + preferences.setUsername(username); + preferences.setAuthentication(password, token, salt, isLowSecurity); + + return preferences; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/broadcast/receiver/ConnectivityStatusBroadcastReceiver.java b/app/src/main/java/com/cappielloantonio/tempo/broadcast/receiver/ConnectivityStatusBroadcastReceiver.java new file mode 100644 index 0000000..21c34fd --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/broadcast/receiver/ConnectivityStatusBroadcastReceiver.java @@ -0,0 +1,34 @@ +package com.cappielloantonio.tempo.broadcast.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.view.View; + +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.ui.activity.MainActivity; + +@OptIn(markerClass = UnstableApi.class) +public class ConnectivityStatusBroadcastReceiver extends BroadcastReceiver { + private final MainActivity activity; + + public ConnectivityStatusBroadcastReceiver(MainActivity activity) { + this.activity = activity; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { + boolean noConnectivity = intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); + + if (noConnectivity) { + activity.bind.offlineModeTextView.setVisibility(View.VISIBLE); + } else { + activity.bind.offlineModeTextView.setVisibility(View.GONE); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java new file mode 100644 index 0000000..3a5e98e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/AppDatabase.java @@ -0,0 +1,69 @@ +package com.cappielloantonio.tempo.database; + +import androidx.media3.common.util.UnstableApi; +import androidx.room.AutoMigration; +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; +import androidx.room.TypeConverters; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.database.converter.DateConverters; +import com.cappielloantonio.tempo.database.dao.ChronologyDao; +import com.cappielloantonio.tempo.database.dao.DownloadDao; +import com.cappielloantonio.tempo.database.dao.FavoriteDao; +import com.cappielloantonio.tempo.database.dao.LyricsDao; +import com.cappielloantonio.tempo.database.dao.PlaylistDao; +import com.cappielloantonio.tempo.database.dao.QueueDao; +import com.cappielloantonio.tempo.database.dao.RecentSearchDao; +import com.cappielloantonio.tempo.database.dao.ServerDao; +import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao; +import com.cappielloantonio.tempo.model.Chronology; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.model.Favorite; +import com.cappielloantonio.tempo.model.LyricsCache; +import com.cappielloantonio.tempo.model.Queue; +import com.cappielloantonio.tempo.model.RecentSearch; +import com.cappielloantonio.tempo.model.Server; +import com.cappielloantonio.tempo.model.SessionMediaItem; +import com.cappielloantonio.tempo.subsonic.models.Playlist; + +@UnstableApi +@Database( + version = 12, + entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class}, + autoMigrations = {@AutoMigration(from = 10, to = 11), @AutoMigration(from = 11, to = 12)} +) +@TypeConverters({DateConverters.class}) +public abstract class AppDatabase extends RoomDatabase { + private final static String DB_NAME = "tempo_db"; + private static AppDatabase instance; + + public static synchronized AppDatabase getInstance() { + if (instance == null) { + instance = Room.databaseBuilder(App.getContext(), AppDatabase.class, DB_NAME) + .fallbackToDestructiveMigration() + .build(); + } + + return instance; + } + + public abstract QueueDao queueDao(); + + public abstract ServerDao serverDao(); + + public abstract RecentSearchDao recentSearchDao(); + + public abstract DownloadDao downloadDao(); + + public abstract ChronologyDao chronologyDao(); + + public abstract FavoriteDao favoriteDao(); + + public abstract SessionMediaItemDao sessionMediaItemDao(); + + public abstract PlaylistDao playlistDao(); + + public abstract LyricsDao lyricsDao(); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/converter/DateConverters.kt b/app/src/main/java/com/cappielloantonio/tempo/database/converter/DateConverters.kt new file mode 100644 index 0000000..132874b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/converter/DateConverters.kt @@ -0,0 +1,16 @@ +package com.cappielloantonio.tempo.database.converter + +import androidx.room.TypeConverter +import java.util.* + +class DateConverters { + @TypeConverter + fun fromTimestamp(value: Long?): Date? { + return value?.let { Date(it) } + } + + @TypeConverter + fun dateToTimestamp(date: Date?): Long? { + return date?.time + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/ChronologyDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/ChronologyDao.java new file mode 100644 index 0000000..1bb02ea --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/ChronologyDao.java @@ -0,0 +1,23 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.model.Chronology; + +import java.util.List; + +@Dao +public interface ChronologyDao { + @Query("SELECT * FROM chronology WHERE server == :server GROUP BY id ORDER BY timestamp DESC LIMIT :count") + LiveData> getLastPlayed(String server, int count); + + @Query("SELECT * FROM chronology WHERE timestamp >= :endDate AND timestamp < :startDate AND server == :server GROUP BY id ORDER BY COUNT(id) DESC LIMIT 20") + LiveData> getAllFrom(long startDate, long endDate, String server); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(Chronology chronologyObject); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/DownloadDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/DownloadDao.java new file mode 100644 index 0000000..a2d49f6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/DownloadDao.java @@ -0,0 +1,41 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.model.Download; + +import java.util.List; + +@Dao +public interface DownloadDao { + @Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC") + LiveData> getAll(); + + @Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC") + List getAllSync(); + + @Query("SELECT * FROM download WHERE id = :id") + Download getOne(String id); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(Download download); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insertAll(List downloads); + + @Query("UPDATE download SET download_state = 1 WHERE id = :id") + void update(String id); + + @Query("DELETE FROM download WHERE id = :id") + void delete(String id); + + @Query("DELETE FROM download WHERE id IN (:ids)") + void deleteByIds(List ids); + + @Query("DELETE FROM download") + void deleteAll(); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/FavoriteDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/FavoriteDao.java new file mode 100644 index 0000000..ec6ae68 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/FavoriteDao.java @@ -0,0 +1,26 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.model.Favorite; + +import java.util.List; + +@Dao +public interface FavoriteDao { + @Query("SELECT * FROM favorite") + List getAll(); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + void insert(Favorite favorite); + + @Delete + void delete(Favorite favorite); + + @Query("DELETE FROM favorite") + void deleteAll(); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/LyricsDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/LyricsDao.java new file mode 100644 index 0000000..89d0d58 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/LyricsDao.java @@ -0,0 +1,24 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.model.LyricsCache; + +@Dao +public interface LyricsDao { + @Query("SELECT * FROM lyrics_cache WHERE song_id = :songId") + LyricsCache getOne(String songId); + + @Query("SELECT * FROM lyrics_cache WHERE song_id = :songId") + LiveData observeOne(String songId); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(LyricsCache lyricsCache); + + @Query("DELETE FROM lyrics_cache WHERE song_id = :songId") + void delete(String songId); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/PlaylistDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/PlaylistDao.java new file mode 100644 index 0000000..52e025d --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/PlaylistDao.java @@ -0,0 +1,27 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.subsonic.models.Playlist; + +import java.util.List; + +@Dao +public interface PlaylistDao { + // @Query("SELECT * FROM playlist WHERE server=:serverId") + // LiveData> getAll(String serverId); + + @Query("SELECT * FROM playlist") + LiveData> getAll(); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(Playlist playlist); + + @Delete + void delete(Playlist playlist); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/QueueDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/QueueDao.java new file mode 100644 index 0000000..4c507b2 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/QueueDao.java @@ -0,0 +1,44 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.model.Queue; + +import java.util.List; + +@Dao +public interface QueueDao { + @Query("SELECT * FROM queue") + LiveData> getAll(); + + @Query("SELECT * FROM queue") + List getAllSimple(); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(Queue songQueueObject); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insertAll(List songQueueObjects); + + @Query("DELETE FROM queue WHERE queue.track_order=:position") + void delete(int position); + + @Query("DELETE FROM queue") + void deleteAll(); + + @Query("SELECT COUNT(*) FROM queue") + int count(); + + @Query("UPDATE queue SET last_play=:timestamp WHERE id=:id") + void setLastPlay(String id, long timestamp); + + @Query("UPDATE queue SET playing_changed=:timestamp WHERE id=:id") + void setPlayingChanged(String id, long timestamp); + + @Query("SELECT * FROM queue ORDER BY last_play DESC LIMIT 1") + Queue getLastPlayed(); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/RecentSearchDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/RecentSearchDao.java new file mode 100644 index 0000000..b1bd6c0 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/RecentSearchDao.java @@ -0,0 +1,23 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.model.RecentSearch; + +import java.util.List; + +@Dao +public interface RecentSearchDao { + @Query("SELECT * FROM recent_search ORDER BY search DESC") + List getRecent(); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(RecentSearch search); + + @Delete + void delete(RecentSearch search); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/ServerDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/ServerDao.java new file mode 100644 index 0000000..b4a6bbc --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/ServerDao.java @@ -0,0 +1,24 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.model.Server; + +import java.util.List; + +@Dao +public interface ServerDao { + @Query("SELECT * FROM server") + LiveData> getAll(); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(Server server); + + @Delete + void delete(Server server); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/database/dao/SessionMediaItemDao.java b/app/src/main/java/com/cappielloantonio/tempo/database/dao/SessionMediaItemDao.java new file mode 100644 index 0000000..a3f415a --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/database/dao/SessionMediaItemDao.java @@ -0,0 +1,29 @@ +package com.cappielloantonio.tempo.database.dao; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import com.cappielloantonio.tempo.model.Queue; +import com.cappielloantonio.tempo.model.SessionMediaItem; + +import java.util.List; + +@Dao +public interface SessionMediaItemDao { + @Query("SELECT * FROM session_media_item WHERE id = :id") + SessionMediaItem get(String id); + + @Query("SELECT * FROM session_media_item WHERE timestamp = :timestamp") + List get(long timestamp); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + void insert(SessionMediaItem sessionMediaItem); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + void insertAll(List sessionMediaItems); + + @Query("DELETE FROM session_media_item") + void deleteAll(); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/github/Github.java b/app/src/main/java/com/cappielloantonio/tempo/github/Github.java new file mode 100644 index 0000000..f0b0de4 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/github/Github.java @@ -0,0 +1,29 @@ +package com.cappielloantonio.tempo.github; + +import com.cappielloantonio.tempo.github.api.release.ReleaseClient; + +public class Github { + private static final String OWNER = "eddyizm"; + private static final String REPO = "Tempus"; + private ReleaseClient releaseClient; + + public ReleaseClient getReleaseClient() { + if (releaseClient == null) { + releaseClient = new ReleaseClient(this); + } + + return releaseClient; + } + + public String getUrl() { + return "https://api.github.com/"; + } + + public static String getOwner() { + return OWNER; + } + + public static String getRepo() { + return REPO; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/github/GithubRetrofitClient.kt b/app/src/main/java/com/cappielloantonio/tempo/github/GithubRetrofitClient.kt new file mode 100644 index 0000000..b9d1035 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/github/GithubRetrofitClient.kt @@ -0,0 +1,30 @@ +package com.cappielloantonio.tempo.github + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +class GithubRetrofitClient(github: Github) { + var retrofit: Retrofit + + init { + retrofit = Retrofit.Builder() + .baseUrl(github.url) + .addConverterFactory(GsonConverterFactory.create()) + .client(getOkHttpClient()) + .build() + } + + private fun getOkHttpClient(): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(getHttpLoggingInterceptor()) + .build() + } + + private fun getHttpLoggingInterceptor(): HttpLoggingInterceptor { + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY) + return loggingInterceptor + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/github/api/release/ReleaseClient.java b/app/src/main/java/com/cappielloantonio/tempo/github/api/release/ReleaseClient.java new file mode 100644 index 0000000..9bcf410 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/github/api/release/ReleaseClient.java @@ -0,0 +1,24 @@ +package com.cappielloantonio.tempo.github.api.release; + +import android.util.Log; + +import com.cappielloantonio.tempo.github.Github; +import com.cappielloantonio.tempo.github.GithubRetrofitClient; +import com.cappielloantonio.tempo.github.models.LatestRelease; + +import retrofit2.Call; + +public class ReleaseClient { + private static final String TAG = "ReleaseClient"; + + private final ReleaseService releaseService; + + public ReleaseClient(Github github) { + this.releaseService = new GithubRetrofitClient(github).getRetrofit().create(ReleaseService.class); + } + + public Call getLatestRelease() { + Log.d(TAG, "getLatestRelease()"); + return releaseService.getLatestRelease(Github.getOwner(), Github.getRepo()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/github/api/release/ReleaseService.java b/app/src/main/java/com/cappielloantonio/tempo/github/api/release/ReleaseService.java new file mode 100644 index 0000000..be6faf3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/github/api/release/ReleaseService.java @@ -0,0 +1,12 @@ +package com.cappielloantonio.tempo.github.api.release; + +import com.cappielloantonio.tempo.github.models.LatestRelease; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Path; + +public interface ReleaseService { + @GET("repos/{owner}/{repo}/releases/latest") + Call getLatestRelease(@Path("owner") String owner, @Path("repo") String repo); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/github/models/Assets.kt b/app/src/main/java/com/cappielloantonio/tempo/github/models/Assets.kt new file mode 100644 index 0000000..6e815a7 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/github/models/Assets.kt @@ -0,0 +1,34 @@ +package com.cappielloantonio.tempo.github.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class Assets( + @SerializedName("url") + var url: String? = null, + @SerializedName("id") + var id: Int? = null, + @SerializedName("node_id") + var nodeId: String? = null, + @SerializedName("name") + var name: String? = null, + @SerializedName("label") + var label: String? = null, + @SerializedName("uploader") + var uploader: Uploader? = Uploader(), + @SerializedName("content_type") + var contentType: String? = null, + @SerializedName("state") + var state: String? = null, + @SerializedName("size") + var size: Int? = null, + @SerializedName("download_count") + var downloadCount: Int? = null, + @SerializedName("created_at") + var createdAt: String? = null, + @SerializedName("updated_at") + var updatedAt: String? = null, + @SerializedName("browser_download_url") + var browserDownloadUrl: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/github/models/Author.kt b/app/src/main/java/com/cappielloantonio/tempo/github/models/Author.kt new file mode 100644 index 0000000..996da2d --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/github/models/Author.kt @@ -0,0 +1,44 @@ +package com.cappielloantonio.tempo.github.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class Author( + @SerializedName("login") + var login: String? = null, + @SerializedName("id") + var id: Int? = null, + @SerializedName("node_id") + var nodeId: String? = null, + @SerializedName("avatar_url") + var avatarUrl: String? = null, + @SerializedName("gravatar_id") + var gravatarId: String? = null, + @SerializedName("url") + var url: String? = null, + @SerializedName("html_url") + var htmlUrl: String? = null, + @SerializedName("followers_url") + var followersUrl: String? = null, + @SerializedName("following_url") + var followingUrl: String? = null, + @SerializedName("gists_url") + var gistsUrl: String? = null, + @SerializedName("starred_url") + var starredUrl: String? = null, + @SerializedName("subscriptions_url") + var subscriptionsUrl: String? = null, + @SerializedName("organizations_url") + var organizationsUrl: String? = null, + @SerializedName("repos_url") + var reposUrl: String? = null, + @SerializedName("events_url") + var eventsUrl: String? = null, + @SerializedName("received_events_url") + var receivedEventsUrl: String? = null, + @SerializedName("type") + var type: String? = null, + @SerializedName("site_admin") + var siteAdmin: Boolean? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/github/models/LatestRelease.kt b/app/src/main/java/com/cappielloantonio/tempo/github/models/LatestRelease.kt new file mode 100644 index 0000000..ae4a8ec --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/github/models/LatestRelease.kt @@ -0,0 +1,46 @@ +package com.cappielloantonio.tempo.github.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class LatestRelease( + @SerializedName("url") + var url: String? = null, + @SerializedName("assets_url") + var assetsUrl: String? = null, + @SerializedName("upload_url") + var uploadUrl: String? = null, + @SerializedName("html_url") + var htmlUrl: String? = null, + @SerializedName("id") + var id: Int? = null, + @SerializedName("author") + var author: Author? = Author(), + @SerializedName("node_id") + var nodeId: String? = null, + @SerializedName("tag_name") + var tagName: String? = null, + @SerializedName("target_commitish") + var targetCommitish: String? = null, + @SerializedName("name") + var name: String? = null, + @SerializedName("draft") + var draft: Boolean? = null, + @SerializedName("prerelease") + var prerelease: Boolean? = null, + @SerializedName("created_at") + var createdAt: String? = null, + @SerializedName("published_at") + var publishedAt: String? = null, + @SerializedName("assets") + var assets: ArrayList = arrayListOf(), + @SerializedName("tarball_url") + var tarballUrl: String? = null, + @SerializedName("zipball_url") + var zipballUrl: String? = null, + @SerializedName("body") + var body: String? = null, + @SerializedName("reactions") + var reactions: Reactions? = Reactions() +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/github/models/Reactions.kt b/app/src/main/java/com/cappielloantonio/tempo/github/models/Reactions.kt new file mode 100644 index 0000000..21f3f90 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/github/models/Reactions.kt @@ -0,0 +1,28 @@ +package com.cappielloantonio.tempo.github.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class Reactions( + @SerializedName("url") + var url: String? = null, + @SerializedName("total_count") + var totalCount: Int? = null, + @SerializedName("+1") + var like: Int? = null, + @SerializedName("-1") + var dislike: Int? = null, + @SerializedName("laugh") + var laugh: Int? = null, + @SerializedName("hooray") + var hooray: Int? = null, + @SerializedName("confused") + var confused: Int? = null, + @SerializedName("heart") + var heart: Int? = null, + @SerializedName("rocket") + var rocket: Int? = null, + @SerializedName("eyes") + var eyes: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/github/models/Uploader.kt b/app/src/main/java/com/cappielloantonio/tempo/github/models/Uploader.kt new file mode 100644 index 0000000..e0c74ec --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/github/models/Uploader.kt @@ -0,0 +1,44 @@ +package com.cappielloantonio.tempo.github.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +data class Uploader( + @SerializedName("login") + var login: String? = null, + @SerializedName("id") + var id: Int? = null, + @SerializedName("node_id") + var nodeId: String? = null, + @SerializedName("avatar_url") + var avatarUrl: String? = null, + @SerializedName("gravatar_id") + var gravatarId: String? = null, + @SerializedName("url") + var url: String? = null, + @SerializedName("html_url") + var htmlUrl: String? = null, + @SerializedName("followers_url") + var followersUrl: String? = null, + @SerializedName("following_url") + var followingUrl: String? = null, + @SerializedName("gists_url") + var gistsUrl: String? = null, + @SerializedName("starred_url") + var starredUrl: String? = null, + @SerializedName("subscriptions_url") + var subscriptionsUrl: String? = null, + @SerializedName("organizations_url") + var organizationsUrl: String? = null, + @SerializedName("repos_url") + var reposUrl: String? = null, + @SerializedName("events_url") + var eventsUrl: String? = null, + @SerializedName("received_events_url") + var receivedEventsUrl: String? = null, + @SerializedName("type") + var type: String? = null, + @SerializedName("site_admin") + var siteAdmin: Boolean? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/github/utils/UpdateUtil.java b/app/src/main/java/com/cappielloantonio/tempo/github/utils/UpdateUtil.java new file mode 100644 index 0000000..a4e61fb --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/github/utils/UpdateUtil.java @@ -0,0 +1,32 @@ +package com.cappielloantonio.tempo.github.utils; + +import com.cappielloantonio.tempo.BuildConfig; +import com.cappielloantonio.tempo.github.models.LatestRelease; + +public class UpdateUtil { + + public static boolean showUpdateDialog(LatestRelease release) { + if (release.getTagName() == null) return false; + String remoteTag = release.getTagName().replaceAll("^\\D+", ""); + + try { + String[] local = BuildConfig.VERSION_NAME.split("\\."); + String[] remote = remoteTag.split("\\."); + + for (int i = 0; i < local.length; i++) { + int localPart = Integer.parseInt(local[i]); + int remotePart = Integer.parseInt(remote[i]); + + if (localPart > remotePart) { + return false; + } else if (localPart < remotePart) { + return true; + } + } + } catch (Exception exception) { + return false; + } + + return false; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/glide/CustomGlideModule.java b/app/src/main/java/com/cappielloantonio/tempo/glide/CustomGlideModule.java new file mode 100644 index 0000000..ccbffb2 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/glide/CustomGlideModule.java @@ -0,0 +1,32 @@ +package com.cappielloantonio.tempo.glide; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.GlideBuilder; +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.load.DecodeFormat; +import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory; +import com.bumptech.glide.Registry; +import com.bumptech.glide.module.AppGlideModule; +import com.bumptech.glide.request.RequestOptions; +import com.cappielloantonio.tempo.util.Preferences; + +import java.io.InputStream; + +@GlideModule +public class CustomGlideModule extends AppGlideModule { + @Override + public void applyOptions(@NonNull Context context, GlideBuilder builder) { + int diskCacheSize = Preferences.getImageCacheSize() * 1024 * 1024; + builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "cache", diskCacheSize)); + builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565)); + } + + @Override + public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { + registry.replace(String.class, InputStream.class, new IPv6StringLoader.Factory()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/glide/CustomGlideRequest.java b/app/src/main/java/com/cappielloantonio/tempo/glide/CustomGlideRequest.java new file mode 100644 index 0000000..a6e650e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/glide/CustomGlideRequest.java @@ -0,0 +1,150 @@ +package com.cappielloantonio.tempo.glide; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.bumptech.glide.RequestManager; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.signature.ObjectKey; +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.util.Util; +import com.google.android.material.elevation.SurfaceColors; + +import java.util.Map; + +public class CustomGlideRequest { + private static final String TAG = "CustomGlideRequest"; + + public static final int CORNER_RADIUS = Preferences.isCornerRoundingEnabled() ? Preferences.getRoundedCornerSize() : 1; + + public static final DiskCacheStrategy DEFAULT_DISK_CACHE_STRATEGY = DiskCacheStrategy.ALL; + + public enum ResourceType { + Unknown, + Album, + Artist, + Folder, + Directory, + Playlist, + Podcast, + Radio, + Song, + } + + public static RequestOptions createRequestOptions(Context context, String item, ResourceType type) { + return new RequestOptions() + .placeholder(new ColorDrawable(SurfaceColors.SURFACE_5.getColor(context))) + .fallback(getPlaceholder(context, type)) + .error(getPlaceholder(context, type)) + .diskCacheStrategy(DEFAULT_DISK_CACHE_STRATEGY) + .signature(new ObjectKey(item != null ? item : 0)) + .transform(new CenterCrop(), new RoundedCorners(CustomGlideRequest.CORNER_RADIUS)); + } + + @Nullable + private static Drawable getPlaceholder(Context context, ResourceType type) { + switch (type) { + case Album: + return AppCompatResources.getDrawable(context, R.drawable.ic_placeholder_album); + case Artist: + return AppCompatResources.getDrawable(context, R.drawable.ic_placeholder_artist); + case Folder: + return AppCompatResources.getDrawable(context, R.drawable.ic_placeholder_folder); + case Directory: + return AppCompatResources.getDrawable(context, R.drawable.ic_placeholder_directory); + case Playlist: + return AppCompatResources.getDrawable(context, R.drawable.ic_placeholder_playlist); + case Podcast: + return AppCompatResources.getDrawable(context, R.drawable.ic_placeholder_podcast); + case Radio: + return AppCompatResources.getDrawable(context, R.drawable.ic_placeholder_radio); + case Song: + return AppCompatResources.getDrawable(context, R.drawable.ic_placeholder_song); + default: + case Unknown: + return new ColorDrawable(SurfaceColors.SURFACE_5.getColor(context)); + } + } + + public static String createUrl(String item, int size) { + Map params = App.getSubsonicClientInstance(false).getParams(); + + StringBuilder uri = new StringBuilder(); + + uri.append(App.getSubsonicClientInstance(false).getUrl()); + uri.append("getCoverArt"); + + if (params.containsKey("u") && params.get("u") != null) + uri.append("?u=").append(Util.encode(params.get("u"))); + if (params.containsKey("p") && params.get("p") != null) + uri.append("&p=").append(params.get("p")); + if (params.containsKey("s") && params.get("s") != null) + uri.append("&s=").append(params.get("s")); + if (params.containsKey("t") && params.get("t") != null) + uri.append("&t=").append(params.get("t")); + if (params.containsKey("v") && params.get("v") != null) + uri.append("&v=").append(params.get("v")); + if (params.containsKey("c") && params.get("c") != null) + uri.append("&c=").append(params.get("c")); + if (size != -1) + uri.append("&size=").append(size); + + uri.append("&id=").append(item); + + Log.d(TAG, "createUrl() " + uri); + + return uri.toString(); + } + + public static void loadAlbumArtBitmap(Context context, + String coverId, + int size, + CustomTarget target) { + String url = createUrl(coverId, size); + Glide.with(context) + .asBitmap() + .load(url) + .apply(createRequestOptions(context, coverId, ResourceType.Album)) + .into(target); + } + + public static class Builder { + private final RequestManager requestManager; + private String item; + + private Builder(Context context, String item, ResourceType type) { + this.requestManager = Glide.with(context); + + if (item != null && !Preferences.isDataSavingMode()) { + this.item = createUrl(item, Preferences.getImageSize()); + } + + requestManager.applyDefaultRequestOptions(createRequestOptions(context, item, type)); + } + + public static Builder from(Context context, String item, ResourceType type) { + return new Builder(context, item, type); + } + + public RequestBuilder build() { + return requestManager + .load(item) + .transition(DrawableTransitionOptions.withCrossFade()); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/glide/IPv6StringLoader.java b/app/src/main/java/com/cappielloantonio/tempo/glide/IPv6StringLoader.java new file mode 100644 index 0000000..85307ac --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/glide/IPv6StringLoader.java @@ -0,0 +1,110 @@ +package com.cappielloantonio.tempo.glide; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.model.ModelLoader; +import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.bumptech.glide.load.model.MultiModelLoaderFactory; +import com.bumptech.glide.signature.ObjectKey; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +public class IPv6StringLoader implements ModelLoader { + private static final int DEFAULT_TIMEOUT_MS = 2500; + + @Override + public boolean handles(@NonNull String model) { + return model.startsWith("http://") || model.startsWith("https://"); + } + + @Override + public LoadData buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) { + if (!handles(model)) { + return null; + } + return new LoadData<>(new ObjectKey(model), new IPv6StreamFetcher(model)); + } + + private static class IPv6StreamFetcher implements DataFetcher { + private final String model; + private InputStream stream; + private HttpURLConnection connection; + + IPv6StreamFetcher(String model) { + this.model = model; + } + + @Override + public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { + try { + URL url = new URL(model); + connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(DEFAULT_TIMEOUT_MS); + connection.setReadTimeout(DEFAULT_TIMEOUT_MS); + connection.setUseCaches(true); + connection.setDoInput(true); + connection.connect(); + + if (connection.getResponseCode() / 100 != 2) { + callback.onLoadFailed(new IOException("Request failed with status code: " + connection.getResponseCode())); + return; + } + + stream = connection.getInputStream(); + callback.onDataReady(stream); + } catch (IOException e) { + callback.onLoadFailed(e); + } + } + + @Override + public void cleanup() { + if (stream != null) { + try { + stream.close(); + } catch (IOException ignored) { + } + } + if (connection != null) { + connection.disconnect(); + } + } + + @Override + public void cancel() { + // HttpURLConnection does not provide a direct cancel mechanism. + } + + @NonNull + @Override + public Class getDataClass() { + return InputStream.class; + } + + @NonNull + @Override + public DataSource getDataSource() { + return DataSource.REMOTE; + } + } + + public static class Factory implements ModelLoaderFactory { + @NonNull + @Override + public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { + return new IPv6StringLoader(); + } + + @Override + public void teardown() { + // No-op + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/helper/ThemeHelper.java b/app/src/main/java/com/cappielloantonio/tempo/helper/ThemeHelper.java new file mode 100644 index 0000000..d10392b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/helper/ThemeHelper.java @@ -0,0 +1,35 @@ +package com.cappielloantonio.tempo.helper; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatDelegate; + +public class ThemeHelper { + private static final String TAG = "ThemeHelper"; + + public static final String LIGHT_MODE = "light"; + public static final String DARK_MODE = "dark"; + public static final String DEFAULT_MODE = "default"; + + public static void applyTheme(@NonNull String themePref) { + switch (themePref) { + case LIGHT_MODE: { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + break; + } + case DARK_MODE: { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + break; + } + default: { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY); + } + break; + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/CustomLinearSnapHelper.java b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/CustomLinearSnapHelper.java new file mode 100644 index 0000000..4d045d0 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/CustomLinearSnapHelper.java @@ -0,0 +1,24 @@ +package com.cappielloantonio.tempo.helper.recyclerview; + +import android.view.View; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.LinearSnapHelper; +import androidx.recyclerview.widget.RecyclerView; + +public class CustomLinearSnapHelper extends LinearSnapHelper { + @Override + public View findSnapView(RecyclerView.LayoutManager layoutManager) { + if (layoutManager instanceof LinearLayoutManager) { + LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; + if (!needToDoSnap(linearLayoutManager)) { + return null; + } + } + return super.findSnapView(layoutManager); + } + + public boolean needToDoSnap(LinearLayoutManager linearLayoutManager) { + return linearLayoutManager.findFirstCompletelyVisibleItemPosition() != 0 && linearLayoutManager.findLastCompletelyVisibleItemPosition() != linearLayoutManager.getItemCount() - 1; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/DotsIndicatorDecoration.java b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/DotsIndicatorDecoration.java new file mode 100644 index 0000000..0dcf9c9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/DotsIndicatorDecoration.java @@ -0,0 +1,116 @@ +package com.cappielloantonio.tempo.helper.recyclerview; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.jetbrains.annotations.NotNull; + +public class DotsIndicatorDecoration extends RecyclerView.ItemDecoration { + private static final String TAG = "DotsIndicatorDecoration"; + + private final int indicatorHeight; + private final int indicatorItemPadding; + private final int radius; + + private final Paint inactivePaint = new Paint(); + private final Paint activePaint = new Paint(); + + public DotsIndicatorDecoration(int radius, int padding, int indicatorHeight, @ColorInt int colorInactive, @ColorInt int colorActive) { + float strokeWidth = Resources.getSystem().getDisplayMetrics().density * 1; + this.radius = radius; + + inactivePaint.setStrokeCap(Paint.Cap.ROUND); + inactivePaint.setStrokeWidth(strokeWidth); + inactivePaint.setStyle(Paint.Style.STROKE); + inactivePaint.setAntiAlias(true); + inactivePaint.setColor(colorInactive); + + activePaint.setStrokeCap(Paint.Cap.ROUND); + activePaint.setStrokeWidth(strokeWidth); + activePaint.setStyle(Paint.Style.FILL); + activePaint.setAntiAlias(true); + activePaint.setColor(colorActive); + + this.indicatorItemPadding = padding; + this.indicatorHeight = indicatorHeight; + } + + @Override + public void onDrawOver(@NotNull Canvas c, @NotNull RecyclerView parent, @NotNull RecyclerView.State state) { + super.onDrawOver(c, parent, state); + + if (parent.getAdapter() == null) return; + + int itemCount = (int) Math.ceil((double) parent.getAdapter().getItemCount() / 5); + + if (itemCount <= 1) { + return; + } + + // center horizontally, calculate width and subtract half from center + float totalLength = this.radius * 2 * itemCount; + float paddingBetweenItems = Math.max(0, itemCount - 1) * indicatorItemPadding; + float indicatorTotalWidth = totalLength + paddingBetweenItems; + float indicatorStartX = (parent.getWidth() - indicatorTotalWidth) / 2f; + + // center vertically in the allotted space + float indicatorPosY = parent.getHeight() - indicatorHeight - (float) indicatorItemPadding / 4; + + drawInactiveDots(c, indicatorStartX, indicatorPosY, itemCount); + + final int activePosition; + + if (parent.getLayoutManager() instanceof GridLayoutManager) { + activePosition = ((GridLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition(); + } else if (parent.getLayoutManager() instanceof LinearLayoutManager) { + activePosition = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition(); + } else { + // not supported layout manager + return; + } + + if (activePosition == RecyclerView.NO_POSITION) { + return; + } + + // find offset of active page if the user is scrolling + final View activeChild = parent.getLayoutManager().findViewByPosition(activePosition); + if (activeChild == null) { + return; + } + + drawActiveDot(c, indicatorStartX, indicatorPosY, activePosition); + } + + private void drawInactiveDots(Canvas c, float indicatorStartX, float indicatorPosY, int itemCount) { + // width of item indicator including padding + final float itemWidth = this.radius * 2 + indicatorItemPadding; + + float start = indicatorStartX + radius; + for (int i = 0; i < itemCount; i++) { + c.drawCircle(start, indicatorPosY, radius, inactivePaint); + start += itemWidth; + } + } + + private void drawActiveDot(Canvas c, float indicatorStartX, float indicatorPosY, int highlightPosition) { + // width of item indicator including padding + final float itemWidth = this.radius * 2 + indicatorItemPadding; + float highlightStart = (float) Math.ceil(indicatorStartX + radius + itemWidth * highlightPosition / 5); + c.drawCircle(highlightStart, indicatorPosY, radius, activePaint); + } + + @Override + public void getItemOffsets(@NotNull Rect outRect, @NotNull View view, @NotNull RecyclerView parent, @NotNull RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + outRect.bottom = indicatorHeight; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/FastScrollbar.java b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/FastScrollbar.java new file mode 100644 index 0000000..d479813 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/FastScrollbar.java @@ -0,0 +1,197 @@ +package com.cappielloantonio.tempo.helper.recyclerview; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.IdRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public class FastScrollbar extends LinearLayout { + private static final int BUBBLE_ANIMATION_DURATION = 100; + private static final int TRACK_SNAP_RANGE = 5; + + private TextView bubble; + private View handle; + private RecyclerView recyclerView; + private int height; + private boolean isInitialized = false; + private ObjectAnimator currentAnimator = null; + + private final RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) { + updateBubbleAndHandlePosition(); + } + }; + + public interface BubbleTextGetter { + String getTextToShowInBubble(int pos); + } + + public FastScrollbar(final Context context, final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + public FastScrollbar(final Context context) { + super(context); + init(context); + } + + public FastScrollbar(final Context context, final AttributeSet attrs) { + super(context, attrs); + init(context); + } + + protected void init(Context context) { + if (isInitialized) return; + isInitialized = true; + setOrientation(HORIZONTAL); + setClipChildren(false); + } + + public void setViewsToUse(@LayoutRes int layoutResId, @IdRes int bubbleResId, @IdRes int handleResId) { + final LayoutInflater inflater = LayoutInflater.from(getContext()); + inflater.inflate(layoutResId, this, true); + bubble = findViewById(bubbleResId); + if (bubble != null) bubble.setVisibility(INVISIBLE); + handle = findViewById(handleResId); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + height = h; + updateBubbleAndHandlePosition(); + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + final int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + if (event.getX() < handle.getX() - ViewCompat.getPaddingStart(handle)) return false; + if (currentAnimator != null) currentAnimator.cancel(); + if (bubble != null && bubble.getVisibility() == INVISIBLE) showBubble(); + handle.setSelected(true); + case MotionEvent.ACTION_MOVE: + final float y = event.getY(); + setBubbleAndHandlePosition(y); + setRecyclerViewPosition(y); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + handle.setSelected(false); + hideBubble(); + return true; + } + return super.onTouchEvent(event); + } + + public void setRecyclerView(final RecyclerView recyclerView) { + if (this.recyclerView != recyclerView) { + if (this.recyclerView != null) + this.recyclerView.removeOnScrollListener(onScrollListener); + this.recyclerView = recyclerView; + if (this.recyclerView == null) return; + recyclerView.addOnScrollListener(onScrollListener); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (recyclerView != null) { + recyclerView.removeOnScrollListener(onScrollListener); + recyclerView = null; + } + } + + private void setRecyclerViewPosition(float y) { + if (recyclerView != null) { + final int itemCount = recyclerView.getAdapter().getItemCount(); + float proportion; + if (handle.getY() == 0) proportion = 0f; + else if (handle.getY() + handle.getHeight() >= height - TRACK_SNAP_RANGE) + proportion = 1f; + else proportion = y / (float) height; + final int targetPos = getValueInRange(0, itemCount - 1, (int) (proportion * (float) itemCount)); + ((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(targetPos, 0); + final String bubbleText = ((BubbleTextGetter) recyclerView.getAdapter()).getTextToShowInBubble(targetPos); + if (bubble != null) { + bubble.setText(bubbleText); + if (TextUtils.isEmpty(bubbleText)) { + hideBubble(); + } else if (bubble.getVisibility() == View.INVISIBLE) { + showBubble(); + } + } + } + } + + private int getValueInRange(int min, int max, int value) { + int minimum = Math.max(min, value); + return Math.min(minimum, max); + } + + private void updateBubbleAndHandlePosition() { + if (bubble == null || handle.isSelected()) return; + + final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset(); + final int verticalScrollRange = recyclerView.computeVerticalScrollRange(); + float proportion = (float) verticalScrollOffset / ((float) verticalScrollRange - height); + setBubbleAndHandlePosition(height * proportion); + } + + private void setBubbleAndHandlePosition(float y) { + final int handleHeight = handle.getHeight(); + handle.setY(getValueInRange(0, height - handleHeight, (int) (y - handleHeight / 2))); + if (bubble != null) { + int bubbleHeight = bubble.getHeight(); + bubble.setY(getValueInRange(0, height - bubbleHeight - handleHeight / 2, (int) (y - bubbleHeight))); + } + } + + private void showBubble() { + if (bubble == null) return; + bubble.setVisibility(VISIBLE); + if (currentAnimator != null) currentAnimator.cancel(); + currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 0f, 1f).setDuration(BUBBLE_ANIMATION_DURATION); + currentAnimator.start(); + } + + private void hideBubble() { + if (bubble == null) return; + if (currentAnimator != null) currentAnimator.cancel(); + currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 1f, 0f).setDuration(BUBBLE_ANIMATION_DURATION); + currentAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + bubble.setVisibility(INVISIBLE); + currentAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + bubble.setVisibility(INVISIBLE); + currentAnimator = null; + } + }); + currentAnimator.start(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/GridItemDecoration.java b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/GridItemDecoration.java new file mode 100644 index 0000000..71f5479 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/GridItemDecoration.java @@ -0,0 +1,41 @@ +package com.cappielloantonio.tempo.helper.recyclerview; + +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +public class GridItemDecoration extends RecyclerView.ItemDecoration { + private final int spanCount; + private final int spacing; + private final boolean includeEdge; + + public GridItemDecoration(int spanCount, int spacing, boolean includeEdge) { + this.spanCount = spanCount; + this.spacing = spacing; + this.includeEdge = includeEdge; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, RecyclerView parent, @NonNull RecyclerView.State state) { + int position = parent.getChildAdapterPosition(view); // item position + int column = position % spanCount; // item column + + if (includeEdge) { + outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing) + outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing) + + if (position < spanCount) { // top edge + outRect.top = spacing; + } + outRect.bottom = spacing; // item bottom + } else { + outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing) + outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing) + if (position >= spanCount) { + outRect.top = spacing; // item top + } + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/NestedScrollableHost.kt b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/NestedScrollableHost.kt new file mode 100644 index 0000000..b2fdba2 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/NestedScrollableHost.kt @@ -0,0 +1,88 @@ +package com.cappielloantonio.tempo.helper.recyclerview + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import android.widget.FrameLayout +import androidx.viewpager2.widget.ViewPager2 +import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL +import kotlin.math.absoluteValue +import kotlin.math.sign + +class NestedScrollableHost : FrameLayout { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + private var touchSlop = 0 + private var initialX = 0f + private var initialY = 0f + private val parentViewPager: ViewPager2? + get() { + var v: View? = parent as? View + while (v != null && v !is ViewPager2) { + v = v.parent as? View + } + return v as? ViewPager2 + } + + private val child: View? get() = if (childCount > 0) getChildAt(0) else null + + init { + touchSlop = ViewConfiguration.get(context).scaledTouchSlop + } + + private fun canChildScroll(orientation: Int, delta: Float): Boolean { + val direction = -delta.sign.toInt() + return when (orientation) { + 0 -> child?.canScrollHorizontally(direction) ?: false + 1 -> child?.canScrollVertically(direction) ?: false + else -> throw IllegalArgumentException() + } + } + + override fun onInterceptTouchEvent(e: MotionEvent): Boolean { + handleInterceptTouchEvent(e) + return super.onInterceptTouchEvent(e) + } + + private fun handleInterceptTouchEvent(e: MotionEvent) { + val orientation = parentViewPager?.orientation ?: return + + // Early return if child can't scroll in same direction as parent + if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) { + return + } + + if (e.action == MotionEvent.ACTION_DOWN) { + initialX = e.x + initialY = e.y + parent.requestDisallowInterceptTouchEvent(true) + } else if (e.action == MotionEvent.ACTION_MOVE) { + val dx = e.x - initialX + val dy = e.y - initialY + val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL + + // assuming ViewPager2 touch-slop is 2x touch-slop of child + val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f + val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f + + if (scaledDx > touchSlop || scaledDy > touchSlop) { + if (isVpHorizontal == (scaledDy > scaledDx)) { + // Gesture is perpendicular, allow all parents to intercept + parent.requestDisallowInterceptTouchEvent(false) + } else { + // Gesture is parallel, query child if movement in that direction is possible + if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) { + // Child can scroll, disallow all parents to intercept + parent.requestDisallowInterceptTouchEvent(true) + } else { + // Child cannot scroll, allow all parents to intercept + parent.requestDisallowInterceptTouchEvent(false) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/PaginationScrollListener.java b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/PaginationScrollListener.java new file mode 100644 index 0000000..2e1c8f4 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/PaginationScrollListener.java @@ -0,0 +1,33 @@ +package com.cappielloantonio.tempo.helper.recyclerview; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + + +public abstract class PaginationScrollListener extends RecyclerView.OnScrollListener { + private final LinearLayoutManager layoutManager; + + protected PaginationScrollListener(LinearLayoutManager layoutManager) { + this.layoutManager = layoutManager; + } + + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + + int visibleItemCount = layoutManager.getChildCount(); + int totalItemCount = layoutManager.getItemCount(); + int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition(); + + if (!isLoading()) { + if (firstVisibleItemPosition >= 0 && (visibleItemCount + firstVisibleItemPosition) >= (totalItemCount / 4 * 3)) { + loadMoreItems(); + } + } + } + + protected abstract void loadMoreItems(); + + public abstract boolean isLoading(); +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/SquareLayout.java b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/SquareLayout.java new file mode 100644 index 0000000..3437e49 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/helper/recyclerview/SquareLayout.java @@ -0,0 +1,28 @@ +package com.cappielloantonio.tempo.helper.recyclerview; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.RelativeLayout; + +public class SquareLayout extends RelativeLayout { + public SquareLayout(Context context) { + super(context); + } + + public SquareLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SquareLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SquareLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/ClickCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/ClickCallback.java new file mode 100644 index 0000000..d65695c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/ClickCallback.java @@ -0,0 +1,35 @@ +package com.cappielloantonio.tempo.interfaces; + + +import android.os.Bundle; + +import androidx.annotation.Keep; + +@Keep +public interface ClickCallback { + default void onMediaClick(Bundle bundle) {} + default void onMediaLongClick(Bundle bundle) {} + default void onAlbumClick(Bundle bundle) {} + default void onAlbumLongClick(Bundle bundle) {} + default void onArtistClick(Bundle bundle) {} + default void onArtistLongClick(Bundle bundle) {} + default void onGenreClick(Bundle bundle) {} + default void onPlaylistClick(Bundle bundle) {} + default void onPlaylistLongClick(Bundle bundle) {} + default void onYearClick(Bundle bundle) {} + default void onServerClick(Bundle bundle) {} + default void onServerLongClick(Bundle bundle) {} + default void onPodcastEpisodeClick(Bundle bundle) {} + default void onPodcastEpisodeAltClick(Bundle bundle) {} + default void onPodcastEpisodeLongClick(Bundle bundle) {} + default void onPodcastChannelClick(Bundle bundle) {} + default void onPodcastChannelLongClick(Bundle bundle) {} + default void onInternetRadioStationClick(Bundle bundle) {} + default void onInternetRadioStationLongClick(Bundle bundle) {} + default void onMusicFolderClick(Bundle bundle) {} + default void onMusicDirectoryClick(Bundle bundle) {} + default void onMusicIndexClick(Bundle bundle) {} + default void onDownloadGroupLongClick(Bundle bundle) {} + default void onShareClick(Bundle bundle) {} + default void onShareLongClick(Bundle bundle) {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/DecadesCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/DecadesCallback.java new file mode 100644 index 0000000..531114e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/DecadesCallback.java @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.interfaces; + +import androidx.annotation.Keep; + +@Keep +public interface DecadesCallback { + default void onLoadYear(int year) {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/DialogClickCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/DialogClickCallback.java new file mode 100644 index 0000000..adc3937 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/DialogClickCallback.java @@ -0,0 +1,13 @@ +package com.cappielloantonio.tempo.interfaces; + + +import androidx.annotation.Keep; + +@Keep +public interface DialogClickCallback { + default void onPositiveClick() {} + + default void onNegativeClick() {} + + default void onNeutralClick() {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/MediaCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/MediaCallback.java new file mode 100644 index 0000000..23fb6c5 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/MediaCallback.java @@ -0,0 +1,11 @@ +package com.cappielloantonio.tempo.interfaces; + +import androidx.annotation.Keep; + +import java.util.List; + +@Keep +public interface MediaCallback { + default void onError(Exception exception) {} + default void onLoadMedia(List media) {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/MediaIndexCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/MediaIndexCallback.java new file mode 100644 index 0000000..6a236e5 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/MediaIndexCallback.java @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.interfaces; + +import androidx.annotation.Keep; + +@Keep +public interface MediaIndexCallback { + default void onRecovery(int index) {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/PlaylistCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/PlaylistCallback.java new file mode 100644 index 0000000..73caf6d --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/PlaylistCallback.java @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.interfaces; + +import androidx.annotation.Keep; + +@Keep +public interface PlaylistCallback { + default void onDismiss() {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/PodcastCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/PodcastCallback.java new file mode 100644 index 0000000..fde54e3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/PodcastCallback.java @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.interfaces; + +import androidx.annotation.Keep; + +@Keep + +public interface PodcastCallback { + default void onDismiss() {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/RadioCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/RadioCallback.java new file mode 100644 index 0000000..42bcea8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/RadioCallback.java @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.interfaces; + +import androidx.annotation.Keep; + +@Keep + +public interface RadioCallback { + default void onDismiss() {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/ScanCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/ScanCallback.java new file mode 100644 index 0000000..6260743 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/ScanCallback.java @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.interfaces; + +import androidx.annotation.Keep; + +@Keep +public interface ScanCallback { + default void onError(Exception exception) {} + default void onSuccess(boolean isScanning, long count) {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/StarCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/StarCallback.java new file mode 100644 index 0000000..a5bb177 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/StarCallback.java @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.interfaces; + +import androidx.annotation.Keep; + +@Keep +public interface StarCallback { + default void onError() {} + default void onSuccess() {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/SystemCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/SystemCallback.java new file mode 100644 index 0000000..dc0b008 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/SystemCallback.java @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.interfaces; + +import androidx.annotation.Keep; + +@Keep +public interface SystemCallback { + default void onError(Exception exception) {} + default void onSuccess(String password, String token, String salt) {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/Chronology.kt b/app/src/main/java/com/cappielloantonio/tempo/model/Chronology.kt new file mode 100644 index 0000000..a3b142e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/Chronology.kt @@ -0,0 +1,59 @@ +package com.cappielloantonio.tempo.model + +import androidx.annotation.Keep +import androidx.media3.common.MediaItem +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.cappielloantonio.tempo.subsonic.models.Child +import com.cappielloantonio.tempo.util.Preferences +import kotlinx.parcelize.Parcelize +import java.util.Date + +@Keep +@Parcelize +@Entity(tableName = "chronology") +class Chronology( + @PrimaryKey override val id: String, + @ColumnInfo(name = "timestamp") + var timestamp: Long = System.currentTimeMillis(), + @ColumnInfo(name = "server") + var server: String? = null, +) : Child(id) { + constructor(mediaItem: MediaItem) : this(mediaItem.mediaMetadata.extras!!.getString("id")!!) { + parentId = mediaItem.mediaMetadata.extras!!.getString("parentId") + isDir = mediaItem.mediaMetadata.extras!!.getBoolean("isDir") + title = mediaItem.mediaMetadata.extras!!.getString("title") + album = mediaItem.mediaMetadata.extras!!.getString("album") + artist = mediaItem.mediaMetadata.extras!!.getString("artist") + track = mediaItem.mediaMetadata.extras!!.getInt("track") + year = mediaItem.mediaMetadata.extras!!.getInt("year") + genre = mediaItem.mediaMetadata.extras!!.getString("genre") + coverArtId = mediaItem.mediaMetadata.extras!!.getString("coverArtId") + size = mediaItem.mediaMetadata.extras!!.getLong("size") + contentType = mediaItem.mediaMetadata.extras!!.getString("contentType") + suffix = mediaItem.mediaMetadata.extras!!.getString("suffix") + transcodedContentType = mediaItem.mediaMetadata.extras!!.getString("transcodedContentType") + transcodedSuffix = mediaItem.mediaMetadata.extras!!.getString("transcodedSuffix") + duration = mediaItem.mediaMetadata.extras!!.getInt("duration") + bitrate = mediaItem.mediaMetadata.extras!!.getInt("bitrate") + samplingRate = mediaItem.mediaMetadata.extras!!.getInt("samplingRate") + bitDepth = mediaItem.mediaMetadata.extras!!.getInt("bitDepth") + path = mediaItem.mediaMetadata.extras!!.getString("path") + isVideo = mediaItem.mediaMetadata.extras!!.getBoolean("isVideo") + userRating = mediaItem.mediaMetadata.extras!!.getInt("userRating") + averageRating = mediaItem.mediaMetadata.extras!!.getDouble("averageRating") + playCount = mediaItem.mediaMetadata.extras!!.getLong("playCount") + discNumber = mediaItem.mediaMetadata.extras!!.getInt("discNumber") + created = Date(mediaItem.mediaMetadata.extras!!.getLong("created")) + starred = Date(mediaItem.mediaMetadata.extras!!.getLong("starred")) + albumId = mediaItem.mediaMetadata.extras!!.getString("albumId") + artistId = mediaItem.mediaMetadata.extras!!.getString("artistId") + type = mediaItem.mediaMetadata.extras!!.getString("type") + bookmarkPosition = mediaItem.mediaMetadata.extras!!.getLong("bookmarkPosition") + originalWidth = mediaItem.mediaMetadata.extras!!.getInt("originalWidth") + originalHeight = mediaItem.mediaMetadata.extras!!.getInt("originalHeight") + server = Preferences.getServerId() + timestamp = Date().time + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/Download.kt b/app/src/main/java/com/cappielloantonio/tempo/model/Download.kt new file mode 100644 index 0000000..0c54e1c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/Download.kt @@ -0,0 +1,64 @@ +package com.cappielloantonio.tempo.model + +import androidx.annotation.Keep +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.cappielloantonio.tempo.subsonic.models.Child +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +@Entity(tableName = "download") +class Download( + @PrimaryKey override val id: String, + @ColumnInfo(name = "playlist_id") + var playlistId: String? = null, + @ColumnInfo(name = "playlist_name") + var playlistName: String? = null, + @ColumnInfo(name = "download_state", defaultValue = "1") + var downloadState: Int = 0, + @ColumnInfo(name = "download_uri", defaultValue = "") + var downloadUri: String? = null, +) : Child(id) { + constructor(child: Child) : this(child.id) { + parentId = child.parentId + isDir = child.isDir + title = child.title + album = child.album + artist = child.artist + track = child.track + year = child.year + genre = child.genre + coverArtId = child.coverArtId + size = child.size + contentType = child.contentType + suffix = child.suffix + transcodedContentType = child.transcodedContentType + transcodedSuffix = child.transcodedSuffix + duration = child.duration + bitrate = child.bitrate + samplingRate = child.samplingRate + bitDepth = child.bitDepth + path = child.path + isVideo = child.isVideo + userRating = child.userRating + averageRating = child.averageRating + playCount = child.playCount + discNumber = child.discNumber + created = child.created + starred = child.starred + albumId = child.albumId + artistId = child.artistId + type = child.type + bookmarkPosition = child.bookmarkPosition + originalWidth = child.originalWidth + originalHeight = child.originalHeight + } +} + +@Keep +data class DownloadStack( + var id: String, + var view: String?, +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/Favorite.kt b/app/src/main/java/com/cappielloantonio/tempo/model/Favorite.kt new file mode 100644 index 0000000..d5bbb8c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/Favorite.kt @@ -0,0 +1,32 @@ +package com.cappielloantonio.tempo.model + +import android.os.Parcelable +import androidx.annotation.Keep +import androidx.annotation.Nullable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +@Entity(tableName = "favorite") +data class Favorite( + @PrimaryKey + @ColumnInfo(name = "timestamp") + var timestamp: Long, + + @ColumnInfo(name = "songId") + val songId: String?, + + @ColumnInfo(name = "albumId") + val albumId: String?, + + @ColumnInfo(name = "artistId") + val artistId: String?, + + @ColumnInfo(name = "toStar") + val toStar: Boolean, +) : Parcelable { + override fun toString(): String = (songId ?: "null") + (albumId ?: "null") + (artistId ?: "null") +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/HomeSector.kt b/app/src/main/java/com/cappielloantonio/tempo/model/HomeSector.kt new file mode 100644 index 0000000..79ca435 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/HomeSector.kt @@ -0,0 +1,11 @@ +package com.cappielloantonio.tempo.model + +import androidx.annotation.Keep + +@Keep +data class HomeSector( + val id: String, + val sectorTitle: String, + var isVisible: Boolean, + val order: Int, +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/LyricsCache.kt b/app/src/main/java/com/cappielloantonio/tempo/model/LyricsCache.kt new file mode 100644 index 0000000..3c437e2 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/LyricsCache.kt @@ -0,0 +1,25 @@ +package com.cappielloantonio.tempo.model + +import androidx.annotation.Keep +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlin.jvm.JvmOverloads + +@Keep +@Entity(tableName = "lyrics_cache") +data class LyricsCache @JvmOverloads constructor( + @PrimaryKey + @ColumnInfo(name = "song_id") + var songId: String, + @ColumnInfo(name = "artist") + var artist: String? = null, + @ColumnInfo(name = "title") + var title: String? = null, + @ColumnInfo(name = "lyrics") + var lyrics: String? = null, + @ColumnInfo(name = "structured_lyrics") + var structuredLyrics: String? = null, + @ColumnInfo(name = "updated_at") + var updatedAt: Long = System.currentTimeMillis() +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/Queue.kt b/app/src/main/java/com/cappielloantonio/tempo/model/Queue.kt new file mode 100644 index 0000000..8784017 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/Queue.kt @@ -0,0 +1,59 @@ +package com.cappielloantonio.tempo.model + +import androidx.annotation.Keep +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.cappielloantonio.tempo.subsonic.models.Child +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +@Entity(tableName = "queue") +class Queue( + override val id: String, + @PrimaryKey + @ColumnInfo(name = "track_order") + var trackOrder: Int = 0, + @ColumnInfo(name = "last_play") + var lastPlay: Long = 0, + @ColumnInfo(name = "playing_changed") + var playingChanged: Long = 0, + @ColumnInfo(name = "stream_id") + var streamId: String? = null, +) : Child(id) { + constructor(child: Child) : this(child.id) { + parentId = child.parentId + isDir = child.isDir + title = child.title + album = child.album + artist = child.artist + track = child.track + year = child.year + genre = child.genre + coverArtId = child.coverArtId + size = child.size + contentType = child.contentType + suffix = child.suffix + transcodedContentType = child.transcodedContentType + transcodedSuffix = child.transcodedSuffix + duration = child.duration + bitrate = child.bitrate + samplingRate = child.samplingRate + bitDepth = child.bitDepth + path = child.path + isVideo = child.isVideo + userRating = child.userRating + averageRating = child.averageRating + playCount = child.playCount + discNumber = child.discNumber + created = child.created + starred = child.starred + albumId = child.albumId + artistId = child.artistId + type = child.type + bookmarkPosition = child.bookmarkPosition + originalWidth = child.originalWidth + originalHeight = child.originalHeight + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/RecentSearch.kt b/app/src/main/java/com/cappielloantonio/tempo/model/RecentSearch.kt new file mode 100644 index 0000000..6004a31 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/RecentSearch.kt @@ -0,0 +1,17 @@ +package com.cappielloantonio.tempo.model + +import android.os.Parcelable +import androidx.annotation.Keep +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +@Entity(tableName = "recent_search") +data class RecentSearch( + @PrimaryKey + @ColumnInfo(name = "search") + var search: String +) : Parcelable diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/ReplayGain.kt b/app/src/main/java/com/cappielloantonio/tempo/model/ReplayGain.kt new file mode 100644 index 0000000..0d46961 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/ReplayGain.kt @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.model + +import androidx.annotation.Keep + +@Keep +data class ReplayGain( + var trackGain: Float = 0f, + var albumGain: Float = 0f, +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/Server.kt b/app/src/main/java/com/cappielloantonio/tempo/model/Server.kt new file mode 100644 index 0000000..78bfa6e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/Server.kt @@ -0,0 +1,39 @@ +package com.cappielloantonio.tempo.model + +import android.os.Parcelable +import androidx.annotation.Keep +import androidx.annotation.Nullable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +@Entity(tableName = "server") +data class Server( + @PrimaryKey + @ColumnInfo(name = "id") + val serverId: String, + + @ColumnInfo(name = "server_name") + val serverName: String, + + @ColumnInfo(name = "username") + val username: String, + + @ColumnInfo(name = "password") + val password: String, + + @ColumnInfo(name = "address") + val address: String, + + @ColumnInfo(name = "local_address") + val localAddress: String?, + + @ColumnInfo(name = "timestamp") + val timestamp: Long, + + @ColumnInfo(name = "low_security", defaultValue = "false") + val isLowSecurity: Boolean +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt new file mode 100644 index 0000000..60d641c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt @@ -0,0 +1,289 @@ +package com.cappielloantonio.tempo.model + +import android.net.Uri +import android.os.Bundle +import androidx.annotation.Keep +import androidx.media3.common.HeartRating +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaItem.RequestMetadata +import androidx.media3.common.MediaMetadata +import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.cappielloantonio.tempo.glide.CustomGlideRequest +import com.cappielloantonio.tempo.subsonic.models.Child +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode +import com.cappielloantonio.tempo.util.Constants +import com.cappielloantonio.tempo.util.MusicUtil +import com.cappielloantonio.tempo.util.Preferences.getImageSize +import java.util.Date + +@UnstableApi +@Keep +@Entity(tableName = "session_media_item") +class SessionMediaItem() { + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "index") + var index: Int = 0 + + @ColumnInfo(name = "id") + var id: String? = null + + @ColumnInfo(name = "parent_id") + var parentId: String? = null + + @ColumnInfo(name = "is_dir") + var isDir: Boolean = false + + @ColumnInfo + var title: String? = null + + @ColumnInfo + var album: String? = null + + @ColumnInfo + var artist: String? = null + + @ColumnInfo + var track: Int? = null + + @ColumnInfo + var year: Int? = null + + @ColumnInfo + var genre: String? = null + + @ColumnInfo(name = "cover_art_id") + var coverArtId: String? = null + + @ColumnInfo + var size: Long? = null + + @ColumnInfo(name = "content_type") + var contentType: String? = null + + @ColumnInfo + var suffix: String? = null + + @ColumnInfo("transcoding_content_type") + var transcodedContentType: String? = null + + @ColumnInfo(name = "transcoded_suffix") + var transcodedSuffix: String? = null + + @ColumnInfo + var duration: Int? = null + + @ColumnInfo("bitrate") + var bitrate: Int? = null + + @ColumnInfo + var path: String? = null + + @ColumnInfo(name = "is_video") + var isVideo: Boolean = false + + @ColumnInfo(name = "user_rating") + var userRating: Int? = null + + @ColumnInfo(name = "average_rating") + var averageRating: Double? = null + + @ColumnInfo(name = "play_count") + var playCount: Long? = null + + @ColumnInfo(name = "disc_number") + var discNumber: Int? = null + + @ColumnInfo + var created: Date? = null + + @ColumnInfo + var starred: Date? = null + + @ColumnInfo(name = "album_id") + var albumId: String? = null + + @ColumnInfo(name = "artist_id") + var artistId: String? = null + + @ColumnInfo + var type: String? = null + + @ColumnInfo(name = "bookmark_position") + var bookmarkPosition: Long? = null + + @ColumnInfo(name = "original_width") + var originalWidth: Int? = null + + @ColumnInfo(name = "original_height") + var originalHeight: Int? = null + + @ColumnInfo(name = "stream_id") + var streamId: String? = null + + @ColumnInfo(name = "stream_url") + var streamUrl: String? = null + + @ColumnInfo(name = "timestamp") + var timestamp: Long? = null + + constructor(child: Child) : this() { + id = child.id + parentId = child.parentId + isDir = child.isDir + title = child.title + album = child.album + artist = child.artist + track = child.track + year = child.year + genre = child.genre + coverArtId = child.coverArtId + size = child.size + contentType = child.contentType + suffix = child.suffix + transcodedContentType = child.transcodedContentType + transcodedSuffix = child.transcodedSuffix + duration = child.duration + bitrate = child.bitrate + path = child.path + isVideo = child.isVideo + userRating = child.userRating + averageRating = child.averageRating + playCount = child.playCount + discNumber = child.discNumber + created = child.created + starred = child.starred + albumId = child.albumId + artistId = child.artistId + type = Constants.MEDIA_TYPE_MUSIC + bookmarkPosition = child.bookmarkPosition + originalWidth = child.originalWidth + originalHeight = child.originalHeight + } + + constructor(podcastEpisode: PodcastEpisode) : this() { + id = podcastEpisode.id + parentId = podcastEpisode.parentId + isDir = podcastEpisode.isDir + title = podcastEpisode.title + album = podcastEpisode.album + artist = podcastEpisode.artist + year = podcastEpisode.year + genre = podcastEpisode.genre + coverArtId = podcastEpisode.coverArtId + size = podcastEpisode.size + contentType = podcastEpisode.contentType + suffix = podcastEpisode.suffix + duration = podcastEpisode.duration + bitrate = podcastEpisode.bitrate + path = podcastEpisode.path + isVideo = podcastEpisode.isVideo + created = podcastEpisode.created + artistId = podcastEpisode.artistId + streamId = podcastEpisode.streamId + type = Constants.MEDIA_TYPE_PODCAST + } + + constructor(internetRadioStation: InternetRadioStation) : this() { + id = internetRadioStation.id + title = internetRadioStation.name + streamUrl = internetRadioStation.streamUrl + type = Constants.MEDIA_TYPE_RADIO + } + + fun getMediaItem(): MediaItem { + val uri: Uri = getStreamUri() + val artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, getImageSize())) + + val bundle = Bundle() + bundle.putString("id", id) + bundle.putString("parentId", parentId) + bundle.putBoolean("isDir", isDir) + bundle.putString("title", title) + bundle.putString("album", album) + bundle.putString("artist", artist) + bundle.putInt("track", track ?: 0) + bundle.putInt("year", year ?: 0) + bundle.putString("genre", genre) + bundle.putString("coverArtId", coverArtId) + bundle.putLong("size", size ?: 0) + bundle.putString("contentType", contentType) + bundle.putString("suffix", suffix) + bundle.putString("transcodedContentType", transcodedContentType) + bundle.putString("transcodedSuffix", transcodedSuffix) + bundle.putInt("duration", duration ?: 0) + bundle.putInt("bitrate", bitrate ?: 0) + bundle.putString("path", path) + bundle.putBoolean("isVideo", isVideo) + bundle.putInt("userRating", userRating ?: 0) + bundle.putDouble("averageRating", averageRating ?: .0) + bundle.putLong("playCount", playCount ?: 0) + bundle.putInt("discNumber", discNumber ?: 0) + bundle.putLong("created", created?.time ?: 0) + bundle.putLong("starred", starred?.time ?: 0) + bundle.putString("albumId", albumId) + bundle.putString("artistId", artistId) + bundle.putString("type", Constants.MEDIA_TYPE_MUSIC) + bundle.putLong("bookmarkPosition", bookmarkPosition ?: 0) + bundle.putInt("originalWidth", originalWidth ?: 0) + bundle.putInt("originalHeight", originalHeight ?: 0) + bundle.putString("uri", uri.toString()) + + return MediaItem.Builder() + .setMediaId(id!!) + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(title) + .setTrackNumber(track ?: 0) + .setDiscNumber(discNumber ?: 0) + .setReleaseYear(year ?: 0) + .setAlbumTitle(album) + .setArtist(artist) + .setArtworkUri(artworkUri) + .setUserRating(HeartRating(starred != null)) + .setSupportedCommands( + listOf( + Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON, + Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF + ) + ) + .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) + .build() + ) + .setRequestMetadata( + RequestMetadata.Builder() + .setMediaUri(uri) + .setExtras(bundle) + .build() + ) + .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .setUri(uri) + .build() + } + + private fun getStreamUri(): Uri { + return when (type) { + Constants.MEDIA_TYPE_MUSIC -> { + MusicUtil.getStreamUri(id) + } + + Constants.MEDIA_TYPE_PODCAST -> { + MusicUtil.getStreamUri(streamId) + } + + Constants.MEDIA_TYPE_RADIO -> { + Uri.parse(streamUrl) + } + + else -> { + MusicUtil.getStreamUri(id) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java new file mode 100644 index 0000000..bcc358b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java @@ -0,0 +1,301 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; +import android.util.Log; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.interfaces.DecadesCallback; +import com.cappielloantonio.tempo.interfaces.MediaCallback; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.AlbumInfo; +import com.cappielloantonio.tempo.subsonic.models.Child; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class AlbumRepository { + public MutableLiveData> getAlbums(String type, int size, Integer fromYear, Integer toYear) { + MutableLiveData> listLiveAlbums = new MutableLiveData<>(new ArrayList<>()); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getAlbumList2(type, size, 0, fromYear, toYear) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() + && response.body() != null + && response.body().getSubsonicResponse().getAlbumList2() != null + && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) { + + listLiveAlbums.setValue(response.body().getSubsonicResponse().getAlbumList2().getAlbums()); + } else { + Log.e("AlbumRepository", "API Error on getAlbums. HTTP Code: " + response.code()); + listLiveAlbums.setValue(new ArrayList<>()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.e("AlbumRepository", "Network Failure on getAlbums: " + t.getMessage()); + listLiveAlbums.setValue(new ArrayList<>()); + } + }); + + return listLiveAlbums; + } + + public MutableLiveData> getStarredAlbums(boolean random, int size) { + MutableLiveData> starredAlbums = new MutableLiveData<>(new ArrayList<>()); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getStarred2() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null) { + List albums = response.body().getSubsonicResponse().getStarred2().getAlbums(); + + if (albums != null) { + if (random) { + Collections.shuffle(albums); + starredAlbums.setValue(albums.subList(0, Math.min(size, albums.size()))); + } else { + starredAlbums.setValue(albums); + } + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return starredAlbums; + } + + public void setRating(String id, int rating) { + App.getSubsonicClientInstance(false) + .getMediaAnnotationClient() + .setRating(id, rating) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public MutableLiveData> getAlbumTracks(String id) { + MutableLiveData> albumTracks = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getAlbum(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List tracks = new ArrayList<>(); + + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { + if (response.body().getSubsonicResponse().getAlbum().getSongs() != null) { + tracks.addAll(response.body().getSubsonicResponse().getAlbum().getSongs()); + } + } + + albumTracks.setValue(tracks); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return albumTracks; + } + + public MutableLiveData> getArtistAlbums(String id) { + MutableLiveData> artistsAlbum = new MutableLiveData<>(new ArrayList<>()); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtist(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtist() != null && response.body().getSubsonicResponse().getArtist().getAlbums() != null) { + List albums = response.body().getSubsonicResponse().getArtist().getAlbums(); + albums.sort(Comparator.comparing(AlbumID3::getYear)); + Collections.reverse(albums); + artistsAlbum.setValue(albums); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return artistsAlbum; + } + + public MutableLiveData getAlbum(String id) { + MutableLiveData album = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getAlbum(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null) { + album.setValue(response.body().getSubsonicResponse().getAlbum()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return album; + } + + public MutableLiveData getAlbumInfo(String id) { + MutableLiveData albumInfo = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getAlbumInfo2(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumInfo() != null) { + albumInfo.setValue(response.body().getSubsonicResponse().getAlbumInfo()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return albumInfo; + } + + public void getInstantMix(AlbumID3 album, int count, MediaCallback callback) { + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getSimilarSongs2(album.getId(), count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = new ArrayList<>(); + + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) { + songs.addAll(response.body().getSubsonicResponse().getSimilarSongs2().getSongs()); + } + + callback.onLoadMedia(songs); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + callback.onLoadMedia(new ArrayList<>()); + } + }); + } + + public MutableLiveData> getDecades() { + MutableLiveData> decades = new MutableLiveData<>(); + + getFirstAlbum(new DecadesCallback() { + @Override + public void onLoadYear(int first) { + getLastAlbum(new DecadesCallback() { + @Override + public void onLoadYear(int last) { + if (first != -1 && last != -1) { + List decadeList = new ArrayList(); + + int startDecade = first - (first % 10); + int lastDecade = last - (last % 10); + + while (startDecade <= lastDecade) { + decadeList.add(startDecade); + startDecade = startDecade + 10; + } + + decades.setValue(decadeList); + } + } + }); + } + }); + + return decades; + } + + private void getFirstAlbum(DecadesCallback callback) { + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getAlbumList2("byYear", 1, 0, 1900, Calendar.getInstance().get(Calendar.YEAR)) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null && !response.body().getSubsonicResponse().getAlbumList2().getAlbums().isEmpty()) { + callback.onLoadYear(response.body().getSubsonicResponse().getAlbumList2().getAlbums().get(0).getYear()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + callback.onLoadYear(-1); + } + }); + } + + private void getLastAlbum(DecadesCallback callback) { + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getAlbumList2("byYear", 1, 0, Calendar.getInstance().get(Calendar.YEAR), 1900) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) { + if (!response.body().getSubsonicResponse().getAlbumList2().getAlbums().isEmpty() && !response.body().getSubsonicResponse().getAlbumList2().getAlbums().isEmpty()) { + callback.onLoadYear(response.body().getSubsonicResponse().getAlbumList2().getAlbums().get(0).getYear()); + } else { + callback.onLoadYear(-1); + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + callback.onLoadYear(-1); + } + }); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java new file mode 100644 index 0000000..5bea391 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/ArtistRepository.java @@ -0,0 +1,387 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; +import android.util.Log; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.IndexID3; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class ArtistRepository { + private final AlbumRepository albumRepository; + + public ArtistRepository() { + this.albumRepository = new AlbumRepository(); + } + + public void getArtistAllSongs(String artistId, ArtistSongsCallback callback) { + Log.d("ArtistSync", "Getting albums for artist: " + artistId); + + // Get the artist info first, which contains the albums + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtist(artistId) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && + response.body().getSubsonicResponse().getArtist() != null && + response.body().getSubsonicResponse().getArtist().getAlbums() != null) { + + List albums = response.body().getSubsonicResponse().getArtist().getAlbums(); + Log.d("ArtistSync", "Got albums directly: " + albums.size()); + + if (!albums.isEmpty()) { + fetchAllAlbumSongsWithCallback(albums, callback); + } else { + Log.d("ArtistSync", "No albums found in artist response"); + callback.onSongsCollected(new ArrayList<>()); + } + } else { + Log.d("ArtistSync", "Failed to get artist info"); + callback.onSongsCollected(new ArrayList<>()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.d("ArtistSync", "Error getting artist info: " + t.getMessage()); + callback.onSongsCollected(new ArrayList<>()); + } + }); + } + + private void fetchAllAlbumSongsWithCallback(List albums, ArtistSongsCallback callback) { + if (albums == null || albums.isEmpty()) { + Log.d("ArtistSync", "No albums to process"); + callback.onSongsCollected(new ArrayList<>()); + return; + } + + List allSongs = new ArrayList<>(); + AtomicInteger remainingAlbums = new AtomicInteger(albums.size()); + Log.d("ArtistSync", "Processing " + albums.size() + " albums"); + + for (AlbumID3 album : albums) { + Log.d("ArtistSync", "Getting tracks for album: " + album.getName()); + MutableLiveData> albumTracks = albumRepository.getAlbumTracks(album.getId()); + albumTracks.observeForever(songs -> { + Log.d("ArtistSync", "Got " + (songs != null ? songs.size() : 0) + " songs from album"); + if (songs != null) { + allSongs.addAll(songs); + } + albumTracks.removeObservers(null); + + int remaining = remainingAlbums.decrementAndGet(); + Log.d("ArtistSync", "Remaining albums: " + remaining); + + if (remaining == 0) { + Log.d("ArtistSync", "All albums processed. Total songs: " + allSongs.size()); + callback.onSongsCollected(allSongs); + } + }); + } + } + + public interface ArtistSongsCallback { + void onSongsCollected(List songs); + } + + public MutableLiveData> getStarredArtists(boolean random, int size) { + MutableLiveData> starredArtists = new MutableLiveData<>(new ArrayList<>()); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getStarred2() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null) { + List artists = response.body().getSubsonicResponse().getStarred2().getArtists(); + + if (artists != null) { + if (!random) { + getArtistInfo(artists, starredArtists); + } else { + Collections.shuffle(artists); + getArtistInfo(artists.subList(0, Math.min(size, artists.size())), starredArtists); + } + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return starredArtists; + } + + public MutableLiveData> getArtists(boolean random, int size) { + MutableLiveData> listLiveArtists = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtists() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + List artists = new ArrayList<>(); + + if(response.body().getSubsonicResponse().getArtists() != null && response.body().getSubsonicResponse().getArtists().getIndices() != null) { + for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) { + if(index != null && index.getArtists() != null) { + artists.addAll(index.getArtists()); + } + } + } + + if (random) { + Collections.shuffle(artists); + getArtistInfo(artists.subList(0, artists.size() / size > 0 ? size : artists.size()), listLiveArtists); + } else { + listLiveArtists.setValue(artists); + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + } + }); + + return listLiveArtists; + } + + /* + * Method that returns essential artist information (cover, album number, etc.) + */ + public void getArtistInfo(List artists, MutableLiveData> list) { + List liveArtists = list.getValue(); + if (liveArtists == null) liveArtists = new ArrayList<>(); + list.setValue(liveArtists); + + for (ArtistID3 artist : artists) { + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtist(artist.getId()) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtist() != null) { + addToMutableLiveData(list, response.body().getSubsonicResponse().getArtist()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + } + + public MutableLiveData getArtistInfo(String id) { + MutableLiveData artist = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtist(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtist() != null) { + artist.setValue(response.body().getSubsonicResponse().getArtist()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return artist; + } + + public MutableLiveData getArtistFullInfo(String id) { + MutableLiveData artistFullInfo = new MutableLiveData<>(null); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtistInfo2(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtistInfo2() != null) { + artistFullInfo.setValue(response.body().getSubsonicResponse().getArtistInfo2()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return artistFullInfo; + } + + public void setRating(String id, int rating) { + App.getSubsonicClientInstance(false) + .getMediaAnnotationClient() + .setRating(id, rating) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public MutableLiveData getArtist(String id) { + MutableLiveData artist = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtist(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtist() != null) { + artist.setValue(response.body().getSubsonicResponse().getArtist()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return artist; + } + + public MutableLiveData> getInstantMix(ArtistID3 artist, int count) { + MutableLiveData> instantMix = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getSimilarSongs2(artist.getId(), count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) { + instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return instantMix; + } + + public MutableLiveData> getRandomSong(ArtistID3 artist, int count) { + MutableLiveData> randomSongs = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtist(artist.getId()) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && + response.body().getSubsonicResponse().getArtist() != null && + response.body().getSubsonicResponse().getArtist().getAlbums() != null) { + + List albums = response.body().getSubsonicResponse().getArtist().getAlbums(); + Log.d("ArtistRepository", "Got albums directly: " + albums.size()); + if (albums.isEmpty()) { + Log.d("ArtistRepository", "No albums found in artist response"); + return; + } + + Collections.shuffle(albums); + int[] counts = albums.stream().mapToInt(AlbumID3::getSongCount).toArray(); + Arrays.parallelPrefix(counts, Integer::sum); + int albumLimit = 0; + int multiplier = 4; // get more than the limit so we can shuffle them + while (albumLimit < albums.size() && counts[albumLimit] < count * multiplier) + albumLimit++; + Log.d("ArtistRepository", String.format("Retaining %d/%d albums", albumLimit, albums.size())); + + fetchAllAlbumSongsWithCallback(albums.stream().limit(albumLimit).collect(Collectors.toList()), songs -> { + Collections.shuffle(songs); + randomSongs.setValue(songs.stream().limit(count).collect(Collectors.toList())); + }); + } else { + Log.d("ArtistRepository", "Failed to get artist info"); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.d("ArtistRepository", "Error getting artist info: " + t.getMessage()); + } + }); + + return randomSongs; + } + + public MutableLiveData> getTopSongs(String artistName, int count) { + MutableLiveData> topSongs = new MutableLiveData<>(new ArrayList<>()); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getTopSongs(artistName, count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getTopSongs() != null && response.body().getSubsonicResponse().getTopSongs().getSongs() != null) { + topSongs.setValue(response.body().getSubsonicResponse().getTopSongs().getSongs()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return topSongs; + } + + private void addToMutableLiveData(MutableLiveData> liveData, ArtistID3 artist) { + List liveArtists = liveData.getValue(); + if (liveArtists != null) liveArtists.add(artist); + liveData.setValue(liveArtists); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java new file mode 100644 index 0000000..fe24d81 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java @@ -0,0 +1,1027 @@ +package com.cappielloantonio.tempo.repository; + + +import android.net.Uri; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.LibraryResult; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.ChronologyDao; +import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.model.Chronology; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.model.SessionMediaItem; +import com.cappielloantonio.tempo.service.DownloaderManager; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.Artist; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Directory; +import com.cappielloantonio.tempo.subsonic.models.Index; +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; +import com.cappielloantonio.tempo.subsonic.models.MusicFolder; +import com.cappielloantonio.tempo.subsonic.models.Playlist; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class AutomotiveRepository { + private final SessionMediaItemDao sessionMediaItemDao = AppDatabase.getInstance().sessionMediaItemDao(); + private final ChronologyDao chronologyDao = AppDatabase.getInstance().chronologyDao(); + + public ListenableFuture>> getAlbums(String prefix, String type, int size) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getAlbumList2(type, size, 0, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) { + List albums = response.body().getSubsonicResponse().getAlbumList2().getAlbums(); + + List mediaItems = new ArrayList<>(); + + for (AlbumID3 album : albums) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(album.getName()) + .setAlbumTitle(album.getName()) + .setArtist(album.getArtist()) + .setGenre(album.getGenre()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + album.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getStarredSongs() { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getStarred2() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getSongs() != null) { + List songs = response.body().getSubsonicResponse().getStarred2().getSongs(); + + setChildrenMetadata(songs); + + List mediaItems = MappingUtil.mapMediaItems(songs); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getRandomSongs(int count) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getRandomSongs(100, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) { + List songs = response.body().getSubsonicResponse().getRandomSongs().getSongs(); + + setChildrenMetadata(songs); + + List mediaItems = MappingUtil.mapMediaItems(songs); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getRecentlyPlayedSongs(String server, int count) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + chronologyDao.getLastPlayed(server, count).observeForever(new Observer>() { + @Override + public void onChanged(List chronology) { + if (chronology != null && !chronology.isEmpty()) { + List songs = new ArrayList<>(chronology); + + setChildrenMetadata(songs); + + List mediaItems = MappingUtil.mapMediaItems(songs); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + + chronologyDao.getLastPlayed(server, count).removeObserver(this); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getStarredAlbums(String prefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getStarred2() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getAlbums() != null) { + List albums = response.body().getSubsonicResponse().getStarred2().getAlbums(); + + List mediaItems = new ArrayList<>(); + + for (AlbumID3 album : albums) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(album.getName()) + .setArtist(album.getArtist()) + .setGenre(album.getGenre()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + album.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getStarredArtists(String prefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getStarred2() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null && response.body().getSubsonicResponse().getStarred2().getArtists() != null) { + List artists = response.body().getSubsonicResponse().getStarred2().getArtists(); + + Collections.shuffle(artists); + + List mediaItems = new ArrayList<>(); + + for (ArtistID3 artist : artists) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(artist.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + artist.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getMusicFolders(String prefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getMusicFolders() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getMusicFolders() != null && response.body().getSubsonicResponse().getMusicFolders().getMusicFolders() != null) { + List musicFolders = response.body().getSubsonicResponse().getMusicFolders().getMusicFolders(); + + List mediaItems = new ArrayList<>(); + + for (MusicFolder musicFolder : musicFolders) { + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(musicFolder.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + musicFolder.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getIndexes(String prefix, String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getIndexes(id, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getIndexes() != null) { + List mediaItems = new ArrayList<>(); + + if (response.body().getSubsonicResponse().getIndexes().getIndices() != null) { + List indices = response.body().getSubsonicResponse().getIndexes().getIndices(); + + for (Index index : indices) { + if (index.getArtists() != null) { + for (Artist artist : index.getArtists()) { + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(artist.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ARTIST) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + artist.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + } + } + } + + if (response.body().getSubsonicResponse().getIndexes().getChildren() != null) { + List children = response.body().getSubsonicResponse().getIndexes().getChildren(); + + for (Child song : children) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(song.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(song.getTitle()) + .setAlbumTitle(song.getAlbum()) + .setArtist(song.getArtist()) + .setIsBrowsable(false) + .setIsPlayable(true) + .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + song.getId()) + .setMediaMetadata(mediaMetadata) + .setUri(MusicUtil.getStreamUri(song.getId())) + .build(); + + mediaItems.add(mediaItem); + } + + setChildrenMetadata(children); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getDirectories(String prefix, String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getMusicDirectory(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getDirectory() != null && response.body().getSubsonicResponse().getDirectory().getChildren() != null) { + Directory directory = response.body().getSubsonicResponse().getDirectory(); + + List mediaItems = new ArrayList<>(); + + for (Child child : directory.getChildren()) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(child.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(child.getTitle()) + .setIsBrowsable(child.isDir()) + .setIsPlayable(!child.isDir()) + .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(child.isDir() ? prefix + child.getId() : child.getId()) + .setMediaMetadata(mediaMetadata) + .setUri(!child.isDir() ? MusicUtil.getStreamUri(child.getId()) : Uri.parse("")) + .build(); + + mediaItems.add(mediaItem); + } + + setChildrenMetadata(directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList())); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getPlaylists(String prefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .getPlaylists() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylists() != null && response.body().getSubsonicResponse().getPlaylists().getPlaylists() != null) { + List playlists = response.body().getSubsonicResponse().getPlaylists().getPlaylists(); + + List mediaItems = new ArrayList<>(); + + for (Playlist playlist : playlists) { + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(playlist.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + playlist.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getNewestPodcastEpisodes(int count) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getPodcastClient() + .getNewestPodcasts(count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getNewestPodcasts() != null && response.body().getSubsonicResponse().getNewestPodcasts().getEpisodes() != null) { + List episodes = response.body().getSubsonicResponse().getNewestPodcasts().getEpisodes(); + + List mediaItems = new ArrayList<>(); + + for (PodcastEpisode episode : episodes) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(episode.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(episode.getTitle()) + .setIsBrowsable(false) + .setIsPlayable(true) + .setMediaType(MediaMetadata.MEDIA_TYPE_PODCAST_EPISODE) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(episode.getId()) + .setMediaMetadata(mediaMetadata) + .setUri(MusicUtil.getStreamUri(episode.getStreamId())) + .build(); + + mediaItems.add(mediaItem); + } + + setPodcastEpisodesMetadata(episodes); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getInternetRadioStations() { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getInternetRadioClient() + .getInternetRadioStations() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getInternetRadioStations() != null && response.body().getSubsonicResponse().getInternetRadioStations().getInternetRadioStations() != null) { + + List radioStations = response.body().getSubsonicResponse().getInternetRadioStations().getInternetRadioStations(); + + List mediaItems = new ArrayList<>(); + + for (InternetRadioStation radioStation : radioStations) { + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(radioStation.getName()) + .setIsBrowsable(false) + .setIsPlayable(true) + .setMediaType(MediaMetadata.MEDIA_TYPE_RADIO_STATION) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(radioStation.getId()) + .setMediaMetadata(mediaMetadata) + .setUri(radioStation.getStreamUrl()) + .build(); + + mediaItems.add(mediaItem); + } + + setInternetRadioStationsMetadata(radioStations); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getAlbumTracks(String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getAlbum(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbum() != null && response.body().getSubsonicResponse().getAlbum().getSongs() != null) { + List tracks = response.body().getSubsonicResponse().getAlbum().getSongs(); + + setChildrenMetadata(tracks); + + List mediaItems = MappingUtil.mapMediaItems(tracks); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } else { + listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getArtistAlbum(String prefix, String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtist(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtist() != null && response.body().getSubsonicResponse().getArtist().getAlbums() != null) { + List albums = response.body().getSubsonicResponse().getArtist().getAlbums(); + + List mediaItems = new ArrayList<>(); + + for (AlbumID3 album : albums) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(album.getName()) + .setAlbumTitle(album.getName()) + .setArtist(album.getArtist()) + .setGenre(album.getGenre()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(prefix + album.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getPlaylistSongs(String id) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .getPlaylist(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylist() != null && response.body().getSubsonicResponse().getPlaylist().getEntries() != null) { + List tracks = response.body().getSubsonicResponse().getPlaylist().getEntries(); + + setChildrenMetadata(tracks); + + List mediaItems = MappingUtil.mapMediaItems(tracks); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> getMadeForYou(String id, int count) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getSimilarSongs2(id, count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null && response.body().getSubsonicResponse().getSimilarSongs2().getSongs() != null) { + List tracks = response.body().getSubsonicResponse().getSimilarSongs2().getSongs(); + + setChildrenMetadata(tracks); + + List mediaItems = MappingUtil.mapMediaItems(tracks); + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + public ListenableFuture>> search(String query, String albumPrefix, String artistPrefix) { + final SettableFuture>> listenableFuture = SettableFuture.create(); + + App.getSubsonicClientInstance(false) + .getSearchingClient() + .search3(query, 20, 20, 20) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSearchResult3() != null) { + List mediaItems = new ArrayList<>(); + + if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) { + for (ArtistID3 artist : response.body().getSubsonicResponse().getSearchResult3().getArtists()) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(artist.getName()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(artistPrefix + artist.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + } + + if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) { + for (AlbumID3 album : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) { + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize())); + + MediaMetadata mediaMetadata = new MediaMetadata.Builder() + .setTitle(album.getName()) + .setAlbumTitle(album.getName()) + .setArtist(album.getArtist()) + .setGenre(album.getGenre()) + .setIsBrowsable(true) + .setIsPlayable(false) + .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) + .setArtworkUri(artworkUri) + .build(); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(albumPrefix + album.getId()) + .setMediaMetadata(mediaMetadata) + .setUri("") + .build(); + + mediaItems.add(mediaItem); + } + } + + if (response.body().getSubsonicResponse().getSearchResult3().getSongs() != null) { + List tracks = response.body().getSubsonicResponse().getSearchResult3().getSongs(); + setChildrenMetadata(tracks); + mediaItems.addAll(MappingUtil.mapMediaItems(tracks)); + } + + LibraryResult> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null); + + listenableFuture.set(libraryResult); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + listenableFuture.setException(t); + } + }); + + return listenableFuture; + } + + @OptIn(markerClass = UnstableApi.class) + public void setChildrenMetadata(List children) { + long timestamp = System.currentTimeMillis(); + ArrayList sessionMediaItems = new ArrayList<>(); + + for (Child child : children) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(child); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItems.add(sessionMediaItem); + } + + InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems); + Thread thread = new Thread(insertAll); + thread.start(); + } + + @OptIn(markerClass = UnstableApi.class) + public void setPodcastEpisodesMetadata(List podcastEpisodes) { + long timestamp = System.currentTimeMillis(); + ArrayList sessionMediaItems = new ArrayList<>(); + + for (PodcastEpisode podcastEpisode : podcastEpisodes) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(podcastEpisode); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItems.add(sessionMediaItem); + } + + InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems); + Thread thread = new Thread(insertAll); + thread.start(); + } + + @OptIn(markerClass = UnstableApi.class) + public void setInternetRadioStationsMetadata(List internetRadioStations) { + long timestamp = System.currentTimeMillis(); + ArrayList sessionMediaItems = new ArrayList<>(); + + for (InternetRadioStation internetRadioStation : internetRadioStations) { + SessionMediaItem sessionMediaItem = new SessionMediaItem(internetRadioStation); + sessionMediaItem.setTimestamp(timestamp); + sessionMediaItems.add(sessionMediaItem); + } + + InsertAllThreadSafe insertAll = new InsertAllThreadSafe(sessionMediaItemDao, sessionMediaItems); + Thread thread = new Thread(insertAll); + thread.start(); + } + + public SessionMediaItem getSessionMediaItem(String id) { + SessionMediaItem sessionMediaItem = null; + + GetMediaItemThreadSafe getMediaItemThreadSafe = new GetMediaItemThreadSafe(sessionMediaItemDao, id); + Thread thread = new Thread(getMediaItemThreadSafe); + thread.start(); + + try { + thread.join(); + sessionMediaItem = getMediaItemThreadSafe.getSessionMediaItem(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return sessionMediaItem; + } + + public List getMetadatas(long timestamp) { + List mediaItems = Collections.emptyList(); + + GetMediaItemsThreadSafe getMediaItemsThreadSafe = new GetMediaItemsThreadSafe(sessionMediaItemDao, timestamp); + Thread thread = new Thread(getMediaItemsThreadSafe); + thread.start(); + + try { + thread.join(); + mediaItems = getMediaItemsThreadSafe.getMediaItems(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return mediaItems; + } + + public void deleteMetadata() { + DeleteAllThreadSafe delete = new DeleteAllThreadSafe(sessionMediaItemDao); + Thread thread = new Thread(delete); + thread.start(); + } + + private static class GetMediaItemThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + private final String id; + + private SessionMediaItem sessionMediaItem; + + public GetMediaItemThreadSafe(SessionMediaItemDao sessionMediaItemDao, String id) { + this.sessionMediaItemDao = sessionMediaItemDao; + this.id = id; + } + + @Override + public void run() { + sessionMediaItem = sessionMediaItemDao.get(id); + } + + public SessionMediaItem getSessionMediaItem() { + return sessionMediaItem; + } + } + + @OptIn(markerClass = UnstableApi.class) + private static class GetMediaItemsThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + private final Long timestamp; + private final List mediaItems = new ArrayList<>(); + + public GetMediaItemsThreadSafe(SessionMediaItemDao sessionMediaItemDao, Long timestamp) { + this.sessionMediaItemDao = sessionMediaItemDao; + this.timestamp = timestamp; + } + + @Override + public void run() { + List sessionMediaItems = sessionMediaItemDao.get(timestamp); + sessionMediaItems.forEach(sessionMediaItem -> mediaItems.add(sessionMediaItem.getMediaItem())); + } + + public List getMediaItems() { + return mediaItems; + } + } + + private static class InsertAllThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + private final List sessionMediaItems; + + public InsertAllThreadSafe(SessionMediaItemDao sessionMediaItemDao, List sessionMediaItems) { + this.sessionMediaItemDao = sessionMediaItemDao; + this.sessionMediaItems = sessionMediaItems; + } + + @Override + public void run() { + sessionMediaItemDao.insertAll(sessionMediaItems); + } + } + + private static class DeleteAllThreadSafe implements Runnable { + private final SessionMediaItemDao sessionMediaItemDao; + + public DeleteAllThreadSafe(SessionMediaItemDao sessionMediaItemDao) { + this.sessionMediaItemDao = sessionMediaItemDao; + } + + @Override + public void run() { + sessionMediaItemDao.deleteAll(); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/ChronologyRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/ChronologyRepository.java new file mode 100644 index 0000000..8c9420c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/ChronologyRepository.java @@ -0,0 +1,39 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.ChronologyDao; +import com.cappielloantonio.tempo.model.Chronology; + +import java.util.Calendar; +import java.util.List; + +public class ChronologyRepository { + private final ChronologyDao chronologyDao = AppDatabase.getInstance().chronologyDao(); + + public LiveData> getChronology(String server, long start, long end) { + return chronologyDao.getAllFrom(start, end, server); + } + + public void insert(Chronology item) { + InsertThreadSafe insert = new InsertThreadSafe(chronologyDao, item); + Thread thread = new Thread(insert); + thread.start(); + } + + private static class InsertThreadSafe implements Runnable { + private final ChronologyDao chronologyDao; + private final Chronology item; + + public InsertThreadSafe(ChronologyDao chronologyDao, Chronology item) { + this.chronologyDao = chronologyDao; + this.item = item; + } + + @Override + public void run() { + chronologyDao.insert(item); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/DirectoryRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/DirectoryRepository.java new file mode 100644 index 0000000..31e96ba --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/DirectoryRepository.java @@ -0,0 +1,89 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.Directory; +import com.cappielloantonio.tempo.subsonic.models.Indexes; +import com.cappielloantonio.tempo.subsonic.models.MusicFolder; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class DirectoryRepository { + private static final String TAG = "DirectoryRepository"; + + public MutableLiveData> getMusicFolders() { + MutableLiveData> liveMusicFolders = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getMusicFolders() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getMusicFolders() != null) { + liveMusicFolders.setValue(response.body().getSubsonicResponse().getMusicFolders().getMusicFolders()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return liveMusicFolders; + } + + public MutableLiveData getIndexes(String musicFolderId, Long ifModifiedSince) { + MutableLiveData liveIndexes = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getIndexes(musicFolderId, ifModifiedSince) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getIndexes() != null) { + liveIndexes.setValue(response.body().getSubsonicResponse().getIndexes()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return liveIndexes; + } + + public MutableLiveData getMusicDirectory(String id) { + MutableLiveData liveMusicDirectory = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getMusicDirectory(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getDirectory() != null) { + liveMusicDirectory.setValue(response.body().getSubsonicResponse().getDirectory()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + t.printStackTrace(); + } + }); + + return liveMusicDirectory; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/DownloadRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/DownloadRepository.java new file mode 100644 index 0000000..1d8c935 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/DownloadRepository.java @@ -0,0 +1,213 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.DownloadDao; +import com.cappielloantonio.tempo.database.dao.FavoriteDao; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.model.Favorite; + +import java.util.ArrayList; +import java.util.List; + +public class DownloadRepository { + private final DownloadDao downloadDao = AppDatabase.getInstance().downloadDao(); + + public LiveData> getLiveDownload() { + return downloadDao.getAll(); + } + + public List getAllDownloads() { + GetAllDownloadsThreadSafe getDownloads = new GetAllDownloadsThreadSafe(downloadDao); + Thread thread = new Thread(getDownloads); + thread.start(); + + try { + thread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return getDownloads.getDownloads(); + } + + public Download getDownload(String id) { + Download download = null; + + GetDownloadThreadSafe getDownloadThreadSafe = new GetDownloadThreadSafe(downloadDao, id); + Thread thread = new Thread(getDownloadThreadSafe); + thread.start(); + + try { + thread.join(); + download = getDownloadThreadSafe.getDownload(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return download; + } + + private static class GetAllDownloadsThreadSafe implements Runnable { + private final DownloadDao downloadDao; + private List downloads; + + public GetAllDownloadsThreadSafe(DownloadDao downloadDao) { + this.downloadDao = downloadDao; + } + + @Override + public void run() { + downloads = downloadDao.getAllSync(); + } + + public List getDownloads() { + return downloads; + } + } + + private static class GetDownloadThreadSafe implements Runnable { + private final DownloadDao downloadDao; + private final String id; + private Download download; + + public GetDownloadThreadSafe(DownloadDao downloadDao, String id) { + this.downloadDao = downloadDao; + this.id = id; + } + + @Override + public void run() { + download = downloadDao.getOne(id); + } + + public Download getDownload() { + return download; + } + } + + public void insert(Download download) { + InsertThreadSafe insert = new InsertThreadSafe(downloadDao, download); + Thread thread = new Thread(insert); + thread.start(); + } + + private static class InsertThreadSafe implements Runnable { + private final DownloadDao downloadDao; + private final Download download; + + public InsertThreadSafe(DownloadDao downloadDao, Download download) { + this.downloadDao = downloadDao; + this.download = download; + } + + @Override + public void run() { + downloadDao.insert(download); + } + } + + public void update(String id) { + UpdateThreadSafe update = new UpdateThreadSafe(downloadDao, id); + Thread thread = new Thread(update); + thread.start(); + } + + private static class UpdateThreadSafe implements Runnable { + private final DownloadDao downloadDao; + private final String id; + + public UpdateThreadSafe(DownloadDao downloadDao, String id) { + this.downloadDao = downloadDao; + this.id = id; + } + + @Override + public void run() { + downloadDao.update(id); + } + } + + public void insertAll(List downloads) { + InsertAllThreadSafe insertAll = new InsertAllThreadSafe(downloadDao, downloads); + Thread thread = new Thread(insertAll); + thread.start(); + } + + private static class InsertAllThreadSafe implements Runnable { + private final DownloadDao downloadDao; + private final List downloads; + + public InsertAllThreadSafe(DownloadDao downloadDao, List downloads) { + this.downloadDao = downloadDao; + this.downloads = downloads; + } + + @Override + public void run() { + downloadDao.insertAll(downloads); + } + } + + public void deleteAll() { + DeleteAllThreadSafe deleteAll = new DeleteAllThreadSafe(downloadDao); + Thread thread = new Thread(deleteAll); + thread.start(); + } + + private static class DeleteAllThreadSafe implements Runnable { + private final DownloadDao downloadDao; + + public DeleteAllThreadSafe(DownloadDao downloadDao) { + this.downloadDao = downloadDao; + } + + @Override + public void run() { + downloadDao.deleteAll(); + } + } + + public void delete(String id) { + DeleteThreadSafe delete = new DeleteThreadSafe(downloadDao, id); + Thread thread = new Thread(delete); + thread.start(); + } + + public void delete(List ids) { + DeleteMultipleThreadSafe delete = new DeleteMultipleThreadSafe(downloadDao, ids); + Thread thread = new Thread(delete); + thread.start(); + } + + private static class DeleteThreadSafe implements Runnable { + private final DownloadDao downloadDao; + private final String id; + + public DeleteThreadSafe(DownloadDao downloadDao, String id) { + this.downloadDao = downloadDao; + this.id = id; + } + + @Override + public void run() { + downloadDao.delete(id); + } + } + + private static class DeleteMultipleThreadSafe implements Runnable { + private final DownloadDao downloadDao; + private final List ids; + + public DeleteMultipleThreadSafe(DownloadDao downloadDao, List ids) { + this.downloadDao = downloadDao; + this.ids = ids; + } + + @Override + public void run() { + downloadDao.deleteByIds(ids); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/FavoriteRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/FavoriteRepository.java new file mode 100644 index 0000000..2510d77 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/FavoriteRepository.java @@ -0,0 +1,140 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.FavoriteDao; +import com.cappielloantonio.tempo.interfaces.StarCallback; +import com.cappielloantonio.tempo.model.Favorite; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class FavoriteRepository { + private final FavoriteDao favoriteDao = AppDatabase.getInstance().favoriteDao(); + + public void star(String id, String albumId, String artistId, StarCallback starCallback) { + App.getSubsonicClientInstance(false) + .getMediaAnnotationClient() + .star(id, albumId, artistId) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + starCallback.onSuccess(); + } else { + starCallback.onError(); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + starCallback.onError(); + } + }); + } + + public void unstar(String id, String albumId, String artistId, StarCallback starCallback) { + App.getSubsonicClientInstance(false) + .getMediaAnnotationClient() + .unstar(id, albumId, artistId) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + starCallback.onSuccess(); + } else { + starCallback.onError(); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + starCallback.onError(); + } + }); + } + + public List getFavorites() { + List favorites = new ArrayList<>(); + + GetAllThreadSafe getAllThreadSafe = new GetAllThreadSafe(favoriteDao); + Thread thread = new Thread(getAllThreadSafe); + thread.start(); + + try { + thread.join(); + favorites = getAllThreadSafe.getFavorites(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return favorites; + } + + private static class GetAllThreadSafe implements Runnable { + private final FavoriteDao favoriteDao; + private List favorites = new ArrayList<>(); + + public GetAllThreadSafe(FavoriteDao favoriteDao) { + this.favoriteDao = favoriteDao; + } + + @Override + public void run() { + favorites = favoriteDao.getAll(); + } + + public List getFavorites() { + return favorites; + } + } + + public void starLater(String id, String albumId, String artistId, boolean toStar) { + InsertThreadSafe insert = new InsertThreadSafe(favoriteDao, new Favorite(System.currentTimeMillis(), id, albumId, artistId, toStar)); + Thread thread = new Thread(insert); + thread.start(); + } + + private static class InsertThreadSafe implements Runnable { + private final FavoriteDao favoriteDao; + private final Favorite favorite; + + public InsertThreadSafe(FavoriteDao favoriteDao, Favorite favorite) { + this.favoriteDao = favoriteDao; + this.favorite = favorite; + } + + @Override + public void run() { + favoriteDao.insert(favorite); + } + } + + public void delete(Favorite favorite) { + DeleteThreadSafe delete = new DeleteThreadSafe(favoriteDao, favorite); + Thread thread = new Thread(delete); + thread.start(); + } + + private static class DeleteThreadSafe implements Runnable { + private final FavoriteDao favoriteDao; + private final Favorite favorite; + + public DeleteThreadSafe(FavoriteDao favoriteDao, Favorite favorite) { + this.favoriteDao = favoriteDao; + this.favorite = favorite; + } + + @Override + public void run() { + favoriteDao.delete(favorite); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/GenreRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/GenreRepository.java new file mode 100644 index 0000000..4e6addb --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/GenreRepository.java @@ -0,0 +1,57 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.Genre; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class GenreRepository { + public MutableLiveData> getGenres(boolean random, int size) { + MutableLiveData> genres = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getGenres() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse() != null && response.body().getSubsonicResponse().getGenres() != null) { + List genreList = response.body().getSubsonicResponse().getGenres().getGenres(); + + if (genreList == null || genreList.isEmpty()) { + genres.setValue(Collections.emptyList()); + return; + } + + if (random) { + Collections.shuffle(genreList); + } + + if (size != -1) { + genres.setValue(genreList.subList(0, Math.min(size, genreList.size()))); + } else { + genres.setValue(genreList.stream().sorted(Comparator.comparing(Genre::getGenre)).collect(Collectors.toList())); + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return genres; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/LyricsRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/LyricsRepository.java new file mode 100644 index 0000000..fb7a05a --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/LyricsRepository.java @@ -0,0 +1,92 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.LyricsDao; +import com.cappielloantonio.tempo.model.LyricsCache; + +public class LyricsRepository { + private final LyricsDao lyricsDao = AppDatabase.getInstance().lyricsDao(); + + public LyricsCache getLyrics(String songId) { + GetLyricsThreadSafe getLyricsThreadSafe = new GetLyricsThreadSafe(lyricsDao, songId); + Thread thread = new Thread(getLyricsThreadSafe); + thread.start(); + + try { + thread.join(); + return getLyricsThreadSafe.getLyrics(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return null; + } + + public LiveData observeLyrics(String songId) { + return lyricsDao.observeOne(songId); + } + + public void insert(LyricsCache lyricsCache) { + InsertThreadSafe insert = new InsertThreadSafe(lyricsDao, lyricsCache); + Thread thread = new Thread(insert); + thread.start(); + } + + public void delete(String songId) { + DeleteThreadSafe delete = new DeleteThreadSafe(lyricsDao, songId); + Thread thread = new Thread(delete); + thread.start(); + } + + private static class GetLyricsThreadSafe implements Runnable { + private final LyricsDao lyricsDao; + private final String songId; + private LyricsCache lyricsCache; + + public GetLyricsThreadSafe(LyricsDao lyricsDao, String songId) { + this.lyricsDao = lyricsDao; + this.songId = songId; + } + + @Override + public void run() { + lyricsCache = lyricsDao.getOne(songId); + } + + public LyricsCache getLyrics() { + return lyricsCache; + } + } + + private static class InsertThreadSafe implements Runnable { + private final LyricsDao lyricsDao; + private final LyricsCache lyricsCache; + + public InsertThreadSafe(LyricsDao lyricsDao, LyricsCache lyricsCache) { + this.lyricsDao = lyricsDao; + this.lyricsCache = lyricsCache; + } + + @Override + public void run() { + lyricsDao.insert(lyricsCache); + } + } + + private static class DeleteThreadSafe implements Runnable { + private final LyricsDao lyricsDao; + private final String songId; + + public DeleteThreadSafe(LyricsDao lyricsDao, String songId) { + this.lyricsDao = lyricsDao; + this.songId = songId; + } + + @Override + public void run() { + lyricsDao.delete(songId); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/OpenRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/OpenRepository.java new file mode 100644 index 0000000..8621603 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/OpenRepository.java @@ -0,0 +1,37 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.LyricsList; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class OpenRepository { + public MutableLiveData getLyricsBySongId(String id) { + MutableLiveData lyricsList = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getOpenClient() + .getLyricsBySongId(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyricsList() != null) { + lyricsList.setValue(response.body().getSubsonicResponse().getLyricsList()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return lyricsList; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java new file mode 100644 index 0000000..66d0a18 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java @@ -0,0 +1,229 @@ +package com.cappielloantonio.tempo.repository; + +import static android.provider.Settings.System.getString; + +import android.provider.Settings; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.PlaylistDao; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Playlist; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class PlaylistRepository { + @androidx.media3.common.util.UnstableApi + private final PlaylistDao playlistDao = AppDatabase.getInstance().playlistDao(); + public MutableLiveData> getPlaylists(boolean random, int size) { + MutableLiveData> listLivePlaylists = new MutableLiveData<>(new ArrayList<>()); + + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .getPlaylists() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylists() != null && response.body().getSubsonicResponse().getPlaylists().getPlaylists() != null) { + List playlists = response.body().getSubsonicResponse().getPlaylists().getPlaylists(); + + if (random) { + Collections.shuffle(playlists); + listLivePlaylists.setValue(playlists.subList(0, Math.min(playlists.size(), size))); + } else { + listLivePlaylists.setValue(playlists); + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + } + }); + + return listLivePlaylists; + } + + public MutableLiveData> getPlaylistSongs(String id) { + MutableLiveData> listLivePlaylistSongs = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .getPlaylist(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylist() != null) { + List songs = response.body().getSubsonicResponse().getPlaylist().getEntries(); + listLivePlaylistSongs.setValue(songs); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + } + }); + + return listLivePlaylistSongs; + } + + public MutableLiveData getPlaylist(String id) { + MutableLiveData playlistLiveData = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .getPlaylist(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() + && response.body() != null + && response.body().getSubsonicResponse().getPlaylist() != null) { + playlistLiveData.setValue(response.body().getSubsonicResponse().getPlaylist()); + } else { + playlistLiveData.setValue(null); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + playlistLiveData.setValue(null); + } + }); + + return playlistLiveData; + } + + public void addSongToPlaylist(String playlistId, ArrayList songsId) { + if (songsId.isEmpty()) { + Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show(); + } else{ + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .updatePlaylist(playlistId, null, true, songsId, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_success), Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_failure), Toast.LENGTH_SHORT).show(); + } + }); + } + } + + public void createPlaylist(String playlistId, String name, ArrayList songsId) { + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .createPlaylist(playlistId, name, songsId) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public void updatePlaylist(String playlistId, String name, ArrayList songsId) { + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .deletePlaylist(playlistId) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + createPlaylist(null, name, songsId); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public void deletePlaylist(String playlistId) { + App.getSubsonicClientInstance(false) + .getPlaylistClient() + .deletePlaylist(playlistId) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + @androidx.media3.common.util.UnstableApi + public LiveData> getPinnedPlaylists() { + return playlistDao.getAll(); + } + + @androidx.media3.common.util.UnstableApi + public void insert(Playlist playlist) { + InsertThreadSafe insert = new InsertThreadSafe(playlistDao, playlist); + Thread thread = new Thread(insert); + thread.start(); + } + + @androidx.media3.common.util.UnstableApi + public void delete(Playlist playlist) { + DeleteThreadSafe delete = new DeleteThreadSafe(playlistDao, playlist); + Thread thread = new Thread(delete); + thread.start(); + } + + private static class InsertThreadSafe implements Runnable { + private final PlaylistDao playlistDao; + private final Playlist playlist; + + public InsertThreadSafe(PlaylistDao playlistDao, Playlist playlist) { + this.playlistDao = playlistDao; + this.playlist = playlist; + } + + @Override + public void run() { + playlistDao.insert(playlist); + } + } + + private static class DeleteThreadSafe implements Runnable { + private final PlaylistDao playlistDao; + private final Playlist playlist; + + public DeleteThreadSafe(PlaylistDao playlistDao, Playlist playlist) { + this.playlistDao = playlistDao; + this.playlist = playlist; + } + + @Override + public void run() { + playlistDao.delete(playlist); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/PodcastRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/PodcastRepository.java new file mode 100644 index 0000000..edfbbcc --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/PodcastRepository.java @@ -0,0 +1,153 @@ +package com.cappielloantonio.tempo.repository; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.PodcastChannel; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class PodcastRepository { + private static final String TAG = "PodcastRepository"; + + public MutableLiveData> getPodcastChannels(boolean includeEpisodes, String channelId) { + MutableLiveData> livePodcastChannel = new MutableLiveData<>(new ArrayList<>()); + + App.getSubsonicClientInstance(false) + .getPodcastClient() + .getPodcasts(includeEpisodes, channelId) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPodcasts() != null) { + livePodcastChannel.setValue(response.body().getSubsonicResponse().getPodcasts().getChannels()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return livePodcastChannel; + } + + public MutableLiveData> getNewestPodcastEpisodes(int count) { + MutableLiveData> liveNewestPodcastEpisodes = new MutableLiveData<>(new ArrayList<>()); + + App.getSubsonicClientInstance(false) + .getPodcastClient() + .getNewestPodcasts(count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getNewestPodcasts() != null) { + liveNewestPodcastEpisodes.setValue(response.body().getSubsonicResponse().getNewestPodcasts().getEpisodes()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return liveNewestPodcastEpisodes; + } + + public void refreshPodcasts() { + App.getSubsonicClientInstance(false) + .getPodcastClient() + .refreshPodcasts() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public void createPodcastChannel(String url) { + App.getSubsonicClientInstance(false) + .getPodcastClient() + .createPodcastChannel(url) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public void deletePodcastChannel(String channelId) { + App.getSubsonicClientInstance(false) + .getPodcastClient() + .deletePodcastChannel(channelId) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public void deletePodcastEpisode(String episodeId) { + App.getSubsonicClientInstance(false) + .getPodcastClient() + .deletePodcastEpisode(episodeId) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public void downloadPodcastEpisode(String episodeId) { + App.getSubsonicClientInstance(false) + .getPodcastClient() + .downloadPodcastEpisode(episodeId) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/QueueRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/QueueRepository.java new file mode 100644 index 0000000..6b3d625 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/QueueRepository.java @@ -0,0 +1,378 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.QueueDao; +import com.cappielloantonio.tempo.model.Queue; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.PlayQueue; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class QueueRepository { + private static final String TAG = "QueueRepository"; + + private final QueueDao queueDao = AppDatabase.getInstance().queueDao(); + + public LiveData> getLiveQueue() { + return queueDao.getAll(); + } + + public List getMedia() { + List media = new ArrayList<>(); + + GetMediaThreadSafe getMedia = new GetMediaThreadSafe(queueDao); + Thread thread = new Thread(getMedia); + thread.start(); + + try { + thread.join(); + media = getMedia.getMedia().stream() + .map(Child.class::cast) + .collect(Collectors.toList()); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return media; + } + + public MutableLiveData getPlayQueue() { + MutableLiveData playQueue = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBookmarksClient() + .getPlayQueue() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlayQueue() != null) { + playQueue.setValue(response.body().getSubsonicResponse().getPlayQueue()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + playQueue.setValue(null); + } + }); + + return playQueue; + } + + public void savePlayQueue(List ids, String current, long position) { + App.getSubsonicClientInstance(false) + .getBookmarksClient() + .savePlayQueue(ids, current, position) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public void insert(Child media, boolean reset, int afterIndex) { + try { + List mediaList = new ArrayList<>(); + + if (!reset) { + GetMediaThreadSafe getMediaThreadSafe = new GetMediaThreadSafe(queueDao); + Thread getMediaThread = new Thread(getMediaThreadSafe); + getMediaThread.start(); + getMediaThread.join(); + + mediaList = getMediaThreadSafe.getMedia(); + } + + Queue queueItem = new Queue(media); + mediaList.add(afterIndex, queueItem); + + for (int i = 0; i < mediaList.size(); i++) { + mediaList.get(i).setTrackOrder(i); + } + + Thread delete = new Thread(new DeleteAllThreadSafe(queueDao)); + delete.start(); + delete.join(); + + Thread insertAll = new Thread(new InsertAllThreadSafe(queueDao, mediaList)); + insertAll.start(); + insertAll.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private boolean isMediaInQueue(List queue, Child media) { + if (queue == null || media == null) return false; + + return queue.stream().anyMatch(queueItem -> + queueItem != null && media.getId() != null && + queueItem.getId().equals(media.getId()) + ); + } + + public void insertAll(List toAdd, boolean reset, int afterIndex) { + try { + List media = new ArrayList<>(); + + if (!reset) { + GetMediaThreadSafe getMediaThreadSafe = new GetMediaThreadSafe(queueDao); + Thread getMediaThread = new Thread(getMediaThreadSafe); + getMediaThread.start(); + getMediaThread.join(); + + media = getMediaThreadSafe.getMedia(); + } + + List filteredToAdd = toAdd; + final List finalMedia = media; + filteredToAdd = toAdd.stream() + .filter(child -> !isMediaInQueue(finalMedia, child)) + .collect(Collectors.toList()); + + for (int i = 0; i < filteredToAdd.size(); i++) { + Queue queueItem = new Queue(filteredToAdd.get(i)); + media.add(afterIndex + i, queueItem); + } + + for (int i = 0; i < media.size(); i++) { + media.get(i).setTrackOrder(i); + } + + Thread delete = new Thread(new DeleteAllThreadSafe(queueDao)); + delete.start(); + delete.join(); + + Thread insertAll = new Thread(new InsertAllThreadSafe(queueDao, media)); + insertAll.start(); + insertAll.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public void delete(int position) { + DeleteThreadSafe delete = new DeleteThreadSafe(queueDao, position); + Thread thread = new Thread(delete); + thread.start(); + } + + public void deleteAll() { + DeleteAllThreadSafe deleteAll = new DeleteAllThreadSafe(queueDao); + Thread thread = new Thread(deleteAll); + thread.start(); + } + + public int count() { + int count = 0; + + CountThreadSafe countThread = new CountThreadSafe(queueDao); + Thread thread = new Thread(countThread); + thread.start(); + + try { + thread.join(); + count = countThread.getCount(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return count; + } + + public void setLastPlayedTimestamp(String id) { + SetLastPlayedTimestampThreadSafe timestamp = new SetLastPlayedTimestampThreadSafe(queueDao, id); + Thread thread = new Thread(timestamp); + thread.start(); + } + + public void setPlayingPausedTimestamp(String id, long ms) { + SetPlayingPausedTimestampThreadSafe timestamp = new SetPlayingPausedTimestampThreadSafe(queueDao, id, ms); + Thread thread = new Thread(timestamp); + thread.start(); + } + + public int getLastPlayedMediaIndex() { + int index = 0; + + GetLastPlayedMediaThreadSafe getLastPlayedMediaThreadSafe = new GetLastPlayedMediaThreadSafe(queueDao); + Thread thread = new Thread(getLastPlayedMediaThreadSafe); + thread.start(); + + try { + thread.join(); + Queue lastMediaPlayed = getLastPlayedMediaThreadSafe.getQueueItem(); + index = lastMediaPlayed.getTrackOrder(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return index; + } + + public long getLastPlayedMediaTimestamp() { + long timestamp = 0; + + GetLastPlayedMediaThreadSafe getLastPlayedMediaThreadSafe = new GetLastPlayedMediaThreadSafe(queueDao); + Thread thread = new Thread(getLastPlayedMediaThreadSafe); + thread.start(); + + try { + thread.join(); + Queue lastMediaPlayed = getLastPlayedMediaThreadSafe.getQueueItem(); + timestamp = lastMediaPlayed.getPlayingChanged(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return timestamp; + } + + private static class GetMediaThreadSafe implements Runnable { + private final QueueDao queueDao; + private List media; + + public GetMediaThreadSafe(QueueDao queueDao) { + this.queueDao = queueDao; + } + + @Override + public void run() { + media = queueDao.getAllSimple(); + } + + public List getMedia() { + return media; + } + } + + private static class InsertAllThreadSafe implements Runnable { + private final QueueDao queueDao; + private final List media; + + public InsertAllThreadSafe(QueueDao queueDao, List media) { + this.queueDao = queueDao; + this.media = media; + } + + @Override + public void run() { + queueDao.insertAll(media); + } + } + + private static class DeleteThreadSafe implements Runnable { + private final QueueDao queueDao; + private final int position; + + public DeleteThreadSafe(QueueDao queueDao, int position) { + this.queueDao = queueDao; + this.position = position; + } + + @Override + public void run() { + queueDao.delete(position); + } + } + + private static class DeleteAllThreadSafe implements Runnable { + private final QueueDao queueDao; + + public DeleteAllThreadSafe(QueueDao queueDao) { + this.queueDao = queueDao; + } + + @Override + public void run() { + queueDao.deleteAll(); + } + } + + private static class CountThreadSafe implements Runnable { + private final QueueDao queueDao; + private int count = 0; + + public CountThreadSafe(QueueDao queueDao) { + this.queueDao = queueDao; + } + + @Override + public void run() { + count = queueDao.count(); + } + + public int getCount() { + return count; + } + } + + private static class SetLastPlayedTimestampThreadSafe implements Runnable { + private final QueueDao queueDao; + private final String mediaId; + + public SetLastPlayedTimestampThreadSafe(QueueDao queueDao, String mediaId) { + this.queueDao = queueDao; + this.mediaId = mediaId; + } + + @Override + public void run() { + queueDao.setLastPlay(mediaId, System.currentTimeMillis()); + } + } + + private static class SetPlayingPausedTimestampThreadSafe implements Runnable { + private final QueueDao queueDao; + private final String mediaId; + private final long ms; + + public SetPlayingPausedTimestampThreadSafe(QueueDao queueDao, String mediaId, long ms) { + this.queueDao = queueDao; + this.mediaId = mediaId; + this.ms = ms; + } + + @Override + public void run() { + queueDao.setPlayingChanged(mediaId, ms); + } + } + + private static class GetLastPlayedMediaThreadSafe implements Runnable { + private final QueueDao queueDao; + private Queue lastMediaPlayed; + + public GetLastPlayedMediaThreadSafe(QueueDao queueDao) { + this.queueDao = queueDao; + } + + @Override + public void run() { + lastMediaPlayed = queueDao.getLastPlayed(); + } + + public Queue getQueueItem() { + return lastMediaPlayed; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/RadioRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/RadioRepository.java new file mode 100644 index 0000000..9ad8a11 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/RadioRepository.java @@ -0,0 +1,91 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class RadioRepository { + public MutableLiveData> getInternetRadioStations() { + MutableLiveData> radioStation = new MutableLiveData<>(new ArrayList<>()); + + App.getSubsonicClientInstance(false) + .getInternetRadioClient() + .getInternetRadioStations() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getInternetRadioStations() != null && response.body().getSubsonicResponse().getInternetRadioStations().getInternetRadioStations() != null) { + radioStation.setValue(response.body().getSubsonicResponse().getInternetRadioStations().getInternetRadioStations()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return radioStation; + } + + public void createInternetRadioStation(String name, String streamURL, String homepageURL) { + App.getSubsonicClientInstance(false) + .getInternetRadioClient() + .createInternetRadioStation(streamURL, name, homepageURL) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public void updateInternetRadioStation(String id, String name, String streamURL, String homepageURL) { + App.getSubsonicClientInstance(false) + .getInternetRadioClient() + .updateInternetRadioStation(id, streamURL, name, homepageURL) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public void deleteInternetRadioStation(String id) { + App.getSubsonicClientInstance(false) + .getInternetRadioClient() + .deleteInternetRadioStation(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/ScanRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/ScanRepository.java new file mode 100644 index 0000000..5beb4c6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/ScanRepository.java @@ -0,0 +1,58 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.interfaces.ScanCallback; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; +import retrofit2.Callback; + +public class ScanRepository { + public void startScan(ScanCallback callback) { + App.getSubsonicClientInstance(false) + .getMediaLibraryScanningClient() + .startScan() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse() != null) { + if (response.body().getSubsonicResponse().getError() != null) { + callback.onError(new Exception(response.body().getSubsonicResponse().getError().getMessage())); + } else if (response.body().getSubsonicResponse().getScanStatus() != null) { + callback.onSuccess(response.body().getSubsonicResponse().getScanStatus().isScanning(), response.body().getSubsonicResponse().getScanStatus().getCount()); + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + callback.onError(new Exception(t.getMessage())); + } + }); + } + + public void getScanStatus(ScanCallback callback) { + App.getSubsonicClientInstance(false) + .getMediaLibraryScanningClient() + .startScan() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse() != null) { + if (response.body().getSubsonicResponse().getError() != null) { + callback.onError(new Exception(response.body().getSubsonicResponse().getError().getMessage())); + } else if (response.body().getSubsonicResponse().getScanStatus() != null) { + callback.onSuccess(response.body().getSubsonicResponse().getScanStatus().isScanning(), response.body().getSubsonicResponse().getScanStatus().getCount()); + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + callback.onError(new Exception(t.getMessage())); + } + }); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SearchingRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SearchingRepository.java new file mode 100644 index 0000000..b52315a --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SearchingRepository.java @@ -0,0 +1,196 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.RecentSearchDao; +import com.cappielloantonio.tempo.model.RecentSearch; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.SearchResult2; +import com.cappielloantonio.tempo.subsonic.models.SearchResult3; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class SearchingRepository { + private final RecentSearchDao recentSearchDao = AppDatabase.getInstance().recentSearchDao(); + + public MutableLiveData search2(String query) { + MutableLiveData result = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getSearchingClient() + .search3(query, 20, 20, 20) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + result.setValue(response.body().getSubsonicResponse().getSearchResult2()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return result; + } + + public MutableLiveData search3(String query) { + MutableLiveData result = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getSearchingClient() + .search3(query, 20, 20, 20) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + result.setValue(response.body().getSubsonicResponse().getSearchResult3()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return result; + } + + public MutableLiveData> getSuggestions(String query) { + MutableLiveData> suggestions = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getSearchingClient() + .search3(query, 5, 5, 5) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List newSuggestions = new ArrayList(); + + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSearchResult3() != null) { + if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) { + for (ArtistID3 artistID3 : response.body().getSubsonicResponse().getSearchResult3().getArtists()) { + newSuggestions.add(artistID3.getName()); + } + } + + if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) { + for (AlbumID3 albumID3 : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) { + newSuggestions.add(albumID3.getName()); + } + } + + if (response.body().getSubsonicResponse().getSearchResult3().getSongs() != null) { + for (Child song : response.body().getSubsonicResponse().getSearchResult3().getSongs()) { + newSuggestions.add(song.getTitle()); + } + } + + LinkedHashSet hashSet = new LinkedHashSet<>(newSuggestions); + ArrayList suggestionsWithoutDuplicates = new ArrayList<>(hashSet); + + suggestions.setValue(suggestionsWithoutDuplicates); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return suggestions; + } + + public void insert(RecentSearch recentSearch) { + InsertThreadSafe insert = new InsertThreadSafe(recentSearchDao, recentSearch); + Thread thread = new Thread(insert); + thread.start(); + } + + public void delete(RecentSearch recentSearch) { + DeleteThreadSafe delete = new DeleteThreadSafe(recentSearchDao, recentSearch); + Thread thread = new Thread(delete); + thread.start(); + } + + public List getRecentSearchSuggestion() { + List recent = new ArrayList<>(); + + RecentThreadSafe suggestionsThread = new RecentThreadSafe(recentSearchDao); + Thread thread = new Thread(suggestionsThread); + thread.start(); + + try { + thread.join(); + recent = suggestionsThread.getRecent(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return recent; + } + + private static class DeleteThreadSafe implements Runnable { + private final RecentSearchDao recentSearchDao; + private final RecentSearch recentSearch; + + public DeleteThreadSafe(RecentSearchDao recentSearchDao, RecentSearch recentSearch) { + this.recentSearchDao = recentSearchDao; + this.recentSearch = recentSearch; + } + + @Override + public void run() { + recentSearchDao.delete(recentSearch); + } + } + + private static class InsertThreadSafe implements Runnable { + private final RecentSearchDao recentSearchDao; + private final RecentSearch recentSearch; + + public InsertThreadSafe(RecentSearchDao recentSearchDao, RecentSearch recentSearch) { + this.recentSearchDao = recentSearchDao; + this.recentSearch = recentSearch; + } + + @Override + public void run() { + recentSearchDao.insert(recentSearch); + } + } + + private static class RecentThreadSafe implements Runnable { + private final RecentSearchDao recentSearchDao; + private List recent = new ArrayList<>(); + + public RecentThreadSafe(RecentSearchDao recentSearchDao) { + this.recentSearchDao = recentSearchDao; + } + + @Override + public void run() { + recent = recentSearchDao.getRecent(); + } + + public List getRecent() { + return recent; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/ServerRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/ServerRepository.java new file mode 100644 index 0000000..8907c5f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/ServerRepository.java @@ -0,0 +1,61 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.database.AppDatabase; +import com.cappielloantonio.tempo.database.dao.ServerDao; +import com.cappielloantonio.tempo.model.Server; + +import java.util.List; + +public class ServerRepository { + private static final String TAG = "QueueRepository"; + + private final ServerDao serverDao = AppDatabase.getInstance().serverDao(); + + public LiveData> getLiveServer() { + return serverDao.getAll(); + } + + public void insert(Server server) { + InsertThreadSafe insert = new InsertThreadSafe(serverDao, server); + Thread thread = new Thread(insert); + thread.start(); + } + + public void delete(Server server) { + DeleteThreadSafe delete = new DeleteThreadSafe(serverDao, server); + Thread thread = new Thread(delete); + thread.start(); + } + + private static class InsertThreadSafe implements Runnable { + private final ServerDao serverDao; + private final Server server; + + public InsertThreadSafe(ServerDao serverDao, Server server) { + this.serverDao = serverDao; + this.server = server; + } + + @Override + public void run() { + serverDao.insert(server); + } + } + + private static class DeleteThreadSafe implements Runnable { + private final ServerDao serverDao; + private final Server server; + + public DeleteThreadSafe(ServerDao serverDao, Server server) { + this.serverDao = serverDao; + this.server = server; + } + + @Override + public void run() { + serverDao.delete(server); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SharingRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SharingRepository.java new file mode 100644 index 0000000..9c53183 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SharingRepository.java @@ -0,0 +1,99 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.Share; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class SharingRepository { + public MutableLiveData> getShares() { + MutableLiveData> shares = new MutableLiveData<>(new ArrayList<>()); + + App.getSubsonicClientInstance(false) + .getSharingClient() + .getShares() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getShares() != null && response.body().getSubsonicResponse().getShares().getShares() != null) { + shares.setValue(response.body().getSubsonicResponse().getShares().getShares()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return shares; + } + + public MutableLiveData createShare(String id, String description, Long expires) { + MutableLiveData share = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getSharingClient() + .createShare(id, description, expires) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getShares() != null && response.body().getSubsonicResponse().getShares().getShares() != null && response.body().getSubsonicResponse().getShares().getShares().get(0) != null) { + share.setValue(response.body().getSubsonicResponse().getShares().getShares().get(0)); + } else { + share.setValue(null); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + share.setValue(null); + } + }); + + return share; + } + + public void updateShare(String id, String description, Long expires) { + App.getSubsonicClientInstance(false) + .getSharingClient() + .updateShare(id, description, expires) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public void deleteShare(String id) { + App.getSubsonicClientInstance(false) + .getSharingClient() + .deleteShare(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java new file mode 100644 index 0000000..a40b3c9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SongRepository.java @@ -0,0 +1,260 @@ +package com.cappielloantonio.tempo.repository; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.Child; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class SongRepository { + private static final String TAG = "SongRepository"; + + public MutableLiveData> getStarredSongs(boolean random, int size) { + MutableLiveData> starredSongs = new MutableLiveData<>(Collections.emptyList()); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getStarred2() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getStarred2() != null) { + List songs = response.body().getSubsonicResponse().getStarred2().getSongs(); + + if (songs != null) { + if (!random) { + starredSongs.setValue(songs); + } else { + Collections.shuffle(songs); + starredSongs.setValue(songs.subList(0, Math.min(size, songs.size()))); + } + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return starredSongs; + } + + public MutableLiveData> getInstantMix(String id, int count) { + MutableLiveData> instantMix = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getSimilarSongs2(id, count) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) { + instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + instantMix.setValue(null); + } + }); + + return instantMix; + } + + public MutableLiveData> getRandomSample(int number, Integer fromYear, Integer toYear) { + MutableLiveData> randomSongsSample = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getRandomSongs(number, fromYear, toYear) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = new ArrayList<>(); + + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) { + songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs()); + } + + randomSongsSample.setValue(songs); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return randomSongsSample; + } + + public MutableLiveData> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) { + MutableLiveData> randomSongsSample = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getRandomSongs(number, fromYear, toYear, genre) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = new ArrayList<>(); + + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) { + songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs()); + } + + randomSongsSample.setValue(songs); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return randomSongsSample; + } + + public void scrobble(String id, boolean submission) { + App.getSubsonicClientInstance(false) + .getMediaAnnotationClient() + .scrobble(id, submission) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public void setRating(String id, int rating) { + App.getSubsonicClientInstance(false) + .getMediaAnnotationClient() + .setRating(id, rating) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } + + public MutableLiveData> getSongsByGenre(String id, int page) { + MutableLiveData> songsByGenre = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getSongsByGenre(id, 100, 100 * page) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) { + songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return songsByGenre; + } + + public MutableLiveData> getSongsByGenres(ArrayList genresId) { + MutableLiveData> songsByGenre = new MutableLiveData<>(); + + for (String id : genresId) + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getSongsByGenre(id, 500, 0) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + List songs = new ArrayList<>(); + + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) { + songs.addAll(response.body().getSubsonicResponse().getSongsByGenre().getSongs()); + } + + songsByGenre.setValue(songs); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return songsByGenre; + } + + public MutableLiveData getSong(String id) { + MutableLiveData song = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getSong(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + song.setValue(response.body().getSubsonicResponse().getSong()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return song; + } + + public MutableLiveData getSongLyrics(Child song) { + MutableLiveData lyrics = new MutableLiveData<>(null); + + App.getSubsonicClientInstance(false) + .getMediaRetrievalClient() + .getLyrics(song.getArtist(), song.getTitle()) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) { + lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return lyrics; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SystemRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SystemRepository.java new file mode 100644 index 0000000..1e0a37e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SystemRepository.java @@ -0,0 +1,123 @@ +package com.cappielloantonio.tempo.repository; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.github.models.LatestRelease; +import com.cappielloantonio.tempo.interfaces.SystemCallback; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension; +import com.cappielloantonio.tempo.subsonic.models.ResponseStatus; +import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class SystemRepository { + public void checkUserCredential(SystemCallback callback) { + App.getSubsonicClientInstance(false) + .getSystemClient() + .ping() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { + if (response.body() != null) { + if (response.body().getSubsonicResponse().getStatus().equals(ResponseStatus.FAILED)) { + callback.onError(new Exception(response.body().getSubsonicResponse().getError().getCode() + " - " + response.body().getSubsonicResponse().getError().getMessage())); + } else if (response.body().getSubsonicResponse().getStatus().equals(ResponseStatus.OK)) { + String password = response.raw().request().url().queryParameter("p"); + String token = response.raw().request().url().queryParameter("t"); + String salt = response.raw().request().url().queryParameter("s"); + callback.onSuccess(password, token, salt); + } else { + callback.onError(new Exception("Empty response")); + } + } else { + callback.onError(new Exception(String.valueOf(response.code()))); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + callback.onError(new Exception(t.getMessage())); + } + }); + } + + public MutableLiveData ping() { + MutableLiveData pingResult = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getSystemClient() + .ping() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + pingResult.postValue(response.body().getSubsonicResponse()); + } else { + pingResult.postValue(null); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + pingResult.postValue(null); + } + }); + + return pingResult; + } + + public MutableLiveData> getOpenSubsonicExtensions() { + MutableLiveData> extensionsResult = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getSystemClient() + .getOpenSubsonicExtensions() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + extensionsResult.postValue(response.body().getSubsonicResponse().getOpenSubsonicExtensions()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + extensionsResult.postValue(null); + } + }); + + return extensionsResult; + } + + public MutableLiveData checkTempoUpdate() { + MutableLiveData latestRelease = new MutableLiveData<>(); + + App.getGithubClientInstance() + .getReleaseClient() + .getLatestRelease() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + latestRelease.postValue(response.body()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + latestRelease.postValue(null); + } + }); + + return latestRelease; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderManager.java new file mode 100644 index 0000000..3a695fe --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderManager.java @@ -0,0 +1,148 @@ +package com.cappielloantonio.tempo.service; + +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import androidx.media3.datasource.DataSource; +import androidx.media3.exoplayer.offline.Download; +import androidx.media3.exoplayer.offline.DownloadCursor; +import androidx.media3.exoplayer.offline.DownloadHelper; +import androidx.media3.exoplayer.offline.DownloadIndex; +import androidx.media3.exoplayer.offline.DownloadManager; +import androidx.media3.exoplayer.offline.DownloadRequest; +import androidx.media3.exoplayer.offline.DownloadService; + +import com.cappielloantonio.tempo.repository.DownloadRepository; +import com.cappielloantonio.tempo.util.DownloadUtil; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; + +@UnstableApi +public class DownloaderManager { + private static final String TAG = "DownloaderManager"; + + private final Context context; + private final DataSource.Factory dataSourceFactory; + private final DownloadIndex downloadIndex; + + private static HashMap downloads; + + public DownloaderManager(Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) { + this.context = context.getApplicationContext(); + this.dataSourceFactory = dataSourceFactory; + + downloads = new HashMap<>(); + downloadIndex = downloadManager.getDownloadIndex(); + + loadDownloads(); + } + + private DownloadRequest buildDownloadRequest(MediaItem mediaItem) { + return DownloadHelper + .forMediaItem( + context, + mediaItem, + DownloadUtil.buildRenderersFactory(context, false), + dataSourceFactory) + .getDownloadRequest(Util.getUtf8Bytes(checkNotNull(mediaItem.mediaId))) + .copyWithId(mediaItem.mediaId); + } + + public boolean isDownloaded(String mediaId) { + @Nullable Download download = downloads.get(mediaId); + return download != null && download.state != Download.STATE_FAILED; + } + + public boolean isDownloaded(MediaItem mediaItem) { + return isDownloaded(mediaItem.mediaId); + } + + public boolean areDownloaded(List mediaItems) { + return mediaItems.stream().anyMatch(this::isDownloaded); + } + + public void download(MediaItem mediaItem, com.cappielloantonio.tempo.model.Download download) { + download.setDownloadUri(mediaItem.requestMetadata.mediaUri.toString()); + + DownloadService.sendAddDownload(context, DownloaderService.class, buildDownloadRequest(mediaItem), false); + insertDatabase(download); + } + + public void download(List mediaItems, List downloads) { + for (int counter = 0; counter < mediaItems.size(); counter++) { + download(mediaItems.get(counter), downloads.get(counter)); + } + } + + public void remove(MediaItem mediaItem, com.cappielloantonio.tempo.model.Download download) { + DownloadService.sendRemoveDownload(context, DownloaderService.class, buildDownloadRequest(mediaItem).id, false); + deleteDatabase(download.getId()); + downloads.remove(download.getId()); + } + + public void remove(List mediaItems, List downloads) { + for (int counter = 0; counter < mediaItems.size(); counter++) { + remove(mediaItems.get(counter), downloads.get(counter)); + } + } + + public void removeAll() { + DownloadService.sendRemoveAllDownloads(context, DownloaderService.class, false); + deleteAllDatabase(); + DownloadUtil.eraseDownloadFolder(context); + } + + private void loadDownloads() { + try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) { + while (loadedDownloads.moveToNext()) { + Download download = loadedDownloads.getDownload(); + downloads.put(download.request.id, download); + } + } catch (IOException e) { + Log.w(TAG, "Failed to query downloads", e); + } + } + + public static String getDownloadNotificationMessage(String id) { + com.cappielloantonio.tempo.model.Download download = getDownloadRepository().getDownload(id); + return download != null ? download.getTitle() : null; + } + + public static void updateRequestDownload(Download download) { + updateDatabase(download.request.id); + downloads.put(download.request.id, download); + } + + public static void removeRequestDownload(Download download) { + deleteDatabase(download.request.id); + downloads.remove(download.request.id); + } + + private static DownloadRepository getDownloadRepository() { + return new DownloadRepository(); + } + + private static void insertDatabase(com.cappielloantonio.tempo.model.Download download) { + getDownloadRepository().insert(download); + } + + private static void deleteDatabase(String id) { + getDownloadRepository().delete(id); + } + + private static void deleteAllDatabase() { + getDownloadRepository().deleteAll(); + } + + private static void updateDatabase(String id) { + getDownloadRepository().update(id); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderService.java b/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderService.java new file mode 100644 index 0000000..b34daf3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderService.java @@ -0,0 +1,115 @@ +package com.cappielloantonio.tempo.service; + +import android.app.Notification; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media3.common.util.NotificationUtil; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.offline.Download; +import androidx.media3.exoplayer.offline.DownloadManager; +import androidx.media3.exoplayer.offline.DownloadNotificationHelper; +import androidx.media3.exoplayer.scheduler.PlatformScheduler; +import androidx.media3.exoplayer.scheduler.Requirements; +import androidx.media3.exoplayer.scheduler.Scheduler; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.util.DownloadUtil; + +import java.util.List; + +@UnstableApi +public class DownloaderService extends androidx.media3.exoplayer.offline.DownloadService { + + private static final int JOB_ID = 1; + private static final int FOREGROUND_NOTIFICATION_ID = 1; + + public DownloaderService() { + super(FOREGROUND_NOTIFICATION_ID, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID, R.string.exo_download_notification_channel_name, 0); + } + + @NonNull + @Override + protected DownloadManager getDownloadManager() { + DownloadManager downloadManager = DownloadUtil.getDownloadManager(this); + DownloadNotificationHelper downloadNotificationHelper = DownloadUtil.getDownloadNotificationHelper(this); + downloadManager.addListener(new TerminalStateNotificationHelper(this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1)); + return downloadManager; + } + + @NonNull + @Override + protected Scheduler getScheduler() { + return new PlatformScheduler(this, JOB_ID); + } + + @NonNull + @Override + protected Notification getForegroundNotification(@NonNull List downloads, @Requirements.RequirementFlags int notMetRequirements) { + return DownloadUtil.getDownloadNotificationHelper(this).buildProgressNotification(this, R.drawable.ic_download, null, null, downloads, notMetRequirements); + } + + private static final class TerminalStateNotificationHelper implements DownloadManager.Listener { + private final Context context; + private final DownloadNotificationHelper notificationHelper; + + private final Notification successfulDownloadGroupNotification; + private final Notification failedDownloadGroupNotification; + + private final int successfulDownloadGroupNotificationId; + private final int failedDownloadGroupNotificationId; + + private int nextNotificationId; + + public TerminalStateNotificationHelper(Context context, DownloadNotificationHelper notificationHelper, int firstNotificationId) { + this.context = context.getApplicationContext(); + this.notificationHelper = notificationHelper; + nextNotificationId = firstNotificationId; + + successfulDownloadGroupNotification = DownloadUtil.buildGroupSummaryNotification( + this.context, + DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID, + DownloadUtil.DOWNLOAD_NOTIFICATION_SUCCESSFUL_GROUP, + R.drawable.ic_check_circle, + "Downloads completed" + ); + + failedDownloadGroupNotification = DownloadUtil.buildGroupSummaryNotification( + this.context, + DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID, + DownloadUtil.DOWNLOAD_NOTIFICATION_FAILED_GROUP, + R.drawable.ic_error, + "Downloads failed" + ); + + successfulDownloadGroupNotificationId = nextNotificationId++; + failedDownloadGroupNotificationId = nextNotificationId++; + } + + @Override + public void onDownloadChanged(@NonNull DownloadManager downloadManager, Download download, @Nullable Exception finalException) { + Notification notification; + + if (download.state == Download.STATE_COMPLETED) { + notification = notificationHelper.buildDownloadCompletedNotification(context, R.drawable.ic_check_circle, null, DownloaderManager.getDownloadNotificationMessage(download.request.id)); + notification = Notification.Builder.recoverBuilder(context, notification).setGroup(DownloadUtil.DOWNLOAD_NOTIFICATION_SUCCESSFUL_GROUP).build(); + NotificationUtil.setNotification(this.context, successfulDownloadGroupNotificationId, successfulDownloadGroupNotification); + DownloaderManager.updateRequestDownload(download); + } else if (download.state == Download.STATE_FAILED) { + notification = notificationHelper.buildDownloadFailedNotification(context, R.drawable.ic_error, null, DownloaderManager.getDownloadNotificationMessage(download.request.id)); + notification = Notification.Builder.recoverBuilder(context, notification).setGroup(DownloadUtil.DOWNLOAD_NOTIFICATION_FAILED_GROUP).build(); + NotificationUtil.setNotification(this.context, failedDownloadGroupNotificationId, failedDownloadGroupNotification); + } else { + return; + } + + NotificationUtil.setNotification(context, nextNotificationId++, notification); + } + + @Override + public void onDownloadRemoved(@NonNull DownloadManager downloadManager, Download download) { + DownloaderManager.removeRequestDownload(download); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/EqualizerManager.kt b/app/src/main/java/com/cappielloantonio/tempo/service/EqualizerManager.kt new file mode 100644 index 0000000..9d8489e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/service/EqualizerManager.kt @@ -0,0 +1,47 @@ +package com.cappielloantonio.tempo.service + +import android.media.audiofx.Equalizer + +class EqualizerManager { + + private var equalizer: Equalizer? = null + + fun attachToSession(audioSessionId: Int): Boolean { + release() + if (audioSessionId != 0 && audioSessionId != -1) { + try { + equalizer = Equalizer(0, audioSessionId).apply { + enabled = true + } + return true + } catch (e: Exception) { + // Some devices may not support Equalizer or audio session may be invalid + equalizer = null + } + } + return false + } + + fun setBandLevel(band: Short, level: Short) { + equalizer?.setBandLevel(band, level) + } + + fun getNumberOfBands(): Short = equalizer?.numberOfBands ?: 0 + + fun getBandLevelRange(): ShortArray? = equalizer?.bandLevelRange + + fun getCenterFreq(band: Short): Int? = + equalizer?.getCenterFreq(band)?.div(1000) + + fun getBandLevel(band: Short): Short? = + equalizer?.getBandLevel(band) + + fun setEnabled(enabled: Boolean) { + equalizer?.enabled = enabled + } + + fun release() { + equalizer?.release() + equalizer = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java new file mode 100644 index 0000000..f7cd8a3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/service/MediaManager.java @@ -0,0 +1,485 @@ +package com.cappielloantonio.tempo.service; + +import android.content.ComponentName; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Player; +import androidx.media3.common.Timeline; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.interfaces.MediaIndexCallback; +import com.cappielloantonio.tempo.model.Chronology; +import com.cappielloantonio.tempo.repository.ChronologyRepository; +import com.cappielloantonio.tempo.repository.QueueRepository; +import com.cappielloantonio.tempo.repository.SongRepository; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.concurrent.ExecutionException; + +public class MediaManager { + private static final String TAG = "MediaManager"; + private static WeakReference attachedBrowserRef = new WeakReference<>(null); + + public static void registerPlaybackObserver( + ListenableFuture browserFuture, + PlaybackViewModel playbackViewModel + ) { + if (browserFuture == null) return; + + Futures.addCallback(browserFuture, new FutureCallback() { + @Override + public void onSuccess(MediaBrowser browser) { + MediaBrowser current = attachedBrowserRef.get(); + if (current != browser) { + browser.addListener(new Player.Listener() { + @Override + public void onEvents(@NonNull Player player, @NonNull Player.Events events) { + if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION) + || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED) + || events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) { + + String mediaId = player.getCurrentMediaItem() != null + ? player.getCurrentMediaItem().mediaId + : null; + + boolean playing = player.getPlaybackState() == Player.STATE_READY + && player.getPlayWhenReady(); + + playbackViewModel.update(mediaId, playing); + } + } + }); + + String mediaId = browser.getCurrentMediaItem() != null + ? browser.getCurrentMediaItem().mediaId + : null; + boolean playing = browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady(); + playbackViewModel.update(mediaId, playing); + + attachedBrowserRef = new WeakReference<>(browser); + } else { + String mediaId = browser.getCurrentMediaItem() != null + ? browser.getCurrentMediaItem().mediaId + : null; + boolean playing = browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady(); + playbackViewModel.update(mediaId, playing); + } + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.e(TAG, "Failed to get MediaBrowser instance", t); + } + }, MoreExecutors.directExecutor()); + } + + public static void onBrowserReleased(@Nullable MediaBrowser released) { + MediaBrowser attached = attachedBrowserRef.get(); + if (attached == released) { + attachedBrowserRef.clear(); + } + } + + public static void reset(ListenableFuture mediaBrowserListenableFuture) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + if (mediaBrowserListenableFuture.get().isPlaying()) { + mediaBrowserListenableFuture.get().pause(); + } + + mediaBrowserListenableFuture.get().stop(); + mediaBrowserListenableFuture.get().clearMediaItems(); + clearDatabase(); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void hide(ListenableFuture mediaBrowserListenableFuture) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + if (mediaBrowserListenableFuture.get().isPlaying()) { + mediaBrowserListenableFuture.get().pause(); + } + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void check(ListenableFuture mediaBrowserListenableFuture) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + if (mediaBrowserListenableFuture.get().getMediaItemCount() < 1) { + List media = getQueueRepository().getMedia(); + if (media != null && media.size() >= 1) { + init(mediaBrowserListenableFuture, media); + } + } + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void init(ListenableFuture mediaBrowserListenableFuture, List media) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + mediaBrowserListenableFuture.get().clearMediaItems(); + mediaBrowserListenableFuture.get().setMediaItems(MappingUtil.mapMediaItems(media)); + mediaBrowserListenableFuture.get().seekTo(getQueueRepository().getLastPlayedMediaIndex(), getQueueRepository().getLastPlayedMediaTimestamp()); + mediaBrowserListenableFuture.get().prepare(); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void startQueue(ListenableFuture mediaBrowserListenableFuture, List media, int startIndex) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser browser = mediaBrowserListenableFuture.get(); + browser.clearMediaItems(); + browser.setMediaItems(MappingUtil.mapMediaItems(media)); + browser.prepare(); + + Player.Listener timelineListener = new Player.Listener() { + @Override + public void onTimelineChanged(Timeline timeline, int reason) { + int itemCount = browser.getMediaItemCount(); + if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) { + browser.seekTo(startIndex, 0); + browser.play(); + browser.removeListener(this); + } + } + }; + browser.addListener(timelineListener); + + enqueueDatabase(media, true, 0); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void startQueue(ListenableFuture mediaBrowserListenableFuture, Child media) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + mediaBrowserListenableFuture.get().clearMediaItems(); + mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(media)); + mediaBrowserListenableFuture.get().prepare(); + mediaBrowserListenableFuture.get().play(); + enqueueDatabase(media, true, 0); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void playDownloadedMediaItem(ListenableFuture mediaBrowserListenableFuture, MediaItem mediaItem) { + if (mediaBrowserListenableFuture != null && mediaItem != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); + mediaBrowser.clearMediaItems(); + mediaBrowser.setMediaItem(mediaItem); + mediaBrowser.prepare(); + mediaBrowser.play(); + clearDatabase(); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void startRadio(ListenableFuture mediaBrowserListenableFuture, InternetRadioStation internetRadioStation) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + mediaBrowserListenableFuture.get().clearMediaItems(); + mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation)); + mediaBrowserListenableFuture.get().prepare(); + mediaBrowserListenableFuture.get().play(); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void startPodcast(ListenableFuture mediaBrowserListenableFuture, PodcastEpisode podcastEpisode) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + mediaBrowserListenableFuture.get().clearMediaItems(); + mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(podcastEpisode)); + mediaBrowserListenableFuture.get().prepare(); + mediaBrowserListenableFuture.get().play(); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void enqueue(ListenableFuture mediaBrowserListenableFuture, List media, boolean playImmediatelyAfter) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) { + enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex()); + mediaBrowserListenableFuture.get().addMediaItems(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItems(media)); + } else { + enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount()); + mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media)); + } + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void enqueue(ListenableFuture mediaBrowserListenableFuture, Child media, boolean playImmediatelyAfter) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) { + enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex()); + mediaBrowserListenableFuture.get().addMediaItem(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItem(media)); + } else { + enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount()); + mediaBrowserListenableFuture.get().addMediaItem(MappingUtil.mapMediaItem(media)); + } + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void shuffle(ListenableFuture mediaBrowserListenableFuture, List media, int startIndex, int endIndex) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + mediaBrowserListenableFuture.get().removeMediaItems(startIndex, endIndex + 1); + mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media).subList(startIndex, endIndex + 1)); + swapDatabase(media); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void swap(ListenableFuture mediaBrowserListenableFuture, List media, int from, int to) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + mediaBrowserListenableFuture.get().moveMediaItem(from, to); + swapDatabase(media); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void remove(ListenableFuture mediaBrowserListenableFuture, List media, int toRemove) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + if (mediaBrowserListenableFuture.get().getMediaItemCount() > 1 && mediaBrowserListenableFuture.get().getCurrentMediaItemIndex() != toRemove) { + mediaBrowserListenableFuture.get().removeMediaItem(toRemove); + removeDatabase(media, toRemove); + } else { + removeDatabase(media, -1); + } + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void removeRange(ListenableFuture mediaBrowserListenableFuture, List media, int fromItem, int toItem) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + mediaBrowserListenableFuture.get().removeMediaItems(fromItem, toItem); + removeRangeDatabase(media, fromItem, toItem); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void getCurrentIndex(ListenableFuture mediaBrowserListenableFuture, MediaIndexCallback callback) { + if (mediaBrowserListenableFuture != null) { + mediaBrowserListenableFuture.addListener(() -> { + try { + if (mediaBrowserListenableFuture.isDone()) { + callback.onRecovery(mediaBrowserListenableFuture.get().getCurrentMediaItemIndex()); + } + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + } + + public static void setLastPlayedTimestamp(MediaItem mediaItem) { + if (mediaItem != null) getQueueRepository().setLastPlayedTimestamp(mediaItem.mediaId); + } + + public static void setPlayingPausedTimestamp(MediaItem mediaItem, long ms) { + if (mediaItem != null) + getQueueRepository().setPlayingPausedTimestamp(mediaItem.mediaId, ms); + } + + public static void scrobble(MediaItem mediaItem, boolean submission) { + if (mediaItem != null && Preferences.isScrobblingEnabled()) { + getSongRepository().scrobble(mediaItem.mediaMetadata.extras.getString("id"), submission); + } + } + + @OptIn(markerClass = UnstableApi.class) + public static void continuousPlay(MediaItem mediaItem) { + if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) { + Preferences.setLastInstantMix(); + + LiveData> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10); + instantMix.observeForever(new Observer>() { + @Override + public void onChanged(List media) { + if (media != null) { + ListenableFuture mediaBrowserListenableFuture = new MediaBrowser.Builder( + App.getContext(), + new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class)) + ).buildAsync(); + + enqueue(mediaBrowserListenableFuture, media, true); + } + + instantMix.removeObserver(this); + } + }); + } + } + + public static void saveChronology(MediaItem mediaItem) { + if (mediaItem != null) { + getChronologyRepository().insert(new Chronology(mediaItem)); + } + } + + private static QueueRepository getQueueRepository() { + return new QueueRepository(); + } + + private static SongRepository getSongRepository() { + return new SongRepository(); + } + + private static ChronologyRepository getChronologyRepository() { + return new ChronologyRepository(); + } + + private static void enqueueDatabase(List media, boolean reset, int afterIndex) { + getQueueRepository().insertAll(media, reset, afterIndex); + } + + private static void enqueueDatabase(Child media, boolean reset, int afterIndex) { + getQueueRepository().insert(media, reset, afterIndex); + } + + private static void swapDatabase(List media) { + getQueueRepository().insertAll(media, true, 0); + } + + private static void removeDatabase(List media, int toRemove) { + if (toRemove != -1) { + media.remove(toRemove); + getQueueRepository().insertAll(media, true, 0); + } + } + + private static void removeRangeDatabase(List media, int fromItem, int toItem) { + List toRemove = media.subList(fromItem, toItem); + + media.removeAll(toRemove); + + getQueueRepository().insertAll(media, true, 0); + } + + public static void clearDatabase() { + getQueueRepository().deleteAll(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt new file mode 100644 index 0000000..3f9868b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt @@ -0,0 +1,66 @@ +package com.cappielloantonio.tempo.subsonic + +import com.cappielloantonio.tempo.App +import com.cappielloantonio.tempo.subsonic.utils.CacheUtil +import com.cappielloantonio.tempo.subsonic.utils.EmptyDateTypeAdapter +import com.google.gson.GsonBuilder +import okhttp3.Cache +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.Date +import java.util.concurrent.TimeUnit + +class RetrofitClient(subsonic: Subsonic) { + var retrofit: Retrofit + + init { + val gson = GsonBuilder() + .registerTypeAdapter(Date::class.java, EmptyDateTypeAdapter()) + .setLenient() + .create() + + retrofit = Retrofit.Builder() + .baseUrl(subsonic.url) + .addConverterFactory(GsonConverterFactory.create(gson)) + .client(getOkHttpClient()) + .build() + } + + private fun getOkHttpClient(): OkHttpClient { + val cacheUtil = CacheUtil(60, 60 * 60 * 24 * 30) + + // BrowsingClient 60 + // MediaAnnotationClient 0 + // MediaLibraryScanningClient 0 + // MediaRetrievalClient 0 + // PlaylistClient 0 + // PodcastClient 60 + // SearchClient 60 + // SystemClient 60 + // AlbumSongListClient 60 + + return OkHttpClient.Builder() + .callTimeout(2, TimeUnit.MINUTES) + .connectTimeout(20, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .addInterceptor(getHttpLoggingInterceptor()) + .addInterceptor(cacheUtil.offlineInterceptor) + // .addNetworkInterceptor(cacheUtil.onlineInterceptor) + .cache(getCache()) + .build() + } + + private fun getHttpLoggingInterceptor(): HttpLoggingInterceptor { + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY) + return loggingInterceptor + } + + private fun getCache(): Cache { + val cacheSize = 10 * 1024 * 1024 + return Cache(App.getContext().cacheDir, cacheSize.toLong()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java new file mode 100644 index 0000000..de4b36b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/Subsonic.java @@ -0,0 +1,162 @@ +package com.cappielloantonio.tempo.subsonic; + +import com.cappielloantonio.tempo.subsonic.api.albumsonglist.AlbumSongListClient; +import com.cappielloantonio.tempo.subsonic.api.bookmarks.BookmarksClient; +import com.cappielloantonio.tempo.subsonic.api.browsing.BrowsingClient; +import com.cappielloantonio.tempo.subsonic.api.internetradio.InternetRadioClient; +import com.cappielloantonio.tempo.subsonic.api.mediaannotation.MediaAnnotationClient; +import com.cappielloantonio.tempo.subsonic.api.medialibraryscanning.MediaLibraryScanningClient; +import com.cappielloantonio.tempo.subsonic.api.mediaretrieval.MediaRetrievalClient; +import com.cappielloantonio.tempo.subsonic.api.open.OpenClient; +import com.cappielloantonio.tempo.subsonic.api.playlist.PlaylistClient; +import com.cappielloantonio.tempo.subsonic.api.podcast.PodcastClient; +import com.cappielloantonio.tempo.subsonic.api.searching.SearchingClient; +import com.cappielloantonio.tempo.subsonic.api.sharing.SharingClient; +import com.cappielloantonio.tempo.subsonic.api.system.SystemClient; +import com.cappielloantonio.tempo.subsonic.base.Version; + +import java.util.HashMap; +import java.util.Map; + +public class Subsonic { + private static final Version API_MAX_VERSION = Version.of("1.15.0"); + + private final Version apiVersion = API_MAX_VERSION; + private final SubsonicPreferences preferences; + + private SystemClient systemClient; + private BrowsingClient browsingClient; + private MediaRetrievalClient mediaRetrievalClient; + private PlaylistClient playlistClient; + private SearchingClient searchingClient; + private AlbumSongListClient albumSongListClient; + private MediaAnnotationClient mediaAnnotationClient; + private PodcastClient podcastClient; + private MediaLibraryScanningClient mediaLibraryScanningClient; + private BookmarksClient bookmarksClient; + private InternetRadioClient internetRadioClient; + private SharingClient sharingClient; + private OpenClient openClient; + + public Subsonic(SubsonicPreferences preferences) { + this.preferences = preferences; + } + + public Version getApiVersion() { + return apiVersion; + } + + public SystemClient getSystemClient() { + if (systemClient == null) { + systemClient = new SystemClient(this); + } + return systemClient; + } + + public BrowsingClient getBrowsingClient() { + if (browsingClient == null) { + browsingClient = new BrowsingClient(this); + } + return browsingClient; + } + + public MediaRetrievalClient getMediaRetrievalClient() { + if (mediaRetrievalClient == null) { + mediaRetrievalClient = new MediaRetrievalClient(this); + } + return mediaRetrievalClient; + } + + public PlaylistClient getPlaylistClient() { + if (playlistClient == null) { + playlistClient = new PlaylistClient(this); + } + return playlistClient; + } + + public SearchingClient getSearchingClient() { + if (searchingClient == null) { + searchingClient = new SearchingClient(this); + } + return searchingClient; + } + + public AlbumSongListClient getAlbumSongListClient() { + if (albumSongListClient == null) { + albumSongListClient = new AlbumSongListClient(this); + } + return albumSongListClient; + } + + public MediaAnnotationClient getMediaAnnotationClient() { + if (mediaAnnotationClient == null) { + mediaAnnotationClient = new MediaAnnotationClient(this); + } + return mediaAnnotationClient; + } + + public PodcastClient getPodcastClient() { + if (podcastClient == null) { + podcastClient = new PodcastClient(this); + } + return podcastClient; + } + + public MediaLibraryScanningClient getMediaLibraryScanningClient() { + if (mediaLibraryScanningClient == null) { + mediaLibraryScanningClient = new MediaLibraryScanningClient(this); + } + return mediaLibraryScanningClient; + } + + public BookmarksClient getBookmarksClient() { + if (bookmarksClient == null) { + bookmarksClient = new BookmarksClient(this); + } + return bookmarksClient; + } + + public InternetRadioClient getInternetRadioClient() { + if (internetRadioClient == null) { + internetRadioClient = new InternetRadioClient(this); + } + return internetRadioClient; + } + + public SharingClient getSharingClient() { + if (sharingClient == null) { + sharingClient = new SharingClient(this); + } + return sharingClient; + } + + public OpenClient getOpenClient() { + if (openClient == null) { + openClient = new OpenClient(this); + } + return openClient; + } + + public String getUrl() { + String url = preferences.getServerUrl() + "/rest/"; + return url.replace("//rest", "/rest"); + } + + public Map getParams() { + Map params = new HashMap<>(); + params.put("u", preferences.getUsername()); + + if (preferences.getAuthentication().getPassword() != null) + params.put("p", preferences.getAuthentication().getPassword()); + if (preferences.getAuthentication().getSalt() != null) + params.put("s", preferences.getAuthentication().getSalt()); + if (preferences.getAuthentication().getToken() != null) + params.put("t", preferences.getAuthentication().getToken()); + + params.put("v", getApiVersion().getVersionString()); + params.put("c", preferences.getClientName()); + params.put("f", "json"); + + return params; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/SubsonicPreferences.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/SubsonicPreferences.java new file mode 100644 index 0000000..6d300c3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/SubsonicPreferences.java @@ -0,0 +1,86 @@ +package com.cappielloantonio.tempo.subsonic; + +import com.cappielloantonio.tempo.subsonic.utils.StringUtil; + +import java.util.UUID; + +public class SubsonicPreferences { + private String serverUrl; + private String username; + private String clientName = "Tempus"; + private SubsonicAuthentication authentication; + + public String getServerUrl() { + return serverUrl; + } + + public String getUsername() { + return username; + } + + public String getClientName() { + return clientName; + } + + public SubsonicAuthentication getAuthentication() { + return authentication; + } + + public void setServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } + + public void setAuthentication(String password, String token, String salt, boolean isLowSecurity) { + if (password != null) { + this.authentication = new SubsonicAuthentication(password, isLowSecurity); + } + + if (token != null && salt != null) { + this.authentication = new SubsonicAuthentication(token, salt); + } + } + + public static class SubsonicAuthentication { + private String password; + private String salt; + private String token; + + public SubsonicAuthentication(String password, boolean isLowSecurity) { + if (isLowSecurity) { + this.password = password; + } else { + update(password); + } + } + + public SubsonicAuthentication(String token, String salt) { + this.token = token; + this.salt = salt; + } + + public String getPassword() { + return password; + } + + public String getSalt() { + return salt; + } + + public String getToken() { + return token; + } + + void update(String password) { + this.salt = UUID.randomUUID().toString(); + this.token = StringUtil.tokenize(password + salt); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/albumsonglist/AlbumSongListClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/albumsonglist/AlbumSongListClient.java new file mode 100644 index 0000000..0a799d4 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/albumsonglist/AlbumSongListClient.java @@ -0,0 +1,61 @@ +package com.cappielloantonio.tempo.subsonic.api.albumsonglist; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class AlbumSongListClient { + private static final String TAG = "BrowsingClient"; + + private final Subsonic subsonic; + private final AlbumSongListService albumSongListService; + + public AlbumSongListClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.albumSongListService = new RetrofitClient(subsonic).getRetrofit().create(AlbumSongListService.class); + } + + public Call getAlbumList(String type, int size, int offset) { + Log.d(TAG, "getAlbumList()"); + return albumSongListService.getAlbumList(subsonic.getParams(), type, size, offset); + } + + public Call getAlbumList2(String type, int size, int offset, Integer fromYear, Integer toYear) { + Log.d(TAG, "getAlbumList2()"); + return albumSongListService.getAlbumList2(subsonic.getParams(), type, size, offset, fromYear, toYear); + } + + public Call getRandomSongs(int size, Integer fromYear, Integer toYear) { + Log.d(TAG, "getRandomSongs()"); + return albumSongListService.getRandomSongs(subsonic.getParams(), size, fromYear, toYear); + } + + public Call getRandomSongs(int size, Integer fromYear, Integer toYear, String genre) { + Log.d(TAG, "getRandomSongs()"); + return albumSongListService.getRandomSongs(subsonic.getParams(), size, fromYear, toYear, genre); + } + + public Call getSongsByGenre(String genre, int count, int offset) { + Log.d(TAG, "getSongsByGenre()"); + return albumSongListService.getSongsByGenre(subsonic.getParams(), genre, count, offset); + } + + public Call getNowPlaying() { + Log.d(TAG, "getNowPlaying()"); + return albumSongListService.getNowPlaying(subsonic.getParams()); + } + + public Call getStarred() { + Log.d(TAG, "getStarred()"); + return albumSongListService.getStarred(subsonic.getParams()); + } + + public Call getStarred2() { + Log.d(TAG, "getStarred2()"); + return albumSongListService.getStarred2(subsonic.getParams()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/albumsonglist/AlbumSongListService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/albumsonglist/AlbumSongListService.java new file mode 100644 index 0000000..e0d8995 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/albumsonglist/AlbumSongListService.java @@ -0,0 +1,36 @@ +package com.cappielloantonio.tempo.subsonic.api.albumsonglist; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface AlbumSongListService { + @GET("getAlbumList") + Call getAlbumList(@QueryMap Map params, @Query("type") String type, @Query("size") int size, @Query("offset") int offset); + + @GET("getAlbumList2") + Call getAlbumList2(@QueryMap Map params, @Query("type") String type, @Query("size") int size, @Query("offset") int offset, @Query("fromYear") Integer fromYear, @Query("toYear") Integer toYear); + + @GET("getRandomSongs") + Call getRandomSongs(@QueryMap Map params, @Query("size") int size, @Query("fromYear") Integer fromYear, @Query("toYear") Integer toYear); + + @GET("getRandomSongs") + Call getRandomSongs(@QueryMap Map params, @Query("size") int size, @Query("fromYear") Integer fromYear, @Query("toYear") Integer toYear, @Query("genre") String genre); + + @GET("getSongsByGenre") + Call getSongsByGenre(@QueryMap Map params, @Query("genre") String genre, @Query("count") int count, @Query("offset") int offset); + + @GET("getNowPlaying") + Call getNowPlaying(@QueryMap Map params); + + @GET("getStarred") + Call getStarred(@QueryMap Map params); + + @GET("getStarred2") + Call getStarred2(@QueryMap Map params); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/bookmarks/BookmarksClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/bookmarks/BookmarksClient.java new file mode 100644 index 0000000..d04ab33 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/bookmarks/BookmarksClient.java @@ -0,0 +1,33 @@ +package com.cappielloantonio.tempo.subsonic.api.bookmarks; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.List; + +import retrofit2.Call; + +public class BookmarksClient { + private static final String TAG = "BookmarksClient"; + + private final Subsonic subsonic; + private final BookmarksService bookmarksService; + + public BookmarksClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.bookmarksService = new RetrofitClient(subsonic).getRetrofit().create(BookmarksService.class); + } + + public Call getPlayQueue() { + Log.d(TAG, "getPlayQueue()"); + return bookmarksService.getPlayQueue(subsonic.getParams()); + } + + public Call savePlayQueue(List ids, String current, long position) { + Log.d(TAG, "savePlayQueue()"); + return bookmarksService.savePlayQueue(subsonic.getParams(), ids, current, position); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/bookmarks/BookmarksService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/bookmarks/BookmarksService.java new file mode 100644 index 0000000..18b622e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/bookmarks/BookmarksService.java @@ -0,0 +1,19 @@ +package com.cappielloantonio.tempo.subsonic.api.bookmarks; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.List; +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface BookmarksService { + @GET("getPlayQueue") + Call getPlayQueue(@QueryMap Map params); + + @GET("savePlayQueue") + Call savePlayQueue(@QueryMap Map params, @Query("id") List ids, @Query("current") String current, @Query("position") long position); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/browsing/BrowsingClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/browsing/BrowsingClient.java new file mode 100644 index 0000000..7f95021 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/browsing/BrowsingClient.java @@ -0,0 +1,106 @@ +package com.cappielloantonio.tempo.subsonic.api.browsing; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class BrowsingClient { + private static final String TAG = "BrowsingClient"; + + private final Subsonic subsonic; + private final BrowsingService browsingService; + + public BrowsingClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.browsingService = new RetrofitClient(subsonic).getRetrofit().create(BrowsingService.class); + } + + public Call getMusicFolders() { + Log.d(TAG, "getMusicFolders()"); + return browsingService.getMusicFolders(subsonic.getParams()); + } + + public Call getIndexes(String musicFolderId, Long ifModifiedSince) { + Log.d(TAG, "getIndexes()"); + return browsingService.getIndexes(subsonic.getParams(), musicFolderId, ifModifiedSince); + } + + public Call getMusicDirectory(String id) { + Log.d(TAG, "getMusicDirectory()"); + return browsingService.getMusicDirectory(subsonic.getParams(), id); + } + + public Call getGenres() { + Log.d(TAG, "getGenres()"); + return browsingService.getGenres(subsonic.getParams()); + } + + public Call getArtists() { + Log.d(TAG, "getArtists()"); + return browsingService.getArtists(subsonic.getParams()); + } + + public Call getArtist(String id) { + Log.d(TAG, "getArtist()"); + return browsingService.getArtist(subsonic.getParams(), id); + } + + public Call getAlbum(String id) { + Log.d(TAG, "getAlbum()"); + return browsingService.getAlbum(subsonic.getParams(), id); + } + + public Call getSong(String id) { + Log.d(TAG, "getSong()"); + return browsingService.getSong(subsonic.getParams(), id); + } + + public Call getVideos() { + Log.d(TAG, "getVideos()"); + return browsingService.getVideos(subsonic.getParams()); + } + + public Call getVideoInfo(String id) { + Log.d(TAG, "getVideoInfo()"); + return browsingService.getVideoInfo(subsonic.getParams(), id); + } + + public Call getArtistInfo(String id) { + Log.d(TAG, "getArtistInfo()"); + return browsingService.getArtistInfo(subsonic.getParams(), id); + } + + public Call getArtistInfo2(String id) { + Log.d(TAG, "getArtistInfo2()"); + return browsingService.getArtistInfo2(subsonic.getParams(), id); + } + + public Call getAlbumInfo(String id) { + Log.d(TAG, "getAlbumInfo()"); + return browsingService.getAlbumInfo(subsonic.getParams(), id); + } + + public Call getAlbumInfo2(String id) { + Log.d(TAG, "getAlbumInfo2()"); + return browsingService.getAlbumInfo2(subsonic.getParams(), id); + } + + public Call getSimilarSongs(String id, int count) { + Log.d(TAG, "getSimilarSongs()"); + return browsingService.getSimilarSongs(subsonic.getParams(), id, count); + } + + public Call getSimilarSongs2(String id, int limit) { + Log.d(TAG, "getSimilarSongs2()"); + return browsingService.getSimilarSongs2(subsonic.getParams(), id, limit); + } + + public Call getTopSongs(String artist, int count) { + Log.d(TAG, "getTopSongs()"); + return browsingService.getTopSongs(subsonic.getParams(), artist, count); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/browsing/BrowsingService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/browsing/BrowsingService.java new file mode 100644 index 0000000..ecfd4df --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/browsing/BrowsingService.java @@ -0,0 +1,63 @@ +package com.cappielloantonio.tempo.subsonic.api.browsing; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface BrowsingService { + @GET("getMusicFolders") + Call getMusicFolders(@QueryMap Map params); + + @GET("getIndexes") + Call getIndexes(@QueryMap Map params, @Query("musicFolderId") String musicFolderId, @Query("ifModifiedSince") Long ifModifiedSince); + + @GET("getMusicDirectory") + Call getMusicDirectory(@QueryMap Map params, @Query("id") String id); + + @GET("getGenres") + Call getGenres(@QueryMap Map params); + + @GET("getArtists") + Call getArtists(@QueryMap Map params); + + @GET("getArtist") + Call getArtist(@QueryMap Map params, @Query("id") String id); + + @GET("getAlbum") + Call getAlbum(@QueryMap Map params, @Query("id") String id); + + @GET("getSong") + Call getSong(@QueryMap Map params, @Query("id") String id); + + @GET("getVideos") + Call getVideos(@QueryMap Map params); + + @GET("getVideoInfo") + Call getVideoInfo(@QueryMap Map params, @Query("id") String id); + + @GET("getArtistInfo") + Call getArtistInfo(@QueryMap Map params, @Query("id") String id); + + @GET("getArtistInfo2") + Call getArtistInfo2(@QueryMap Map params, @Query("id") String id); + + @GET("getAlbumInfo") + Call getAlbumInfo(@QueryMap Map params, @Query("id") String id); + + @GET("getAlbumInfo2") + Call getAlbumInfo2(@QueryMap Map params, @Query("id") String id); + + @GET("getSimilarSongs") + Call getSimilarSongs(@QueryMap Map params, @Query("id") String id, @Query("count") int count); + + @GET("getSimilarSongs2") + Call getSimilarSongs2(@QueryMap Map params, @Query("id") String id, @Query("count") int count); + + @GET("getTopSongs") + Call getTopSongs(@QueryMap Map params, @Query("artist") String artist, @Query("count") int count); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/internetradio/InternetRadioClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/internetradio/InternetRadioClient.java new file mode 100644 index 0000000..b83a656 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/internetradio/InternetRadioClient.java @@ -0,0 +1,41 @@ +package com.cappielloantonio.tempo.subsonic.api.internetradio; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class InternetRadioClient { + private static final String TAG = "InternetRadioClient"; + + private final Subsonic subsonic; + private final InternetRadioService internetRadioService; + + public InternetRadioClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.internetRadioService = new RetrofitClient(subsonic).getRetrofit().create(InternetRadioService.class); + } + + public Call getInternetRadioStations() { + Log.d(TAG, "getInternetRadioStations()"); + return internetRadioService.getInternetRadioStations(subsonic.getParams()); + } + + public Call createInternetRadioStation(String streamUrl, String name, String homepageUrl) { + Log.d(TAG, "createInternetRadioStation()"); + return internetRadioService.createInternetRadioStation(subsonic.getParams(), streamUrl, name, homepageUrl); + } + + public Call updateInternetRadioStation(String id, String streamUrl, String name, String homepageUrl) { + Log.d(TAG, "updateInternetRadioStation()"); + return internetRadioService.updateInternetRadioStation(subsonic.getParams(), id, streamUrl, name, homepageUrl); + } + + public Call deleteInternetRadioStation(String id) { + Log.d(TAG, "deleteInternetRadioStation()"); + return internetRadioService.deleteInternetRadioStation(subsonic.getParams(), id); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/internetradio/InternetRadioService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/internetradio/InternetRadioService.java new file mode 100644 index 0000000..f244dc2 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/internetradio/InternetRadioService.java @@ -0,0 +1,24 @@ +package com.cappielloantonio.tempo.subsonic.api.internetradio; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface InternetRadioService { + @GET("getInternetRadioStations") + Call getInternetRadioStations(@QueryMap Map params); + + @GET("createInternetRadioStation") + Call createInternetRadioStation(@QueryMap Map params, @Query("streamUrl") String streamUrl, @Query("name") String name, @Query("homepageUrl") String homepageUrl); + + @GET("updateInternetRadioStation") + Call updateInternetRadioStation(@QueryMap Map params, @Query("id") String id, @Query("streamUrl") String streamUrl, @Query("name") String name, @Query("homepageUrl") String homepageUrl); + + @GET("deleteInternetRadioStation") + Call deleteInternetRadioStation(@QueryMap Map params, @Query("id") String id); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationClient.java new file mode 100644 index 0000000..c229aa8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationClient.java @@ -0,0 +1,41 @@ +package com.cappielloantonio.tempo.subsonic.api.mediaannotation; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class MediaAnnotationClient { + private static final String TAG = "MediaAnnotationClient"; + + private final Subsonic subsonic; + private final MediaAnnotationService mediaAnnotationService; + + public MediaAnnotationClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.mediaAnnotationService = new RetrofitClient(subsonic).getRetrofit().create(MediaAnnotationService.class); + } + + public Call star(String id, String albumId, String artistId) { + Log.d(TAG, "star()"); + return mediaAnnotationService.star(subsonic.getParams(), id, albumId, artistId); + } + + public Call unstar(String id, String albumId, String artistId) { + Log.d(TAG, "unstar()"); + return mediaAnnotationService.unstar(subsonic.getParams(), id, albumId, artistId); + } + + public Call setRating(String id, int rating) { + Log.d(TAG, "setRating()"); + return mediaAnnotationService.setRating(subsonic.getParams(), id, rating); + } + + public Call scrobble(String id, boolean submission) { + Log.d(TAG, "scrobble()"); + return mediaAnnotationService.scrobble(subsonic.getParams(), id, submission); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationService.java new file mode 100644 index 0000000..65c20e9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaannotation/MediaAnnotationService.java @@ -0,0 +1,24 @@ +package com.cappielloantonio.tempo.subsonic.api.mediaannotation; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface MediaAnnotationService { + @GET("star") + Call star(@QueryMap Map params, @Query("id") String id, @Query("albumId") String albumId, @Query("artistId") String artistId); + + @GET("unstar") + Call unstar(@QueryMap Map params, @Query("id") String id, @Query("albumId") String albumId, @Query("artistId") String artistId); + + @GET("setRating") + Call setRating(@QueryMap Map params, @Query("id") String id, @Query("rating") int rating); + + @GET("scrobble") + Call scrobble(@QueryMap Map params, @Query("id") String id, @Query("submission") Boolean submission); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/medialibraryscanning/MediaLibraryScanningClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/medialibraryscanning/MediaLibraryScanningClient.java new file mode 100644 index 0000000..f448515 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/medialibraryscanning/MediaLibraryScanningClient.java @@ -0,0 +1,31 @@ +package com.cappielloantonio.tempo.subsonic.api.medialibraryscanning; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class MediaLibraryScanningClient { + private static final String TAG = "MediaLibraryScanningClient"; + + private final Subsonic subsonic; + private final MediaLibraryScanningService mediaLibraryScanningService; + + public MediaLibraryScanningClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.mediaLibraryScanningService = new RetrofitClient(subsonic).getRetrofit().create(MediaLibraryScanningService.class); + } + + public Call startScan() { + Log.d(TAG, "startScan()"); + return mediaLibraryScanningService.startScan(subsonic.getParams()); + } + + public Call getScanStatus() { + Log.d(TAG, "getScanStatus()"); + return mediaLibraryScanningService.getScanStatus(subsonic.getParams()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/medialibraryscanning/MediaLibraryScanningService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/medialibraryscanning/MediaLibraryScanningService.java new file mode 100644 index 0000000..9d9403f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/medialibraryscanning/MediaLibraryScanningService.java @@ -0,0 +1,17 @@ +package com.cappielloantonio.tempo.subsonic.api.medialibraryscanning; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.QueryMap; + +public interface MediaLibraryScanningService { + @GET("startScan") + Call startScan(@QueryMap Map params); + + @GET("getScanStatus") + Call getScanStatus(@QueryMap Map params); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaretrieval/MediaRetrievalClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaretrieval/MediaRetrievalClient.java new file mode 100644 index 0000000..742a77d --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaretrieval/MediaRetrievalClient.java @@ -0,0 +1,36 @@ +package com.cappielloantonio.tempo.subsonic.api.mediaretrieval; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class MediaRetrievalClient { + private static final String TAG = "MediaRetrievalClient"; + + private final Subsonic subsonic; + private final MediaRetrievalService mediaRetrievalService; + + public MediaRetrievalClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.mediaRetrievalService = new RetrofitClient(subsonic).getRetrofit().create(MediaRetrievalService.class); + } + + public Call stream(String id, Integer maxBitRate, String format) { + Log.d(TAG, "stream()"); + return mediaRetrievalService.stream(subsonic.getParams(), id, maxBitRate, format); + } + + public Call download(String id) { + Log.d(TAG, "download()"); + return mediaRetrievalService.download(subsonic.getParams(), id); + } + + public Call getLyrics(String artist, String title) { + Log.d(TAG, "getLyrics()"); + return mediaRetrievalService.getLyrics(subsonic.getParams(), artist, title); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaretrieval/MediaRetrievalService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaretrieval/MediaRetrievalService.java new file mode 100644 index 0000000..04e7a89 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/mediaretrieval/MediaRetrievalService.java @@ -0,0 +1,21 @@ +package com.cappielloantonio.tempo.subsonic.api.mediaretrieval; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface MediaRetrievalService { + @GET("stream") + Call stream(@QueryMap Map params, @Query("id") String id, @Query("maxBitRate") Integer maxBitRate, @Query("format") String format); + + @GET("download") + Call download(@QueryMap Map params, @Query("id") String id); + + @GET("getLyrics") + Call getLyrics(@QueryMap Map params, @Query("artist") String artist, @Query("title") String title); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenClient.java new file mode 100644 index 0000000..b37b03b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenClient.java @@ -0,0 +1,26 @@ +package com.cappielloantonio.tempo.subsonic.api.open; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class OpenClient { + private static final String TAG = "OpenClient"; + + private final Subsonic subsonic; + private final OpenService openService; + + public OpenClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.openService = new RetrofitClient(subsonic).getRetrofit().create(OpenService.class); + } + + public Call getLyricsBySongId(String id) { + Log.d(TAG, "getLyricsBySongId()"); + return openService.getLyricsBySongId(subsonic.getParams(), id); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenService.java new file mode 100644 index 0000000..3122a97 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/open/OpenService.java @@ -0,0 +1,15 @@ +package com.cappielloantonio.tempo.subsonic.api.open; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface OpenService { + @GET("getLyricsBySongId") + Call getLyricsBySongId(@QueryMap Map params, @Query("id") String id); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/playlist/PlaylistClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/playlist/PlaylistClient.java new file mode 100644 index 0000000..98d319e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/playlist/PlaylistClient.java @@ -0,0 +1,48 @@ +package com.cappielloantonio.tempo.subsonic.api.playlist; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.ArrayList; + +import retrofit2.Call; + +public class PlaylistClient { + private static final String TAG = "BrowsingClient"; + + private final Subsonic subsonic; + private final PlaylistService playlistService; + + public PlaylistClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.playlistService = new RetrofitClient(subsonic).getRetrofit().create(PlaylistService.class); + } + + public Call getPlaylists() { + Log.d(TAG, "getPlaylists()"); + return playlistService.getPlaylists(subsonic.getParams()); + } + + public Call getPlaylist(String id) { + Log.d(TAG, "getPlaylist()"); + return playlistService.getPlaylist(subsonic.getParams(), id); + } + + public Call createPlaylist(String playlistId, String name, ArrayList songsId) { + Log.d(TAG, "createPlaylist()"); + return playlistService.createPlaylist(subsonic.getParams(), playlistId, name, songsId); + } + + public Call updatePlaylist(String playlistId, String name, boolean isPublic, ArrayList songIdToAdd, ArrayList songIndexToRemove) { + Log.d(TAG, "updatePlaylist()"); + return playlistService.updatePlaylist(subsonic.getParams(), playlistId, name, isPublic, songIdToAdd, songIndexToRemove); + } + + public Call deletePlaylist(String id) { + Log.d(TAG, "deletePlaylist()"); + return playlistService.deletePlaylist(subsonic.getParams(), id); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/playlist/PlaylistService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/playlist/PlaylistService.java new file mode 100644 index 0000000..51f8d48 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/playlist/PlaylistService.java @@ -0,0 +1,28 @@ +package com.cappielloantonio.tempo.subsonic.api.playlist; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.ArrayList; +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface PlaylistService { + @GET("getPlaylists") + Call getPlaylists(@QueryMap Map params); + + @GET("getPlaylist") + Call getPlaylist(@QueryMap Map params, @Query("id") String id); + + @GET("createPlaylist") + Call createPlaylist(@QueryMap Map params, @Query("playlistId") String playlistId, @Query("name") String name, @Query("songId") ArrayList songsId); + + @GET("updatePlaylist") + Call updatePlaylist(@QueryMap Map params, @Query("playlistId") String playlistId, @Query("name") String name, @Query("public") boolean isPublic, @Query("songIdToAdd") ArrayList songIdToAdd, @Query("songIndexToRemove") ArrayList songIndexToRemove); + + @GET("deletePlaylist") + Call deletePlaylist(@QueryMap Map params, @Query("id") String id); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/podcast/PodcastClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/podcast/PodcastClient.java new file mode 100644 index 0000000..ca7fab8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/podcast/PodcastClient.java @@ -0,0 +1,56 @@ +package com.cappielloantonio.tempo.subsonic.api.podcast; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class PodcastClient { + private static final String TAG = "PodcastClient"; + + private final Subsonic subsonic; + private final PodcastService podcastService; + + public PodcastClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.podcastService = new RetrofitClient(subsonic).getRetrofit().create(PodcastService.class); + } + + public Call getPodcasts(boolean includeEpisodes, String channelId) { + Log.d(TAG, "getPodcasts()"); + return podcastService.getPodcasts(subsonic.getParams(), includeEpisodes, channelId); + } + + public Call getNewestPodcasts(int count) { + Log.d(TAG, "getNewestPodcasts()"); + return podcastService.getNewestPodcasts(subsonic.getParams(), count); + } + + public Call refreshPodcasts() { + Log.d(TAG, "refreshPodcasts()"); + return podcastService.refreshPodcasts(subsonic.getParams()); + } + + public Call createPodcastChannel(String url) { + Log.d(TAG, "createPodcastChannel()"); + return podcastService.createPodcastChannel(subsonic.getParams(), url); + } + + public Call deletePodcastChannel(String channelId) { + Log.d(TAG, "deletePodcastChannel()"); + return podcastService.deletePodcastChannel(subsonic.getParams(), channelId); + } + + public Call deletePodcastEpisode(String episodeId) { + Log.d(TAG, "deletePodcastEpisode()"); + return podcastService.deletePodcastEpisode(subsonic.getParams(), episodeId); + } + + public Call downloadPodcastEpisode(String episodeId) { + Log.d(TAG, "downloadPodcastEpisode()"); + return podcastService.downloadPodcastEpisode(subsonic.getParams(), episodeId); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/podcast/PodcastService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/podcast/PodcastService.java new file mode 100644 index 0000000..be819f8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/podcast/PodcastService.java @@ -0,0 +1,33 @@ +package com.cappielloantonio.tempo.subsonic.api.podcast; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface PodcastService { + @GET("getPodcasts") + Call getPodcasts(@QueryMap Map params, @Query("includeEpisodes") boolean includeEpisodes, @Query("id") String id); + + @GET("getNewestPodcasts") + Call getNewestPodcasts(@QueryMap Map params, @Query("count") int count); + + @GET("refreshPodcasts") + Call refreshPodcasts(@QueryMap Map params); + + @GET("createPodcastChannel") + Call createPodcastChannel(@QueryMap Map params, @Query("url") String url); + + @GET("deletePodcastChannel") + Call deletePodcastChannel(@QueryMap Map params, @Query("id") String id); + + @GET("deletePodcastEpisode") + Call deletePodcastEpisode(@QueryMap Map params, @Query("id") String id); + + @GET("downloadPodcastEpisode") + Call downloadPodcastEpisode(@QueryMap Map params, @Query("id") String id); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingClient.java new file mode 100644 index 0000000..f494104 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingClient.java @@ -0,0 +1,31 @@ +package com.cappielloantonio.tempo.subsonic.api.searching; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class SearchingClient { + private static final String TAG = "BrowsingClient"; + + private final Subsonic subsonic; + private final SearchingService searchingService; + + public SearchingClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.searchingService = new RetrofitClient(subsonic).getRetrofit().create(SearchingService.class); + } + + public Call search2(String query, int songCount, int albumCount, int artistCount) { + Log.d(TAG, "search2()"); + return searchingService.search2(subsonic.getParams(), query, songCount, albumCount, artistCount); + } + + public Call search3(String query, int songCount, int albumCount, int artistCount) { + Log.d(TAG, "search3()"); + return searchingService.search3(subsonic.getParams(), query, songCount, albumCount, artistCount); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingService.java new file mode 100644 index 0000000..d8dc149 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingService.java @@ -0,0 +1,18 @@ +package com.cappielloantonio.tempo.subsonic.api.searching; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface SearchingService { + @GET("search2") + Call search2(@QueryMap Map params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount); + + @GET("search3") + Call search3(@QueryMap Map params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/sharing/SharingClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/sharing/SharingClient.java new file mode 100644 index 0000000..a406036 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/sharing/SharingClient.java @@ -0,0 +1,41 @@ +package com.cappielloantonio.tempo.subsonic.api.sharing; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import retrofit2.Call; + +public class SharingClient { + private static final String TAG = "BrowsingClient"; + + private final Subsonic subsonic; + private final SharingService sharingService; + + public SharingClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.sharingService = new RetrofitClient(subsonic).getRetrofit().create(SharingService.class); + } + + public Call getShares() { + Log.d(TAG, "getShares()"); + return sharingService.getShares(subsonic.getParams()); + } + + public Call createShare(String id, String description, Long expires) { + Log.d(TAG, "createShare()"); + return sharingService.createShare(subsonic.getParams(), id, description, expires); + } + + public Call updateShare(String id, String description, Long expires) { + Log.d(TAG, "updateShare()"); + return sharingService.updateShare(subsonic.getParams(), id, description, expires); + } + + public Call deleteShare(String id) { + Log.d(TAG, "deleteShare()"); + return sharingService.deleteShare(subsonic.getParams(), id); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/sharing/SharingService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/sharing/SharingService.java new file mode 100644 index 0000000..0a18f33 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/sharing/SharingService.java @@ -0,0 +1,24 @@ +package com.cappielloantonio.tempo.subsonic.api.sharing; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; +import retrofit2.http.QueryMap; + +public interface SharingService { + @GET("getShares") + Call getShares(@QueryMap Map params); + + @GET("createShare") + Call createShare(@QueryMap Map params, @Query("id") String id, @Query("description") String description, @Query("expires") Long expires); + + @GET("updateShare") + Call updateShare(@QueryMap Map params, @Query("id") String id, @Query("description") String description, @Query("expires") Long expires); + + @GET("deleteShare") + Call deleteShare(@QueryMap Map params, @Query("id") String id); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemClient.java new file mode 100644 index 0000000..c5227da --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemClient.java @@ -0,0 +1,47 @@ +package com.cappielloantonio.tempo.subsonic.api.system; + +import android.util.Log; + +import com.cappielloantonio.tempo.subsonic.RetrofitClient; +import com.cappielloantonio.tempo.subsonic.Subsonic; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.util.Preferences; + +import java.util.concurrent.TimeUnit; + +import retrofit2.Call; + +public class SystemClient { + private static final String TAG = "SystemClient"; + + private final Subsonic subsonic; + private final SystemService systemService; + + public SystemClient(Subsonic subsonic) { + this.subsonic = subsonic; + this.systemService = new RetrofitClient(subsonic).getRetrofit().create(SystemService.class); + } + + public Call ping() { + Log.d(TAG, "ping()"); + Call pingCall = systemService.ping(subsonic.getParams()); + if (Preferences.isInUseServerAddressLocal()) { + pingCall.timeout() + .timeout(1, TimeUnit.SECONDS); + } else { + pingCall.timeout() + .timeout(3, TimeUnit.SECONDS); + } + return pingCall; + } + + public Call getLicense() { + Log.d(TAG, "getLicense()"); + return systemService.getLicense(subsonic.getParams()); + } + + public Call getOpenSubsonicExtensions() { + Log.d(TAG, "getOpenSubsonicExtensions()"); + return systemService.getOpenSubsonicExtensions(subsonic.getParams()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemService.java new file mode 100644 index 0000000..bc0f4c9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/system/SystemService.java @@ -0,0 +1,20 @@ +package com.cappielloantonio.tempo.subsonic.api.system; + +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.util.Map; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.QueryMap; + +public interface SystemService { + @GET("ping") + Call ping(@QueryMap Map params); + + @GET("getLicense") + Call getLicense(@QueryMap Map params); + + @GET("getOpenSubsonicExtensions") + Call getOpenSubsonicExtensions(@QueryMap Map params); +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/base/ApiResponse.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/base/ApiResponse.kt new file mode 100644 index 0000000..7874520 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/base/ApiResponse.kt @@ -0,0 +1,11 @@ +package com.cappielloantonio.tempo.subsonic.base + +import androidx.annotation.Keep +import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse +import com.google.gson.annotations.SerializedName + +@Keep +class ApiResponse { + @SerializedName("subsonic-response") + lateinit var subsonicResponse: SubsonicResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/base/Version.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/base/Version.java new file mode 100644 index 0000000..04659cd --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/base/Version.java @@ -0,0 +1,59 @@ +package com.cappielloantonio.tempo.subsonic.base; + +import androidx.annotation.NonNull; + +public class Version implements Comparable { + + private static final String VERSION_PATTERN = "\\d+(\\.\\d+)*"; + private final String versionString; + + public static Version of(String versionString) { + return new Version(versionString); + } + + private Version(String versionString) { + if (versionString == null || !versionString.matches(VERSION_PATTERN)) { + throw new IllegalArgumentException("Invalid version format"); + } + this.versionString = versionString; + } + + public String getVersionString() { + return versionString; + } + + public boolean isLowerThan(Version version) { + return compareTo(version) < 0; + } + + @Override + public int compareTo(Version that) { + if (that == null) { + return 1; + } + + String[] thisParts = this.getVersionString().split("\\."); + String[] thatParts = that.getVersionString().split("\\."); + + int length = Math.max(thisParts.length, thatParts.length); + + for (int i = 0; i < length; i++) { + int thisPart = i < thisParts.length ? Integer.parseInt(thisParts[i]) : 0; + int thatPart = i < thatParts.length ? Integer.parseInt(thatParts[i]) : 0; + + if (thisPart < thatPart) { + return -1; + } + if (thisPart > thatPart) { + return 1; + } + } + return 0; + } + + @NonNull + @Override + public String toString() { + return versionString; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumID3.kt new file mode 100644 index 0000000..95c67da --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumID3.kt @@ -0,0 +1,39 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize +import java.util.Date + +@Keep +@Parcelize +open class AlbumID3( + var id: String? = null, + var name: String? = null, + var artist: String? = null, + var artistId: String? = null, + @SerializedName("coverArt") + var coverArtId: String? = null, + var songCount: Int? = 0, + var duration: Int? = 0, + var playCount: Long? = 0, + var created: Date? = null, + var starred: Date? = null, + var year: Int = 0, + var genre: String? = null, + var played: Date? = Date(0), + var userRating: Int? = 0, + var recordLabels: List? = null, + var musicBrainzId: String? = null, + var genres: List? = null, + var artists: List? = null, + var displayArtist: String? = null, + var releaseTypes: List? = null, + var moods: List? = null, + var sortName: String? = null, + var originalReleaseDate: ItemDate? = null, + var releaseDate: ItemDate? = null, + var isCompilation: Boolean? = null, + var discTitles: List? = null, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumInfo.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumInfo.kt new file mode 100644 index 0000000..79c32cd --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumInfo.kt @@ -0,0 +1,13 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class AlbumInfo { + var notes: String? = null + var musicBrainzId: String? = null + var lastFmUrl: String? = null + var smallImageUrl: String? = null + var mediumImageUrl: String? = null + var largeImageUrl: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumList.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumList.kt new file mode 100644 index 0000000..f167f0e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumList.kt @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class AlbumList { + var albums: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumList2.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumList2.kt new file mode 100644 index 0000000..a4d68ed --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumList2.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class AlbumList2 { + @SerializedName("album") + var albums: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumWithSongsID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumWithSongsID3.kt new file mode 100644 index 0000000..8498e77 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumWithSongsID3.kt @@ -0,0 +1,13 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +class AlbumWithSongsID3( + @SerializedName("song") + var songs: List? = null, +) : AlbumID3(), Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Artist.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Artist.kt new file mode 100644 index 0000000..22aa527 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Artist.kt @@ -0,0 +1,16 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.parcelize.Parcelize +import java.util.Date + +@Keep +@Parcelize +class Artist( + var id: String? = null, + var name: String? = null, + var starred: Date? = null, + var userRating: Int? = null, + var averageRating: Double? = null, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistID3.kt new file mode 100644 index 0000000..ccf4ee7 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistID3.kt @@ -0,0 +1,18 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize +import java.util.Date + +@Keep +@Parcelize +open class ArtistID3( + var id: String? = null, + var name: String? = null, + @SerializedName("coverArt") + var coverArtId: String? = null, + var albumCount: Int = 0, + var starred: Date? = null, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistInfo.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistInfo.kt new file mode 100644 index 0000000..21f7a9f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistInfo.kt @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class ArtistInfo : ArtistInfoBase() { + var similarArtists: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistInfo2.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistInfo2.kt new file mode 100644 index 0000000..3aa2da5 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistInfo2.kt @@ -0,0 +1,11 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import java.util.* + +@Keep +class ArtistInfo2 : ArtistInfoBase() { + @SerializedName("similarArtist") + var similarArtists: List? = emptyList() +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistInfoBase.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistInfoBase.kt new file mode 100644 index 0000000..46b8f89 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistInfoBase.kt @@ -0,0 +1,13 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +open class ArtistInfoBase { + var biography: String? = null + var musicBrainzId: String? = null + var lastFmUrl: String? = null + var smallImageUrl: String? = null + var mediumImageUrl: String? = null + var largeImageUrl: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt new file mode 100644 index 0000000..2e21e11 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt @@ -0,0 +1,13 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +class ArtistWithAlbumsID3( + @SerializedName("album") + var albums: List? = null, +) : ArtistID3(), Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistsID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistsID3.kt new file mode 100644 index 0000000..341ab28 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistsID3.kt @@ -0,0 +1,11 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class ArtistsID3 { + @SerializedName("index") + var indices: List? = null + var ignoredArticles: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AudioTrack.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AudioTrack.kt new file mode 100644 index 0000000..536e3d4 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AudioTrack.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class AudioTrack { + var id: String? = null + var name: String? = null + var languageCode: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Bookmark.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Bookmark.kt new file mode 100644 index 0000000..cd9ef41 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Bookmark.kt @@ -0,0 +1,14 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import java.util.* + +@Keep +class Bookmark { + var entry: Child? = null + var position: Long = 0 + var username: String? = null + var comment: String? = null + var created: Date? = null + var changed: Date? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Bookmarks.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Bookmarks.kt new file mode 100644 index 0000000..41c6874 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Bookmarks.kt @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class Bookmarks { + var bookmarks: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Captions.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Captions.kt new file mode 100644 index 0000000..ef0ecba --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Captions.kt @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class Captions { + var id: String? = null + var name: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ChatMessage.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ChatMessage.kt new file mode 100644 index 0000000..873dde3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ChatMessage.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class ChatMessage { + var username: String? = null + var time: Long = 0 + var message: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ChatMessages.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ChatMessages.kt new file mode 100644 index 0000000..42002b1 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ChatMessages.kt @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class ChatMessages { + var chatMessages: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Child.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Child.kt new file mode 100644 index 0000000..15057cf --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Child.kt @@ -0,0 +1,88 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import androidx.room.ColumnInfo +import androidx.room.PrimaryKey +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize +import java.util.* + +@Keep +@Parcelize +open class Child( + @PrimaryKey + @ColumnInfo(name = "id") + open val id: String, + @ColumnInfo(name = "parent_id") + @SerializedName("parent") + var parentId: String? = null, + @ColumnInfo(name = "is_dir") + var isDir: Boolean = false, + @ColumnInfo + var title: String? = null, + @ColumnInfo + var album: String? = null, + @ColumnInfo + var artist: String? = null, + @ColumnInfo + var track: Int? = null, + @ColumnInfo + var year: Int? = null, + @ColumnInfo + @SerializedName("genre") + var genre: String? = null, + @ColumnInfo(name = "cover_art_id") + @SerializedName("coverArt") + var coverArtId: String? = null, + @ColumnInfo + var size: Long? = null, + @ColumnInfo(name = "content_type") + var contentType: String? = null, + @ColumnInfo + var suffix: String? = null, + @ColumnInfo("transcoding_content_type") + var transcodedContentType: String? = null, + @ColumnInfo(name = "transcoded_suffix") + var transcodedSuffix: String? = null, + @ColumnInfo + var duration: Int? = null, + @ColumnInfo("bitrate") + @SerializedName("bitRate") + var bitrate: Int? = null, + @ColumnInfo("sampling_rate") + @SerializedName("samplingRate") + var samplingRate: Int? = null, + @ColumnInfo("bit_depth") + @SerializedName("bitDepth") + var bitDepth: Int? = null, + @ColumnInfo + var path: String? = null, + @ColumnInfo(name = "is_video") + @SerializedName("isVideo") + var isVideo: Boolean = false, + @ColumnInfo(name = "user_rating") + var userRating: Int? = null, + @ColumnInfo(name = "average_rating") + var averageRating: Double? = null, + @ColumnInfo(name = "play_count") + var playCount: Long? = null, + @ColumnInfo(name = "disc_number") + var discNumber: Int? = null, + @ColumnInfo + var created: Date? = null, + @ColumnInfo + var starred: Date? = null, + @ColumnInfo(name = "album_id") + var albumId: String? = null, + @ColumnInfo(name = "artist_id") + var artistId: String? = null, + @ColumnInfo + var type: String? = null, + @ColumnInfo(name = "bookmark_position") + var bookmarkPosition: Long? = null, + @ColumnInfo(name = "original_width") + var originalWidth: Int? = null, + @ColumnInfo(name = "original_height") + var originalHeight: Int? = null +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Directory.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Directory.kt new file mode 100644 index 0000000..f189589 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Directory.kt @@ -0,0 +1,22 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize +import java.util.Date + +@Keep +@Parcelize +class Directory( + @SerializedName("child") + var children: List? = null, + var id: String? = null, + @SerializedName("parent") + var parentId: String? = null, + var name: String? = null, + var starred: Date? = null, + var userRating: Int? = null, + var averageRating: Double? = null, + var playCount: Long? = null, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/DiscTitle.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/DiscTitle.kt new file mode 100644 index 0000000..32caa8c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/DiscTitle.kt @@ -0,0 +1,12 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +open class DiscTitle( + var disc: Int? = null, + var title: String? = null, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Error.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Error.kt new file mode 100644 index 0000000..f50b8c4 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Error.kt @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class Error { + var code: Int? = null + var message: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ErrorCode.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ErrorCode.kt new file mode 100644 index 0000000..7dbacf3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ErrorCode.kt @@ -0,0 +1,18 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class ErrorCode(val value: Int) { + companion object { + var GENERIC_ERROR = 0 + var REQUIRED_PARAMETER_MISSING = 10 + var INCOMPATIBLE_VERSION_CLIENT = 20 + var INCOMPATIBLE_VERSION_SERVER = 30 + var WRONG_USERNAME_OR_PASSWORD = 40 + var TOKEN_AUTHENTICATION_NOT_SUPPORTED = 41 + var USER_NOT_AUTHORIZED = 50 + var TRIAL_PERIOD_OVER = 60 + var DATA_NOT_FOUND = 70 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Genre.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Genre.kt new file mode 100644 index 0000000..1db88c4 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Genre.kt @@ -0,0 +1,15 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +class Genre( + @SerializedName("value") + var genre: String? = null, + var songCount: Int = 0, + var albumCount: Int = 0, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Genres.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Genres.kt new file mode 100644 index 0000000..5a0ff7f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Genres.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class Genres { + @SerializedName("genre") + var genres: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Index.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Index.kt new file mode 100644 index 0000000..1a25b2d --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Index.kt @@ -0,0 +1,11 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class Index { + @SerializedName("artist") + var artists: List? = null + var name: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/IndexID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/IndexID3.kt new file mode 100644 index 0000000..9879191 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/IndexID3.kt @@ -0,0 +1,11 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class IndexID3 { + @SerializedName("artist") + var artists: List? = null + var name: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Indexes.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Indexes.kt new file mode 100644 index 0000000..fa3d8ef --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Indexes.kt @@ -0,0 +1,14 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class Indexes { + var shortcuts: List? = null + @SerializedName("index") + var indices: List? = null + var children: List? = null + var lastModified: Long = 0 + var ignoredArticles: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStation.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStation.kt new file mode 100644 index 0000000..07f00c5 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStation.kt @@ -0,0 +1,14 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +class InternetRadioStation( + var id: String? = null, + var name: String? = null, + var streamUrl: String? = null, + var homePageUrl: String? = null, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStations.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStations.kt new file mode 100644 index 0000000..16577f6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStations.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class InternetRadioStations { + @SerializedName("internetRadioStation") + var internetRadioStations: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemDate.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemDate.kt new file mode 100644 index 0000000..385b7fd --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemDate.kt @@ -0,0 +1,33 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.parcelize.Parcelize +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +@Keep +@Parcelize +open class ItemDate( + var year: Int? = null, + var month: Int? = null, + var day: Int? = null, +) : Parcelable { + fun getFormattedDate(): String? { + if (year == null && month == null && day == null) return null + + val calendar = Calendar.getInstance() + val dateFormat = if (month == null && day == null) { + SimpleDateFormat("yyyy", Locale.getDefault()) + } else if (day == null) { + SimpleDateFormat("MMMM yyyy", Locale.getDefault()) + } else { + SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault()) + } + + calendar.set(year ?: 0, month ?: 1, day ?: 1) + + return dateFormat.format(calendar.time) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemGenre.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemGenre.kt new file mode 100644 index 0000000..971809f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemGenre.kt @@ -0,0 +1,11 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +open class ItemGenre( + var name: String? = null, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/JukeboxPlaylist.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/JukeboxPlaylist.kt new file mode 100644 index 0000000..76efa3d --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/JukeboxPlaylist.kt @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class JukeboxPlaylist : JukeboxStatus() { + var entries: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/JukeboxStatus.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/JukeboxStatus.kt new file mode 100644 index 0000000..9efb4dc --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/JukeboxStatus.kt @@ -0,0 +1,11 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +open class JukeboxStatus { + var currentIndex = 0 + var isPlaying = false + var gain = 0f + var position: Int? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/License.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/License.kt new file mode 100644 index 0000000..4868ceb --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/License.kt @@ -0,0 +1,12 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import java.util.* + +@Keep +class License { + var isValid = false + var email: String? = null + var licenseExpires: Date? = null + var trialExpires: Date? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Line.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Line.kt new file mode 100644 index 0000000..1809091 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Line.kt @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class Line { + var start: Int? = null + lateinit var value: String +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Lyrics.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Lyrics.kt new file mode 100644 index 0000000..c01d4b9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Lyrics.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class Lyrics { + var value: String? = null + var artist: String? = null + var title: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/LyricsList.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/LyricsList.kt new file mode 100644 index 0000000..9c3b65e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/LyricsList.kt @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class LyricsList { + var structuredLyrics: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MediaType.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MediaType.kt new file mode 100644 index 0000000..0d950fb --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MediaType.kt @@ -0,0 +1,15 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class MediaType { + var value: String? = null + + companion object { + var MUSIC = "music" + var PODCAST = "podcast" + var AUDIOBOOK = "audiobook" + var VIDEO = "video" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MusicFolder.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MusicFolder.kt new file mode 100644 index 0000000..e31bf7c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MusicFolder.kt @@ -0,0 +1,12 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +class MusicFolder( + var id: String? = null, + var name: String? = null, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MusicFolders.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MusicFolders.kt new file mode 100644 index 0000000..3d2d123 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MusicFolders.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class MusicFolders { + @SerializedName("musicFolder") + var musicFolders: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NewestPodcasts.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NewestPodcasts.kt new file mode 100644 index 0000000..9850220 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NewestPodcasts.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class NewestPodcasts { + @SerializedName("episode") + var episodes: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NowPlaying.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NowPlaying.kt new file mode 100644 index 0000000..532566c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NowPlaying.kt @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class NowPlaying { + var entries: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NowPlayingEntry.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NowPlayingEntry.kt new file mode 100644 index 0000000..bd69808 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NowPlayingEntry.kt @@ -0,0 +1,16 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +class NowPlayingEntry( + @SerializedName("_id") + override val id: String, + var username: String? = null, + var minutesAgo: Int = 0, + var playerId: Int = 0, + var playerName: String? = null, +) : Child(id) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/OpenSubsonicExtension.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/OpenSubsonicExtension.kt new file mode 100644 index 0000000..dd170c8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/OpenSubsonicExtension.kt @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class OpenSubsonicExtension { + var name: String? = null + var versions: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PlayQueue.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PlayQueue.kt new file mode 100644 index 0000000..1143fa8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PlayQueue.kt @@ -0,0 +1,16 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import java.util.* + +@Keep +class PlayQueue { + @SerializedName("entry") + var entries: List? = null + var current: String? = null + var position: Long? = null + var username: String? = null + var changed: Date? = null + var changedBy: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlist.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlist.kt new file mode 100644 index 0000000..926c239 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlist.kt @@ -0,0 +1,72 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.PrimaryKey +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import java.util.Date + +@Keep +@Parcelize +@Entity(tableName = "playlist") +open class Playlist( + @PrimaryKey + @ColumnInfo(name = "id") + open var id: String, + @ColumnInfo(name = "name") + var name: String? = null, + @ColumnInfo(name = "duration") + var duration: Long = 0, + @ColumnInfo(name = "coverArt") + var coverArtId: String? = null, +) : Parcelable { + @Ignore + @IgnoredOnParcel + var comment: String? = null + @Ignore + @IgnoredOnParcel + var owner: String? = null + @Ignore + @IgnoredOnParcel + @SerializedName("public") + var isUniversal: Boolean? = null + @Ignore + @IgnoredOnParcel + var songCount: Int = 0 + @Ignore + @IgnoredOnParcel + var created: Date? = null + @Ignore + @IgnoredOnParcel + var changed: Date? = null + @Ignore + @IgnoredOnParcel + var allowedUsers: List? = null + @Ignore + constructor( + id: String, + name: String?, + comment: String?, + owner: String?, + isUniversal: Boolean?, + songCount: Int, + duration: Long, + created: Date?, + changed: Date?, + coverArtId: String?, + allowedUsers: List?, + ) : this(id, name, duration, coverArtId) { + this.comment = comment + this.owner = owner + this.isUniversal = isUniversal + this.songCount = songCount + this.created = created + this.changed = changed + this.allowedUsers = allowedUsers + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PlaylistWithSongs.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PlaylistWithSongs.kt new file mode 100644 index 0000000..350dcbd --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PlaylistWithSongs.kt @@ -0,0 +1,15 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +class PlaylistWithSongs( + @SerializedName("_id") + override var id: String, + @SerializedName("entry") + var entries: List? = null, +) : Playlist(id), Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlists.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlists.kt new file mode 100644 index 0000000..34079c7 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlists.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class Playlists( + @SerializedName("playlist") + var playlists: List? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastChannel.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastChannel.kt new file mode 100644 index 0000000..b4d124f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastChannel.kt @@ -0,0 +1,23 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +class PodcastChannel( + @SerializedName("episode") + var episodes: List? = null, + var id: String? = null, + var url: String? = null, + var title: String? = null, + var description: String? = null, + @SerializedName("coverArt") + var coverArtId: String? = null, + var originalImageUrl: String? = null, + var status: String? = null, + var errorMessage: String? = null, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastEpisode.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastEpisode.kt new file mode 100644 index 0000000..fc3fab2 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastEpisode.kt @@ -0,0 +1,40 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import java.util.* + +@Keep +@Parcelize +class PodcastEpisode( + var id: String? = null, + @SerializedName("parent") + var parentId: String? = null, + var isDir: Boolean = false, + var title: String? = null, + var album: String? = null, + var artist: String? = null, + var year: Int? = null, + var genre: String? = null, + @SerializedName("coverArt") + var coverArtId: String? = null, + var size: Long? = null, + var contentType: String? = null, + var suffix: String? = null, + var duration: Int? = null, + @SerializedName("bitRate") + var bitrate: Int? = null, + var path: String? = null, + var isVideo: Boolean = false, + var created: Date? = null, + var artistId: String? = null, + var type: String? = null, + var streamId: String? = null, + var channelId: String? = null, + var description: String? = null, + var status: String? = null, + var publishDate: Date? = null, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastStatus.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastStatus.kt new file mode 100644 index 0000000..f0f7c66 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastStatus.kt @@ -0,0 +1,17 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class PodcastStatus { + var value: String? = null + + companion object { + var NEW = "new" + var DOWNLOADING = "downloading" + var COMPLETED = "completed" + var ERROR = "error" + var DELETED = "deleted" + var SKIPPED = "skipped" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Podcasts.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Podcasts.kt new file mode 100644 index 0000000..d09f9fd --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Podcasts.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class Podcasts { + @SerializedName("channel") + var channels: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/RecordLabel.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/RecordLabel.kt new file mode 100644 index 0000000..52531d9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/RecordLabel.kt @@ -0,0 +1,12 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +open class RecordLabel( + var name: String? = null, +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ResponseStatus.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ResponseStatus.kt new file mode 100644 index 0000000..9e6b36c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ResponseStatus.kt @@ -0,0 +1,13 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class ResponseStatus(val value: String) { + companion object { + @JvmField + var OK = "ok" + @JvmField + var FAILED = "failed" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ScanStatus.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ScanStatus.kt new file mode 100644 index 0000000..c9735f0 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ScanStatus.kt @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class ScanStatus { + var isScanning = false + var count: Long? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SearchResult.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SearchResult.kt new file mode 100644 index 0000000..f7b772a --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SearchResult.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class SearchResult { + var matches: List? = null + var offset = 0 + var totalHits = 0 +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SearchResult2.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SearchResult2.kt new file mode 100644 index 0000000..66a9560 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SearchResult2.kt @@ -0,0 +1,14 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class SearchResult2 { + @SerializedName("artist") + var artists: List? = null + @SerializedName("album") + var albums: List? = null + @SerializedName("song") + var songs: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SearchResult3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SearchResult3.kt new file mode 100644 index 0000000..4a1ae32 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SearchResult3.kt @@ -0,0 +1,14 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class SearchResult3 { + @SerializedName("artist") + var artists: List? = null + @SerializedName("album") + var albums: List? = null + @SerializedName("song") + var songs: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Share.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Share.kt new file mode 100644 index 0000000..986e4b5 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Share.kt @@ -0,0 +1,23 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import java.util.Date + +@Keep +@Parcelize +data class Share( + @SerializedName("entry") + var entries: List? = null, + var id: String? = null, + var url: String? = null, + var description: String? = null, + var username: String? = null, + var created: Date? = null, + var expires: Date? = null, + var lastVisited: Date? = null, + var visitCount: Int = 0 +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Shares.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Shares.kt new file mode 100644 index 0000000..4c0f30e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Shares.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class Shares { + @SerializedName("share") + var shares: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarArtistID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarArtistID3.kt new file mode 100644 index 0000000..7275ea0 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarArtistID3.kt @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.subsonic.models + +import android.os.Parcelable +import androidx.annotation.Keep +import kotlinx.parcelize.Parcelize + +@Keep +@Parcelize +class SimilarArtistID3 : ArtistID3(), Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs.kt new file mode 100644 index 0000000..d9bb205 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs.kt @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class SimilarSongs { + var songs: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs2.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs2.kt new file mode 100644 index 0000000..ccce209 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SimilarSongs2.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class SimilarSongs2 { + @SerializedName("song") + var songs: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Songs.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Songs.kt new file mode 100644 index 0000000..43744f5 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Songs.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class Songs { + @SerializedName("song") + var songs: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Starred.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Starred.kt new file mode 100644 index 0000000..2900786 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Starred.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class Starred { + var artists: List? = null + var albums: List? = null + var songs: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Starred2.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Starred2.kt new file mode 100644 index 0000000..0a94b03 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Starred2.kt @@ -0,0 +1,14 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class Starred2 { + @SerializedName("artist") + var artists: List? = null + @SerializedName("album") + var albums: List? = null + @SerializedName("song") + var songs: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/StructuredLyrics.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/StructuredLyrics.kt new file mode 100644 index 0000000..d77b2e0 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/StructuredLyrics.kt @@ -0,0 +1,13 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class StructuredLyrics { + var displayArtist: String? = null + var displayTitle: String? = null + var lang: String? = null + var offset: Int = 0 + var synced: Boolean = false + var line: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SubsonicResponse.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SubsonicResponse.kt new file mode 100644 index 0000000..f886f55 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/SubsonicResponse.kt @@ -0,0 +1,57 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class SubsonicResponse { + var error: Error? = null + var scanStatus: ScanStatus? = null + var topSongs: TopSongs? = null + var similarSongs2: SimilarSongs2? = null + var similarSongs: SimilarSongs? = null + var artistInfo2: ArtistInfo2? = null + var artistInfo: ArtistInfo? = null + var albumInfo: AlbumInfo? = null + var starred2: Starred2? = null + var starred: Starred? = null + var shares: Shares? = null + var playQueue: PlayQueue? = null + var bookmarks: Bookmarks? = null + var internetRadioStations: InternetRadioStations? = null + var newestPodcasts: NewestPodcasts? = null + var podcasts: Podcasts? = null + var lyrics: Lyrics? = null + var songsByGenre: Songs? = null + var randomSongs: Songs? = null + var albumList2: AlbumList2? = null + var albumList: AlbumList? = null + var chatMessages: ChatMessages? = null + var user: User? = null + var users: Users? = null + var license: License? = null + var jukeboxPlaylist: JukeboxPlaylist? = null + var jukeboxStatus: JukeboxStatus? = null + var playlist: PlaylistWithSongs? = null + var playlists: Playlists? = null + var searchResult3: SearchResult3? = null + var searchResult2: SearchResult2? = null + var searchResult: SearchResult? = null + var nowPlaying: NowPlaying? = null + var videoInfo: VideoInfo? = null + var videos: Videos? = null + var song: Child? = null + var album: AlbumWithSongsID3? = null + var artist: ArtistWithAlbumsID3? = null + var artists: ArtistsID3? = null + var genres: Genres? = null + var directory: Directory? = null + var indexes: Indexes? = null + var musicFolders: MusicFolders? = null + var status: String? = null + var version: String? = null + var type: String? = null + var serverVersion: String? = null + var openSubsonic: Boolean? = null + var openSubsonicExtensions: List? = null + var lyricsList: LyricsList? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/TopSongs.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/TopSongs.kt new file mode 100644 index 0000000..076c7e4 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/TopSongs.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +@Keep +class TopSongs { + @SerializedName("song") + var songs: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/User.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/User.kt new file mode 100644 index 0000000..261c0e3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/User.kt @@ -0,0 +1,26 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep +import java.util.* + +@Keep +class User { + var folders: List? = null + var username: String? = null + var email: String? = null + var isScrobblingEnabled = false + var maxBitRate: Int? = null + var isAdminRole = false + var isSettingsRole = false + var isDownloadRole = false + var isUploadRole = false + var isPlaylistRole = false + var isCoverArtRole = false + var isCommentRole = false + var isPodcastRole = false + var isStreamRole = false + var isJukeboxRole = false + var isShareRole = false + var isVideoConversionRole = false + var avatarLastChanged: Date? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Users.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Users.kt new file mode 100644 index 0000000..c6de9fc --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Users.kt @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class Users { + var users: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/VideoConversion.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/VideoConversion.kt new file mode 100644 index 0000000..4e106fa --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/VideoConversion.kt @@ -0,0 +1,10 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class VideoConversion { + var id: String? = null + var bitRate: Int? = null + var audioTrackId: Int? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/VideoInfo.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/VideoInfo.kt new file mode 100644 index 0000000..5ec9e87 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/VideoInfo.kt @@ -0,0 +1,11 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class VideoInfo { + var captions: List? = null + var audioTracks: List? = null + var conversions: List? = null + var id: String? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Videos.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Videos.kt new file mode 100644 index 0000000..e9b772c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Videos.kt @@ -0,0 +1,8 @@ +package com.cappielloantonio.tempo.subsonic.models + +import androidx.annotation.Keep + +@Keep +class Videos { + var videos: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/CacheUtil.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/CacheUtil.java new file mode 100644 index 0000000..69dc9dd --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/CacheUtil.java @@ -0,0 +1,73 @@ +package com.cappielloantonio.tempo.subsonic.utils; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; + +import com.cappielloantonio.tempo.App; + +import okhttp3.Interceptor; +import okhttp3.Request; + +public class CacheUtil { + private int maxAge; // 60 seconds + private int maxStale; // 60 * 60 * 24 * 30 = 30 days (60 seconds * 60 minutes * 24 hours * 30 days) + + public CacheUtil(int maxAge, int maxStale) { + this.maxAge = maxAge; + this.maxStale = maxStale; + } + + public Interceptor onlineInterceptor = chain -> { + okhttp3.Response response = chain.proceed(chain.request()); + return response.newBuilder() + .header("Cache-Control", "public, max-age=" + maxAge) + .removeHeader("Pragma") + .build(); + }; + + public Interceptor offlineInterceptor = chain -> { + Request request = chain.request(); + if (!isConnected()) { + request = request.newBuilder() + .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale) + .removeHeader("Pragma") + .build(); + } + return chain.proceed(request); + }; + + + private boolean isConnected() { + ConnectivityManager connectivityManager = (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager == null) { + return false; + } + + Network network = connectivityManager.getActiveNetwork(); + if (network == null) { + return false; + } + + NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network); + if (capabilities == null) { + return false; + } + + boolean hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + if (!hasInternet) { + return false; + } + + boolean hasAppropriateTransport = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET); + if (!hasAppropriateTransport) { + return false; + } + + return true; + } + +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/EmptyDateTypeAdapter.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/EmptyDateTypeAdapter.kt new file mode 100644 index 0000000..bcdd5ee --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/EmptyDateTypeAdapter.kt @@ -0,0 +1,42 @@ +package com.cappielloantonio.tempo.subsonic.utils + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import java.lang.reflect.Type +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +// This adapter handles Date objects, returning null if the JSON string is empty or unparsable. +class EmptyDateTypeAdapter : JsonDeserializer { + + // Define the date formats expected from the Subsonic server. + private val dateFormats: List = listOf( + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }, + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }, + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") } + ) + + @Throws(JsonParseException::class) + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Date? { + val jsonString = json.asString.trim() + + if (jsonString.isEmpty()) { + return null + } + + for (format in dateFormats) { + try { + return format.parse(jsonString) + } catch (e: ParseException) { + // Ignore and try the next format + } + } + + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/StringUtil.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/StringUtil.java new file mode 100644 index 0000000..d62aecf --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/StringUtil.java @@ -0,0 +1,28 @@ +package com.cappielloantonio.tempo.subsonic.utils; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class StringUtil { + public static String tokenize(String s) { + final String MD5 = "MD5"; + try { + MessageDigest digest = java.security.MessageDigest.getInstance(MD5); + digest.update(s.getBytes()); + byte[] messageDigest = digest.digest(); + + StringBuilder hexString = new StringBuilder(); + for (byte aMessageDigest : messageDigest) { + StringBuilder h = new StringBuilder(Integer.toHexString(0xFF & aMessageDigest)); + while (h.length() < 2) { + h.insert(0, "0"); + } + hexString.append(h); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return ""; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java new file mode 100644 index 0000000..156805a --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java @@ -0,0 +1,556 @@ +package com.cappielloantonio.tempo.ui.activity; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.core.splashscreen.SplashScreen; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.Player; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.NavController; +import androidx.navigation.fragment.NavHostFragment; +import androidx.navigation.ui.NavigationUI; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.BuildConfig; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.broadcast.receiver.ConnectivityStatusBroadcastReceiver; +import com.cappielloantonio.tempo.databinding.ActivityMainBinding; +import com.cappielloantonio.tempo.github.utils.UpdateUtil; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.ui.activity.base.BaseActivity; +import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog; +import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog; +import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog; +import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment; +import com.cappielloantonio.tempo.util.AssetLinkNavigator; +import com.cappielloantonio.tempo.util.AssetLinkUtil; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.MainViewModel; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.color.DynamicColors; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.Objects; +import java.util.concurrent.ExecutionException; + +@UnstableApi +public class MainActivity extends BaseActivity { + private static final String TAG = "MainActivityLogs"; + + public ActivityMainBinding bind; + private MainViewModel mainViewModel; + + private FragmentManager fragmentManager; + private NavHostFragment navHostFragment; + private BottomNavigationView bottomNavigationView; + public NavController navController; + private BottomSheetBehavior bottomSheetBehavior; + private AssetLinkNavigator assetLinkNavigator; + private AssetLinkUtil.AssetLink pendingAssetLink; + + ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver; + private Intent pendingDownloadPlaybackIntent; + + @Override + protected void onCreate(Bundle savedInstanceState) { + SplashScreen.installSplashScreen(this); + DynamicColors.applyToActivityIfAvailable(this); + + super.onCreate(savedInstanceState); + + bind = ActivityMainBinding.inflate(getLayoutInflater()); + View view = bind.getRoot(); + setContentView(view); + + mainViewModel = new ViewModelProvider(this).get(MainViewModel.class); + assetLinkNavigator = new AssetLinkNavigator(this); + + connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this); + connectivityStatusReceiverManager(true); + + init(); + checkConnectionType(); + getOpenSubsonicExtensions(); + checkTempoUpdate(); + + maybeSchedulePlaybackIntent(getIntent()); + } + + @Override + protected void onStart() { + super.onStart(); + pingServer(); + initService(); + consumePendingPlaybackIntent(); + } + + @Override + protected void onResume() { + super.onResume(); + pingServer(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + connectivityStatusReceiverManager(false); + bind = null; + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + maybeSchedulePlaybackIntent(intent); + consumePendingPlaybackIntent(); + } + + @Override + public void onBackPressed() { + if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) + collapseBottomSheetDelayed(); + else + super.onBackPressed(); + } + + public void init() { + fragmentManager = getSupportFragmentManager(); + + initBottomSheet(); + initNavigation(); + + if (Preferences.getPassword() != null || (Preferences.getToken() != null && Preferences.getSalt() != null)) { + goFromLogin(); + } else { + goToLogin(); + } + } + + // BOTTOM SHEET/NAVIGATION + private void initBottomSheet() { + bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.player_bottom_sheet)); + bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback); + fragmentManager.beginTransaction().replace(R.id.player_bottom_sheet, new PlayerBottomSheetFragment(), "PlayerBottomSheet").commit(); + + checkBottomSheetAfterStateChanged(); + } + + public void setBottomSheetInPeek(Boolean isVisible) { + if (isVisible) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } else { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + } + } + + public void setBottomSheetVisibility(boolean visibility) { + if (visibility) { + findViewById(R.id.player_bottom_sheet).setVisibility(View.VISIBLE); + } else { + findViewById(R.id.player_bottom_sheet).setVisibility(View.GONE); + } + } + + private void checkBottomSheetAfterStateChanged() { + final Handler handler = new Handler(); + final Runnable runnable = () -> setBottomSheetInPeek(mainViewModel.isQueueLoaded()); + handler.postDelayed(runnable, 100); + } + + public void collapseBottomSheetDelayed() { + final Handler handler = new Handler(); + final Runnable runnable = () -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + handler.postDelayed(runnable, 100); + } + + public void expandBottomSheet() { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } + + public void setBottomSheetDraggableState(Boolean isDraggable) { + bottomSheetBehavior.setDraggable(isDraggable); + } + + private final BottomSheetBehavior.BottomSheetCallback bottomSheetCallback = + new BottomSheetBehavior.BottomSheetCallback() { + int navigationHeight; + + @Override + public void onStateChanged(@NonNull View view, int state) { + PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet"); + + switch (state) { + case BottomSheetBehavior.STATE_HIDDEN: + resetMusicSession(); + break; + case BottomSheetBehavior.STATE_COLLAPSED: + if (playerBottomSheetFragment != null) + playerBottomSheetFragment.goBackToFirstPage(); + break; + case BottomSheetBehavior.STATE_SETTLING: + case BottomSheetBehavior.STATE_EXPANDED: + case BottomSheetBehavior.STATE_DRAGGING: + case BottomSheetBehavior.STATE_HALF_EXPANDED: + break; + } + } + + @Override + public void onSlide(@NonNull View view, float slideOffset) { + animateBottomSheet(slideOffset); + animateBottomNavigation(slideOffset, navigationHeight); + } + }; + + private void animateBottomSheet(float slideOffset) { + PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet"); + if (playerBottomSheetFragment != null) { + float condensedSlideOffset = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f; + playerBottomSheetFragment.getPlayerHeader().setAlpha(1 - condensedSlideOffset); + playerBottomSheetFragment.getPlayerHeader().setVisibility(condensedSlideOffset > 0.99 ? View.GONE : View.VISIBLE); + } + } + + private void animateBottomNavigation(float slideOffset, int navigationHeight) { + if (slideOffset < 0) return; + + if (navigationHeight == 0) { + navigationHeight = bind.bottomNavigation.getHeight(); + } + + float slideY = navigationHeight - navigationHeight * (1 - slideOffset); + + bind.bottomNavigation.setTranslationY(slideY); + } + + private void initNavigation() { + bottomNavigationView = findViewById(R.id.bottom_navigation); + navHostFragment = (NavHostFragment) fragmentManager.findFragmentById(R.id.nav_host_fragment); + navController = Objects.requireNonNull(navHostFragment).getNavController(); + + /* + * In questo modo intercetto il cambio schermata tramite navbar e se il bottom sheet è aperto, + * lo chiudo + */ + navController.addOnDestinationChangedListener((controller, destination, arguments) -> { + if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED && ( + destination.getId() == R.id.homeFragment || + destination.getId() == R.id.libraryFragment || + destination.getId() == R.id.downloadFragment) + ) { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); + } + }); + + NavigationUI.setupWithNavController(bottomNavigationView, navController); + } + + public void setBottomNavigationBarVisibility(boolean visibility) { + if (visibility) { + bottomNavigationView.setVisibility(View.VISIBLE); + } else { + bottomNavigationView.setVisibility(View.GONE); + } + } + + private void initService() { + MediaManager.check(getMediaBrowserListenableFuture()); + + getMediaBrowserListenableFuture().addListener(() -> { + try { + getMediaBrowserListenableFuture().get().addListener(new Player.Listener() { + @Override + public void onIsPlayingChanged(boolean isPlaying) { + if (isPlaying && bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN) { + setBottomSheetInPeek(true); + } + } + }); + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + + private void goToLogin() { + bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); + setBottomNavigationBarVisibility(false); + setBottomSheetVisibility(false); + + if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.landingFragment) { + navController.navigate(R.id.action_landingFragment_to_loginFragment); + } else if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.settingsFragment) { + navController.navigate(R.id.action_settingsFragment_to_loginFragment); + } else if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.homeFragment) { + navController.navigate(R.id.action_homeFragment_to_loginFragment); + } + } + + private void goToHome() { + bottomNavigationView.setVisibility(View.VISIBLE); + + if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.landingFragment) { + navController.navigate(R.id.action_landingFragment_to_homeFragment); + } else if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.loginFragment) { + navController.navigate(R.id.action_loginFragment_to_homeFragment); + } + } + + public void goFromLogin() { + setBottomSheetInPeek(mainViewModel.isQueueLoaded()); + goToHome(); + consumePendingAssetLink(); + } + + public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink) { + openAssetLink(assetLink, true); + } + + public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink, boolean collapsePlayer) { + if (!isUserAuthenticated()) { + pendingAssetLink = assetLink; + return; + } + if (collapsePlayer) { + setBottomSheetInPeek(true); + } + if (assetLinkNavigator != null) { + assetLinkNavigator.open(assetLink); + } + } + + public void quit() { + resetUserSession(); + resetMusicSession(); + resetViewModel(); + goToLogin(); + } + + private void resetUserSession() { + Preferences.setServerId(null); + Preferences.setSalt(null); + Preferences.setToken(null); + Preferences.setPassword(null); + Preferences.setServer(null); + Preferences.setLocalAddress(null); + Preferences.setUser(null); + + // TODO Enter all settings to be reset + Preferences.setOpenSubsonic(false); + Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_100); + Preferences.setSkipSilenceMode(false); + Preferences.setDataSavingMode(false); + Preferences.setStarredSyncEnabled(false); + Preferences.setStarredAlbumsSyncEnabled(false); + } + + private void resetMusicSession() { + MediaManager.reset(getMediaBrowserListenableFuture()); + } + + private void hideMusicSession() { + MediaManager.hide(getMediaBrowserListenableFuture()); + } + + private void resetViewModel() { + this.getViewModelStore().clear(); + } + + // CONNECTION + private void connectivityStatusReceiverManager(boolean isActive) { + if (isActive) { + IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + registerReceiver(connectivityStatusBroadcastReceiver, filter); + } else { + unregisterReceiver(connectivityStatusBroadcastReceiver); + } + } + + private void pingServer() { + if (Preferences.getToken() == null) return; + + if (Preferences.isInUseServerAddressLocal()) { + mainViewModel.ping().observe(this, subsonicResponse -> { + if (subsonicResponse == null) { + Preferences.setServerSwitchableTimer(); + Preferences.switchInUseServerAddress(); + App.refreshSubsonicClient(); + pingServer(); + resetView(); + } else { + Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic()); + } + }); + } else { + if (Preferences.isServerSwitchable()) { + Preferences.setServerSwitchableTimer(); + Preferences.switchInUseServerAddress(); + App.refreshSubsonicClient(); + pingServer(); + resetView(); + } else { + mainViewModel.ping().observe(this, subsonicResponse -> { + if (subsonicResponse == null) { + if (Preferences.showServerUnreachableDialog()) { + ServerUnreachableDialog dialog = new ServerUnreachableDialog(); + dialog.show(getSupportFragmentManager(), null); + } + } else { + Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic()); + } + }); + } + } + } + + private void resetView() { + resetViewModel(); + int id = Objects.requireNonNull(navController.getCurrentDestination()).getId(); + navController.popBackStack(id, true); + navController.navigate(id); + } + + private void getOpenSubsonicExtensions() { + if (Preferences.getToken() != null) { + mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> { + if (openSubsonicExtensions != null) { + Preferences.setOpenSubsonicExtensions(openSubsonicExtensions); + } + }); + } + } + + private void checkTempoUpdate() { + if (BuildConfig.FLAVOR.equals("tempus") && Preferences.isGithubUpdateEnabled() && Preferences.showTempusUpdateDialog()) { + mainViewModel.checkTempoUpdate().observe(this, latestRelease -> { + if (latestRelease != null && UpdateUtil.showUpdateDialog(latestRelease)) { + GithubTempoUpdateDialog dialog = new GithubTempoUpdateDialog(latestRelease); + dialog.show(getSupportFragmentManager(), null); + } + }); + } + } + + private void checkConnectionType() { + if (Preferences.isWifiOnly()) { + ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + + if (networkInfo != null && networkInfo.getType() != ConnectivityManager.TYPE_WIFI) { + ConnectionAlertDialog dialog = new ConnectionAlertDialog(); + dialog.show(getSupportFragmentManager(), null); + } + } + } + + private void maybeSchedulePlaybackIntent(Intent intent) { + if (intent == null) return; + if (Constants.ACTION_PLAY_EXTERNAL_DOWNLOAD.equals(intent.getAction()) + || intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) { + pendingDownloadPlaybackIntent = new Intent(intent); + } + handleAssetLinkIntent(intent); + } + + private void consumePendingPlaybackIntent() { + if (pendingDownloadPlaybackIntent == null) return; + Intent intent = pendingDownloadPlaybackIntent; + pendingDownloadPlaybackIntent = null; + playDownloadedMedia(intent); + } + + private void handleAssetLinkIntent(Intent intent) { + AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.parse(intent); + if (assetLink == null) { + return; + } + if (!isUserAuthenticated()) { + pendingAssetLink = assetLink; + intent.setData(null); + return; + } + if (assetLinkNavigator != null) { + assetLinkNavigator.open(assetLink); + } + intent.setData(null); + } + + private boolean isUserAuthenticated() { + return Preferences.getPassword() != null + || (Preferences.getToken() != null && Preferences.getSalt() != null); + } + + private void consumePendingAssetLink() { + if (pendingAssetLink == null || assetLinkNavigator == null) { + return; + } + assetLinkNavigator.open(pendingAssetLink); + pendingAssetLink = null; + } + + private void playDownloadedMedia(Intent intent) { + String uriString = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_URI); + if (TextUtils.isEmpty(uriString)) { + return; + } + + Uri uri = Uri.parse(uriString); + String mediaId = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_MEDIA_ID); + if (TextUtils.isEmpty(mediaId)) { + mediaId = uri.toString(); + } + + String title = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_TITLE); + String artist = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ARTIST); + String album = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ALBUM); + int duration = intent.getIntExtra(Constants.EXTRA_DOWNLOAD_DURATION, 0); + + Bundle extras = new Bundle(); + extras.putString("id", mediaId); + extras.putString("title", title); + extras.putString("artist", artist); + extras.putString("album", album); + extras.putString("uri", uri.toString()); + extras.putString("type", Constants.MEDIA_TYPE_MUSIC); + extras.putInt("duration", duration); + + MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder() + .setExtras(extras) + .setIsBrowsable(false) + .setIsPlayable(true); + + if (!TextUtils.isEmpty(title)) metadataBuilder.setTitle(title); + if (!TextUtils.isEmpty(artist)) metadataBuilder.setArtist(artist); + if (!TextUtils.isEmpty(album)) metadataBuilder.setAlbumTitle(album); + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId(mediaId) + .setMediaMetadata(metadataBuilder.build()) + .setUri(uri) + .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .setRequestMetadata(new MediaItem.RequestMetadata.Builder() + .setMediaUri(uri) + .setExtras(extras) + .build()) + .build(); + + MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java new file mode 100644 index 0000000..9eaa2c3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/base/BaseActivity.java @@ -0,0 +1,112 @@ +package com.cappielloantonio.tempo.ui.activity.base; + +import android.Manifest; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; +import android.view.WindowManager; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.offline.DownloadService; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.service.DownloaderService; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.dialog.BatteryOptimizationDialog; +import com.cappielloantonio.tempo.util.Flavors; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.elevation.SurfaceColors; +import com.google.common.util.concurrent.ListenableFuture; + +@UnstableApi +public class BaseActivity extends AppCompatActivity { + private static final String TAG = "BaseActivity"; + + private ListenableFuture mediaBrowserListenableFuture; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Flavors.initializeCastContext(this); + initializeDownloader(); + checkBatteryOptimization(); + checkPermission(); + checkAlwaysOnDisplay(); + } + + @Override + protected void onStart() { + super.onStart(); + setNavigationBarColor(); + initializeBrowser(); + } + + @Override + protected void onStop() { + releaseBrowser(); + super.onStop(); + } + + private void checkBatteryOptimization() { + if (detectBatteryOptimization() && Preferences.askForOptimization()) { + showBatteryOptimizationDialog(); + } + } + + private void checkPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, 101); + } + } + } + + private void checkAlwaysOnDisplay() { + if (Preferences.isDisplayAlwaysOn()) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + + private boolean detectBatteryOptimization() { + String packageName = getPackageName(); + PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); + return !powerManager.isIgnoringBatteryOptimizations(packageName); + } + + private void showBatteryOptimizationDialog() { + BatteryOptimizationDialog dialog = new BatteryOptimizationDialog(); + dialog.show(getSupportFragmentManager(), null); + } + + private void initializeBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(this, new SessionToken(this, new ComponentName(this, MediaService.class))).buildAsync(); + } + + private void releaseBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + public ListenableFuture getMediaBrowserListenableFuture() { + return mediaBrowserListenableFuture; + } + + private void initializeDownloader() { + try { + DownloadService.start(this, DownloaderService.class); + } catch (IllegalStateException e) { + DownloadService.startForeground(this, DownloaderService.class); + } + } + + private void setNavigationBarColor() { + getWindow().setNavigationBarColor(SurfaceColors.getColorForElevation(this, 8)); + getWindow().setStatusBarColor(SurfaceColors.getColorForElevation(this, 0)); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumAdapter.java new file mode 100644 index 0000000..498bf23 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumAdapter.java @@ -0,0 +1,95 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLibraryAlbumBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.Collections; +import java.util.List; + +public class AlbumAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List albums; + + public AlbumAdapter(ClickCallback click) { + this.click = click; + this.albums = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLibraryAlbumBinding view = ItemLibraryAlbumBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + AlbumID3 album = albums.get(position); + + holder.item.albumNameLabel.setText(album.getName()); + holder.item.artistNameLabel.setText(album.getArtist()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album) + .build() + .into(holder.item.albumCoverImageView); + } + + @Override + public int getItemCount() { + return albums.size(); + } + + public AlbumID3 getItem(int position) { + return albums.get(position); + } + + public void setItems(List albums) { + this.albums = albums; + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLibraryAlbumBinding item; + + ViewHolder(ItemLibraryAlbumBinding item) { + super(item.getRoot()); + + this.item = item; + + item.albumNameLabel.setSelected(true); + item.artistNameLabel.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + } + + private void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + + click.onAlbumClick(bundle); + } + + private boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + + click.onAlbumLongClick(bundle); + + return true; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumArtistPageOrSimilarAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumArtistPageOrSimilarAdapter.java new file mode 100644 index 0000000..4230340 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumArtistPageOrSimilarAdapter.java @@ -0,0 +1,95 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLibraryArtistPageOrSimilarAlbumBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.Collections; +import java.util.List; + +public class AlbumArtistPageOrSimilarAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List albums; + + public AlbumArtistPageOrSimilarAdapter(ClickCallback click) { + this.click = click; + this.albums = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLibraryArtistPageOrSimilarAlbumBinding view = ItemLibraryArtistPageOrSimilarAlbumBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + AlbumID3 album = albums.get(position); + + holder.item.albumNameLabel.setText(album.getName()); + holder.item.artistNameLabel.setText(album.getArtist()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album) + .build() + .into(holder.item.artistPageAlbumCoverImageView); + } + + @Override + public int getItemCount() { + return albums.size(); + } + + public AlbumID3 getItem(int position) { + return albums.get(position); + } + + public void setItems(List albums) { + this.albums = albums; + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLibraryArtistPageOrSimilarAlbumBinding item; + + ViewHolder(ItemLibraryArtistPageOrSimilarAlbumBinding item) { + super(item.getRoot()); + + this.item = item; + + item.albumNameLabel.setSelected(true); + item.artistNameLabel.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + } + + private void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + + click.onAlbumClick(bundle); + } + + private boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + + click.onAlbumLongClick(bundle); + + return true; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java new file mode 100644 index 0000000..e2d0563 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java @@ -0,0 +1,207 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLibraryCatalogueAlbumBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +public class AlbumCatalogueAdapter extends RecyclerView.Adapter implements Filterable { + private final ClickCallback click; + private String currentFilter; + private boolean showArtist; + + private final Filter filtering = new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + List filteredList = new ArrayList<>(); + + if (constraint == null || constraint.length() == 0) { + filteredList.addAll(albumsFull); + } else { + String filterPattern = constraint.toString().toLowerCase().trim(); + currentFilter = filterPattern; + + for (AlbumID3 item : albumsFull) { + if (item.getName().toLowerCase().contains(filterPattern)) { + filteredList.add(item); + } + } + } + + FilterResults results = new FilterResults(); + results.values = filteredList; + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + albums = (List) results.values; + notifyDataSetChanged(); + } + }; + + private List albums; + private List albumsFull; + + public AlbumCatalogueAdapter(ClickCallback click, boolean showArtist) { + this.click = click; + this.albums = Collections.emptyList(); + this.albumsFull = Collections.emptyList(); + this.currentFilter = ""; + this.showArtist = showArtist; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLibraryCatalogueAlbumBinding view = ItemLibraryCatalogueAlbumBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + AlbumID3 album = albums.get(position); + + holder.item.albumNameLabel.setText(album.getName()); + holder.item.artistNameLabel.setText(album.getArtist()); + holder.item.artistNameLabel.setVisibility(showArtist ? View.VISIBLE : View.GONE); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album) + .build() + .into(holder.item.albumCatalogueCoverImageView); + } + + @Override + public int getItemCount() { + return albums.size(); + } + + public AlbumID3 getItem(int position) { + return albums.get(position); + } + + public void setItems(List albums) { + this.albumsFull = new ArrayList<>(albums); + filtering.filter(currentFilter); + } + + @Override + public int getItemViewType(int position) { + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public Filter getFilter() { + return filtering; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLibraryCatalogueAlbumBinding item; + + ViewHolder(ItemLibraryCatalogueAlbumBinding item) { + super(item.getRoot()); + + this.item = item; + + item.albumNameLabel.setSelected(true); + item.artistNameLabel.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + } + + private void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + + click.onAlbumClick(bundle); + } + + private boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + + click.onAlbumLongClick(bundle); + + return true; + } + } + + public void setItemsWithoutFilter(List albums) { + this.albumsFull = new ArrayList<>(albums); + this.albums = new ArrayList<>(albums); + notifyDataSetChanged(); + } + + public void sort(String order) { + if (albums == null) return; + + switch (order) { + case Constants.ALBUM_ORDER_BY_NAME: + albums.sort(Comparator.comparing( + album -> album.getName() != null ? album.getName() : "", + String.CASE_INSENSITIVE_ORDER + )); + break; + case Constants.ALBUM_ORDER_BY_ARTIST: + albums.sort(Comparator.comparing( + album -> album.getArtist() != null ? album.getArtist() : "", + String.CASE_INSENSITIVE_ORDER + )); + break; + case Constants.ALBUM_ORDER_BY_YEAR: + albums.sort(Comparator.comparing(AlbumID3::getYear)); + break; + case Constants.ALBUM_ORDER_BY_RANDOM: + Collections.shuffle(albums); + break; + case Constants.ALBUM_ORDER_BY_RECENTLY_ADDED: + albums.sort(Comparator.comparing( + album -> album.getCreated() != null ? album.getCreated() : new Date(0), + Comparator.nullsLast(Date::compareTo) + )); + Collections.reverse(albums); + break; + case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED: + albums.sort(Comparator.comparing( + album -> album.getPlayed() != null ? album.getPlayed() : new Date(0), + Comparator.nullsLast(Date::compareTo) + )); + Collections.reverse(albums); + break; + case Constants.ALBUM_ORDER_BY_MOST_PLAYED: + albums.sort(Comparator.comparing( + album -> album.getPlayCount() != null ? album.getPlayCount() : 0L + )); + Collections.reverse(albums); + break; + } + + notifyDataSetChanged(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumHorizontalAdapter.java new file mode 100644 index 0000000..f19eae9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumHorizontalAdapter.java @@ -0,0 +1,161 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemHorizontalAlbumBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class AlbumHorizontalAdapter extends RecyclerView.Adapter implements Filterable { + private final ClickCallback click; + private final boolean isOffline; + + private List albumsFull; + private List albums; + private String currentFilter; + + private final Filter filtering = new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + List filteredList = new ArrayList<>(); + + if (constraint == null || constraint.length() == 0) { + filteredList.addAll(albumsFull); + } else { + String filterPattern = constraint.toString().toLowerCase().trim(); + currentFilter = filterPattern; + + for (AlbumID3 item : albumsFull) { + if (item.getName().toLowerCase().contains(filterPattern)) { + filteredList.add(item); + } + } + } + + FilterResults results = new FilterResults(); + results.values = filteredList; + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + albums = (List) results.values; + notifyDataSetChanged(); + } + }; + + public AlbumHorizontalAdapter(ClickCallback click, boolean isOffline) { + this.click = click; + this.isOffline = isOffline; + this.albums = Collections.emptyList(); + this.albumsFull = Collections.emptyList(); + this.currentFilter = ""; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHorizontalAlbumBinding view = ItemHorizontalAlbumBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + AlbumID3 album = albums.get(position); + + holder.item.albumTitleTextView.setText(album.getName()); + holder.item.albumArtistTextView.setText(album.getArtist()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album) + .build() + .into(holder.item.albumCoverImageView); + } + + @Override + public int getItemCount() { + return albums.size(); + } + + public void setItems(List albums) { + this.albumsFull = albums != null ? albums : Collections.emptyList(); + filtering.filter(currentFilter); + notifyDataSetChanged(); + } + + @Override + public Filter getFilter() { + return filtering; + } + + public AlbumID3 getItem(int id) { + return albums.get(id); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHorizontalAlbumBinding item; + + ViewHolder(ItemHorizontalAlbumBinding item) { + super(item.getRoot()); + + this.item = item; + + item.albumTitleTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + + item.albumMoreButton.setOnClickListener(v -> onLongClick()); + } + + private void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + + click.onAlbumClick(bundle); + } + + private boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + + click.onAlbumLongClick(bundle); + + return true; + } + } + + public void sort(String order) { + switch (order) { + case Constants.ALBUM_ORDER_BY_NAME: + albums.sort(Comparator.comparing(AlbumID3::getName)); + break; + case Constants.ALBUM_ORDER_BY_MOST_RECENTLY_STARRED: + albums.sort(Comparator.comparing(AlbumID3::getStarred, Comparator.nullsLast(Comparator.reverseOrder()))); + break; + case Constants.ALBUM_ORDER_BY_LEAST_RECENTLY_STARRED: + albums.sort(Comparator.comparing(AlbumID3::getStarred, Comparator.nullsLast(Comparator.naturalOrder()))); + + break; + } + + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistAdapter.java new file mode 100644 index 0000000..44e8402 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistAdapter.java @@ -0,0 +1,111 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.media3.common.util.UnstableApi; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLibraryArtistBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.Collections; +import java.util.List; + +@UnstableApi +public class ArtistAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + private final boolean mix; + private final boolean bestOf; + + private List artists; + + public ArtistAdapter(ClickCallback click, Boolean mix, Boolean bestOf) { + this.click = click; + this.mix = mix; + this.bestOf = bestOf; + this.artists = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLibraryArtistBinding view = ItemLibraryArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + ArtistID3 artist = artists.get(position); + + holder.item.artistNameLabel.setText(artist.getName()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), artist.getCoverArtId(), CustomGlideRequest.ResourceType.Artist) + .build() + .into(holder.item.artistCoverImageView); + } + + @Override + public int getItemCount() { + return artists.size(); + } + + public ArtistID3 getItem(int position) { + return artists.get(position); + } + + public void setItems(List artists) { + this.artists = artists; + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLibraryArtistBinding item; + + ViewHolder(ItemLibraryArtistBinding item) { + super(item.getRoot()); + + this.item = item; + + item.artistNameLabel.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + bundle.putBoolean(Constants.MEDIA_MIX, mix); + bundle.putBoolean(Constants.MEDIA_BEST_OF, bestOf); + + click.onArtistClick(bundle); + } + + public boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + + click.onArtistLongClick(bundle); + + return true; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistCatalogueAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistCatalogueAdapter.java new file mode 100644 index 0000000..b1defe4 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistCatalogueAdapter.java @@ -0,0 +1,161 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLibraryCatalogueArtistBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class ArtistCatalogueAdapter extends RecyclerView.Adapter implements Filterable { + private final ClickCallback click; + + private final Filter filtering = new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + List filteredList = new ArrayList<>(); + + if (constraint == null || constraint.length() == 0) { + filteredList.addAll(artistFull); + } else { + String filterPattern = constraint.toString().toLowerCase().trim(); + + for (ArtistID3 item : artistFull) { + if (item.getName().toLowerCase().contains(filterPattern)) { + filteredList.add(item); + } + } + } + + FilterResults results = new FilterResults(); + results.values = filteredList; + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + artists.clear(); + if (results.count > 0) artists.addAll((List) results.values); + notifyDataSetChanged(); + } + }; + + private List artists; + private List artistFull; + + public ArtistCatalogueAdapter(ClickCallback click) { + this.click = click; + this.artists = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLibraryCatalogueArtistBinding view = ItemLibraryCatalogueArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + ArtistID3 artist = artists.get(position); + + holder.item.artistNameLabel.setText(artist.getName()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), artist.getCoverArtId(), CustomGlideRequest.ResourceType.Artist) + .build() + .into(holder.item.artistCatalogueCoverImageView); + } + + @Override + public int getItemCount() { + return artists.size(); + } + + public ArtistID3 getItem(int position) { + return artists.get(position); + } + + public void setItems(List artists) { + this.artists = artists; + this.artistFull = new ArrayList<>(artists); + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public Filter getFilter() { + return filtering; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLibraryCatalogueArtistBinding item; + + ViewHolder(ItemLibraryCatalogueArtistBinding item) { + super(item.getRoot()); + + this.item = item; + + item.artistNameLabel.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + + click.onArtistClick(bundle); + } + + public boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + + click.onArtistLongClick(bundle); + + return true; + } + } + + public void sort(String order) { + switch (order) { + case Constants.ARTIST_ORDER_BY_NAME: + artists.sort(Comparator.comparing(ArtistID3::getName)); + break; + case Constants.ARTIST_ORDER_BY_RANDOM: + Collections.shuffle(artists); + break; + case Constants.ARTIST_ORDER_BY_ALBUM_COUNT: + artists.sort(Comparator.comparing(ArtistID3::getAlbumCount).reversed()); + break; + } + + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistHorizontalAdapter.java new file mode 100644 index 0000000..85b6fd6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistHorizontalAdapter.java @@ -0,0 +1,175 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemHorizontalArtistBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class ArtistHorizontalAdapter extends RecyclerView.Adapter implements Filterable { + private final ClickCallback click; + + private List artistsFull; + private List artists; + private String currentFilter; + + private final Filter filtering = new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + List filteredList = new ArrayList<>(); + + if (constraint == null || constraint.length() == 0) { + filteredList.addAll(artistsFull); + } else { + String filterPattern = constraint.toString().toLowerCase().trim(); + currentFilter = filterPattern; + + for (ArtistID3 item : artistsFull) { + if (item.getName().toLowerCase().contains(filterPattern)) { + filteredList.add(item); + } + } + } + + FilterResults results = new FilterResults(); + results.values = filteredList; + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + artists = (List) results.values; + notifyDataSetChanged(); + } + }; + + public ArtistHorizontalAdapter(ClickCallback click) { + this.click = click; + this.artists = Collections.emptyList(); + this.artistsFull = Collections.emptyList(); + this.currentFilter = ""; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHorizontalArtistBinding view = ItemHorizontalArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + ArtistID3 artist = artists.get(position); + + holder.item.artistNameTextView.setText(artist.getName()); + + if (artist.getAlbumCount() > 0) { + holder.item.artistInfoTextView.setText("Album count: " + artist.getAlbumCount()); + } else { + holder.item.artistInfoTextView.setVisibility(View.GONE); + } + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), artist.getCoverArtId(), CustomGlideRequest.ResourceType.Artist) + .build() + .into(holder.item.artistCoverImageView); + } + + @Override + public int getItemCount() { + return artists.size(); + } + + public void setItems(List artists) { + this.artistsFull = artists != null ? artists : Collections.emptyList(); + filtering.filter(currentFilter); + notifyDataSetChanged(); + } + + @Override + public Filter getFilter() { + return filtering; + } + + public ArtistID3 getItem(int id) { + return artists.get(id); + } + + @Override + public int getItemViewType(int position) { + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHorizontalArtistBinding item; + + ViewHolder(ItemHorizontalArtistBinding item) { + super(item.getRoot()); + + this.item = item; + + item.artistNameTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + + item.artistMoreButton.setOnClickListener(v -> onLongClick()); + } + + private void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + + click.onArtistClick(bundle); + } + + public boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + + click.onArtistLongClick(bundle); + + return true; + } + } + + public void sort(String order) { + switch (order) { + case Constants.ARTIST_ORDER_BY_NAME: + artists.sort(Comparator.comparing(ArtistID3::getName)); + break; + case Constants.ARTIST_ORDER_BY_MOST_RECENTLY_STARRED: + artists.sort(Comparator.comparing(ArtistID3::getStarred, Comparator.nullsLast(Comparator.reverseOrder()))); + break; + case Constants.ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED: + artists.sort(Comparator.comparing(ArtistID3::getStarred, Comparator.nullsLast(Comparator.naturalOrder()))); + + break; + } + + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistSimilarAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistSimilarAdapter.java new file mode 100644 index 0000000..4e8dffe --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistSimilarAdapter.java @@ -0,0 +1,103 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLibrarySimilarArtistBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.SimilarArtistID3; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.Collections; +import java.util.List; + +public class ArtistSimilarAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List artists; + + public ArtistSimilarAdapter(ClickCallback click) { + this.click = click; + this.artists = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLibrarySimilarArtistBinding view = ItemLibrarySimilarArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + SimilarArtistID3 artist = artists.get(position); + + holder.item.artistNameLabel.setText(artist.getName()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), artist.getCoverArtId(), CustomGlideRequest.ResourceType.Artist) + .build() + .into(holder.item.similarArtistCoverImageView); + } + + @Override + public int getItemCount() { + return artists.size(); + } + + public SimilarArtistID3 getItem(int position) { + return artists.get(position); + } + + public void setItems(List artists) { + this.artists = artists; + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLibrarySimilarArtistBinding item; + + ViewHolder(ItemLibrarySimilarArtistBinding item) { + super(item.getRoot()); + + this.item = item; + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + + item.artistNameLabel.setSelected(true); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + + click.onArtistClick(bundle); + } + + public boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + + click.onArtistLongClick(bundle); + + return true; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/DiscoverSongAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/DiscoverSongAdapter.java new file mode 100644 index 0000000..c9b21d8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/DiscoverSongAdapter.java @@ -0,0 +1,108 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.view.animation.AccelerateDecelerateInterpolator; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemHomeDiscoverSongBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.Collections; +import java.util.List; + +public class DiscoverSongAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List songs; + + public DiscoverSongAdapter(ClickCallback click) { + this.click = click; + this.songs = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHomeDiscoverSongBinding view = ItemHomeDiscoverSongBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Child song = songs.get(position); + + holder.item.titleDiscoverSongLabel.setText(song.getTitle()); + holder.item.albumDiscoverSongLabel.setText(song.getAlbum()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .into(holder.item.discoverSongCoverImageView); + } + + @Override + public void onViewAttachedToWindow(@NonNull ViewHolder holder) { + super.onViewAttachedToWindow(holder); + startAnimation(holder); + } + + @Override + public int getItemCount() { + return songs.size(); + } + + public void setItems(List songs) { + this.songs = songs; + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHomeDiscoverSongBinding item; + + ViewHolder(ItemHomeDiscoverSongBinding item) { + super(item.getRoot()); + + this.item = item; + + itemView.setOnClickListener(v -> onClick()); + + itemView.setOnLongClickListener(v -> { + onLongClick(); + return true; + }); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition())); + bundle.putBoolean(Constants.MEDIA_MIX, true); + + click.onMediaClick(bundle); + } + + private boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition())); + click.onMediaLongClick(bundle); + return true; + } + } + + private void startAnimation(ViewHolder holder) { + holder.item.discoverSongCoverImageView.animate() + .setDuration(20000) + .setStartDelay(10) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .scaleX(1.4f) + .scaleY(1.4f) + .start(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/DownloadHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/DownloadHorizontalAdapter.java new file mode 100644 index 0000000..ff06f27 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/DownloadHorizontalAdapter.java @@ -0,0 +1,355 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.media3.common.util.UnstableApi; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.ItemHorizontalDownloadBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@UnstableApi +public class DownloadHorizontalAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private String view; + private String filterKey; + private String filterValue; + + private List songs; + private List shuffling; + private List grouped; + + public DownloadHorizontalAdapter(ClickCallback click) { + this.click = click; + this.view = Constants.DOWNLOAD_TYPE_TRACK; + this.songs = Collections.emptyList(); + this.grouped = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHorizontalDownloadBinding view = ItemHorizontalDownloadBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + switch (view) { + case Constants.DOWNLOAD_TYPE_TRACK: + initTrackLayout(holder, position); + break; + case Constants.DOWNLOAD_TYPE_ALBUM: + initAlbumLayout(holder, position); + break; + case Constants.DOWNLOAD_TYPE_ARTIST: + initArtistLayout(holder, position); + break; + case Constants.DOWNLOAD_TYPE_GENRE: + initGenreLayout(holder, position); + break; + case Constants.DOWNLOAD_TYPE_YEAR: + initYearLayout(holder, position); + break; + } + } + + @Override + public int getItemCount() { + return grouped.size(); + } + + public void setItems(String view, String filterKey, String filterValue, List songs) { + this.view = filterValue != null ? view : filterKey; + this.filterKey = filterKey; + this.filterValue = filterValue; + + this.songs = songs; + this.grouped = groupSong(songs); + this.shuffling = shufflingSong(new ArrayList<>(songs)); + + notifyDataSetChanged(); + } + + public Child getItem(int id) { + return grouped.get(id); + } + + public List getShuffling() { + return shuffling; + } + + @Override + public int getItemViewType(int position) { + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + private List groupSong(List songs) { + switch (view) { + case Constants.DOWNLOAD_TYPE_TRACK: + return filterSong(filterKey, filterValue, songs.stream().filter(song -> Objects.nonNull(song.getId())).filter(Util.distinctByKey(Child::getId)).collect(Collectors.toList())); + case Constants.DOWNLOAD_TYPE_ALBUM: + return filterSong(filterKey, filterValue, songs.stream().filter(song -> Objects.nonNull(song.getAlbumId())).filter(Util.distinctByKey(Child::getAlbumId)).collect(Collectors.toList())); + case Constants.DOWNLOAD_TYPE_ARTIST: + return filterSong(filterKey, filterValue, songs.stream().filter(song -> Objects.nonNull(song.getArtistId())).filter(Util.distinctByKey(Child::getArtistId)).collect(Collectors.toList())); + case Constants.DOWNLOAD_TYPE_GENRE: + return filterSong(filterKey, filterValue, songs.stream().filter(song -> Objects.nonNull(song.getGenre())).filter(Util.distinctByKey(Child::getGenre)).collect(Collectors.toList())); + case Constants.DOWNLOAD_TYPE_YEAR: + return filterSong(filterKey, filterValue, songs.stream().filter(song -> Objects.nonNull(song.getYear())).filter(Util.distinctByKey(Child::getYear)).collect(Collectors.toList())); + } + + return Collections.emptyList(); + } + + private List filterSong(String filterKey, String filterValue, List songs) { + if (filterValue != null) { + switch (filterKey) { + case Constants.DOWNLOAD_TYPE_TRACK: + return songs.stream().filter(child -> child.getId().equals(filterValue)).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_ALBUM: + return songs.stream().filter(child -> Objects.equals(child.getAlbumId(), filterValue)).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_GENRE: + return songs.stream().filter(child -> Objects.equals(child.getGenre(), filterValue)).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_YEAR: + return songs.stream().filter(child -> Objects.equals(child.getYear(), Integer.valueOf(filterValue))).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_ARTIST: + return songs.stream().filter(child -> Objects.equals(child.getArtistId(), filterValue)).collect(Collectors.toList()); + } + } + + return songs; + } + + private List shufflingSong(List songs) { + if (filterValue == null) { + return songs; + } + + switch (filterKey) { + case Constants.DOWNLOAD_TYPE_TRACK: + return songs.stream().filter(child -> child.getId().equals(filterValue)).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_ALBUM: + return songs.stream().filter(child -> Objects.equals(child.getAlbumId(), filterValue)).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_GENRE: + return songs.stream().filter(child -> Objects.equals(child.getGenre(), filterValue)).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_YEAR: + return songs.stream().filter(child -> Objects.equals(child.getYear(), Integer.valueOf(filterValue))).collect(Collectors.toList()); + case Constants.DOWNLOAD_TYPE_ARTIST: + return songs.stream().filter(child -> Objects.equals(child.getArtistId(), filterValue)).collect(Collectors.toList()); + default: + return songs; + } + } + + private String countSong(String filterKey, String filterValue, List songs) { + if (filterValue != null) { + switch (filterKey) { + case Constants.DOWNLOAD_TYPE_TRACK: + return String.valueOf(songs.stream().filter(child -> child.getId().equals(filterValue)).count()); + case Constants.DOWNLOAD_TYPE_ALBUM: + return String.valueOf(songs.stream().filter(child -> Objects.equals(child.getAlbumId(), filterValue)).count()); + case Constants.DOWNLOAD_TYPE_GENRE: + return String.valueOf(songs.stream().filter(child -> Objects.equals(child.getGenre(), filterValue)).count()); + case Constants.DOWNLOAD_TYPE_YEAR: + return String.valueOf(songs.stream().filter(child -> Objects.equals(child.getYear(), Integer.valueOf(filterValue))).count()); + case Constants.DOWNLOAD_TYPE_ARTIST: + return String.valueOf(songs.stream().filter(child -> Objects.equals(child.getArtistId(), filterValue)).count()); + } + } + + return "0"; + } + + private void initTrackLayout(ViewHolder holder, int position) { + Child song = grouped.get(position); + + holder.item.downloadedItemTitleTextView.setText(song.getTitle()); + holder.item.downloadedItemSubtitleTextView.setText( + holder.itemView.getContext().getString( + R.string.song_subtitle_formatter, + song.getArtist(), + MusicUtil.getReadableDurationString(song.getDuration(), false), + MusicUtil.getReadableAudioQualityString(song) + ) + ); + + holder.item.downloadedItemPreTextView.setText(song.getAlbum()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .into(holder.item.itemCoverImageView); + + holder.item.itemCoverImageView.setVisibility(View.VISIBLE); + holder.item.downloadedItemMoreButton.setVisibility(View.VISIBLE); + holder.item.divider.setVisibility(View.VISIBLE); + + if (position > 0 && grouped.get(position - 1) != null && !Objects.equals(grouped.get(position - 1).getAlbum(), grouped.get(position).getAlbum())) { + holder.item.divider.setPadding(0, (int) holder.itemView.getContext().getResources().getDimension(R.dimen.downloaded_item_padding), 0, 0); + } else { + if (position > 0) holder.item.divider.setVisibility(View.GONE); + } + } + + private void initAlbumLayout(ViewHolder holder, int position) { + Child song = grouped.get(position); + + holder.item.downloadedItemTitleTextView.setText(song.getAlbum()); + holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.download_item_single_subtitle_formatter, countSong(Constants.DOWNLOAD_TYPE_ALBUM, song.getAlbumId(), songs))); + holder.item.downloadedItemPreTextView.setText(song.getArtist()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .into(holder.item.itemCoverImageView); + + holder.item.itemCoverImageView.setVisibility(View.VISIBLE); + holder.item.downloadedItemMoreButton.setVisibility(View.VISIBLE); + holder.item.divider.setVisibility(View.VISIBLE); + + if (position > 0 && grouped.get(position - 1) != null && !Objects.equals(grouped.get(position - 1).getArtist(), grouped.get(position).getArtist())) { + holder.item.divider.setPadding(0, (int) holder.itemView.getContext().getResources().getDimension(R.dimen.downloaded_item_padding), 0, 0); + } else { + if (position > 0) holder.item.divider.setVisibility(View.GONE); + } + } + + private void initArtistLayout(ViewHolder holder, int position) { + Child song = grouped.get(position); + + holder.item.downloadedItemTitleTextView.setText(song.getArtist()); + holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.download_item_single_subtitle_formatter, countSong(Constants.DOWNLOAD_TYPE_ARTIST, song.getArtistId(), songs))); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .into(holder.item.itemCoverImageView); + + holder.item.itemCoverImageView.setVisibility(View.VISIBLE); + holder.item.downloadedItemMoreButton.setVisibility(View.VISIBLE); + holder.item.divider.setVisibility(View.GONE); + } + + private void initGenreLayout(ViewHolder holder, int position) { + Child song = grouped.get(position); + + holder.item.downloadedItemTitleTextView.setText(song.getGenre()); + holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.download_item_single_subtitle_formatter, countSong(Constants.DOWNLOAD_TYPE_GENRE, song.getGenre(), songs))); + + holder.item.itemCoverImageView.setVisibility(View.GONE); + holder.item.downloadedItemMoreButton.setVisibility(View.VISIBLE); + holder.item.divider.setVisibility(View.GONE); + } + + private void initYearLayout(ViewHolder holder, int position) { + Child song = grouped.get(position); + + holder.item.downloadedItemTitleTextView.setText(String.valueOf(song.getYear())); + holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.download_item_single_subtitle_formatter, countSong(Constants.DOWNLOAD_TYPE_YEAR, song.getYear().toString(), songs))); + + holder.item.itemCoverImageView.setVisibility(View.GONE); + holder.item.downloadedItemMoreButton.setVisibility(View.VISIBLE); + holder.item.divider.setVisibility(View.GONE); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHorizontalDownloadBinding item; + + ViewHolder(ItemHorizontalDownloadBinding item) { + super(item.getRoot()); + + this.item = item; + + item.downloadedItemTitleTextView.setSelected(true); + item.downloadedItemSubtitleTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + + item.downloadedItemMoreButton.setOnClickListener(v -> onLongClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + + switch (view) { + case Constants.DOWNLOAD_TYPE_TRACK: + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(grouped)); + bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition()); + click.onMediaClick(bundle); + break; + case Constants.DOWNLOAD_TYPE_ALBUM: + bundle.putString(Constants.DOWNLOAD_TYPE_ALBUM, grouped.get(getBindingAdapterPosition()).getAlbumId()); + click.onAlbumClick(bundle); + break; + case Constants.DOWNLOAD_TYPE_ARTIST: + bundle.putString(Constants.DOWNLOAD_TYPE_ARTIST, grouped.get(getBindingAdapterPosition()).getArtistId()); + click.onArtistClick(bundle); + break; + case Constants.DOWNLOAD_TYPE_GENRE: + bundle.putString(Constants.DOWNLOAD_TYPE_GENRE, grouped.get(getBindingAdapterPosition()).getGenre()); + click.onGenreClick(bundle); + break; + case Constants.DOWNLOAD_TYPE_YEAR: + bundle.putString(Constants.DOWNLOAD_TYPE_YEAR, grouped.get(getBindingAdapterPosition()).getYear().toString()); + click.onYearClick(bundle); + break; + } + } + + private boolean onLongClick() { + ArrayList filteredSongs = new ArrayList<>(); + + Bundle bundle = new Bundle(); + + switch (view) { + case Constants.DOWNLOAD_TYPE_TRACK: + filteredSongs.add(grouped.get(getBindingAdapterPosition())); + break; + case Constants.DOWNLOAD_TYPE_ALBUM: + filteredSongs.addAll(filterSong(Constants.DOWNLOAD_TYPE_ALBUM, grouped.get(getBindingAdapterPosition()).getAlbumId(), songs)); + break; + case Constants.DOWNLOAD_TYPE_ARTIST: + filteredSongs.addAll(filterSong(Constants.DOWNLOAD_TYPE_ARTIST, grouped.get(getBindingAdapterPosition()).getArtistId(), songs)); + break; + case Constants.DOWNLOAD_TYPE_GENRE: + filteredSongs.addAll(filterSong(Constants.DOWNLOAD_TYPE_GENRE, grouped.get(getBindingAdapterPosition()).getGenre(), songs)); + break; + case Constants.DOWNLOAD_TYPE_YEAR: + filteredSongs.addAll(filterSong(Constants.DOWNLOAD_TYPE_YEAR, grouped.get(getBindingAdapterPosition()).getYear().toString(), songs)); + break; + } + + if (filteredSongs.isEmpty()) return false; + + bundle.putParcelableArrayList(Constants.DOWNLOAD_GROUP, new ArrayList<>(filteredSongs)); + bundle.putString(Constants.DOWNLOAD_GROUP_TITLE, item.downloadedItemTitleTextView.getText().toString()); + bundle.putString(Constants.DOWNLOAD_GROUP_SUBTITLE, item.downloadedItemSubtitleTextView.getText().toString()); + click.onDownloadGroupLongClick(bundle); + + return true; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/GenreAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/GenreAdapter.java new file mode 100644 index 0000000..c99193c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/GenreAdapter.java @@ -0,0 +1,76 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLibraryGenreBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.Genre; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.Collections; +import java.util.List; + +public class GenreAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List genres; + + public GenreAdapter(ClickCallback click) { + this.click = click; + this.genres = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLibraryGenreBinding view = ItemLibraryGenreBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Genre genre = genres.get(position); + + holder.item.genreLabel.setText(genre.getGenre()); + } + + @Override + public int getItemCount() { + return genres.size(); + } + + public Genre getItem(int position) { + return genres.get(position); + } + + public void setItems(List genres) { + this.genres = genres; + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLibraryGenreBinding item; + + ViewHolder(ItemLibraryGenreBinding item) { + super(item.getRoot()); + + this.item = item; + + itemView.setOnClickListener(v -> onClick()); + } + + private void onClick() { + Bundle bundle = new Bundle(); + bundle.putString(Constants.MEDIA_BY_GENRE, Constants.MEDIA_BY_GENRE); + bundle.putParcelable(Constants.GENRE_OBJECT, genres.get(getBindingAdapterPosition())); + + click.onGenreClick(bundle); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/GenreCatalogueAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/GenreCatalogueAdapter.java new file mode 100644 index 0000000..835b32b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/GenreCatalogueAdapter.java @@ -0,0 +1,128 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLibraryCatalogueGenreBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.Genre; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class GenreCatalogueAdapter extends RecyclerView.Adapter implements Filterable { + private final ClickCallback click; + + private final Filter filtering = new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + List filteredList = new ArrayList<>(); + + if (constraint == null || constraint.length() == 0) { + filteredList.addAll(genresFull); + } else { + String filterPattern = constraint.toString().toLowerCase().trim(); + + for (Genre item : genresFull) { + if (item.getGenre().toLowerCase().contains(filterPattern)) { + filteredList.add(item); + } + } + } + + FilterResults results = new FilterResults(); + results.values = filteredList; + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + genres.clear(); + if (results.count > 0) genres.addAll((List) results.values); + notifyDataSetChanged(); + } + }; + + private List genres; + private List genresFull; + + public GenreCatalogueAdapter(ClickCallback click) { + this.click = click; + this.genres = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLibraryCatalogueGenreBinding view = ItemLibraryCatalogueGenreBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Genre genre = genres.get(position); + + holder.item.genreLabel.setText(genre.getGenre()); + } + + @Override + public int getItemCount() { + return genres.size(); + } + + public Genre getItem(int position) { + return genres.get(position); + } + + public void setItems(List genres) { + this.genres = genres; + this.genresFull = new ArrayList<>(genres); + notifyDataSetChanged(); + } + + @Override + public Filter getFilter() { + return filtering; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLibraryCatalogueGenreBinding item; + + ViewHolder(ItemLibraryCatalogueGenreBinding item) { + super(item.getRoot()); + + this.item = item; + + itemView.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.MEDIA_BY_GENRE, Constants.MEDIA_BY_GENRE); + bundle.putParcelable(Constants.GENRE_OBJECT, genres.get(getBindingAdapterPosition())); + + click.onGenreClick(bundle); + }); + } + } + + public void sort(String order) { + switch (order) { + case Constants.GENRE_ORDER_BY_NAME: + genres.sort(Comparator.comparing(Genre::getGenre)); + break; + case Constants.GENRE_ORDER_BY_RANDOM: + Collections.shuffle(genres); + break; + } + + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/GridTrackAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/GridTrackAdapter.java new file mode 100644 index 0000000..0816e4f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/GridTrackAdapter.java @@ -0,0 +1,81 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemHomeGridTrackBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.model.Chronology; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class GridTrackAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List items; + + public GridTrackAdapter(ClickCallback click) { + this.click = click; + this.items = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHomeGridTrackBinding view = ItemHomeGridTrackBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Chronology item = items.get(position); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), item.getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .into(holder.item.trackCoverImageView); + } + + @Override + public int getItemCount() { + return items.size(); + } + + public Chronology getItem(int position) { + return items.get(position); + } + + public void setItems(List items) { + this.items = items; + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHomeGridTrackBinding item; + + ViewHolder(ItemHomeGridTrackBinding item) { + super(item.getRoot()); + + this.item = item; + + itemView.setOnClickListener(v -> onClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(items)); + bundle.putBoolean(Constants.MEDIA_CHRONOLOGY, true); + bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition()); + + click.onMediaClick(bundle); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/HomeSectorHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/HomeSectorHorizontalAdapter.java new file mode 100644 index 0000000..71855a7 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/HomeSectorHorizontalAdapter.java @@ -0,0 +1,76 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemHorizontalHomeSectorBinding; +import com.cappielloantonio.tempo.databinding.ItemHorizontalPlaylistDialogTrackBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.model.HomeSector; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.Collections; +import java.util.List; + +public class HomeSectorHorizontalAdapter extends RecyclerView.Adapter { + private List sectors; + + public HomeSectorHorizontalAdapter() { + this.sectors = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHorizontalHomeSectorBinding view = ItemHorizontalHomeSectorBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + HomeSector sector = sectors.get(position); + + holder.item.homeSectorTitleCheckBox.setText(sector.getSectorTitle()); + holder.item.homeSectorTitleCheckBox.setChecked(sector.isVisible()); + } + + @Override + public int getItemCount() { + return sectors.size(); + } + + public List getItems() { + return this.sectors; + } + + public void setItems(List sectors) { + this.sectors = sectors; + notifyDataSetChanged(); + } + + public HomeSector getItem(int id) { + return sectors.get(id); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHorizontalHomeSectorBinding item; + + ViewHolder(ItemHorizontalHomeSectorBinding item) { + super(item.getRoot()); + + this.item = item; + + this.item.homeSectorTitleCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> onCheck(isChecked)); + } + + private void onCheck(boolean isChecked) { + sectors.get(getBindingAdapterPosition()).setVisible(isChecked); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/InternetRadioStationAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/InternetRadioStationAdapter.java new file mode 100644 index 0000000..b4bef70 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/InternetRadioStationAdapter.java @@ -0,0 +1,98 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.media3.common.util.UnstableApi; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemHomeInternetRadioStationBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.Collections; +import java.util.List; + +@UnstableApi +public class InternetRadioStationAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List internetRadioStations; + + public InternetRadioStationAdapter(ClickCallback click) { + this.click = click; + this.internetRadioStations = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHomeInternetRadioStationBinding view = ItemHomeInternetRadioStationBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + InternetRadioStation internetRadioStation = internetRadioStations.get(position); + + holder.item.internetRadioStationTitleTextView.setText(internetRadioStation.getName()); + holder.item.internetRadioStationSubtitleTextView.setText(internetRadioStation.getStreamUrl()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), internetRadioStation.getStreamUrl(), CustomGlideRequest.ResourceType.Radio) + .build() + .into(holder.item.internetRadioStationCoverImageView); + } + + @Override + public int getItemCount() { + return internetRadioStations.size(); + } + + public void setItems(List internetRadioStations) { + this.internetRadioStations = internetRadioStations; + notifyDataSetChanged(); + } + + public InternetRadioStation getItem(int position) { + return internetRadioStations.get(position); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHomeInternetRadioStationBinding item; + + ViewHolder(ItemHomeInternetRadioStationBinding item) { + super(item.getRoot()); + + this.item = item; + + item.internetRadioStationTitleTextView.setSelected(true); + item.internetRadioStationSubtitleTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + + item.internetRadioStationMoreButton.setOnClickListener(v -> onLongClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.INTERNET_RADIO_STATION_OBJECT, internetRadioStations.get(getBindingAdapterPosition())); + + click.onInternetRadioStationClick(bundle); + } + + private boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.INTERNET_RADIO_STATION_OBJECT, internetRadioStations.get(getBindingAdapterPosition())); + + click.onInternetRadioStationLongClick(bundle); + + return true; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/MusicDirectoryAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/MusicDirectoryAdapter.java new file mode 100644 index 0000000..f186bee --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/MusicDirectoryAdapter.java @@ -0,0 +1,111 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.media3.common.util.UnstableApi; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLibraryMusicDirectoryBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@UnstableApi +public class MusicDirectoryAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List children; + + public MusicDirectoryAdapter(ClickCallback click) { + this.click = click; + this.children = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLibraryMusicDirectoryBinding view = ItemLibraryMusicDirectoryBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Child child = children.get(position); + + holder.item.musicDirectoryTitleTextView.setText(child.getTitle()); + + CustomGlideRequest.ResourceType type = child.isDir() + ? CustomGlideRequest.ResourceType.Directory + : CustomGlideRequest.ResourceType.Song; + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), child.getCoverArtId(), type) + .build() + .into(holder.item.musicDirectoryCoverImageView); + + holder.item.musicDirectoryMoreButton.setVisibility(child.isDir() ? View.VISIBLE : View.INVISIBLE); + holder.item.musicDirectoryPlayButton.setVisibility(child.isDir() ? View.INVISIBLE : View.VISIBLE); + } + + @Override + public int getItemCount() { + return children.size(); + } + + public void setItems(List children) { + this.children = children != null ? children : Collections.emptyList(); + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLibraryMusicDirectoryBinding item; + + ViewHolder(ItemLibraryMusicDirectoryBinding item) { + super(item.getRoot()); + + this.item = item; + + item.musicDirectoryTitleTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + + item.musicDirectoryMoreButton.setOnClickListener(v -> onClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + + if (children.get(getBindingAdapterPosition()).isDir()) { + bundle.putString(Constants.MUSIC_DIRECTORY_ID, children.get(getBindingAdapterPosition()).getId()); + click.onMusicDirectoryClick(bundle); + } else { + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(children)); + bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition()); + click.onMediaClick(bundle); + } + } + + private boolean onLongClick() { + if (!children.get(getBindingAdapterPosition()).isDir()) { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.TRACK_OBJECT, children.get(getBindingAdapterPosition())); + + click.onMediaLongClick(bundle); + + return true; + } else { + return false; + } + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/MusicFolderAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/MusicFolderAdapter.java new file mode 100644 index 0000000..c5435be --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/MusicFolderAdapter.java @@ -0,0 +1,85 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.media3.common.util.UnstableApi; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLibraryMusicFolderBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.MusicFolder; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.Collections; +import java.util.List; + +@UnstableApi +public class MusicFolderAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List musicFolders; + + public MusicFolderAdapter(ClickCallback click) { + this.click = click; + this.musicFolders = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLibraryMusicFolderBinding view = ItemLibraryMusicFolderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + MusicFolder musicFolder = musicFolders.get(position); + + holder.item.musicFolderTitleTextView.setText(musicFolder.getName()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), musicFolder.getName(), CustomGlideRequest.ResourceType.Folder) + .build() + .into(holder.item.musicFolderCoverImageView); + } + + @Override + public int getItemCount() { + return musicFolders.size(); + } + + public void setItems(List musicFolders) { + this.musicFolders = musicFolders; + notifyDataSetChanged(); + } + + public MusicFolder getItem(int position) { + return musicFolders.get(position); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLibraryMusicFolderBinding item; + + ViewHolder(ItemLibraryMusicFolderBinding item) { + super(item.getRoot()); + + this.item = item; + + item.musicFolderTitleTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + + item.musicFolderMoreButton.setOnClickListener(v -> onClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.MUSIC_FOLDER_OBJECT, musicFolders.get(getBindingAdapterPosition())); + click.onMusicFolderClick(bundle); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/MusicIndexAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/MusicIndexAdapter.java new file mode 100644 index 0000000..77402f6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/MusicIndexAdapter.java @@ -0,0 +1,87 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.media3.common.util.UnstableApi; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLibraryMusicIndexBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.helper.recyclerview.FastScrollbar; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.Artist; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@UnstableApi +public class MusicIndexAdapter extends RecyclerView.Adapter implements FastScrollbar.BubbleTextGetter { + private final ClickCallback click; + + private List artists; + + public MusicIndexAdapter(ClickCallback click) { + this.click = click; + this.artists = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLibraryMusicIndexBinding view = ItemLibraryMusicIndexBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Artist artist = artists.get(position); + + holder.item.musicIndexTitleTextView.setText(artist.getName()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), artist.getName(), CustomGlideRequest.ResourceType.Directory) + .build() + .into(holder.item.musicIndexCoverImageView); + } + + @Override + public int getItemCount() { + return artists.size(); + } + + public void setItems(List artists) { + this.artists = artists; + notifyDataSetChanged(); + } + + @Override + public String getTextToShowInBubble(int pos) { + return artists != null && !artists.isEmpty() ? Character.toString(Objects.requireNonNull(artists.get(pos).getName().toUpperCase()).charAt(0)) : null; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLibraryMusicIndexBinding item; + + ViewHolder(ItemLibraryMusicIndexBinding item) { + super(item.getRoot()); + + this.item = item; + + item.musicIndexTitleTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + item.musicIndexMoreButton.setOnClickListener(v -> onClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putString(Constants.MUSIC_DIRECTORY_ID, artists.get(getBindingAdapterPosition()).getId()); + click.onMusicIndexClick(bundle); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlayerSongQueueAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlayerSongQueueAdapter.java new file mode 100644 index 0000000..4db3a57 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlayerSongQueueAdapter.java @@ -0,0 +1,249 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.media3.session.MediaBrowser; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.RequestBuilder; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.ItemPlayerQueueSongBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.interfaces.MediaIndexCallback; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class PlayerSongQueueAdapter extends RecyclerView.Adapter { + private static final String TAG = "PlayerSongQueueAdapter"; + private final ClickCallback click; + + private ListenableFuture mediaBrowserListenableFuture; + private List songs; + + private String currentPlayingId; + private boolean isPlaying; + private List currentPlayingPositions = Collections.emptyList(); + + public PlayerSongQueueAdapter(ClickCallback click) { + this.click = click; + this.songs = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemPlayerQueueSongBinding view = ItemPlayerQueueSongBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Child song = songs.get(holder.getLayoutPosition()); + + holder.item.queueSongTitleTextView.setText(song.getTitle()); + holder.item.queueSongSubtitleTextView.setText( + holder.itemView.getContext().getString( + R.string.song_subtitle_formatter, + song.getArtist(), + MusicUtil.getReadableDurationString(song.getDuration(), false), + MusicUtil.getReadableAudioQualityString(song) + ) + ); + + RequestBuilder thumbnail = CustomGlideRequest.Builder + .from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .sizeMultiplier(0.1f); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .thumbnail(thumbnail) + .into(holder.item.queueSongCoverImageView); + + MediaManager.getCurrentIndex(mediaBrowserListenableFuture, new MediaIndexCallback() { + @Override + public void onRecovery(int index) { + if (holder.getLayoutPosition() < index) { + holder.item.queueSongTitleTextView.setAlpha(0.2f); + holder.item.queueSongSubtitleTextView.setAlpha(0.2f); + holder.item.ratingIndicatorImageView.setAlpha(0.2f); + } else { + holder.item.queueSongTitleTextView.setAlpha(1.0f); + holder.item.queueSongSubtitleTextView.setAlpha(1.0f); + holder.item.ratingIndicatorImageView.setAlpha(1.0f); + } + } + }); + + if (Preferences.showItemRating()) { + if (song.getStarred() == null && song.getUserRating() == null) { + holder.item.ratingIndicatorImageView.setVisibility(View.GONE); + } + + holder.item.preferredIcon.setVisibility(song.getStarred() != null ? View.VISIBLE : View.GONE); + holder.item.ratingBarLayout.setVisibility(song.getUserRating() != null ? View.VISIBLE : View.GONE); + + if (song.getUserRating() != null) { + holder.item.oneStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 1 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.twoStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 2 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.threeStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 3 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.fourStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 4 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.fiveStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 5 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + } + } else { + holder.item.ratingIndicatorImageView.setVisibility(View.GONE); + } + holder.itemView.setOnClickListener(v -> { + mediaBrowserListenableFuture.addListener(() -> { + try { + MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); + int pos = holder.getBindingAdapterPosition(); + Child s = songs.get(pos); + if (currentPlayingId != null && currentPlayingId.equals(s.getId())) { + if (isPlaying) { + mediaBrowser.pause(); + } else { + mediaBrowser.play(); + } + } else { + mediaBrowser.seekTo(pos, 0); + mediaBrowser.play(); + } + } catch (Exception e) { + Log.w(TAG, "Error obtaining MediaBrowser", e); + } + }, MoreExecutors.directExecutor()); + + }); + bindPlaybackState(holder, song); + } + + private void bindPlaybackState(@NonNull PlayerSongQueueAdapter.ViewHolder holder, @NonNull Child song) { + boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId()); + + if (isCurrent) { + holder.item.playPauseIcon.setVisibility(View.VISIBLE); + if (isPlaying) { + holder.item.playPauseIcon.setImageResource(R.drawable.ic_pause); + } else { + holder.item.playPauseIcon.setImageResource(R.drawable.ic_play); + } + holder.item.coverArtOverlay.setVisibility(View.VISIBLE); + } else { + holder.item.playPauseIcon.setVisibility(View.INVISIBLE); + holder.item.coverArtOverlay.setVisibility(View.INVISIBLE); + } + } + + public List getItems() { + return this.songs; + } + + public void setItems(List songs) { + this.songs = songs; + notifyDataSetChanged(); + } + + @Override + public int getItemCount() { + if (songs == null) { + return 0; + } + return songs.size(); + } + + @Override + public long getItemId(int position) { + return position; + } + + public void setMediaBrowserListenableFuture(ListenableFuture mediaBrowserListenableFuture) { + this.mediaBrowserListenableFuture = mediaBrowserListenableFuture; + } + + public void setPlaybackState(String mediaId, boolean playing) { + String oldId = this.currentPlayingId; + boolean oldPlaying = this.isPlaying; + List oldPositions = currentPlayingPositions; + + this.currentPlayingId = mediaId; + this.isPlaying = playing; + + if (Objects.equals(oldId, mediaId) && oldPlaying == playing) { + List newPositionsCheck = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList(); + if (oldPositions.equals(newPositionsCheck)) { + return; + } + } + + currentPlayingPositions = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList(); + + for (int pos : oldPositions) { + if (pos >= 0 && pos < songs.size()) { + notifyItemChanged(pos, "payload_playback"); + } + } + for (int pos : currentPlayingPositions) { + if (!oldPositions.contains(pos) && pos >= 0 && pos < songs.size()) { + notifyItemChanged(pos, "payload_playback"); + } + } + } + + private List findPositionsById(String id) { + if (id == null) return Collections.emptyList(); + List positions = new ArrayList<>(); + for (int i = 0; i < songs.size(); i++) { + if (id.equals(songs.get(i).getId())) { + positions.add(i); + } + } + return positions; + } + + public Child getItem(int id) { + return songs.get(id); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemPlayerQueueSongBinding item; + + ViewHolder(ItemPlayerQueueSongBinding item) { + super(item.getRoot()); + + this.item = item; + + item.queueSongTitleTextView.setSelected(true); + item.queueSongSubtitleTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs)); + bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition()); + + click.onMediaClick(bundle); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlaylistDialogHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlaylistDialogHorizontalAdapter.java new file mode 100644 index 0000000..67bce7a --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlaylistDialogHorizontalAdapter.java @@ -0,0 +1,79 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.ItemHorizontalPlaylistDialogBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.Playlist; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.Collections; +import java.util.List; + +public class PlaylistDialogHorizontalAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List playlists; + + public PlaylistDialogHorizontalAdapter(ClickCallback click) { + this.click = click; + this.playlists = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHorizontalPlaylistDialogBinding view = ItemHorizontalPlaylistDialogBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Playlist playlist = playlists.get(position); + + holder.item.playlistDialogTitleTextView.setText(playlist.getName()); + holder.item.playlistDialogCountTextView.setText(holder.itemView.getContext().getString(R.string.playlist_counted_tracks, playlist.getSongCount(), MusicUtil.getReadableDurationString(playlist.getDuration(), false))); + } + + @Override + public int getItemCount() { + return playlists.size(); + } + + public void setItems(List playlists) { + this.playlists = playlists; + notifyDataSetChanged(); + } + + public Playlist getItem(int id) { + return playlists.get(id); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHorizontalPlaylistDialogBinding item; + + ViewHolder(ItemHorizontalPlaylistDialogBinding item) { + super(item.getRoot()); + + this.item = item; + + item.playlistDialogTitleTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.PLAYLIST_OBJECT, playlists.get(getBindingAdapterPosition())); + + click.onPlaylistClick(bundle); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlaylistDialogSongHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlaylistDialogSongHorizontalAdapter.java new file mode 100644 index 0000000..98f8541 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlaylistDialogSongHorizontalAdapter.java @@ -0,0 +1,74 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemHorizontalPlaylistDialogTrackBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.Collections; +import java.util.List; + +public class PlaylistDialogSongHorizontalAdapter extends RecyclerView.Adapter { + private List songs; + + public PlaylistDialogSongHorizontalAdapter() { + this.songs = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHorizontalPlaylistDialogTrackBinding view = ItemHorizontalPlaylistDialogTrackBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Child song = songs.get(position); + + holder.item.playlistDialogSongTitleTextView.setText(song.getTitle()); + holder.item.playlistDialogAlbumArtistTextView.setText(song.getArtist()); + holder.item.playlistDialogSongDurationTextView.setText(MusicUtil.getReadableDurationString(song.getDuration(), false)); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .into(holder.item.playlistDialogSongCoverImageView); + } + + @Override + public int getItemCount() { + return songs.size(); + } + + public List getItems() { + return this.songs; + } + + public void setItems(List songs) { + this.songs = songs; + notifyDataSetChanged(); + } + + public Child getItem(int id) { + return songs.get(id); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + ItemHorizontalPlaylistDialogTrackBinding item; + + ViewHolder(ItemHorizontalPlaylistDialogTrackBinding item) { + super(item.getRoot()); + + this.item = item; + + item.playlistDialogSongTitleTextView.setSelected(true); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlaylistHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlaylistHorizontalAdapter.java new file mode 100644 index 0000000..baa4005 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PlaylistHorizontalAdapter.java @@ -0,0 +1,151 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.ItemHorizontalPlaylistBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.Playlist; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class PlaylistHorizontalAdapter extends RecyclerView.Adapter implements Filterable { + private final ClickCallback click; + + private List playlists; + private List playlistsFull; + + private final Filter filtering = new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + List filteredList = new ArrayList<>(); + + if (constraint == null || constraint.length() == 0) { + filteredList.addAll(playlistsFull); + } else { + String filterPattern = constraint.toString().toLowerCase().trim(); + + for (Playlist item : playlistsFull) { + if (item.getName().toLowerCase().contains(filterPattern)) { + filteredList.add(item); + } + } + } + + FilterResults results = new FilterResults(); + results.values = filteredList; + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + playlists.clear(); + if (results.count > 0) playlists.addAll((List) results.values); + notifyDataSetChanged(); + } + }; + + public PlaylistHorizontalAdapter(ClickCallback click) { + this.click = click; + this.playlists = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHorizontalPlaylistBinding view = ItemHorizontalPlaylistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Playlist playlist = playlists.get(position); + + holder.item.playlistTitleTextView.setText(playlist.getName()); + holder.item.playlistSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.playlist_counted_tracks, playlist.getSongCount(), MusicUtil.getReadableDurationString(playlist.getDuration(), false))); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), playlist.getCoverArtId(), CustomGlideRequest.ResourceType.Playlist) + .build() + .into(holder.item.playlistCoverImageView); + } + + @Override + public int getItemCount() { + return playlists.size(); + } + + public Playlist getItem(int id) { + return playlists.get(id); + } + + public void setItems(List playlists) { + this.playlists = playlists; + this.playlistsFull = new ArrayList<>(playlists); + notifyDataSetChanged(); + } + + @Override + public Filter getFilter() { + return filtering; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHorizontalPlaylistBinding item; + + ViewHolder(ItemHorizontalPlaylistBinding item) { + super(item.getRoot()); + + this.item = item; + item.playlistTitleTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + + item.playlistMoreButton.setOnClickListener(v -> onLongClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.PLAYLIST_OBJECT, playlists.get(getBindingAdapterPosition())); + + click.onPlaylistClick(bundle); + } + + public boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.PLAYLIST_OBJECT, playlists.get(getBindingAdapterPosition())); + + click.onPlaylistLongClick(bundle); + + return true; + } + } + + public void sort(String order) { + switch (order) { + case Constants.PLAYLIST_ORDER_BY_NAME: + playlists.sort(Comparator.comparing(Playlist::getName)); + break; + case Constants.PLAYLIST_ORDER_BY_RANDOM: + Collections.shuffle(playlists); + break; + } + + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PodcastChannelCatalogueAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PodcastChannelCatalogueAdapter.java new file mode 100644 index 0000000..3eb6bc0 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PodcastChannelCatalogueAdapter.java @@ -0,0 +1,143 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemHomeCataloguePodcastChannelBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.PodcastChannel; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class PodcastChannelCatalogueAdapter extends RecyclerView.Adapter implements Filterable { + private final ClickCallback click; + private final Filter filtering = new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + List filteredList = new ArrayList<>(); + + if (constraint == null || constraint.length() == 0) { + filteredList.addAll(podcastChannelsFull); + } else { + String filterPattern = constraint.toString().toLowerCase().trim(); + + for (PodcastChannel item : podcastChannelsFull) { + if (item.getTitle().toLowerCase().contains(filterPattern)) { + filteredList.add(item); + } + } + } + + FilterResults results = new FilterResults(); + results.values = filteredList; + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + podcastChannels.clear(); + if (results.count > 0) podcastChannels.addAll((List) results.values); + notifyDataSetChanged(); + } + }; + + private List podcastChannels; + private List podcastChannelsFull; + + public PodcastChannelCatalogueAdapter(ClickCallback click) { + this.click = click; + this.podcastChannels = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHomeCataloguePodcastChannelBinding view = ItemHomeCataloguePodcastChannelBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + PodcastChannel podcastChannel = podcastChannels.get(position); + + holder.item.podcastChannelTitleLabel.setText(podcastChannel.getTitle()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), podcastChannel.getCoverArtId(), CustomGlideRequest.ResourceType.Podcast) + .build() + .into(holder.item.podcastChannelCatalogueCoverImageView); + } + + @Override + public int getItemCount() { + return podcastChannels.size(); + } + + public PodcastChannel getItem(int position) { + return podcastChannels.get(position); + } + + public void setItems(List podcastChannels) { + this.podcastChannels = podcastChannels; + this.podcastChannelsFull = new ArrayList<>(podcastChannels); + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public Filter getFilter() { + return filtering; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHomeCataloguePodcastChannelBinding item; + + ViewHolder(ItemHomeCataloguePodcastChannelBinding item) { + super(item.getRoot()); + + this.item = item; + + item.podcastChannelTitleLabel.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + } + + private void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.PODCAST_CHANNEL_OBJECT, podcastChannels.get(getBindingAdapterPosition())); + + click.onPodcastChannelClick(bundle); + } + + private boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.PODCAST_CHANNEL_OBJECT, podcastChannels.get(getBindingAdapterPosition())); + + click.onPodcastChannelLongClick(bundle); + + return true; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PodcastChannelHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PodcastChannelHorizontalAdapter.java new file mode 100644 index 0000000..dfdfab3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PodcastChannelHorizontalAdapter.java @@ -0,0 +1,97 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemHorizontalPodcastChannelBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.PodcastChannel; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.Collections; +import java.util.List; + +public class PodcastChannelHorizontalAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List podcastChannels; + + public PodcastChannelHorizontalAdapter(ClickCallback click) { + this.click = click; + this.podcastChannels = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHorizontalPodcastChannelBinding view = ItemHorizontalPodcastChannelBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + PodcastChannel podcastChannel = podcastChannels.get(position); + + holder.item.podcastChannelTitleTextView.setText(podcastChannel.getTitle()); + holder.item.podcastChannelDescriptionTextView.setText(MusicUtil.getReadableString(podcastChannel.getDescription())); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), podcastChannel.getCoverArtId(), CustomGlideRequest.ResourceType.Podcast) + .build() + .into(holder.item.podcastChannelCoverImageView); + } + + @Override + public int getItemCount() { + return podcastChannels.size(); + } + + public void setItems(List podcastChannels) { + this.podcastChannels = podcastChannels; + notifyDataSetChanged(); + } + + public PodcastChannel getItem(int id) { + return podcastChannels.get(id); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHorizontalPodcastChannelBinding item; + + ViewHolder(ItemHorizontalPodcastChannelBinding item) { + super(item.getRoot()); + + this.item = item; + + item.podcastChannelTitleTextView.setSelected(true); + item.podcastChannelDescriptionTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + + item.podcastChannelMoreButton.setOnClickListener(v -> onLongClick()); + } + + private void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.PODCAST_CHANNEL_OBJECT, podcastChannels.get(getBindingAdapterPosition())); + + click.onPodcastChannelClick(bundle); + } + + private boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.PODCAST_CHANNEL_OBJECT, podcastChannels.get(getBindingAdapterPosition())); + + click.onPodcastChannelLongClick(bundle); + + return true; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PodcastEpisodeAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PodcastEpisodeAdapter.java new file mode 100644 index 0000000..79f9536 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/PodcastEpisodeAdapter.java @@ -0,0 +1,150 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.ItemHomePodcastEpisodeBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class PodcastEpisodeAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List podcastEpisodes; + private List podcastEpisodesFull; + + public PodcastEpisodeAdapter(ClickCallback click) { + this.click = click; + this.podcastEpisodes = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHomePodcastEpisodeBinding view = ItemHomePodcastEpisodeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + PodcastEpisode podcastEpisode = podcastEpisodes.get(position); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MMM d"); + + holder.item.podcastTitleLabel.setText(podcastEpisode.getTitle()); + holder.item.podcastSubtitleLabel.setText(podcastEpisode.getArtist()); + holder.item.podcastReleasesAndDurationLabel.setText(holder.itemView.getContext().getString(R.string.podcast_release_date_duration_formatter, simpleDateFormat.format(podcastEpisode.getPublishDate()), MusicUtil.getReadablePodcastDurationString(podcastEpisode.getDuration()))); + holder.item.podcastDescriptionText.setText(MusicUtil.getReadableString(podcastEpisode.getDescription())); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), podcastEpisode.getCoverArtId(), CustomGlideRequest.ResourceType.Podcast) + .build() + .into(holder.item.podcastCoverImageView); + + holder.item.podcastPlayButton.setEnabled(podcastEpisode.getStatus().equals("completed")); + holder.item.podcastMoreButton.setVisibility(podcastEpisode.getStatus().equals("completed") ? View.VISIBLE : View.GONE); + holder.item.podcastDownloadRequestButton.setVisibility(podcastEpisode.getStatus().equals("completed") ? View.GONE : View.VISIBLE); + } + + @Override + public int getItemCount() { + return podcastEpisodes.size(); + } + + public void setItems(List podcastEpisodes) { + this.podcastEpisodesFull = podcastEpisodes; + this.podcastEpisodes = podcastEpisodesFull.stream().filter(podcastEpisode -> Objects.equals(podcastEpisode.getStatus(), "completed")).collect(Collectors.toList()); + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHomePodcastEpisodeBinding item; + + ViewHolder(ItemHomePodcastEpisodeBinding item) { + super(item.getRoot()); + + this.item = item; + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> openMore()); + + item.podcastPlayButton.setOnClickListener(v -> onClick()); + item.podcastMoreButton.setOnClickListener(v -> openMore()); + item.podcastDownloadRequestButton.setOnClickListener(v -> requestDownload()); + } + + public void onClick() { + PodcastEpisode podcastEpisode = podcastEpisodes.get(getBindingAdapterPosition()); + + if (podcastEpisode.getStatus().equals("completed")) { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.PODCAST_OBJECT, podcastEpisodes.get(getBindingAdapterPosition())); + + click.onPodcastEpisodeClick(bundle); + } + } + + private boolean openMore() { + PodcastEpisode podcastEpisode = podcastEpisodes.get(getBindingAdapterPosition()); + + if (podcastEpisode.getStatus().equals("completed")) { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.PODCAST_OBJECT, podcastEpisodes.get(getBindingAdapterPosition())); + + click.onPodcastEpisodeLongClick(bundle); + + return true; + } + + return false; + } + + public void requestDownload() { + PodcastEpisode podcastEpisode = podcastEpisodes.get(getBindingAdapterPosition()); + + if (!podcastEpisode.getStatus().equals("completed")) { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.PODCAST_OBJECT, podcastEpisodes.get(getBindingAdapterPosition())); + + click.onPodcastEpisodeAltClick(bundle); + } + } + } + + public void sort(String order) { + switch (order) { + case Constants.PODCAST_FILTER_BY_DOWNLOAD: + podcastEpisodes = podcastEpisodesFull.stream().filter(podcastEpisode -> Objects.equals(podcastEpisode.getStatus(), "completed")).collect(Collectors.toList()); + break; + case Constants.PODCAST_FILTER_BY_ALL: + podcastEpisodes = podcastEpisodesFull; + break; + } + + notifyDataSetChanged(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ServerAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ServerAdapter.java new file mode 100644 index 0000000..c78c7ae --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ServerAdapter.java @@ -0,0 +1,86 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemLoginServerBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.model.Server; + +import java.util.ArrayList; +import java.util.List; + +public class ServerAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List servers; + + public ServerAdapter(ClickCallback click) { + this.click = click; + this.servers = new ArrayList<>(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemLoginServerBinding view = ItemLoginServerBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Server server = servers.get(position); + + holder.item.serverNameTextView.setText(server.getServerName()); + holder.item.serverAddressTextView.setText(server.getAddress()); + } + + @Override + public int getItemCount() { + return servers.size(); + } + + public void setItems(List servers) { + this.servers = servers; + notifyDataSetChanged(); + } + + public Server getItem(int id) { + return servers.get(id); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemLoginServerBinding item; + + ViewHolder(ItemLoginServerBinding item) { + super(item.getRoot()); + + this.item = item; + + item.serverNameTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable("server_object", servers.get(getBindingAdapterPosition())); + + click.onServerClick(bundle); + } + + public boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable("server_object", servers.get(getBindingAdapterPosition())); + + click.onServerLongClick(bundle); + + return true; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ShareHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ShareHorizontalAdapter.java new file mode 100644 index 0000000..213914f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ShareHorizontalAdapter.java @@ -0,0 +1,99 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.ItemHorizontalShareBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.Share; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.UIUtil; + +import java.util.Collections; +import java.util.List; + +public class ShareHorizontalAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List shares; + + public ShareHorizontalAdapter(ClickCallback click) { + this.click = click; + this.shares = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHorizontalShareBinding view = ItemHorizontalShareBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Share share = shares.get(position); + + holder.item.shareTitleTextView.setText(share.getDescription()); + holder.item.shareSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.share_subtitle_item, UIUtil.getReadableDate(share.getExpires()))); + + if (share.getEntries() != null && !share.getEntries().isEmpty()) CustomGlideRequest.Builder + .from(holder.itemView.getContext(), share.getEntries().get(0).getCoverArtId(), CustomGlideRequest.ResourceType.Album) + .build() + .into(holder.item.shareCoverImageView); + } + + @Override + public int getItemCount() { + return shares.size(); + } + + public void setItems(List shares) { + this.shares = shares; + notifyDataSetChanged(); + } + + public Share getItem(int id) { + return shares.get(id); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHorizontalShareBinding item; + + ViewHolder(ItemHorizontalShareBinding item) { + super(item.getRoot()); + + this.item = item; + + item.shareTitleTextView.setSelected(true); + item.shareSubtitleTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + + item.shareButton.setOnClickListener(v -> onLongClick()); + } + + private void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.SHARE_OBJECT, shares.get(getBindingAdapterPosition())); + + click.onShareClick(bundle); + } + + private boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.SHARE_OBJECT, shares.get(getBindingAdapterPosition())); + + click.onShareLongClick(bundle); + + return true; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SimilarTrackAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SimilarTrackAdapter.java new file mode 100644 index 0000000..8c720c9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SimilarTrackAdapter.java @@ -0,0 +1,92 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemHomeSimilarTrackBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; + +import java.util.Collections; +import java.util.List; + +public class SimilarTrackAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List songs; + + public SimilarTrackAdapter(ClickCallback click) { + this.click = click; + this.songs = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHomeSimilarTrackBinding view = ItemHomeSimilarTrackBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Child song = songs.get(position); + + holder.item.titleTrackLabel.setText(song.getTitle()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .into(holder.item.trackCoverImageView); + } + + @Override + public int getItemCount() { + return songs.size(); + } + + public Child getItem(int position) { + return songs.get(position); + } + + public void setItems(List songs) { + this.songs = songs; + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHomeSimilarTrackBinding item; + + ViewHolder(ItemHomeSimilarTrackBinding item) { + super(item.getRoot()); + + this.item = item; + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition())); + bundle.putBoolean(Constants.MEDIA_MIX, true); + + click.onMediaClick(bundle); + } + + public boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition())); + + click.onMediaLongClick(bundle); + + return true; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java new file mode 100644 index 0000000..1d78f2e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java @@ -0,0 +1,386 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; + +import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.lifecycle.LifecycleOwner; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.ItemHorizontalTrackBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.DiscTitle; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +@UnstableApi +public class SongHorizontalAdapter extends RecyclerView.Adapter implements Filterable { + private final ClickCallback click; + private final boolean showCoverArt; + private final boolean showAlbum; + private final AlbumID3 album; + + private List songsFull; + private List songs; + private String currentFilter; + + private String currentPlayingId; + private boolean isPlaying; + private List currentPlayingPositions = Collections.emptyList(); + private ListenableFuture mediaBrowserListenableFuture; + + private final Filter filtering = new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + List filteredList = new ArrayList<>(); + + if (constraint == null || constraint.length() == 0) { + filteredList.addAll(songsFull); + } else { + String filterPattern = constraint.toString().toLowerCase().trim(); + currentFilter = filterPattern; + + for (Child item : songsFull) { + if (item.getTitle().toLowerCase().contains(filterPattern)) { + filteredList.add(item); + } + } + } + + FilterResults results = new FilterResults(); + results.values = filteredList; + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + songs = (List) results.values; + notifyDataSetChanged(); + + for (int pos : currentPlayingPositions) { + if (pos >= 0 && pos < songs.size()) { + notifyItemChanged(pos, "payload_playback"); + } + } + } + }; + + public SongHorizontalAdapter(LifecycleOwner lifecycleOwner, ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) { + this.click = click; + this.showCoverArt = showCoverArt; + this.showAlbum = showAlbum; + this.songs = Collections.emptyList(); + this.songsFull = Collections.emptyList(); + this.currentFilter = ""; + this.album = album; + setHasStableIds(false); + + if (lifecycleOwner != null) { + MappingUtil.observeExternalAudioRefresh(lifecycleOwner, this::handleExternalAudioRefresh); + } + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHorizontalTrackBinding view = ItemHorizontalTrackBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List payloads) { + if (!payloads.isEmpty() && payloads.contains("payload_playback")) { + bindPlaybackState(holder, songs.get(position)); + } else { + super.onBindViewHolder(holder, position, payloads); + } + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + Child song = songs.get(position); + + holder.item.searchResultSongTitleTextView.setText(song.getTitle()); + + holder.item.searchResultSongSubtitleTextView.setText( + holder.itemView.getContext().getString( + R.string.song_subtitle_formatter, + this.showAlbum ? + song.getAlbum() : + song.getArtist(), + MusicUtil.getReadableDurationString(song.getDuration(), false), + MusicUtil.getReadableAudioQualityString(song) + ) + ); + + holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack())); + + if (Preferences.getDownloadDirectoryUri() == null) { + if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) { + holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE); + } else { + holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE); + } + } else { + if (ExternalAudioReader.getUri(song) != null) { + holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE); + } else { + holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE); + } + } + + if (showCoverArt) CustomGlideRequest.Builder + .from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .into(holder.item.songCoverImageView); + + holder.item.trackNumberTextView.setVisibility(showCoverArt ? View.INVISIBLE : View.VISIBLE); + holder.item.songCoverImageView.setVisibility(showCoverArt ? View.VISIBLE : View.INVISIBLE); + + if (!showCoverArt && + (position == 0 || + (position > 0 && songs.get(position - 1) != null && + songs.get(position - 1).getDiscNumber() != null && + songs.get(position).getDiscNumber() != null && + songs.get(position - 1).getDiscNumber() < songs.get(position).getDiscNumber() + ) + ) + ) { + holder.item.differentDiskDividerSector.setVisibility(View.VISIBLE); + + if (songs.get(position).getDiscNumber() != null && !Objects.requireNonNull(songs.get(position).getDiscNumber()).toString().isBlank()) { + holder.item.discTitleTextView.setText(holder.itemView.getContext().getString(R.string.disc_titleless, songs.get(position).getDiscNumber().toString())); + } + + if (album.getDiscTitles() != null) { + Optional discTitle = album.getDiscTitles().stream().filter(title -> Objects.equals(title.getDisc(), songs.get(position).getDiscNumber())).findFirst(); + + if (discTitle.isPresent() && discTitle.get().getDisc() != null && discTitle.get().getTitle() != null && !discTitle.get().getTitle().isEmpty()) { + holder.item.discTitleTextView.setText(holder.itemView.getContext().getString(R.string.disc_titlefull, discTitle.get().getDisc().toString() , discTitle.get().getTitle())); + } + } + } + + if (Preferences.showItemRating()) { + if (song.getStarred() == null && song.getUserRating() == null) { + holder.item.ratingIndicatorImageView.setVisibility(View.GONE); + } + + holder.item.preferredIcon.setVisibility(song.getStarred() != null ? View.VISIBLE : View.GONE); + holder.item.ratingBarLayout.setVisibility(song.getUserRating() != null ? View.VISIBLE : View.GONE); + + if (song.getUserRating() != null) { + holder.item.oneStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 1 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.twoStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 2 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.threeStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 3 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.fourStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 4 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + holder.item.fiveStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 5 ? R.drawable.ic_star : R.drawable.ic_star_outlined)); + } + } else { + holder.item.ratingIndicatorImageView.setVisibility(View.GONE); + } + + bindPlaybackState(holder, song); + } + + private void handleExternalAudioRefresh() { + if (Preferences.getDownloadDirectoryUri() != null) { + notifyDataSetChanged(); + } + } + + private void bindPlaybackState(@NonNull ViewHolder holder, @NonNull Child song) { + boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId()); + + if (isCurrent) { + holder.item.playPauseIcon.setVisibility(View.VISIBLE); + if (isPlaying) { + holder.item.playPauseIcon.setImageResource(R.drawable.ic_pause); + } else { + holder.item.playPauseIcon.setImageResource(R.drawable.ic_play); + } + if (!showCoverArt) { + holder.item.trackNumberTextView.setVisibility(View.INVISIBLE); + } else { + holder.item.coverArtOverlay.setVisibility(View.VISIBLE); + } + } else { + holder.item.playPauseIcon.setVisibility(View.INVISIBLE); + if (!showCoverArt) { + holder.item.trackNumberTextView.setVisibility(View.VISIBLE); + } else { + holder.item.coverArtOverlay.setVisibility(View.INVISIBLE); + } + } + } + + @Override + public int getItemCount() { + return songs.size(); + } + + public void setItems(List songs) { + this.songsFull = songs != null ? songs : Collections.emptyList(); + filtering.filter(currentFilter); + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + public void setPlaybackState(String mediaId, boolean playing) { + String oldId = this.currentPlayingId; + boolean oldPlaying = this.isPlaying; + List oldPositions = currentPlayingPositions; + + this.currentPlayingId = mediaId; + this.isPlaying = playing; + + if (Objects.equals(oldId, mediaId) && oldPlaying == playing) { + List newPositionsCheck = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList(); + if (oldPositions.equals(newPositionsCheck)) { + return; + } + } + + currentPlayingPositions = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList(); + + for (int pos : oldPositions) { + if (pos >= 0 && pos < songs.size()) { + notifyItemChanged(pos, "payload_playback"); + } + } + for (int pos : currentPlayingPositions) { + if (!oldPositions.contains(pos) && pos >= 0 && pos < songs.size()) { + notifyItemChanged(pos, "payload_playback"); + } + } + } + + private List findPositionsById(String id) { + if (id == null) return Collections.emptyList(); + List positions = new ArrayList<>(); + for (int i = 0; i < songs.size(); i++) { + if (id.equals(songs.get(i).getId())) { + positions.add(i); + } + } + return positions; + } + + @Override + public Filter getFilter() { + return filtering; + } + + public Child getItem(int id) { + return songs.get(id); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHorizontalTrackBinding item; + + ViewHolder(ItemHorizontalTrackBinding item) { + super(item.getRoot()); + + this.item = item; + + item.searchResultSongTitleTextView.setSelected(true); + item.searchResultSongSubtitleTextView.setSelected(true); + + itemView.setOnClickListener(v -> onClick()); + itemView.setOnLongClickListener(v -> onLongClick()); + + item.searchResultSongMoreButton.setOnClickListener(v -> onLongClick()); + } + + public void onClick() { + int pos = getBindingAdapterPosition(); + Child tappedSong = songs.get(pos); + + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(MusicUtil.limitPlayableMedia(songs, getBindingAdapterPosition()))); + bundle.putInt(Constants.ITEM_POSITION, MusicUtil.getPlayableMediaPosition(songs, getBindingAdapterPosition())); + + if (tappedSong.getId().equals(currentPlayingId)) { + Log.i("SongHorizontalAdapter", "Tapping on currently playing song, toggling playback"); + try{ + MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); + Log.i("SongHorizontalAdapter", "MediaBrowser retrieved, isPlaying: " + isPlaying); + if (isPlaying) { + mediaBrowser.pause(); + } else { + mediaBrowser.play(); + } + } catch (ExecutionException | InterruptedException e) { + Log.e("SongHorizontalAdapter", "Error getting MediaBrowser", e); + } + } else { + click.onMediaClick(bundle); + } + } + + private boolean onLongClick() { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition())); + + click.onMediaLongClick(bundle); + + return true; + } + } + + public void sort(String order) { + switch (order) { + case Constants.MEDIA_BY_TITLE: + songs.sort(Comparator.comparing(Child::getTitle)); + break; + case Constants.MEDIA_MOST_RECENTLY_STARRED: + songs.sort(Comparator.comparing(Child::getStarred, Comparator.nullsLast(Comparator.reverseOrder()))); + break; + case Constants.MEDIA_LEAST_RECENTLY_STARRED: + songs.sort(Comparator.comparing(Child::getStarred, Comparator.nullsLast(Comparator.naturalOrder()))); + break; + } + + notifyDataSetChanged(); + } + + public void setMediaBrowserListenableFuture(ListenableFuture mediaBrowserListenableFuture) { + this.mediaBrowserListenableFuture = mediaBrowserListenableFuture; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/YearAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/YearAdapter.java new file mode 100644 index 0000000..1ecf37e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/YearAdapter.java @@ -0,0 +1,74 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemHomeYearBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.Collections; +import java.util.List; + +public class YearAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + + private List years; + + public YearAdapter(ClickCallback click) { + this.click = click; + this.years = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemHomeYearBinding view = ItemHomeYearBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + int year = years.get(position); + + holder.item.yearLabel.setText(Integer.toString(year)); + } + + @Override + public int getItemCount() { + return years.size(); + } + + public Integer getItem(int position) { + return years.get(position); + } + + public void setItems(List years) { + this.years = years; + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemHomeYearBinding item; + + ViewHolder(ItemHomeYearBinding item) { + super(item.getRoot()); + + this.item = item; + + itemView.setOnClickListener(v -> onClick()); + } + + public void onClick() { + Bundle bundle = new Bundle(); + bundle.putString(Constants.MEDIA_BY_YEAR, Constants.MEDIA_BY_YEAR); + bundle.putInt("year_object", years.get(getBindingAdapterPosition())); + + click.onYearClick(bundle); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/BatteryOptimizationDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/BatteryOptimizationDialog.java new file mode 100644 index 0000000..110beac --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/BatteryOptimizationDialog.java @@ -0,0 +1,41 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.DialogFragment; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogBatteryOptimizationBinding; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +@OptIn(markerClass = UnstableApi.class) +public class BatteryOptimizationDialog extends DialogFragment { + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogBatteryOptimizationBinding bind = DialogBatteryOptimizationBinding.inflate(getLayoutInflater()); + + return new MaterialAlertDialogBuilder(requireContext()) + .setView(bind.getRoot()) + .setTitle(R.string.activity_battery_optimizations_title) + .setPositiveButton(R.string.battery_optimization_positive_button, (dialog, listener) -> openPowerSettings()) + .setNeutralButton(R.string.battery_optimization_neutral_button, (dialog, listener) -> Preferences.dontAskForOptimization()) + .setNegativeButton(R.string.battery_optimization_negative_button, null) + .create(); + } + + private void openPowerSettings() { + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); + startActivity(intent); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ConnectionAlertDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ConnectionAlertDialog.java new file mode 100644 index 0000000..3650015 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ConnectionAlertDialog.java @@ -0,0 +1,51 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogConnectionAlertBinding; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Objects; + +public class ConnectionAlertDialog extends DialogFragment { + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogConnectionAlertBinding bind = DialogConnectionAlertBinding.inflate(getLayoutInflater()); + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.connection_alert_dialog_title) + .setPositiveButton(R.string.connection_alert_dialog_positive_button, (dialog, id) -> dialog.cancel()) + .setNegativeButton(R.string.connection_alert_dialog_negative_button, (dialog, id) -> dialog.cancel()); + + if (!Preferences.isDataSavingMode()) { + builder.setNeutralButton(R.string.connection_alert_dialog_neutral_button, (dialog, id) -> { + }); + } + + return builder.create(); + } + + @Override + public void onStart() { + super.onStart(); + + setButtonAction(); + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog()); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { + Preferences.setDataSavingMode(true); + Objects.requireNonNull(getDialog()).dismiss(); + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DeleteDownloadStorageDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DeleteDownloadStorageDialog.java new file mode 100644 index 0000000..877831f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DeleteDownloadStorageDialog.java @@ -0,0 +1,75 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.widget.Button; +import android.net.Uri; + +import androidx.documentfile.provider.DocumentFile; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.DialogFragment; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.util.ExternalDownloadMetadataStore; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +@OptIn(markerClass = UnstableApi.class) +public class DeleteDownloadStorageDialog extends DialogFragment { + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogDeleteDownloadStorageBinding bind = DialogDeleteDownloadStorageBinding.inflate(getLayoutInflater()); + + return new MaterialAlertDialogBuilder(requireContext()) + .setView(bind.getRoot()) + .setTitle(R.string.delete_download_storage_dialog_title) + .setPositiveButton(R.string.delete_download_storage_dialog_positive_button, null) + .setNegativeButton(R.string.delete_download_storage_dialog_negative_button, null) + .create(); + } + + @Override + public void onResume() { + super.onResume(); + setButtonAction(); + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog(); + + if (dialog != null) { + Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(v -> { + if (Preferences.getDownloadDirectoryUri() == null) { + DownloadUtil.getDownloadTracker(requireContext()).removeAll(); + } + + String uriString = Preferences.getDownloadDirectoryUri(); + if (uriString != null) { + DocumentFile directory = DocumentFile.fromTreeUri(requireContext(), Uri.parse(uriString)); + if (directory != null && directory.canWrite()) { + for (DocumentFile file : directory.listFiles()) { + file.delete(); + } + } + ExternalAudioReader.refreshCache(); + ExternalDownloadMetadataStore.clear(); + } + dialog.dismiss(); + }); + + Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE); + negativeButton.setOnClickListener(v -> { + dialog.dismiss(); + }); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadDirectoryDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadDirectoryDialog.java new file mode 100644 index 0000000..e781f44 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadDirectoryDialog.java @@ -0,0 +1,61 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.DialogFragment; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogDownloadDirectoryBinding; +import com.cappielloantonio.tempo.interfaces.DialogClickCallback; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +@OptIn(markerClass = UnstableApi.class) +public class DownloadDirectoryDialog extends DialogFragment { + private final DialogClickCallback dialogClickCallback; + + public DownloadDirectoryDialog(DialogClickCallback dialogClickCallback) { + this.dialogClickCallback = dialogClickCallback; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogDownloadDirectoryBinding bind = DialogDownloadDirectoryBinding.inflate(getLayoutInflater()); + + return new MaterialAlertDialogBuilder(requireContext()) + .setView(bind.getRoot()) + .setTitle(R.string.download_directory_dialog_title) + .setPositiveButton(R.string.download_directory_dialog_positive_button, null) + .setNegativeButton(R.string.download_directory_dialog_negative_button, null) + .create(); + } + + @Override + public void onResume() { + super.onResume(); + setButtonAction(); + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog(); + + if (dialog != null) { + Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(v -> { + dialogClickCallback.onPositiveClick(); + dialog.dismiss(); + }); + + Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE); + negativeButton.setOnClickListener(v -> { + dialogClickCallback.onNegativeClick(); + dialog.dismiss(); + }); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadDirectoryPickerDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadDirectoryPickerDialog.java new file mode 100644 index 0000000..62dcd40 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadDirectoryPickerDialog.java @@ -0,0 +1,63 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.util.Preferences; + +public class DownloadDirectoryPickerDialog extends DialogFragment { + + private ActivityResultLauncher folderPickerLauncher; + + @NonNull + @Override + public android.app.Dialog onCreateDialog(Bundle savedInstanceState) { + // Register launcher *before* button triggers + folderPickerLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == android.app.Activity.RESULT_OK) { + Intent data = result.getData(); + if (data != null) { + Uri uri = data.getData(); + if (uri != null) { + requireContext().getContentResolver().takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ); + + Preferences.setDownloadDirectoryUri(uri.toString()); + ExternalAudioReader.refreshCache(); + + Toast.makeText(requireContext(), "Download directory set:\n" + uri.toString(), Toast.LENGTH_LONG).show(); + } + } + } + } + ); + + return new MaterialAlertDialogBuilder(requireContext()) + .setTitle("Set Download Directory") + .setMessage("Choose a folder where downloaded songs will be stored.") + .setPositiveButton("Choose Folder", (dialog, which) -> { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + folderPickerLauncher.launch(intent); + }) + .setNegativeButton(android.R.string.cancel, null) + .create(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadStorageDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadStorageDialog.java new file mode 100644 index 0000000..766064e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadStorageDialog.java @@ -0,0 +1,94 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.DialogFragment; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogDownloadStorageBinding; +import com.cappielloantonio.tempo.interfaces.DialogClickCallback; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +@OptIn(markerClass = UnstableApi.class) +public class DownloadStorageDialog extends DialogFragment { + private final DialogClickCallback dialogClickCallback; + + public DownloadStorageDialog(DialogClickCallback dialogClickCallback) { + this.dialogClickCallback = dialogClickCallback; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogDownloadStorageBinding bind = DialogDownloadStorageBinding.inflate(getLayoutInflater()); + + return new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.download_storage_dialog_title) + .setPositiveButton(R.string.download_storage_external_dialog_positive_button, null) + .setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null) + .setNeutralButton(R.string.download_storage_directory_dialog_neutral_button, null) + .create(); + } + + @Override + public void onResume() { + super.onResume(); + setButtonAction(); + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog(); + + if (dialog != null) { + Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(v -> { + int currentPreference = Preferences.getDownloadStoragePreference(); + int newPreference = 1; + + if (currentPreference != newPreference) { + Preferences.setDownloadStoragePreference(newPreference); + DownloadUtil.getDownloadTracker(requireContext()).removeAll(); + dialogClickCallback.onPositiveClick(); + } + + dialog.dismiss(); + }); + + Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE); + negativeButton.setOnClickListener(v -> { + int currentPreference = Preferences.getDownloadStoragePreference(); + int newPreference = 0; + + if (currentPreference != newPreference) { + Preferences.setDownloadStoragePreference(newPreference); + DownloadUtil.getDownloadTracker(requireContext()).removeAll(); + dialogClickCallback.onNegativeClick(); + } + + dialog.dismiss(); + }); + + Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL); + neutralButton.setOnClickListener(v -> { + int currentPreference = Preferences.getDownloadStoragePreference(); + int newPreference = 2; + + if (currentPreference != newPreference) { + Preferences.setDownloadStoragePreference(newPreference); + DownloadUtil.getDownloadTracker(requireContext()).removeAll(); + dialogClickCallback.onNeutralClick(); + } + + dialog.dismiss(); + }); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/GithubTempoUpdateDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/GithubTempoUpdateDialog.java new file mode 100644 index 0000000..55e0701 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/GithubTempoUpdateDialog.java @@ -0,0 +1,73 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogGithubTempoUpdateBinding; +import com.cappielloantonio.tempo.github.models.LatestRelease; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Objects; + +public class GithubTempoUpdateDialog extends DialogFragment { + private final LatestRelease latestRelease; + + public GithubTempoUpdateDialog(LatestRelease latestRelease) { + this.latestRelease = latestRelease; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogGithubTempoUpdateBinding bind = DialogGithubTempoUpdateBinding.inflate(getLayoutInflater()); + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.github_update_dialog_title) + .setPositiveButton(R.string.github_update_dialog_positive_button, (dialog, id) -> { }) + .setNegativeButton(R.string.github_update_dialog_negative_button, (dialog, id) -> { }) + .setNeutralButton(R.string.github_update_dialog_neutral_button, (dialog, id) -> { }); + + return builder.create(); + } + + @Override + public void onStart() { + super.onStart(); + + setButtonAction(); + } + + private void setButtonAction() { + AlertDialog alertDialog = (AlertDialog) Objects.requireNonNull(getDialog()); + + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { + openLink(latestRelease.getHtmlUrl()); + Objects.requireNonNull(getDialog()).dismiss(); + }); + + alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v -> { + Preferences.setTempusUpdateReminder(); + Objects.requireNonNull(getDialog()).dismiss(); + }); + + alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { + openLink(getString(R.string.support_url)); + Objects.requireNonNull(getDialog()).dismiss(); + }); + } + + private void openLink(String link) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/HomeRearrangementDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/HomeRearrangementDialog.java new file mode 100644 index 0000000..8ed67e4 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/HomeRearrangementDialog.java @@ -0,0 +1,115 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogHomeRearrangementBinding; +import com.cappielloantonio.tempo.ui.adapter.HomeSectorHorizontalAdapter; +import com.cappielloantonio.tempo.viewmodel.HomeRearrangementViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Collections; +import java.util.Objects; + +public class HomeRearrangementDialog extends DialogFragment { + private DialogHomeRearrangementBinding bind; + private HomeRearrangementViewModel homeRearrangementViewModel; + private HomeSectorHorizontalAdapter homeSectorHorizontalAdapter; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + bind = DialogHomeRearrangementBinding.inflate(getLayoutInflater()); + + homeRearrangementViewModel = new ViewModelProvider(requireActivity()).get(HomeRearrangementViewModel.class); + + return new MaterialAlertDialogBuilder(requireContext()) + .setView(bind.getRoot()) + .setTitle(R.string.home_rearrangement_dialog_title) + .setPositiveButton(R.string.home_rearrangement_dialog_positive_button, (dialog, id) -> { }) + .setNeutralButton(R.string.home_rearrangement_dialog_neutral_button, (dialog, id) -> { }) + .setNegativeButton(R.string.home_rearrangement_dialog_negative_button, (dialog, id) -> dialog.cancel()) + .create(); + } + + @Override + public void onStart() { + super.onStart(); + + setButtonAction(); + initSectorView(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + homeRearrangementViewModel.closeDialog(); + bind = null; + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog()); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { + homeRearrangementViewModel.saveHomeSectorList(homeSectorHorizontalAdapter.getItems()); + dismiss(); + }); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { + homeRearrangementViewModel.resetHomeSectorList(); + dismiss(); + }); + } + + private void initSectorView() { + bind.homeSectorItemRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.homeSectorItemRecyclerView.setHasFixedSize(true); + + homeSectorHorizontalAdapter = new HomeSectorHorizontalAdapter(); + bind.homeSectorItemRecyclerView.setAdapter(homeSectorHorizontalAdapter); + homeSectorHorizontalAdapter.setItems(homeRearrangementViewModel.getHomeSectorList()); + + new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { + int originalPosition = -1; + int fromPosition = -1; + int toPosition = -1; + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { + if (originalPosition == -1) originalPosition = viewHolder.getBindingAdapterPosition(); + + fromPosition = viewHolder.getBindingAdapterPosition(); + toPosition = target.getBindingAdapterPosition(); + + Collections.swap(homeSectorHorizontalAdapter.getItems(), fromPosition, toPosition); + Objects.requireNonNull(recyclerView.getAdapter()).notifyItemMoved(fromPosition, toPosition); + + return false; + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + + homeRearrangementViewModel.orderSectorLiveListAfterSwap(homeSectorHorizontalAdapter.getItems()); + + originalPosition = -1; + fromPosition = -1; + toPosition = -1; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + + } + } + ).attachToRecyclerView(bind.homeSectorItemRecyclerView); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistChooserDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistChooserDialog.java new file mode 100644 index 0000000..a684251 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistChooserDialog.java @@ -0,0 +1,109 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogPlaylistChooserBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.Playlist; +import com.cappielloantonio.tempo.ui.adapter.PlaylistDialogHorizontalAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.PlaylistChooserViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Objects; + +public class PlaylistChooserDialog extends DialogFragment implements ClickCallback { + private DialogPlaylistChooserBinding bind; + private PlaylistChooserViewModel playlistChooserViewModel; + + private PlaylistDialogHorizontalAdapter playlistDialogHorizontalAdapter; + + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + bind = DialogPlaylistChooserBinding.inflate(getLayoutInflater()); + + playlistChooserViewModel = new ViewModelProvider(requireActivity()).get(PlaylistChooserViewModel.class); + + return new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.playlist_chooser_dialog_title) + .setNeutralButton(R.string.playlist_chooser_dialog_neutral_button, (dialog, id) -> { }) + .setNegativeButton(R.string.playlist_chooser_dialog_negative_button, (dialog, id) -> dialog.cancel()) + .create(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @Override + public void onStart() { + super.onStart(); + + initPlaylistView(); + setSongInfo(); + setButtonAction(); + } + + private void setSongInfo() { + playlistChooserViewModel.setSongsToAdd(requireArguments().getParcelableArrayList(Constants.TRACKS_OBJECT)); + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog()); + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, playlistChooserViewModel.getSongsToAdd()); + + PlaylistEditorDialog dialog = new PlaylistEditorDialog(null); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + + Objects.requireNonNull(getDialog()).dismiss(); + }); + } + + private void initPlaylistView() { + bind.playlistDialogRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.playlistDialogRecyclerView.setHasFixedSize(true); + + playlistDialogHorizontalAdapter = new PlaylistDialogHorizontalAdapter(this); + bind.playlistDialogRecyclerView.setAdapter(playlistDialogHorizontalAdapter); + + playlistChooserViewModel.getPlaylistList(requireActivity()).observe(requireActivity(), playlists -> { + if (playlists != null) { + if (!playlists.isEmpty()) { + if (bind != null) bind.noPlaylistsCreatedTextView.setVisibility(View.GONE); + if (bind != null) bind.playlistDialogRecyclerView.setVisibility(View.VISIBLE); + playlistDialogHorizontalAdapter.setItems(playlists); + } else { + if (bind != null) bind.noPlaylistsCreatedTextView.setVisibility(View.VISIBLE); + if (bind != null) bind.playlistDialogRecyclerView.setVisibility(View.GONE); + } + } + }); + } + + @Override + public void onPlaylistClick(Bundle bundle) { + if (playlistChooserViewModel.getSongsToAdd() != null && !playlistChooserViewModel.getSongsToAdd().isEmpty()) { + Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT); + playlistChooserViewModel.addSongsToPlaylist(this, getDialog(), playlist.getId()); + } else { + Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_add_failure, Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistEditorDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistEditorDialog.java new file mode 100644 index 0000000..dea70d7 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PlaylistEditorDialog.java @@ -0,0 +1,195 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogPlaylistEditorBinding; +import com.cappielloantonio.tempo.interfaces.PlaylistCallback; +import com.cappielloantonio.tempo.ui.adapter.PlaylistDialogSongHorizontalAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.PlaylistEditorViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Collections; +import java.util.Objects; + +public class PlaylistEditorDialog extends DialogFragment { + private DialogPlaylistEditorBinding bind; + private PlaylistEditorViewModel playlistEditorViewModel; + + private final PlaylistCallback playlistCallback; + + private String playlistName; + private PlaylistDialogSongHorizontalAdapter playlistDialogSongHorizontalAdapter; + + public PlaylistEditorDialog(PlaylistCallback playlistCallback) { + this.playlistCallback = playlistCallback; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + bind = DialogPlaylistEditorBinding.inflate(getLayoutInflater()); + + playlistEditorViewModel = new ViewModelProvider(requireActivity()).get(PlaylistEditorViewModel.class); + + return new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.playlist_editor_dialog_title) + .setPositiveButton(R.string.playlist_editor_dialog_positive_button, (dialog, id) -> { }) + .setNeutralButton(R.string.playlist_editor_dialog_neutral_button, (dialog, id) -> dialog.cancel()) + .setNegativeButton(R.string.playlist_editor_dialog_negative_button, (dialog, id) -> dialog.cancel()) + .create(); + } + + @Override + public void onStart() { + super.onStart(); + + setParameterInfo(); + setButtonAction(); + initSongsView(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void setParameterInfo() { + if (requireArguments().getParcelableArrayList(Constants.TRACKS_OBJECT) != null) { + playlistEditorViewModel.setSongsToAdd(requireArguments().getParcelableArrayList(Constants.TRACKS_OBJECT)); + playlistEditorViewModel.setPlaylistToEdit(null); + } else if (requireArguments().getParcelable(Constants.PLAYLIST_OBJECT) != null) { + playlistEditorViewModel.setSongsToAdd(null); + playlistEditorViewModel.setPlaylistToEdit(requireArguments().getParcelable(Constants.PLAYLIST_OBJECT)); + + if (playlistEditorViewModel.getPlaylistToEdit() != null) { + bind.playlistNameTextView.setText(playlistEditorViewModel.getPlaylistToEdit().getName()); + } + } + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog()); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { + if (validateInput()) { + if (playlistEditorViewModel.getSongsToAdd() != null) { + playlistEditorViewModel.createPlaylist(playlistName); + } else if (playlistEditorViewModel.getPlaylistToEdit() != null) { + playlistEditorViewModel.updatePlaylist(playlistName); + } + + dialogDismiss(); + } + }); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> Toast.makeText(requireContext(), R.string.playlist_editor_dialog_action_delete_toast, Toast.LENGTH_SHORT).show()); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnLongClickListener(v -> { + playlistEditorViewModel.deletePlaylist(); + dialogDismiss(); + return false; + }); + + bind.playlistShareButton.setOnClickListener(view -> { + playlistEditorViewModel.sharePlaylist().observe(requireActivity(), sharedPlaylist -> { + ClipboardManager clipboardManager = (ClipboardManager) requireActivity().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = ClipData.newPlainText(getString(R.string.app_name), sharedPlaylist.getUrl()); + clipboardManager.setPrimaryClip(clipData); + }); + }); + + bind.playlistShareButton.setVisibility(Preferences.isSharingEnabled() ? View.VISIBLE : View.GONE); + } + + private void initSongsView() { + bind.playlistSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.playlistSongRecyclerView.setHasFixedSize(true); + + playlistDialogSongHorizontalAdapter = new PlaylistDialogSongHorizontalAdapter(); + bind.playlistSongRecyclerView.setAdapter(playlistDialogSongHorizontalAdapter); + + playlistEditorViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> { + if (songs != null) playlistDialogSongHorizontalAdapter.setItems(songs); + }); + + new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT) { + int originalPosition = -1; + int fromPosition = -1; + int toPosition = -1; + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { + if (originalPosition == -1) + originalPosition = viewHolder.getBindingAdapterPosition(); + + fromPosition = viewHolder.getBindingAdapterPosition(); + toPosition = target.getBindingAdapterPosition(); + + Collections.swap(playlistDialogSongHorizontalAdapter.getItems(), fromPosition, toPosition); + Objects.requireNonNull(recyclerView.getAdapter()).notifyItemMoved(fromPosition, toPosition); + + return false; + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + + /* + * Qui vado a riscivere tutta la table Queue, quando teoricamente potrei solo swappare l'ordine degli elementi interessati + * Nel caso la coda contenesse parecchi brani, potrebbero verificarsi rallentamenti pesanti + */ + playlistEditorViewModel.orderPlaylistSongLiveListAfterSwap(playlistDialogSongHorizontalAdapter.getItems()); + + originalPosition = -1; + fromPosition = -1; + toPosition = -1; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + playlistEditorViewModel.removeFromPlaylistSongLiveList(viewHolder.getBindingAdapterPosition()); + Objects.requireNonNull(bind.playlistSongRecyclerView.getAdapter()).notifyItemRemoved(viewHolder.getBindingAdapterPosition()); + } + } + ).attachToRecyclerView(bind.playlistSongRecyclerView); + } + + private boolean validateInput() { + playlistName = Objects.requireNonNull(bind.playlistNameTextView.getText()).toString().trim(); + + if (TextUtils.isEmpty(playlistName)) { + bind.playlistNameTextView.setError(getString(R.string.error_required)); + return false; + } + + return true; + } + + private void dialogDismiss() { + Objects.requireNonNull(getDialog()).dismiss(); + if (playlistCallback != null) { + playlistCallback.onDismiss(); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PodcastChannelEditorDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PodcastChannelEditorDialog.java new file mode 100644 index 0000000..2226ab8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/PodcastChannelEditorDialog.java @@ -0,0 +1,89 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.text.TextUtils; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogPodcastChannelEditorBinding; +import com.cappielloantonio.tempo.interfaces.PodcastCallback; +import com.cappielloantonio.tempo.viewmodel.PodcastChannelEditorViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Objects; + +public class PodcastChannelEditorDialog extends DialogFragment { + private DialogPodcastChannelEditorBinding bind; + private PodcastChannelEditorViewModel podcastChannelEditorViewModel; + + private final PodcastCallback podcastCallback; + + private String channelUrl; + + public PodcastChannelEditorDialog(PodcastCallback podcastCallback) { + this.podcastCallback = podcastCallback; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + bind = DialogPodcastChannelEditorBinding.inflate(getLayoutInflater()); + + podcastChannelEditorViewModel = new ViewModelProvider(requireActivity()).get(PodcastChannelEditorViewModel.class); + + return new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.podcast_channel_editor_dialog_title) + .setPositiveButton(R.string.radio_editor_dialog_positive_button, (dialog, id) -> { }) + .setNegativeButton(R.string.radio_editor_dialog_negative_button, (dialog, id) -> dialog.cancel()) + .create(); + } + + @Override + public void onStart() { + super.onStart(); + + setButtonAction(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog(); + if (dialog != null) { + Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(v -> { + if (validateInput()) { + podcastChannelEditorViewModel.createChannel(channelUrl); + dismissDialog(); + } + }); + } + } + + + private boolean validateInput() { + channelUrl = Objects.requireNonNull(bind.podcastChannelRssUrlNameTextView.getText()).toString().trim(); + + if (TextUtils.isEmpty(channelUrl)) { + bind.podcastChannelRssUrlNameTextView.setError(getString(R.string.error_required)); + return false; + } + + return true; + } + + private void dismissDialog() { + podcastCallback.onDismiss(); + Objects.requireNonNull(getDialog()).dismiss(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java new file mode 100644 index 0000000..b4aba96 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java @@ -0,0 +1,111 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogRadioEditorBinding; +import com.cappielloantonio.tempo.interfaces.RadioCallback; +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.RadioEditorViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Objects; + +public class RadioEditorDialog extends DialogFragment { + private DialogRadioEditorBinding bind; + private RadioEditorViewModel radioEditorViewModel; + + private final RadioCallback radioCallback; + + private String radioName; + private String radioStreamURL; + private String radioHomepageURL; + + public RadioEditorDialog(RadioCallback radioCallback) { + this.radioCallback = radioCallback; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + bind = DialogRadioEditorBinding.inflate(getLayoutInflater()); + + radioEditorViewModel = new ViewModelProvider(requireActivity()).get(RadioEditorViewModel.class); + + return new MaterialAlertDialogBuilder(requireContext()) + .setView(bind.getRoot()) + .setTitle(R.string.radio_editor_dialog_title) + .setPositiveButton(R.string.radio_editor_dialog_positive_button, (dialog, id) -> { + if (validateInput()) { + if (radioEditorViewModel.getRadioToEdit() == null) { + radioEditorViewModel.createRadio(radioName, radioStreamURL, radioHomepageURL.isEmpty() ? null : radioHomepageURL); + } else { + radioEditorViewModel.updateRadio(radioName, radioStreamURL, radioHomepageURL.isEmpty() ? null : radioHomepageURL); + } + dismissDialog(); + } + }) + .setNeutralButton(R.string.radio_editor_dialog_neutral_button, (dialog, id) -> { + radioEditorViewModel.deleteRadio(); + dismissDialog(); + }) + .setNegativeButton(R.string.radio_editor_dialog_negative_button, (dialog, id) -> { + dialog.cancel(); + }) + .create(); + } + + @Override + public void onStart() { + super.onStart(); + setParameterInfo(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void setParameterInfo() { + if (getArguments() != null && getArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT) != null) { + InternetRadioStation toEdit = requireArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT); + + radioEditorViewModel.setRadioToEdit(toEdit); + + bind.internetRadioStationNameTextView.setText(toEdit.getName()); + bind.internetRadioStationStreamUrlTextView.setText(toEdit.getStreamUrl()); + bind.internetRadioStationHomepageUrlTextView.setText(toEdit.getHomePageUrl()); + } + } + + private boolean validateInput() { + radioName = Objects.requireNonNull(bind.internetRadioStationNameTextView.getText()).toString().trim(); + radioStreamURL = Objects.requireNonNull(bind.internetRadioStationStreamUrlTextView.getText()).toString().trim(); + radioHomepageURL = Objects.requireNonNull(bind.internetRadioStationHomepageUrlTextView.getText()).toString().trim(); + + if (TextUtils.isEmpty(radioName)) { + bind.internetRadioStationNameTextView.setError(getString(R.string.error_required)); + return false; + } + + if (TextUtils.isEmpty(radioStreamURL)) { + bind.internetRadioStationStreamUrlTextView.setError(getString(R.string.error_required)); + return false; + } + + return true; + } + + private void dismissDialog() { + radioCallback.onDismiss(); + Objects.requireNonNull(getDialog()).dismiss(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RatingDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RatingDialog.java new file mode 100644 index 0000000..a2d1600 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RatingDialog.java @@ -0,0 +1,75 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogRatingBinding; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.RatingViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +public class RatingDialog extends DialogFragment { + private static final String TAG = "ServerSignupDialog"; + + private DialogRatingBinding bind; + private RatingViewModel ratingViewModel; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + bind = DialogRatingBinding.inflate(getLayoutInflater()); + ratingViewModel = new ViewModelProvider(requireActivity()).get(RatingViewModel.class); + + return new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.rating_dialog_title) + .setNegativeButton(R.string.rating_dialog_negative_button, (dialog, id) -> dialog.cancel()) + .setPositiveButton(R.string.rating_dialog_positive_button, (dialog, id) -> ratingViewModel.rate((int) bind.ratingBar.getRating())) + .create(); + } + + @Override + public void onStart() { + super.onStart(); + + setElementInfo(); + setRating(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void setElementInfo() { + if (requireArguments().getParcelable(Constants.TRACK_OBJECT) != null) { + ratingViewModel.setSong(requireArguments().getParcelable(Constants.TRACK_OBJECT)); + } else if (requireArguments().getParcelable(Constants.ALBUM_OBJECT) != null) { + ratingViewModel.setAlbum(requireArguments().getParcelable(Constants.ALBUM_OBJECT)); + } else if (requireArguments().getParcelable(Constants.ARTIST_OBJECT) != null) { + ratingViewModel.setArtist(requireArguments().getParcelable(Constants.ARTIST_OBJECT)); + } + } + + private void setRating() { + if (ratingViewModel.getSong() != null) { + ratingViewModel.getLiveSong().observe(this, song -> { + bind.ratingBar.setRating(song.getUserRating() != null ? song.getUserRating() : 0); + }); + } else if (ratingViewModel.getAlbum() != null) { + ratingViewModel.getLiveAlbum().observe(this, album -> { + if (album != null) { + bind.ratingBar.setRating(album.getUserRating() != null ? album.getUserRating() : 0); + } + }); + } else if (ratingViewModel.getArtist() != null) { + ratingViewModel.getLiveArtist().observe(this, artist -> bind.ratingBar.setRating(/*artist.getRating()*/ 0)); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ServerSignupDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ServerSignupDialog.java new file mode 100644 index 0000000..93381d3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ServerSignupDialog.java @@ -0,0 +1,142 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogServerSignupBinding; +import com.cappielloantonio.tempo.model.Server; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.viewmodel.LoginViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Objects; +import java.util.UUID; + +public class ServerSignupDialog extends DialogFragment { + private static final String TAG = "ServerSignupDialog"; + + private DialogServerSignupBinding bind; + private LoginViewModel loginViewModel; + + private String serverName; + private String username; + private String password; + private String server; + private String localAddress; + private boolean lowSecurity = false; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + bind = DialogServerSignupBinding.inflate(getLayoutInflater()); + + loginViewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class); + + return new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.server_signup_dialog_title) + .setNeutralButton(R.string.server_signup_dialog_neutral_button, (dialog, id) -> { }) + .setPositiveButton(R.string.server_signup_dialog_positive_button, (dialog, id) -> { }) + .setNegativeButton(R.string.server_signup_dialog_negative_button, (dialog, id) -> dialog.cancel()) + .create(); + } + + @Override + public void onStart() { + super.onStart(); + + setServerInfo(); + setButtonAction(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void setServerInfo() { + if (getArguments() != null) { + loginViewModel.setServerToEdit(requireArguments().getParcelable("server_object")); + + if (loginViewModel.getServerToEdit() != null) { + bind.serverNameTextView.setText(loginViewModel.getServerToEdit().getServerName()); + bind.usernameTextView.setText(loginViewModel.getServerToEdit().getUsername()); + bind.passwordTextView.setText(""); + bind.serverTextView.setText(loginViewModel.getServerToEdit().getAddress()); + bind.localAddressTextView.setText(loginViewModel.getServerToEdit().getLocalAddress()); + bind.lowSecurityCheckbox.setChecked(loginViewModel.getServerToEdit().isLowSecurity()); + } + } else { + loginViewModel.setServerToEdit(null); + } + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog()); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { + if (validateInput()) { + saveServerPreference(); + Objects.requireNonNull(getDialog()).dismiss(); + } + }); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> Toast.makeText(requireContext(), R.string.server_signup_dialog_action_delete_toast, Toast.LENGTH_SHORT).show()); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnLongClickListener(v -> { + loginViewModel.deleteServer(null); + Objects.requireNonNull(getDialog()).dismiss(); + return true; + }); + } + + private boolean validateInput() { + serverName = Objects.requireNonNull(bind.serverNameTextView.getText()).toString().trim(); + username = Objects.requireNonNull(bind.usernameTextView.getText()).toString().trim(); + password = bind.lowSecurityCheckbox.isChecked() ? MusicUtil.passwordHexEncoding(Objects.requireNonNull(bind.passwordTextView.getText()).toString()) : Objects.requireNonNull(bind.passwordTextView.getText()).toString(); + server = bind.serverTextView.getText() != null && !bind.serverTextView.getText().toString().trim().isBlank() ? bind.serverTextView.getText().toString().trim() : null; + localAddress = bind.localAddressTextView.getText() != null && !bind.localAddressTextView.getText().toString().trim().isBlank() ? bind.localAddressTextView.getText().toString().trim() : null; + lowSecurity = bind.lowSecurityCheckbox.isChecked(); + + if (TextUtils.isEmpty(serverName)) { + bind.serverNameTextView.setError(getString(R.string.error_required)); + return false; + } + + if (TextUtils.isEmpty(username)) { + bind.usernameTextView.setError(getString(R.string.error_required)); + return false; + } + + if (TextUtils.isEmpty(server)) { + bind.serverTextView.setError(getString(R.string.error_required)); + return false; + } + + if (!TextUtils.isEmpty(localAddress) && !localAddress.matches("^https?://(.*)")) { + bind.localAddressTextView.setError(getString(R.string.error_server_prefix)); + return false; + } + + if (!server.matches("^https?://(.*)")) { + bind.serverTextView.setError(getString(R.string.error_server_prefix)); + return false; + } + + return true; + } + + private void saveServerPreference() { + String serverID = loginViewModel.getServerToEdit() != null ? loginViewModel.getServerToEdit().getServerId() : UUID.randomUUID().toString(); + loginViewModel.addServer(new Server(serverID, this.serverName, this.username, this.password, this.server, this.localAddress, System.currentTimeMillis(), this.lowSecurity)); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ServerUnreachableDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ServerUnreachableDialog.java new file mode 100644 index 0000000..fc128d2 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ServerUnreachableDialog.java @@ -0,0 +1,63 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogServerUnreachableBinding; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Objects; + +@OptIn(markerClass = UnstableApi.class) +public class ServerUnreachableDialog extends DialogFragment { + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogServerUnreachableBinding bind = DialogServerUnreachableBinding.inflate(getLayoutInflater()); + + AlertDialog popup = new MaterialAlertDialogBuilder(getActivity()).setView(bind.getRoot()) + .setTitle(R.string.server_unreachable_dialog_title) + .setPositiveButton(R.string.server_unreachable_dialog_positive_button, null) + .setNeutralButton(R.string.server_unreachable_dialog_neutral_button, null) + .setNegativeButton(R.string.server_unreachable_dialog_negative_button, (dialog, id) -> dialog.cancel()) + .create(); + + popup.setCanceledOnTouchOutside(false); + popup.setCancelable(false); + + return popup; + } + + + @Override + public void onStart() { + super.onStart(); + + setButtonAction(); + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog()); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { + MainActivity activity = (MainActivity) getActivity(); + if (activity != null) activity.quit(); + alertDialog.dismiss(); + }); + + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { + Preferences.setServerUnreachableDatetime(); + alertDialog.dismiss(); + }); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ShareUpdateDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ShareUpdateDialog.java new file mode 100644 index 0000000..86634d9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/ShareUpdateDialog.java @@ -0,0 +1,133 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogShareUpdateBinding; +import com.cappielloantonio.tempo.util.UIUtil; +import com.cappielloantonio.tempo.viewmodel.HomeViewModel; +import com.cappielloantonio.tempo.viewmodel.ShareBottomSheetViewModel; +import com.google.android.material.datepicker.CalendarConstraints; +import com.google.android.material.datepicker.DateValidatorPointForward; +import com.google.android.material.datepicker.MaterialDatePicker; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Date; +import java.util.Objects; + +public class ShareUpdateDialog extends DialogFragment { + private DialogShareUpdateBinding bind; + private HomeViewModel homeViewModel; + private ShareBottomSheetViewModel shareBottomSheetViewModel; + + private MaterialDatePicker datePicker; + + private String descriptionTextView; + private String expirationTextView; + private long expiration; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class); + + shareBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(ShareBottomSheetViewModel.class); + + bind = DialogShareUpdateBinding.inflate(getLayoutInflater()); + + return new MaterialAlertDialogBuilder(requireContext()) + .setView(bind.getRoot()) + .setTitle(R.string.share_update_dialog_title) + .setPositiveButton(R.string.share_update_dialog_positive_button, (dialog, id) -> { + }) + .setNegativeButton(R.string.share_update_dialog_negative_button, (dialog, id) -> dialog.cancel()) + .create(); + } + + @Override + public void onStart() { + super.onStart(); + + setShareInfo(); + setShareCalendar(); + setButtonAction(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void setShareInfo() { + if (shareBottomSheetViewModel.getShare() != null) { + bind.shareDescriptionTextView.setText(shareBottomSheetViewModel.getShare().getDescription()); + // bind.shareExpirationTextView.setText(shareBottomSheetViewModel.getShare().getExpires()); + } + } + + private void setShareCalendar() { + expiration = shareBottomSheetViewModel.getShare().getExpires().getTime(); + + bind.shareExpirationTextView.setText(UIUtil.getReadableDate(new Date(expiration))); + + bind.shareExpirationTextView.setFocusable(false); + bind.shareExpirationTextView.setOnLongClickListener(null); + + bind.shareExpirationTextView.setOnClickListener(view -> { + CalendarConstraints constraints = new CalendarConstraints.Builder() + .setValidator(DateValidatorPointForward.now()) + .build(); + + datePicker = MaterialDatePicker.Builder.datePicker() + .setCalendarConstraints(constraints) + .setSelection(expiration) + .build(); + + datePicker.addOnPositiveButtonClickListener(selection -> { + expiration = selection; + bind.shareExpirationTextView.setText(UIUtil.getReadableDate(new Date(selection))); + }); + + datePicker.show(requireActivity().getSupportFragmentManager(), null); + }); + } + + private void setButtonAction() { + ((AlertDialog) Objects.requireNonNull(getDialog())).getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { + if (validateInput()) { + updateShare(); + Objects.requireNonNull(getDialog()).dismiss(); + } + }); + } + + private boolean validateInput() { + descriptionTextView = Objects.requireNonNull(bind.shareDescriptionTextView.getText()).toString().trim(); + expirationTextView = Objects.requireNonNull(bind.shareExpirationTextView.getText()).toString().trim(); + + if (TextUtils.isEmpty(descriptionTextView)) { + bind.shareDescriptionTextView.setError(getString(R.string.error_required)); + return false; + } + + if (TextUtils.isEmpty(expirationTextView)) { + bind.shareExpirationTextView.setError(getString(R.string.error_required)); + return false; + } + + return true; + } + + private void updateShare() { + shareBottomSheetViewModel.updateShare(descriptionTextView, expiration); + homeViewModel.refreshShares(requireActivity()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredAlbumSyncDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredAlbumSyncDialog.java new file mode 100644 index 0000000..64500ea --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredAlbumSyncDialog.java @@ -0,0 +1,88 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogStarredAlbumSyncBinding; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.StarredAlbumsSyncViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.stream.Collectors; + +@OptIn(markerClass = UnstableApi.class) +public class StarredAlbumSyncDialog extends DialogFragment { + private StarredAlbumsSyncViewModel starredAlbumsSyncViewModel; + + private Runnable onCancel; + + public StarredAlbumSyncDialog(Runnable onCancel) { + this.onCancel = onCancel; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogStarredAlbumSyncBinding bind = DialogStarredAlbumSyncBinding.inflate(getLayoutInflater()); + + starredAlbumsSyncViewModel = new ViewModelProvider(requireActivity()).get(StarredAlbumsSyncViewModel.class); + + return new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.starred_album_sync_dialog_title) + .setPositiveButton(R.string.starred_sync_dialog_positive_button, null) + .setNeutralButton(R.string.starred_sync_dialog_neutral_button, null) + .setNegativeButton(R.string.starred_sync_dialog_negative_button, null) + .create(); + } + + @Override + public void onResume() { + super.onResume(); + setButtonAction(requireContext()); + } + + private void setButtonAction(Context context) { + androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog(); + + if (dialog != null) { + Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(v -> { + starredAlbumsSyncViewModel.getStarredAlbumSongs(requireActivity()).observe(this, allSongs -> { + if (allSongs != null && !allSongs.isEmpty()) { + DownloadUtil.getDownloadTracker(context).download( + MappingUtil.mapDownloads(allSongs), + allSongs.stream().map(Download::new).collect(Collectors.toList()) + ); + } + dialog.dismiss(); + }); + }); + + Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL); + neutralButton.setOnClickListener(v -> { + Preferences.setStarredAlbumsSyncEnabled(true); + dialog.dismiss(); + }); + + Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE); + negativeButton.setOnClickListener(v -> { + Preferences.setStarredAlbumsSyncEnabled(false); + if (onCancel != null) onCancel.run(); + dialog.dismiss(); + }); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredArtistSyncDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredArtistSyncDialog.java new file mode 100644 index 0000000..448ca07 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredArtistSyncDialog.java @@ -0,0 +1,88 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogStarredArtistSyncBinding; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.StarredArtistsSyncViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.stream.Collectors; + +@OptIn(markerClass = UnstableApi.class) +public class StarredArtistSyncDialog extends DialogFragment { + private StarredArtistsSyncViewModel starredArtistsSyncViewModel; + + private Runnable onCancel; + + public StarredArtistSyncDialog(Runnable onCancel) { + this.onCancel = onCancel; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogStarredArtistSyncBinding bind = DialogStarredArtistSyncBinding.inflate(getLayoutInflater()); + + starredArtistsSyncViewModel = new ViewModelProvider(requireActivity()).get(StarredArtistsSyncViewModel.class); + + return new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.starred_artist_sync_dialog_title) + .setPositiveButton(R.string.starred_sync_dialog_positive_button, null) + .setNeutralButton(R.string.starred_sync_dialog_neutral_button, null) + .setNegativeButton(R.string.starred_sync_dialog_negative_button, null) + .create(); + } + + @Override + public void onResume() { + super.onResume(); + setButtonAction(requireContext()); + } + + private void setButtonAction(Context context) { + androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog(); + + if (dialog != null) { + Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(v -> { + starredArtistsSyncViewModel.getStarredArtistSongs(requireActivity()).observe(this, allSongs -> { + if (allSongs != null && !allSongs.isEmpty()) { + DownloadUtil.getDownloadTracker(context).download( + MappingUtil.mapDownloads(allSongs), + allSongs.stream().map(Download::new).collect(Collectors.toList()) + ); + } + dialog.dismiss(); + }); + }); + + Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL); + neutralButton.setOnClickListener(v -> { + Preferences.setStarredArtistsSyncEnabled(true); + dialog.dismiss(); + }); + + Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE); + negativeButton.setOnClickListener(v -> { + Preferences.setStarredArtistsSyncEnabled(false); + if (onCancel != null) onCancel.run(); + dialog.dismiss(); + }); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredSyncDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredSyncDialog.java new file mode 100644 index 0000000..d3edfdf --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StarredSyncDialog.java @@ -0,0 +1,89 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogStarredSyncBinding; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.StarredSyncViewModel; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.stream.Collectors; + +@OptIn(markerClass = UnstableApi.class) +public class StarredSyncDialog extends DialogFragment { + private StarredSyncViewModel starredSyncViewModel; + + private Runnable onCancel; + + public StarredSyncDialog(Runnable onCancel) { + this.onCancel = onCancel; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogStarredSyncBinding bind = DialogStarredSyncBinding.inflate(getLayoutInflater()); + + starredSyncViewModel = new ViewModelProvider(requireActivity()).get(StarredSyncViewModel.class); + + return new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.starred_sync_dialog_title) + .setPositiveButton(R.string.starred_sync_dialog_positive_button, null) + .setNeutralButton(R.string.starred_sync_dialog_neutral_button, null) + .setNegativeButton(R.string.starred_sync_dialog_negative_button, null) + .create(); + } + + @Override + public void onResume() { + super.onResume(); + setButtonAction(requireContext()); + } + + private void setButtonAction(Context context) { + androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog(); + + if (dialog != null) { + Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(v -> { + starredSyncViewModel.getStarredTracks(requireActivity()).observe(requireActivity(), songs -> { + if (songs != null && Preferences.getDownloadDirectoryUri() == null) { + DownloadUtil.getDownloadTracker(context).download( + MappingUtil.mapDownloads(songs), + songs.stream().map(Download::new).collect(Collectors.toList()) + ); + } + + dialog.dismiss(); + }); + }); + + Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL); + neutralButton.setOnClickListener(v -> { + Preferences.setStarredSyncEnabled(true); + dialog.dismiss(); + }); + + Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE); + negativeButton.setOnClickListener(v -> { + Preferences.setStarredSyncEnabled(false); + if (onCancel != null) onCancel.run(); + dialog.dismiss(); + }); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StreamingCacheStorageDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StreamingCacheStorageDialog.java new file mode 100644 index 0000000..964da01 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/StreamingCacheStorageDialog.java @@ -0,0 +1,76 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.DialogFragment; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogStreamingCacheStorageBinding; +import com.cappielloantonio.tempo.interfaces.DialogClickCallback; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +@OptIn(markerClass = UnstableApi.class) +public class StreamingCacheStorageDialog extends DialogFragment { + private final DialogClickCallback dialogClickCallback; + + public StreamingCacheStorageDialog(DialogClickCallback dialogClickCallback) { + this.dialogClickCallback = dialogClickCallback; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + DialogStreamingCacheStorageBinding bind = DialogStreamingCacheStorageBinding.inflate(getLayoutInflater()); + + return new MaterialAlertDialogBuilder(getActivity()) + .setView(bind.getRoot()) + .setTitle(R.string.streaming_cache_storage_dialog_title) + .setPositiveButton(R.string.streaming_cache_storage_external_dialog_positive_button, null) + .setNegativeButton(R.string.streaming_cache_storage_internal_dialog_negative_button, null) + .create(); + } + + @Override + public void onResume() { + super.onResume(); + setButtonAction(); + } + + private void setButtonAction() { + androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog(); + + if (dialog != null) { + Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(v -> { + int currentPreference = Preferences.getStreamingCacheStoragePreference(); + int newPreference = 1; + + if (currentPreference != newPreference) { + Preferences.setStreamingCacheStoragePreference(newPreference); + dialogClickCallback.onPositiveClick(); + } + + dialog.dismiss(); + }); + + Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE); + negativeButton.setOnClickListener(v -> { + int currentPreference = Preferences.getStreamingCacheStoragePreference(); + int newPreference = 0; + + if (currentPreference != newPreference) { + Preferences.setStreamingCacheStoragePreference(newPreference); + dialogClickCallback.onNegativeClick(); + } + + dialog.dismiss(); + }); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/TrackInfoDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/TrackInfoDialog.java new file mode 100644 index 0000000..e6b91f0 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/TrackInfoDialog.java @@ -0,0 +1,204 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.media3.common.MediaMetadata; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogTrackInfoBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.util.AssetLinkUtil; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.Objects; + +public class TrackInfoDialog extends DialogFragment { + private DialogTrackInfoBinding bind; + + private final MediaMetadata mediaMetadata; + private AssetLinkUtil.AssetLink songLink; + private AssetLinkUtil.AssetLink albumLink; + private AssetLinkUtil.AssetLink artistLink; + private AssetLinkUtil.AssetLink genreLink; + private AssetLinkUtil.AssetLink yearLink; + + public TrackInfoDialog(MediaMetadata mediaMetadata) { + this.mediaMetadata = mediaMetadata; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + bind = DialogTrackInfoBinding.inflate(getLayoutInflater()); + + return new MaterialAlertDialogBuilder(requireActivity()) + .setView(bind.getRoot()) + .setPositiveButton(R.string.track_info_dialog_positive_button, (dialog, id) -> dialog.cancel()) + .create(); + } + + @Override + public void onStart() { + super.onStart(); + + setTrackInfo(); + setTrackTranscodingInfo(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void setTrackInfo() { + genreLink = null; + yearLink = null; + bind.trakTitleInfoTextView.setText(mediaMetadata.title); + bind.trakArtistInfoTextView.setText( + mediaMetadata.artist != null + ? mediaMetadata.artist + : mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) + ? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder)) + : ""); + + if (mediaMetadata.extras != null) { + songLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_SONG, mediaMetadata.extras.getString("id")); + albumLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ALBUM, mediaMetadata.extras.getString("albumId")); + artistLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ARTIST, mediaMetadata.extras.getString("artistId")); + genreLink = AssetLinkUtil.parseLinkString(mediaMetadata.extras.getString("assetLinkGenre")); + yearLink = AssetLinkUtil.parseLinkString(mediaMetadata.extras.getString("assetLinkYear")); + + CustomGlideRequest.Builder + .from(requireContext(), mediaMetadata.extras.getString("coverArtId", ""), CustomGlideRequest.ResourceType.Song) + .build() + .into(bind.trackCoverInfoImageView); + + bindAssetLink(bind.trackCoverInfoImageView, albumLink != null ? albumLink : songLink); + bindAssetLink(bind.trakTitleInfoTextView, songLink); + bindAssetLink(bind.trakArtistInfoTextView, artistLink != null ? artistLink : songLink); + + String titleValue = mediaMetadata.extras.getString("title", getString(R.string.label_placeholder)); + String albumValue = mediaMetadata.extras.getString("album", getString(R.string.label_placeholder)); + String artistValue = mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder)); + String genreValue = mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder)); + int yearValue = mediaMetadata.extras.getInt("year", 0); + + if (genreLink == null && genreValue != null && !genreValue.isEmpty() && !getString(R.string.label_placeholder).contentEquals(genreValue)) { + genreLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_GENRE, genreValue); + } + + if (yearLink == null && yearValue != 0) { + yearLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(yearValue)); + } + + bind.titleValueSector.setText(titleValue); + bind.albumValueSector.setText(albumValue); + bind.artistValueSector.setText(artistValue); + bind.trackNumberValueSector.setText(mediaMetadata.extras.getInt("track", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("track", 0)) : getString(R.string.label_placeholder)); + bind.yearValueSector.setText(yearValue != 0 ? String.valueOf(yearValue) : getString(R.string.label_placeholder)); + bind.genreValueSector.setText(genreValue); + bind.sizeValueSector.setText(mediaMetadata.extras.getLong("size", 0) != 0 ? MusicUtil.getReadableByteCount(mediaMetadata.extras.getLong("size", 0)) : getString(R.string.label_placeholder)); + bind.contentTypeValueSector.setText(mediaMetadata.extras.getString("contentType", getString(R.string.label_placeholder))); + bind.suffixValueSector.setText(mediaMetadata.extras.getString("suffix", getString(R.string.label_placeholder))); + bind.transcodedContentTypeValueSector.setText(mediaMetadata.extras.getString("transcodedContentType", getString(R.string.label_placeholder))); + bind.transcodedSuffixValueSector.setText(mediaMetadata.extras.getString("transcodedSuffix", getString(R.string.label_placeholder))); + bind.durationValueSector.setText(mediaMetadata.extras.getInt("duration", 0) != 0 ? MusicUtil.getReadableDurationString(mediaMetadata.extras.getInt("duration", 0), false) : getString(R.string.label_placeholder)); + bind.bitrateValueSector.setText(mediaMetadata.extras.getInt("bitrate", 0) != 0 ? mediaMetadata.extras.getInt("bitrate", 0) + " kbps" : getString(R.string.label_placeholder)); + bind.samplingRateValueSector.setText(mediaMetadata.extras.getInt("samplingRate", 0) != 0 ? mediaMetadata.extras.getInt("samplingRate", 0) + " Hz" : getString(R.string.label_placeholder)); + bind.bitDepthValueSector.setText(mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + " bits" : getString(R.string.label_placeholder)); + bind.pathValueSector.setText(mediaMetadata.extras.getString("path", getString(R.string.label_placeholder))); + bind.discNumberValueSector.setText(mediaMetadata.extras.getInt("discNumber", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("discNumber", 0)) : getString(R.string.label_placeholder)); + + bindAssetLink(bind.titleValueSector, songLink); + bindAssetLink(bind.albumValueSector, albumLink); + bindAssetLink(bind.artistValueSector, artistLink); + bindAssetLink(bind.genreValueSector, genreLink); + bindAssetLink(bind.yearValueSector, yearLink); + } + } + + private void setTrackTranscodingInfo() { + StringBuilder info = new StringBuilder(); + + boolean prioritizeServerTranscoding = Preferences.isServerPrioritized(); + + String transcodingExtension = MusicUtil.getTranscodingFormatPreference(); + String transcodingBitrate = Integer.parseInt(MusicUtil.getBitratePreference()) != 0 ? Integer.parseInt(MusicUtil.getBitratePreference()) + "kbps" : "Original"; + + if (mediaMetadata.extras != null && mediaMetadata.extras.getString("uri", "").contains(Constants.DOWNLOAD_URI)) { + info.append(getString(R.string.track_info_summary_downloaded_file)); + + bind.trakTranscodingInfoTextView.setText(info); + return; + } + + if (prioritizeServerTranscoding) { + info.append(getString(R.string.track_info_summary_server_prioritized)); + + bind.trakTranscodingInfoTextView.setText(info); + return; + } + + if (!prioritizeServerTranscoding && transcodingExtension.equals("raw") && transcodingBitrate.equals("Original")) { + info.append(getString(R.string.track_info_summary_original_file)); + + bind.trakTranscodingInfoTextView.setText(info); + return; + } + + if (!prioritizeServerTranscoding && !transcodingExtension.equals("raw") && transcodingBitrate.equals("Original")) { + info.append(getString(R.string.track_info_summary_transcoding_codec, transcodingExtension)); + + bind.trakTranscodingInfoTextView.setText(info); + return; + } + + if (!prioritizeServerTranscoding && transcodingExtension.equals("raw") && !transcodingBitrate.equals("Original")) { + info.append(getString(R.string.track_info_summary_transcoding_bitrate, transcodingBitrate)); + + bind.trakTranscodingInfoTextView.setText(info); + return; + } + + if (!prioritizeServerTranscoding && !transcodingExtension.equals("raw") && !transcodingBitrate.equals("Original")) { + info.append(getString(R.string.track_info_summary_full_transcode, transcodingExtension, transcodingBitrate)); + + bind.trakTranscodingInfoTextView.setText(info); + } + } + + private void bindAssetLink(android.view.View view, AssetLinkUtil.AssetLink assetLink) { + if (view == null) return; + if (assetLink == null) { + AssetLinkUtil.clearLinkAppearance(view); + view.setOnClickListener(null); + view.setOnLongClickListener(null); + view.setClickable(false); + view.setLongClickable(false); + return; + } + + view.setClickable(true); + view.setLongClickable(true); + AssetLinkUtil.applyLinkAppearance(view); + view.setOnClickListener(v -> { + dismissAllowingStateLoss(); + boolean collapse = !AssetLinkUtil.TYPE_SONG.equals(assetLink.type); + ((com.cappielloantonio.tempo.ui.activity.MainActivity) requireActivity()).openAssetLink(assetLink, collapse); + }); + view.setOnLongClickListener(v -> { + AssetLinkUtil.copyToClipboard(requireContext(), assetLink); + Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show(); + return true; + }); + } + +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumCatalogueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumCatalogueFragment.java new file mode 100644 index 0000000..2b04e7e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumCatalogueFragment.java @@ -0,0 +1,288 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.PopupMenu; +import android.widget.SearchView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentAlbumCatalogueBinding; +import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.AlbumCatalogueViewModel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@OptIn(markerClass = UnstableApi.class) +public class AlbumCatalogueFragment extends Fragment implements ClickCallback { + private static final String TAG = "AlbumCatalogueFragment"; + + private FragmentAlbumCatalogueBinding bind; + private MainActivity activity; + private AlbumCatalogueViewModel albumCatalogueViewModel; + + private AlbumCatalogueAdapter albumAdapter; + private String currentSortOrder; + private List originalAlbums; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + currentSortOrder = Preferences.getAlbumSortOrder(); + + initData(); + } + + @Override + public void onResume() { + super.onResume(); + String latestSort = Preferences.getAlbumSortOrder(); + + if (!latestSort.equals(currentSortOrder)) { + currentSortOrder = latestSort; + } + // Re-apply sort when returning to fragment + if (originalAlbums != null && currentSortOrder != null) { + applySortToAlbums(currentSortOrder); + } else { + Log.d(TAG, "onResume - Cannot re-sort, missing data"); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + albumCatalogueViewModel.stopLoading(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentAlbumCatalogueBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + + initAppBar(); + initAlbumCatalogueView(); + initProgressLoader(); + + return view; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void initData() { + albumCatalogueViewModel = new ViewModelProvider(requireActivity()).get(AlbumCatalogueViewModel.class); + albumCatalogueViewModel.loadAlbums(); + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + bind.toolbar.setNavigationOnClickListener(v -> { + hideKeyboard(v); + activity.navController.navigateUp(); + }); + + + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.albumInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(R.string.album_catalogue_title); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + } + + @SuppressLint("ClickableViewAccessibility") + private void initAlbumCatalogueView() { + bind.albumCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); + bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); + bind.albumCatalogueRecyclerView.setHasFixedSize(true); + + albumAdapter = new AlbumCatalogueAdapter(this, true); + albumAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); + bind.albumCatalogueRecyclerView.setAdapter(albumAdapter); + albumCatalogueViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> { + originalAlbums = albums; + currentSortOrder = Preferences.getAlbumSortOrder(); + applySortToAlbums(currentSortOrder); + updateSortIndicator(); + }); + + bind.albumCatalogueRecyclerView.setOnTouchListener((v, event) -> { + hideKeyboard(v); + return false; + }); + + bind.albumListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_album_popup_menu)); + } + + private void applySortToAlbums(String sortOrder) { + if (originalAlbums == null) { + return; + } + albumAdapter.setItemsWithoutFilter(originalAlbums); + if (sortOrder != null) { + albumAdapter.sort(sortOrder); + } + } + + private void initProgressLoader() { + albumCatalogueViewModel.getLoadingStatus().observe(getViewLifecycleOwner(), isLoading -> { + if (isLoading) { + bind.albumListSortImageView.setEnabled(false); + bind.albumListProgressLoader.setVisibility(View.VISIBLE); + } else { + bind.albumListSortImageView.setEnabled(true); + bind.albumListProgressLoader.setVisibility(View.GONE); + } + }); + } + + private void updateSortIndicator() { + if (bind == null) return; + + String sortText = getSortDisplayText(currentSortOrder); + bind.albumListSortTextView.setText(sortText); + bind.albumListSortTextView.setVisibility(View.VISIBLE); + } + + private String getSortDisplayText(String sortOrder) { + if (sortOrder == null) return ""; + + switch (sortOrder) { + case Constants.ALBUM_ORDER_BY_NAME: + return getString(R.string.menu_sort_name); + case Constants.ALBUM_ORDER_BY_ARTIST: + return getString(R.string.menu_group_by_artist); + case Constants.ALBUM_ORDER_BY_YEAR: + return getString(R.string.menu_sort_year); + case Constants.ALBUM_ORDER_BY_RANDOM: + return getString(R.string.menu_sort_random); + case Constants.ALBUM_ORDER_BY_RECENTLY_ADDED: + return getString(R.string.menu_sort_recently_added); + case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED: + return getString(R.string.menu_sort_recently_played); + case Constants.ALBUM_ORDER_BY_MOST_PLAYED: + return getString(R.string.menu_sort_most_played); + default: + return ""; + } + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.toolbar_menu, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + + SearchView searchView = (SearchView) searchItem.getActionView(); + searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + albumAdapter.getFilter().filter(newText); + return false; + } + }); + + searchView.setPadding(-32, 0, 0, 0); + } + + private void hideKeyboard(View view) { + InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void showPopupMenu(View view, int menuResource) { + PopupMenu popup = new PopupMenu(requireContext(), view); + popup.getMenuInflater().inflate(menuResource, popup.getMenu()); + + popup.setOnMenuItemClickListener(menuItem -> { + String newSortOrder = null; + + if (menuItem.getItemId() == R.id.menu_album_sort_name) { + newSortOrder = Constants.ALBUM_ORDER_BY_NAME; + } else if (menuItem.getItemId() == R.id.menu_album_sort_artist) { + newSortOrder = Constants.ALBUM_ORDER_BY_ARTIST; + } else if (menuItem.getItemId() == R.id.menu_album_sort_year) { + newSortOrder = Constants.ALBUM_ORDER_BY_YEAR; + } else if (menuItem.getItemId() == R.id.menu_album_sort_random) { + newSortOrder = Constants.ALBUM_ORDER_BY_RANDOM; + } else if (menuItem.getItemId() == R.id.menu_album_sort_recently_added) { + newSortOrder = Constants.ALBUM_ORDER_BY_RECENTLY_ADDED; + } else if (menuItem.getItemId() == R.id.menu_album_sort_recently_played) { + newSortOrder = Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED; + } else if (menuItem.getItemId() == R.id.menu_album_sort_most_played) { + newSortOrder = Constants.ALBUM_ORDER_BY_MOST_PLAYED; + } + + if (newSortOrder != null) { + currentSortOrder = newSortOrder; + Preferences.setAlbumSortOrder(newSortOrder); + applySortToAlbums(newSortOrder); + updateSortIndicator(); + return true; + } + + return false; + }); + + popup.show(); + } + + @Override + public void onAlbumClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle); + hideKeyboard(requireView()); + } + + @Override + public void onAlbumLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumBottomSheetDialog, bundle); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java new file mode 100644 index 0000000..38dc9b1 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java @@ -0,0 +1,236 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.PopupMenu; +import androidx.appcompat.widget.SearchView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentAlbumListPageBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.AlbumListPageViewModel; + +import java.util.List; + +@OptIn(markerClass = UnstableApi.class) +public class AlbumListPageFragment extends Fragment implements ClickCallback { + private FragmentAlbumListPageBinding bind; + + private MainActivity activity; + private AlbumListPageViewModel albumListPageViewModel; + private AlbumHorizontalAdapter albumHorizontalAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentAlbumListPageBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + albumListPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumListPageViewModel.class); + + init(); + initAppBar(); + initAlbumListView(); + + return view; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + if (requireArguments().getString(Constants.ALBUM_RECENTLY_PLAYED) != null) { + albumListPageViewModel.title = Constants.ALBUM_RECENTLY_PLAYED; + bind.pageTitleLabel.setText(R.string.album_list_page_recently_played); + } else if (requireArguments().getString(Constants.ALBUM_MOST_PLAYED) != null) { + albumListPageViewModel.title = Constants.ALBUM_MOST_PLAYED; + bind.pageTitleLabel.setText(R.string.album_list_page_most_played); + } else if (requireArguments().getString(Constants.ALBUM_RECENTLY_ADDED) != null) { + albumListPageViewModel.title = Constants.ALBUM_RECENTLY_ADDED; + bind.pageTitleLabel.setText(R.string.album_list_page_recently_added); + } else if (requireArguments().getString(Constants.ALBUM_STARRED) != null) { + albumListPageViewModel.title = Constants.ALBUM_STARRED; + bind.pageTitleLabel.setText(R.string.album_list_page_starred); + } else if (requireArguments().getString(Constants.ALBUM_NEW_RELEASES) != null) { + albumListPageViewModel.title = Constants.ALBUM_NEW_RELEASES; + bind.pageTitleLabel.setText(R.string.album_list_page_new_releases); + } else if (requireArguments().getString(Constants.ALBUM_DOWNLOADED) != null) { + albumListPageViewModel.title = Constants.ALBUM_DOWNLOADED; + bind.pageTitleLabel.setText(R.string.album_list_page_downloaded); + } else if (requireArguments().getParcelable(Constants.ARTIST_OBJECT) != null) { + albumListPageViewModel.artist = requireArguments().getParcelable(Constants.ARTIST_OBJECT); + albumListPageViewModel.title = Constants.ALBUM_FROM_ARTIST; + bind.pageTitleLabel.setText(albumListPageViewModel.artist.getName()); + } + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + bind.toolbar.setNavigationOnClickListener(v -> { + hideKeyboard(v); + activity.navController.navigateUp(); + }); + + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.albumInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(R.string.album_list_page_title); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + } + + @SuppressLint("ClickableViewAccessibility") + private void initAlbumListView() { + bind.albumListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.albumListRecyclerView.setHasFixedSize(true); + + albumHorizontalAdapter = new AlbumHorizontalAdapter( + this, + (albumListPageViewModel.title.equals(Constants.ALBUM_DOWNLOADED) || albumListPageViewModel.title.equals(Constants.ALBUM_FROM_ARTIST)) + ); + + bind.albumListRecyclerView.setAdapter(albumHorizontalAdapter); + albumListPageViewModel.getAlbumList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { + albumHorizontalAdapter.setItems(albums); + setAlbumListPageSubtitle(albums); + setAlbumListPageSorter(); + }); + + bind.albumListRecyclerView.setOnTouchListener((v, event) -> { + hideKeyboard(v); + return false; + }); + + bind.albumListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_horizontal_album_popup_menu)); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.artist_list_menu, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + + SearchView searchView = (SearchView) searchItem.getActionView(); + searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + albumHorizontalAdapter.getFilter().filter(newText); + return false; + } + }); + + searchView.setPadding(-32, 0, 0, 0); + } + + private void hideKeyboard(View view) { + InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void showPopupMenu(View view, int menuResource) { + PopupMenu popup = new PopupMenu(requireContext(), view); + popup.getMenuInflater().inflate(menuResource, popup.getMenu()); + + popup.setOnMenuItemClickListener(menuItem -> { + if (menuItem.getItemId() == R.id.menu_horizontal_album_sort_name) { + albumHorizontalAdapter.sort(Constants.ALBUM_ORDER_BY_NAME); + return true; + } else if (menuItem.getItemId() == R.id.menu_horizontal_album_sort_most_recently_starred) { + albumHorizontalAdapter.sort(Constants.ALBUM_ORDER_BY_MOST_RECENTLY_STARRED); + return true; + } else if (menuItem.getItemId() == R.id.menu_horizontal_album_sort_least_recently_starred) { + albumHorizontalAdapter.sort(Constants.ALBUM_ORDER_BY_LEAST_RECENTLY_STARRED); + return true; + } + + return false; + }); + + popup.show(); + } + + private void setAlbumListPageSubtitle(List albums) { + switch (albumListPageViewModel.title) { + case Constants.ALBUM_RECENTLY_PLAYED: + case Constants.ALBUM_MOST_PLAYED: + case Constants.ALBUM_RECENTLY_ADDED: + bind.pageSubtitleLabel.setText(albums.size() < albumListPageViewModel.maxNumber ? + getString(R.string.generic_list_page_count, albums.size()) : + getString(R.string.generic_list_page_count_unknown, albumListPageViewModel.maxNumber) + ); + break; + case Constants.ALBUM_STARRED: + bind.pageSubtitleLabel.setText(getString(R.string.generic_list_page_count, albums.size())); + break; + } + } + + private void setAlbumListPageSorter() { + switch (albumListPageViewModel.title) { + case Constants.ALBUM_RECENTLY_PLAYED: + case Constants.ALBUM_MOST_PLAYED: + case Constants.ALBUM_RECENTLY_ADDED: + bind.albumListSortImageView.setVisibility(View.GONE); + break; + case Constants.ALBUM_STARRED: + bind.albumListSortImageView.setVisibility(View.VISIBLE); + break; + } + } + + @Override + public void onAlbumClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle); + } + + @Override + public void onAlbumLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumBottomSheetDialog, bundle); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java new file mode 100644 index 0000000..9bf9580 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java @@ -0,0 +1,401 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcelable; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentAlbumPageBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; +import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog; +import com.cappielloantonio.tempo.ui.dialog.RatingDialog; +import com.cappielloantonio.tempo.util.AssetLinkUtil; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.ExternalAudioWriter; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Objects; +import java.util.stream.Collectors; + +@UnstableApi +public class AlbumPageFragment extends Fragment implements ClickCallback { + private FragmentAlbumPageBinding bind; + private MainActivity activity; + private AlbumPageViewModel albumPageViewModel; + private PlaybackViewModel playbackViewModel; + private SongHorizontalAdapter songHorizontalAdapter; + private ListenableFuture mediaBrowserListenableFuture; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.album_page_menu, menu); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentAlbumPageBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + albumPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumPageViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); + + init(); + initAppBar(); + initAlbumInfoTextButton(); + initAlbumNotes(); + initMusicButton(); + initBackCover(); + initSongsView(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); + } + + public void onResume() { + super.onResume(); + if (songHorizontalAdapter != null) setMediaBrowserListenableFuture(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_rate_album) { + Bundle bundle = new Bundle(); + AlbumID3 album = albumPageViewModel.getAlbum().getValue(); + bundle.putParcelable(Constants.ALBUM_OBJECT, (Parcelable) album); + RatingDialog dialog = new RatingDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + return true; + } + + if (item.getItemId() == R.id.action_download_album) { + albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> { + if (Preferences.getDownloadDirectoryUri() == null) { + DownloadUtil.getDownloadTracker(requireContext()).download( + MappingUtil.mapDownloads(songs), + songs.stream().map(Download::new).collect(Collectors.toList()) + ); + } else { + songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child)); + } + }); + return true; + } + if (item.getItemId() == R.id.action_add_to_playlist) { + albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs)); + + PlaylistChooserDialog dialog = new PlaylistChooserDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + }); + return true; + } + + return false; + } + + private void init() { + albumPageViewModel.setAlbum(getViewLifecycleOwner(), requireArguments().getParcelable(Constants.ALBUM_OBJECT)); + } + + private void initAppBar() { + activity.setSupportActionBar(bind.animToolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + + } + + albumPageViewModel.getAlbum().observe(getViewLifecycleOwner(), album -> { + if (bind != null && album != null) { + bind.animToolbar.setTitle(album.getName()); + + bind.albumNameLabel.setText(album.getName()); + bind.albumArtistLabel.setText(album.getArtist()); + AssetLinkUtil.applyLinkAppearance(bind.albumArtistLabel); + AssetLinkUtil.AssetLink artistLink = buildArtistLink(album); + bind.albumArtistLabel.setOnLongClickListener(v -> { + if (artistLink != null) { + AssetLinkUtil.copyToClipboard(requireContext(), artistLink); + Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, artistLink.id), Toast.LENGTH_SHORT).show(); + return true; + } + return false; + }); + bind.albumReleaseYearLabel.setText(album.getYear() != 0 ? String.valueOf(album.getYear()) : ""); + if (album.getYear() != 0) { + bind.albumReleaseYearLabel.setVisibility(View.VISIBLE); + AssetLinkUtil.applyLinkAppearance(bind.albumReleaseYearLabel); + bind.albumReleaseYearLabel.setOnClickListener(v -> openYearLink(album.getYear())); + bind.albumReleaseYearLabel.setOnLongClickListener(v -> { + AssetLinkUtil.AssetLink yearLink = buildYearLink(album.getYear()); + if (yearLink != null) { + AssetLinkUtil.copyToClipboard(requireContext(), yearLink); + Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, yearLink.id), Toast.LENGTH_SHORT).show(); + } + return true; + }); + } else { + bind.albumReleaseYearLabel.setVisibility(View.GONE); + bind.albumReleaseYearLabel.setOnClickListener(null); + bind.albumReleaseYearLabel.setOnLongClickListener(null); + AssetLinkUtil.clearLinkAppearance(bind.albumReleaseYearLabel); + } + bind.albumSongCountDurationTextview.setText(getString(R.string.album_page_tracks_count_and_duration, album.getSongCount(), album.getDuration() != null ? album.getDuration() / 60 : 0)); + if (album.getGenre() != null && !album.getGenre().isEmpty()) { + bind.albumGenresTextview.setText(album.getGenre()); + bind.albumGenresTextview.setVisibility(View.VISIBLE); + } + else{ + bind.albumGenresTextview.setVisibility(View.GONE); + } + + if (album.getReleaseDate() != null && album.getOriginalReleaseDate() != null) { + if (album.getReleaseDate().getFormattedDate() != null || album.getOriginalReleaseDate().getFormattedDate() != null) + bind.albumReleaseYearsTextview.setVisibility(View.VISIBLE); + else + bind.albumReleaseYearsTextview.setVisibility(View.GONE); + + if (album.getReleaseDate().getFormattedDate() == null || album.getOriginalReleaseDate().getFormattedDate() == null) { + bind.albumReleaseYearsTextview.setText(getString(R.string.album_page_release_date_label, album.getReleaseDate() != null ? album.getReleaseDate().getFormattedDate() : album.getOriginalReleaseDate().getFormattedDate())); + } + + if (album.getReleaseDate().getFormattedDate() != null && album.getOriginalReleaseDate().getFormattedDate() != null) { + if (Objects.equals(album.getReleaseDate().getYear(), album.getOriginalReleaseDate().getYear()) && Objects.equals(album.getReleaseDate().getMonth(), album.getOriginalReleaseDate().getMonth()) && Objects.equals(album.getReleaseDate().getDay(), album.getOriginalReleaseDate().getDay())) { + bind.albumReleaseYearsTextview.setText(getString(R.string.album_page_release_date_label, album.getReleaseDate().getFormattedDate())); + } else { + bind.albumReleaseYearsTextview.setText(getString(R.string.album_page_release_dates_label, album.getReleaseDate().getFormattedDate(), album.getOriginalReleaseDate().getFormattedDate())); + } + } + } + } + }); + + bind.animToolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp()); + + Objects.requireNonNull(bind.animToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + + bind.albumOtherInfoButton.setOnClickListener(v -> { + if (bind.albumDetailView.getVisibility() == View.GONE) { + bind.albumDetailView.setVisibility(View.VISIBLE); + } else if (bind.albumDetailView.getVisibility() == View.VISIBLE) { + bind.albumDetailView.setVisibility(View.GONE); + } + }); + + if(Preferences.showAlbumDetail()){ + bind.albumDetailView.setVisibility(View.VISIBLE); + } + } + + private void initAlbumInfoTextButton() { + bind.albumArtistLabel.setOnClickListener(v -> albumPageViewModel.getArtist().observe(getViewLifecycleOwner(), artist -> { + if (artist != null) { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artist); + activity.navController.navigate(R.id.action_albumPageFragment_to_artistPageFragment, bundle); + } else + Toast.makeText(requireContext(), getString(R.string.album_error_retrieving_artist), Toast.LENGTH_SHORT).show(); + })); + } + + private void initAlbumNotes() { + albumPageViewModel.getAlbumInfo().observe(getViewLifecycleOwner(), albumInfo -> { + if (albumInfo != null) { + if (bind != null) bind.albumNotesTextview.setVisibility(View.VISIBLE); + if (bind != null) + bind.albumNotesTextview.setText(MusicUtil.forceReadableString(albumInfo.getNotes())); + + if (bind != null && albumInfo.getLastFmUrl() != null && !albumInfo.getLastFmUrl().isEmpty()) { + bind.albumNotesTextview.setOnClickListener(v -> { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(albumInfo.getLastFmUrl())); + startActivity(intent); + }); + } + } else { + if (bind != null) bind.albumNotesTextview.setVisibility(View.GONE); + } + }); + } + + private void initMusicButton() { + albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> { + if (bind != null && !songs.isEmpty()) { + bind.albumPagePlayButton.setOnClickListener(v -> { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + }); + + bind.albumPageShuffleButton.setOnClickListener(v -> { + Collections.shuffle(songs); + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + }); + } + + if (bind != null && songs.isEmpty()) { + bind.albumPagePlayButton.setEnabled(false); + bind.albumPageShuffleButton.setEnabled(false); + } + }); + } + + private void initBackCover() { + albumPageViewModel.getAlbum().observe(getViewLifecycleOwner(), album -> { + if (bind != null && album != null) { + CustomGlideRequest.Builder.from(requireContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album).build().into(bind.albumCoverImageView); + } + }); + } + + private void initSongsView() { + albumPageViewModel.getAlbum().observe(getViewLifecycleOwner(), album -> { + if (bind != null && album != null) { + bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.songRecyclerView.setHasFixedSize(true); + + songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, false, false, album); + bind.songRecyclerView.setAdapter(songHorizontalAdapter); + setMediaBrowserListenableFuture(); + reapplyPlayback(); + + albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> { + songHorizontalAdapter.setItems(songs); + reapplyPlayback(); + }); + } + }); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onMediaClick(Bundle bundle) { + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); + activity.setBottomSheetInPeek(true); + } + + @Override + public void onMediaLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); + } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> { + if (songHorizontalAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + } + + private void setMediaBrowserListenableFuture() { + songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + } + + private void openYearLink(int year) { + AssetLinkUtil.AssetLink link = buildYearLink(year); + if (link != null) { + activity.openAssetLink(link); + } + } + + private AssetLinkUtil.AssetLink buildYearLink(int year) { + if (year <= 0) return null; + return AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year)); + } + + private AssetLinkUtil.AssetLink buildArtistLink(AlbumID3 album) { + if (album == null || album.getArtistId() == null || album.getArtistId().isEmpty()) { + return null; + } + return AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ARTIST, album.getArtistId()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistCatalogueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistCatalogueFragment.java new file mode 100644 index 0000000..7e7ce75 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistCatalogueFragment.java @@ -0,0 +1,220 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.PopupMenu; +import android.widget.SearchView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import android.util.Log; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentArtistCatalogueBinding; +import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; + +import java.util.ArrayList; +import java.util.List; + +@UnstableApi +public class ArtistCatalogueFragment extends Fragment implements ClickCallback { + private static final String TAG = "ArtistCatalogueFragment"; + + private FragmentArtistCatalogueBinding bind; + private MainActivity activity; + private ArtistCatalogueViewModel artistCatalogueViewModel; + + private ArtistCatalogueAdapter artistAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + + initData(); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentArtistCatalogueBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + + initAppBar(); + initArtistCatalogueView(); + + return view; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void initData() { + artistCatalogueViewModel = new ViewModelProvider(requireActivity()).get(ArtistCatalogueViewModel.class); + artistCatalogueViewModel.loadArtists(); + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + bind.toolbar.setNavigationOnClickListener(v -> { + hideKeyboard(v); + activity.navController.navigateUp(); + }); + + + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.artistInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(R.string.artist_catalogue_title); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + } + + @SuppressLint("ClickableViewAccessibility") + private void initArtistCatalogueView() { + bind.artistCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); + bind.artistCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); + bind.artistCatalogueRecyclerView.setHasFixedSize(true); + + artistAdapter = new ArtistCatalogueAdapter(this); + artistAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); + bind.artistCatalogueRecyclerView.setAdapter(artistAdapter); + artistCatalogueViewModel.getArtistList().observe(getViewLifecycleOwner(), artistList -> { + artistAdapter.setItems(artistList); + artistAdapter.sort(Preferences.getArtistSortOrder()); + }); + + bind.artistCatalogueRecyclerView.setOnTouchListener((v, event) -> { + hideKeyboard(v); + return false; + }); + + bind.artistListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_artist_popup_menu)); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.toolbar_menu, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + + SearchView searchView = (SearchView) searchItem.getActionView(); + searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + + searchView.setQueryHint(getString(R.string.filter_artist)); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + // this toast may be overkill... + Toast.makeText(requireContext(), "Search: " + query, Toast.LENGTH_SHORT).show(); + filterArtists(query); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + filterArtists(newText); + return true; + } + }); + + searchView.setPadding(-32, 0, 0, 0); + } + + private void filterArtists(String query) { + List allArtists = artistCatalogueViewModel.getArtistList().getValue(); + + if (allArtists == null || allArtists.isEmpty()) { + return; + } + + if (query == null || query.trim().isEmpty()) { + artistAdapter.setItems(allArtists); + } else { + String searchQuery = query.toLowerCase().trim(); + List filteredArtists = new ArrayList<>(); + + for (ArtistID3 artist : allArtists) { + if (artist.getName() != null && + artist.getName().toLowerCase().contains(searchQuery)) { + filteredArtists.add(artist); + } + } + artistAdapter.setItems(filteredArtists); + } + } + + private void hideKeyboard(View view) { + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void showPopupMenu(View view, int menuResource) { + PopupMenu popup = new PopupMenu(requireContext(), view); + popup.getMenuInflater().inflate(menuResource, popup.getMenu()); + + popup.setOnMenuItemClickListener(menuItem -> { + if (menuItem.getItemId() == R.id.menu_artist_sort_name) { + artistAdapter.sort(Constants.ARTIST_ORDER_BY_NAME); + return true; + } else if (menuItem.getItemId() == R.id.menu_artist_sort_random) { + artistAdapter.sort(Constants.ARTIST_ORDER_BY_RANDOM); + return true; + } else if (menuItem.getItemId() == R.id.menu_artist_sort_album_count) { + artistAdapter.sort(Constants.ARTIST_ORDER_BY_ALBUM_COUNT); + return true; + } + + return false; + }); + + popup.show(); + } + + @Override + public void onArtistClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistPageFragment, bundle); + hideKeyboard(requireView()); + } + + @Override + public void onArtistLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistListPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistListPageFragment.java new file mode 100644 index 0000000..6b48230 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistListPageFragment.java @@ -0,0 +1,206 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.PopupMenu; +import android.widget.SearchView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentArtistListPageBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.ArtistHorizontalAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.ArtistListPageViewModel; + +import java.util.List; + +@UnstableApi +public class ArtistListPageFragment extends Fragment implements ClickCallback { + private FragmentArtistListPageBinding bind; + + private MainActivity activity; + private ArtistListPageViewModel artistListPageViewModel; + + private ArtistHorizontalAdapter artistHorizontalAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentArtistListPageBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + artistListPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistListPageViewModel.class); + + init(); + initAppBar(); + initArtistListView(); + + return view; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + if (requireArguments().getString(Constants.ARTIST_STARRED) != null) { + artistListPageViewModel.title = Constants.ARTIST_STARRED; + bind.pageTitleLabel.setText(R.string.artist_list_page_starred); + } else if (requireArguments().getString(Constants.ARTIST_DOWNLOADED) != null) { + artistListPageViewModel.title = Constants.ARTIST_DOWNLOADED; + bind.pageTitleLabel.setText(R.string.artist_list_page_downloaded); + } + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + bind.toolbar.setNavigationOnClickListener(v -> { + hideKeyboard(v); + activity.navController.navigateUp(); + }); + + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.artistInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(R.string.artist_list_page_title); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + } + + @SuppressLint("ClickableViewAccessibility") + private void initArtistListView() { + bind.artistListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.artistListRecyclerView.setHasFixedSize(true); + + artistHorizontalAdapter = new ArtistHorizontalAdapter(this); + bind.artistListRecyclerView.setAdapter(artistHorizontalAdapter); + artistListPageViewModel.getArtistList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> { + artistHorizontalAdapter.setItems(artists); + setArtistListPageSubtitle(artists); + setArtistListPageSorter(); + }); + + bind.artistListRecyclerView.setOnTouchListener((v, event) -> { + hideKeyboard(v); + return false; + }); + + bind.artistListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_horizontal_artist_popup_menu)); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.toolbar_menu, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + + SearchView searchView = (SearchView) searchItem.getActionView(); + searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + artistHorizontalAdapter.getFilter().filter(newText); + return false; + } + }); + + searchView.setPadding(-32, 0, 0, 0); + } + + private void hideKeyboard(View view) { + InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void showPopupMenu(View view, int menuResource) { + PopupMenu popup = new PopupMenu(requireContext(), view); + popup.getMenuInflater().inflate(menuResource, popup.getMenu()); + + popup.setOnMenuItemClickListener(menuItem -> { + if (menuItem.getItemId() == R.id.menu_horizontal_artist_sort_name) { + artistHorizontalAdapter.sort(Constants.ARTIST_ORDER_BY_NAME); + return true; + } else if (menuItem.getItemId() == R.id.menu_horizontal_artist_sort_most_recently_starred) { + artistHorizontalAdapter.sort(Constants.ARTIST_ORDER_BY_MOST_RECENTLY_STARRED); + return true; + } else if (menuItem.getItemId() == R.id.menu_horizontal_artist_sort_least_recently_starred) { + artistHorizontalAdapter.sort(Constants.ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED); + return true; + } + + return false; + }); + + popup.show(); + } + + private void setArtistListPageSubtitle(List artists) { + switch (artistListPageViewModel.title) { + case Constants.ARTIST_STARRED: + case Constants.ARTIST_DOWNLOADED: + bind.pageSubtitleLabel.setText(getString(R.string.generic_list_page_count, artists.size())); + break; + } + } + + private void setArtistListPageSorter() { + switch (artistListPageViewModel.title) { + case Constants.ARTIST_STARRED: + case Constants.ARTIST_DOWNLOADED: + bind.artistListSortImageView.setVisibility(View.VISIBLE); + break; + } + } + + @Override + public void onArtistClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistPageFragment, bundle); + } + + @Override + public void onArtistLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java new file mode 100644 index 0000000..2d39eea --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java @@ -0,0 +1,310 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentArtistPageBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.helper.recyclerview.CustomLinearSnapHelper; +import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter; +import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter; +import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.ArrayList; +import java.util.List; + +@UnstableApi +public class ArtistPageFragment extends Fragment implements ClickCallback { + private FragmentArtistPageBinding bind; + private MainActivity activity; + private ArtistPageViewModel artistPageViewModel; + private PlaybackViewModel playbackViewModel; + + private SongHorizontalAdapter songHorizontalAdapter; + private AlbumCatalogueAdapter albumCatalogueAdapter; + private ArtistCatalogueAdapter artistCatalogueAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentArtistPageBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); + + init(); + initAppBar(); + initArtistInfo(); + initPlayButtons(); + initTopSongsView(); + initAlbumsView(); + initSimilarArtistsView(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); + } + + public void onResume() { + super.onResume(); + if (songHorizontalAdapter != null) setMediaBrowserListenableFuture(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + artistPageViewModel.setArtist(requireArguments().getParcelable(Constants.ARTIST_OBJECT)); + + bind.mostStreamedSongTextViewClickable.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.MEDIA_BY_ARTIST, Constants.MEDIA_BY_ARTIST); + bundle.putParcelable(Constants.ARTIST_OBJECT, artistPageViewModel.getArtist()); + activity.navController.navigate(R.id.action_artistPageFragment_to_songListPageFragment, bundle); + }); + } + + private void initAppBar() { + activity.setSupportActionBar(bind.animToolbar); + if (activity.getSupportActionBar() != null) + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + bind.collapsingToolbar.setTitle(artistPageViewModel.getArtist().getName()); + bind.animToolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp()); + bind.collapsingToolbar.setExpandedTitleColor(getResources().getColor(R.color.white, null)); + } + + private void initArtistInfo() { + artistPageViewModel.getArtistInfo(artistPageViewModel.getArtist().getId()).observe(getViewLifecycleOwner(), artistInfo -> { + if (artistInfo == null) { + if (bind != null) bind.artistPageBioSector.setVisibility(View.GONE); + } else { + String normalizedBio = MusicUtil.forceReadableString(artistInfo.getBiography()); + + if (bind != null) + bind.artistPageBioSector.setVisibility(!normalizedBio.trim().isEmpty() ? View.VISIBLE : View.GONE); + if (bind != null) + bind.bioMoreTextViewClickable.setVisibility(artistInfo.getLastFmUrl() != null ? View.VISIBLE : View.GONE); + + if (getContext() != null && bind != null) CustomGlideRequest.Builder + .from(requireContext(), artistPageViewModel.getArtist().getId(), CustomGlideRequest.ResourceType.Artist) + .build() + .into(bind.artistBackdropImageView); + + if (bind != null) bind.bioTextView.setText(normalizedBio); + + if (bind != null) bind.bioMoreTextViewClickable.setOnClickListener(v -> { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(artistInfo.getLastFmUrl())); + startActivity(intent); + }); + + if (bind != null) bind.artistPageBioSector.setVisibility(View.VISIBLE); + } + }); + } + + private void initPlayButtons() { + bind.artistPageShuffleButton.setOnClickListener(v -> { + artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), songs -> { + if (!songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + } else { + Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show(); + } + }); + }); + + bind.artistPageRadioButton.setOnClickListener(v -> { + artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> { + if (songs != null && !songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + } else { + Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_radio), Toast.LENGTH_SHORT).show(); + } + }); + }); + } + + private void initTopSongsView() { + bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + + songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, true, null); + bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter); + setMediaBrowserListenableFuture(); + reapplyPlayback(); + artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> { + if (songs == null) { + if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.artistPageTopSongsSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); + songHorizontalAdapter.setItems(songs); + reapplyPlayback(); + } + }); + } + + private void initAlbumsView() { + bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); + bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); + bind.albumsRecyclerView.setHasFixedSize(true); + + albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false); + bind.albumsRecyclerView.setAdapter(albumCatalogueAdapter); + + artistPageViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> { + if (albums == null) { + if (bind != null) bind.artistPageAlbumsSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.artistPageAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); + albumCatalogueAdapter.setItems(albums); + } + }); + } + + private void initSimilarArtistsView() { + bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); + bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); + bind.similarArtistsRecyclerView.setHasFixedSize(true); + + artistCatalogueAdapter = new ArtistCatalogueAdapter(this); + bind.similarArtistsRecyclerView.setAdapter(artistCatalogueAdapter); + + artistPageViewModel.getArtistInfo(artistPageViewModel.getArtist().getId()).observe(getViewLifecycleOwner(), artist -> { + if (artist == null) { + if (bind != null) bind.similarArtistSector.setVisibility(View.GONE); + } else { + if (bind != null && artist.getSimilarArtists() != null) + bind.similarArtistSector.setVisibility(!artist.getSimilarArtists().isEmpty() ? View.VISIBLE : View.GONE); + + List artists = new ArrayList<>(); + + if (artist.getSimilarArtists() != null) { + artists.addAll(artist.getSimilarArtists()); + } + + artistCatalogueAdapter.setItems(artists); + } + }); + + CustomLinearSnapHelper similarArtistSnapHelper = new CustomLinearSnapHelper(); + similarArtistSnapHelper.attachToRecyclerView(bind.similarArtistsRecyclerView); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onMediaClick(Bundle bundle) { + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); + activity.setBottomSheetInPeek(true); + } + + @Override + public void onMediaLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); + } + + @Override + public void onAlbumClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle); + } + + @Override + public void onAlbumLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumBottomSheetDialog, bundle); + } + + @Override + public void onArtistClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistPageFragment, bundle); + } + + @Override + public void onArtistLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); + } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> { + if (songHorizontalAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + } + + private void setMediaBrowserListenableFuture() { + songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DirectoryFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DirectoryFragment.java new file mode 100644 index 0000000..104d79f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DirectoryFragment.java @@ -0,0 +1,200 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentDirectoryBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.interfaces.DialogClickCallback; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter; +import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.ExternalAudioWriter; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; +import java.util.stream.Collectors; + +@UnstableApi +public class DirectoryFragment extends Fragment implements ClickCallback { + private static final String TAG = "DirectoryFragment"; + + private FragmentDirectoryBinding bind; + private MainActivity activity; + private DirectoryViewModel directoryViewModel; + + private MusicDirectoryAdapter musicDirectoryAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + private MenuItem menuItem; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.directory_page_menu, menu); + + menuItem = menu.getItem(0); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentDirectoryBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + directoryViewModel = new ViewModelProvider(requireActivity()).get(DirectoryViewModel.class); + + initAppBar(); + initDirectoryListView(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + initializeMediaBrowser(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_download_directory) { + DownloadDirectoryDialog dialog = new DownloadDirectoryDialog(new DialogClickCallback() { + @Override + public void onPositiveClick() { + directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> { + if (isVisible() && getActivity() != null) { + List songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList()); + if (Preferences.getDownloadDirectoryUri() == null) { + DownloadUtil.getDownloadTracker(requireContext()).download( + MappingUtil.mapDownloads(songs), + songs.stream().map(Download::new).collect(Collectors.toList()) + ); + } else { + songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child)); + } + } + }); + } + }); + + dialog.show(activity.getSupportFragmentManager(), null); + + return true; + } + + return false; + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + if (bind != null) { + bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp()); + bind.directoryBackImageView.setOnClickListener(v -> activity.navController.navigateUp()); + } + } + + private void initDirectoryListView() { + bind.directoryRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.directoryRecyclerView.setHasFixedSize(true); + + musicDirectoryAdapter = new MusicDirectoryAdapter(this); + bind.directoryRecyclerView.setAdapter(musicDirectoryAdapter); + directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> { + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.directoryInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(directory.getName()); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + + bind.directoryTitleLabel.setText(directory.getName()); + + musicDirectoryAdapter.setItems(directory.getChildren()); + + menuItem.setVisible( + directory.getChildren() != null && directory.getChildren() + .stream() + .filter(child -> !child.isDir()) + .findFirst() + .orElse(null) != null + ); + }); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onMediaClick(Bundle bundle) { + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); + } + + @Override + public void onMediaLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); + } + + @Override + public void onMusicDirectoryClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.directoryFragment, bundle); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java new file mode 100644 index 0000000..12ca243 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java @@ -0,0 +1,315 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.PopupMenu; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentDownloadBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.model.DownloadStack; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.DownloadViewModel; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.common.util.concurrent.ListenableFuture; + +import android.content.Intent; +import android.app.Activity; +import android.net.Uri; +import android.widget.Toast; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@UnstableApi +public class DownloadFragment extends Fragment implements ClickCallback { + private static final String TAG = "DownloadFragment"; + private static final int REQUEST_CODE_PICK_DIRECTORY = 1002; + + private FragmentDownloadBinding bind; + private MainActivity activity; + private DownloadViewModel downloadViewModel; + + private DownloadHorizontalAdapter downloadHorizontalAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + private MaterialToolbar materialToolbar; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentDownloadBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + downloadViewModel = new ViewModelProvider(requireActivity()).get(DownloadViewModel.class); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initAppBar(); + initDownloadedView(); + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + activity.setBottomNavigationBarVisibility(true); + activity.setBottomSheetVisibility(true); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void initAppBar() { + materialToolbar = bind.getRoot().findViewById(R.id.toolbar); + + activity.setSupportActionBar(materialToolbar); + Objects.requireNonNull(materialToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + } + + private void initDownloadedView() { + bind.downloadedRecyclerView.setHasFixedSize(true); + + downloadHorizontalAdapter = new DownloadHorizontalAdapter(this); + bind.downloadedRecyclerView.setAdapter(downloadHorizontalAdapter); + + downloadViewModel.getDownloadedTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> { + if (songs != null) { + if (songs.isEmpty()) { + if (bind != null) { + bind.emptyDownloadLayout.setVisibility(View.VISIBLE); + bind.downloadDownloadedSector.setVisibility(View.GONE); + bind.downloadedGroupByImageView.setVisibility(View.GONE); + } + } else { + if (bind != null) { + bind.emptyDownloadLayout.setVisibility(View.GONE); + bind.downloadDownloadedSector.setVisibility(View.VISIBLE); + bind.downloadedGroupByImageView.setVisibility(View.VISIBLE); + + finishDownloadView(songs); + } + } + + if (bind != null) bind.loadingProgressBar.setVisibility(View.GONE); + } + }); + + downloadViewModel.getRefreshResult().observe(getViewLifecycleOwner(), count -> { + if (count == null || bind == null) { + return; + } + + if (count == -1) { + Toast.makeText(requireContext(), R.string.download_refresh_no_directory, Toast.LENGTH_SHORT).show(); + } else if (count == 0) { + Toast.makeText(requireContext(), R.string.download_refresh_no_changes, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText( + requireContext(), + getResources().getQuantityString(R.plurals.download_refresh_removed, count, count), + Toast.LENGTH_SHORT + ).show(); + } + }); + + bind.downloadedGroupByImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.download_popup_menu)); + bind.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack()); + bind.downloadedRefreshImageView.setOnClickListener(view -> downloadViewModel.refreshExternalDownloads()); + } + + private void finishDownloadView(List songs) { + downloadViewModel.getViewStack().observe(getViewLifecycleOwner(), stack -> { + bind.downloadedRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + + DownloadStack lastLevel = stack.get(stack.size() - 1); + + switch (lastLevel.getId()) { + case Constants.DOWNLOAD_TYPE_TRACK: + downloadHorizontalAdapter.setItems(Constants.DOWNLOAD_TYPE_TRACK, lastLevel.getId(), lastLevel.getView(), songs); + break; + case Constants.DOWNLOAD_TYPE_ALBUM: + downloadHorizontalAdapter.setItems(Constants.DOWNLOAD_TYPE_TRACK, lastLevel.getId(), lastLevel.getView(), songs); + break; + case Constants.DOWNLOAD_TYPE_ARTIST: + downloadHorizontalAdapter.setItems(Constants.DOWNLOAD_TYPE_ALBUM, lastLevel.getId(), lastLevel.getView(), songs); + break; + case Constants.DOWNLOAD_TYPE_GENRE: + downloadHorizontalAdapter.setItems(Constants.DOWNLOAD_TYPE_TRACK, lastLevel.getId(), lastLevel.getView(), songs); + break; + case Constants.DOWNLOAD_TYPE_YEAR: + downloadHorizontalAdapter.setItems(Constants.DOWNLOAD_TYPE_TRACK, lastLevel.getId(), lastLevel.getView(), songs); + break; + } + + bind.downloadedGoBackImageView.setVisibility(stack.size() > 1 ? View.VISIBLE : View.GONE); + + setupBackPressing(stack.size()); + setupShuffleButton(); + }); + } + + private void setupShuffleButton() { + bind.shuffleDownloadedTextViewClickable.setOnClickListener(view -> { + List songs = downloadHorizontalAdapter.getShuffling(); + + if (songs != null && !songs.isEmpty()) { + Collections.shuffle(songs); + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + } + }); + } + + private void setupBackPressing(int stackSize) { + requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (stackSize > 1) { + downloadViewModel.popViewStack(); + } else { + activity.navController.navigateUp(); + } + + remove(); + } + }); + } + + private void showPopupMenu(View view, int menuResource) { + PopupMenu popup = new PopupMenu(requireContext(), view); + popup.getMenuInflater().inflate(menuResource, popup.getMenu()); + + popup.setOnMenuItemClickListener(menuItem -> { + if (menuItem.getItemId() == R.id.menu_download_group_by_track) { + downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_TRACK, null)); + Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_TRACK); + return true; + } else if (menuItem.getItemId() == R.id.menu_download_group_by_album) { + downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_ALBUM, null)); + Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_ALBUM); + return true; + } else if (menuItem.getItemId() == R.id.menu_download_group_by_artist) { + downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_ARTIST, null)); + Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_ARTIST); + return true; + } else if (menuItem.getItemId() == R.id.menu_download_group_by_genre) { + downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_GENRE, null)); + Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_GENRE); + return true; + } else if (menuItem.getItemId() == R.id.menu_download_group_by_year) { + downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null)); + Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR); + return true; + } else if (menuItem.getItemId() == R.id.menu_download_set_directory) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + startActivityForResult(intent, REQUEST_CODE_PICK_DIRECTORY); + return true; + } + + return false; + }); + + popup.show(); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onYearClick(Bundle bundle) { + downloadViewModel.pushViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, bundle.getString(Constants.DOWNLOAD_TYPE_YEAR))); + } + + @Override + public void onGenreClick(Bundle bundle) { + downloadViewModel.pushViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_GENRE, bundle.getString(Constants.DOWNLOAD_TYPE_GENRE))); + } + + @Override + public void onArtistClick(Bundle bundle) { + downloadViewModel.pushViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_ARTIST, bundle.getString(Constants.DOWNLOAD_TYPE_ARTIST))); + } + + @Override + public void onAlbumClick(Bundle bundle) { + downloadViewModel.pushViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_ALBUM, bundle.getString(Constants.DOWNLOAD_TYPE_ALBUM))); + } + + @Override + public void onMediaClick(Bundle bundle) { + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); + activity.setBottomSheetInPeek(true); + } + + @Override + public void onMediaLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); + } + + @Override + public void onDownloadGroupLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.downloadBottomSheetDialog, bundle); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_CODE_PICK_DIRECTORY && resultCode == Activity.RESULT_OK) { + Uri uri = data.getData(); + if (uri != null) { + requireContext().getContentResolver().takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ); + Preferences.setDownloadDirectoryUri(uri.toString()); + ExternalAudioReader.refreshCache(); + Toast.makeText(requireContext(), "Download directory set", Toast.LENGTH_SHORT).show(); + } + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt new file mode 100644 index 0000000..0b97d51 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt @@ -0,0 +1,268 @@ +package com.cappielloantonio.tempo.ui.fragment + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.ServiceConnection +import android.content.BroadcastReceiver +import android.os.Bundle +import android.os.IBinder +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.annotation.OptIn +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.media3.common.util.UnstableApi +import com.cappielloantonio.tempo.R +import com.cappielloantonio.tempo.service.EqualizerManager +import com.cappielloantonio.tempo.service.MediaService +import com.cappielloantonio.tempo.util.Preferences + +class EqualizerFragment : Fragment() { + + private var equalizerManager: EqualizerManager? = null + private lateinit var eqBandsContainer: LinearLayout + private lateinit var eqSwitch: Switch + private lateinit var resetButton: Button + private lateinit var safeSpace: Space + private val bandSeekBars = mutableListOf() + + private var receiverRegistered = false + private val equalizerUpdatedReceiver = object : BroadcastReceiver() { + @OptIn(UnstableApi::class) + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == MediaService.ACTION_EQUALIZER_UPDATED) { + initUI() + restoreEqualizerPreferences() + } + } + } + + private val connection = object : ServiceConnection { + @OptIn(UnstableApi::class) + override fun onServiceConnected(className: ComponentName, service: IBinder) { + val binder = service as MediaService.LocalBinder + equalizerManager = binder.getEqualizerManager() + initUI() + restoreEqualizerPreferences() + } + + override fun onServiceDisconnected(arg0: ComponentName) { + equalizerManager = null + } + } + + @OptIn(UnstableApi::class) + override fun onStart() { + super.onStart() + Intent(requireContext(), MediaService::class.java).also { intent -> + intent.action = MediaService.ACTION_BIND_EQUALIZER + requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE) + } + if (!receiverRegistered) { + ContextCompat.registerReceiver( + requireContext(), + equalizerUpdatedReceiver, + IntentFilter(MediaService.ACTION_EQUALIZER_UPDATED), + ContextCompat.RECEIVER_NOT_EXPORTED + ) + receiverRegistered = true + } + } + + override fun onStop() { + super.onStop() + requireActivity().unbindService(connection) + equalizerManager = null + if (receiverRegistered) { + try { + requireContext().unregisterReceiver(equalizerUpdatedReceiver) + } catch (_: Exception) { + // ignore if not registered + } + receiverRegistered = false + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val root = inflater.inflate(R.layout.fragment_equalizer, container, false) + eqSwitch = root.findViewById(R.id.equalizer_switch) + eqSwitch.isChecked = Preferences.isEqualizerEnabled() + eqSwitch.jumpDrawablesToCurrentState() + return root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + eqBandsContainer = view.findViewById(R.id.eq_bands_container) + resetButton = view.findViewById(R.id.equalizer_reset_button) + safeSpace = view.findViewById(R.id.equalizer_bottom_space) + } + + private fun initUI() { + val manager = equalizerManager + val notSupportedView = view?.findViewById(R.id.equalizer_not_supported_container) + val switchRow = view?.findViewById(R.id.equalizer_switch_row) + + if (manager == null || manager.getNumberOfBands().toInt() == 0) { + switchRow?.visibility = View.GONE + resetButton.visibility = View.GONE + eqBandsContainer.visibility = View.GONE + safeSpace.visibility = View.GONE + notSupportedView?.visibility = View.VISIBLE + return + } + + notSupportedView?.visibility = View.GONE + switchRow?.visibility = View.VISIBLE + resetButton.visibility = View.VISIBLE + eqBandsContainer.visibility = View.VISIBLE + safeSpace.visibility = View.VISIBLE + + eqSwitch.setOnCheckedChangeListener(null) + updateUiEnabledState(eqSwitch.isChecked) + eqSwitch.setOnCheckedChangeListener { _, isChecked -> + manager.setEnabled(isChecked) + Preferences.setEqualizerEnabled(isChecked) + updateUiEnabledState(isChecked) + } + + createBandSliders() + + resetButton.setOnClickListener { + resetEqualizer() + saveBandLevelsToPreferences() + } + } + + private fun updateUiEnabledState(isEnabled: Boolean) { + resetButton.isEnabled = isEnabled + bandSeekBars.forEach { it.isEnabled = isEnabled } + } + + private fun formatDb(value: Int): String = if (value > 0) "+$value dB" else "$value dB" + + private fun createBandSliders() { + val manager = equalizerManager ?: return + eqBandsContainer.removeAllViews() + bandSeekBars.clear() + val bands = manager.getNumberOfBands() + val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500) + val minLevelDb = bandLevelRange[0] / 100 + val maxLevelDb = bandLevelRange[1] / 100 + + val savedLevels = Preferences.getEqualizerBandLevels(bands) + for (i in 0 until bands) { + val band = i.toShort() + val freq = manager.getCenterFreq(band) ?: 0 + + val row = LinearLayout(requireContext()).apply { + orientation = LinearLayout.HORIZONTAL + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + val topBottomMarginDp = 16 + topMargin = topBottomMarginDp.dpToPx(context) + bottomMargin = topBottomMarginDp.dpToPx(context) + } + setPadding(0, 8, 0, 8) + } + + val freqLabel = TextView(requireContext(), null, 0, R.style.LabelSmall).apply { + text = if (freq >= 1000) { + if (freq % 1000 == 0) { + "${freq / 1000} kHz" + } else { + String.format("%.1f kHz", freq / 1000f) + } + } else { + "$freq Hz" + } + gravity = Gravity.START + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f) + } + row.addView(freqLabel) + + val initialLevelDb = (savedLevels.getOrNull(i) ?: (manager.getBandLevel(band) ?: 0)) / 100 + val dbLabel = TextView(requireContext(), null, 0, R.style.LabelSmall).apply { + text = formatDb(initialLevelDb) + setPadding(12, 0, 0, 0) + gravity = Gravity.END + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f) + } + + val seekBar = SeekBar(requireContext()).apply { + max = maxLevelDb - minLevelDb + progress = initialLevelDb - minLevelDb + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 6f) + setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + val thisLevelDb = progress + minLevelDb + if (fromUser) { + manager.setBandLevel(band, (thisLevelDb * 100).toShort()) + saveBandLevelsToPreferences() + } + dbLabel.text = formatDb(thisLevelDb) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) {} + override fun onStopTrackingTouch(seekBar: SeekBar) {} + }) + } + bandSeekBars.add(seekBar) + row.addView(seekBar) + row.addView(dbLabel) + eqBandsContainer.addView(row) + } + } + + private fun resetEqualizer() { + val manager = equalizerManager ?: return + val bands = manager.getNumberOfBands() + val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500) + val minLevelDb = bandLevelRange[0] / 100 + val midLevelDb = 0 + + for (i in 0 until bands) { + manager.setBandLevel(i.toShort(), (0).toShort()) + bandSeekBars.getOrNull(i)?.progress = midLevelDb - minLevelDb + } + Preferences.setEqualizerBandLevels(ShortArray(bands.toInt())) + } + + private fun saveBandLevelsToPreferences() { + val manager = equalizerManager ?: return + val bands = manager.getNumberOfBands() + val levels = ShortArray(bands.toInt()) { i -> manager.getBandLevel(i.toShort()) ?: 0 } + Preferences.setEqualizerBandLevels(levels) + } + + private fun restoreEqualizerPreferences() { + val manager = equalizerManager ?: return + eqSwitch.isChecked = Preferences.isEqualizerEnabled() + updateUiEnabledState(eqSwitch.isChecked) + + val bands = manager.getNumberOfBands() + val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500) + val minLevelDb = bandLevelRange[0] / 100 + + val savedLevels = Preferences.getEqualizerBandLevels(bands) + for (i in 0 until bands) { + val savedDb = savedLevels[i] / 100 + manager.setBandLevel(i.toShort(), (savedDb * 100).toShort()) + bandSeekBars.getOrNull(i)?.progress = savedDb - minLevelDb + } + } + +} + +private fun Int.dpToPx(context: Context): Int = + (this * context.resources.displayMetrics.density).toInt() diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/FilterFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/FilterFragment.java new file mode 100644 index 0000000..54fd2be --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/FilterFragment.java @@ -0,0 +1,106 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentFilterBinding; +import com.cappielloantonio.tempo.subsonic.models.Genre; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.viewmodel.FilterViewModel; +import com.google.android.material.chip.Chip; + +@OptIn(markerClass = UnstableApi.class) +public class FilterFragment extends Fragment { + private static final String TAG = "FilterFragment"; + + private MainActivity activity; + private FragmentFilterBinding bind; + private FilterViewModel filterViewModel; + + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + bind = FragmentFilterBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class); + + init(); + initAppBar(); + setFilterChips(); + return view; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + Bundle bundle = new Bundle(); + bundle.putString(Constants.MEDIA_BY_GENRES, Constants.MEDIA_BY_GENRES); + bundle.putStringArrayList("filters_list", filterViewModel.getFilters()); + bundle.putStringArrayList("filter_name_list", filterViewModel.getFilterNames()); + bind.finishFilteringTextViewClickable.setOnClickListener(v -> { + if (filterViewModel.getFilters().size() > 1) + activity.navController.navigate(R.id.action_filterFragment_to_songListPageFragment, bundle); + else + Toast.makeText(requireContext(), getString(R.string.filter_info_selection), Toast.LENGTH_SHORT).show(); + }); + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp()); + + + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.genreFilterInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(R.string.filter_title); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + } + + private void setFilterChips() { + filterViewModel.getGenreList().observe(getViewLifecycleOwner(), genres -> { + bind.loadingProgressBar.setVisibility(View.GONE); + bind.filterContainer.setVisibility(View.VISIBLE); + for (Genre genre : genres) { + Chip chip = (Chip) requireActivity().getLayoutInflater().inflate(R.layout.chip_search_filter_genre, null, false); + chip.setText(genre.getGenre()); + chip.setChecked(filterViewModel.getFilters().contains(genre.getGenre())); + chip.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) + filterViewModel.addFilter(genre.getGenre(), buttonView.getText().toString()); + else + filterViewModel.removeFilter(genre.getGenre(), buttonView.getText().toString()); + }); + bind.filtersChipsGroup.addView(chip); + } + }); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/GenreCatalogueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/GenreCatalogueFragment.java new file mode 100644 index 0000000..801c323 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/GenreCatalogueFragment.java @@ -0,0 +1,172 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.PopupMenu; +import android.widget.SearchView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentGenreCatalogueBinding; +import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.GenreCatalogueAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.GenreCatalogueViewModel; + +@OptIn(markerClass = UnstableApi.class) +public class GenreCatalogueFragment extends Fragment implements ClickCallback { + private FragmentGenreCatalogueBinding bind; + private MainActivity activity; + private GenreCatalogueViewModel genreCatalogueViewModel; + + private GenreCatalogueAdapter genreCatalogueAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentGenreCatalogueBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + genreCatalogueViewModel = new ViewModelProvider(requireActivity()).get(GenreCatalogueViewModel.class); + + init(); + initAppBar(); + initGenreCatalogueView(); + + return view; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + bind.filterGenresTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_genreCatalogueFragment_to_filterFragment)); + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + bind.toolbar.setNavigationOnClickListener(v -> { + hideKeyboard(v); + activity.navController.navigateUp(); + }); + + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.genreInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(R.string.genre_catalogue_title); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + } + + @SuppressLint("ClickableViewAccessibility") + private void initGenreCatalogueView() { + bind.genreCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); + bind.genreCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 16, false)); + bind.genreCatalogueRecyclerView.setHasFixedSize(true); + + genreCatalogueAdapter = new GenreCatalogueAdapter(this); + genreCatalogueAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); + bind.genreCatalogueRecyclerView.setAdapter(genreCatalogueAdapter); + + genreCatalogueViewModel.getGenreList().observe(getViewLifecycleOwner(), genres -> genreCatalogueAdapter.setItems(genres) ); + + bind.genreCatalogueRecyclerView.setOnTouchListener((v, event) -> { + hideKeyboard(v); + return false; + }); + + bind.genreListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_genre_popup_menu)); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.toolbar_menu, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + + SearchView searchView = (SearchView) searchItem.getActionView(); + searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + genreCatalogueAdapter.getFilter().filter(newText); + return false; + } + }); + + searchView.setPadding(-32, 0, 0, 0); + } + + private void hideKeyboard(View view) { + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void showPopupMenu(View view, int menuResource) { + PopupMenu popup = new PopupMenu(requireContext(), view); + popup.getMenuInflater().inflate(menuResource, popup.getMenu()); + + popup.setOnMenuItemClickListener(menuItem -> { + if (menuItem.getItemId() == R.id.menu_genre_sort_name) { + genreCatalogueAdapter.sort(Constants.GENRE_ORDER_BY_NAME); + return true; + } else if (menuItem.getItemId() == R.id.menu_genre_sort_random) { + genreCatalogueAdapter.sort(Constants.GENRE_ORDER_BY_RANDOM); + return true; + } + + return false; + }); + + popup.show(); + } + + @Override + public void onGenreClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songListPageFragment, bundle); + hideKeyboard(requireView()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java new file mode 100644 index 0000000..a0d0380 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java @@ -0,0 +1,104 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentHomeBinding; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.fragment.pager.HomePager; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +import java.util.Objects; + +@UnstableApi +public class HomeFragment extends Fragment { + private static final String TAG = "HomeFragment"; + + private FragmentHomeBinding bind; + private MainActivity activity; + + private MaterialToolbar materialToolbar; + private AppBarLayout appBarLayout; + private TabLayout tabLayout; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + bind = FragmentHomeBinding.inflate(inflater, container, false); + return bind.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initAppBar(); + initHomePager(); + } + + @Override + public void onStart() { + super.onStart(); + + activity.setBottomNavigationBarVisibility(true); + activity.setBottomSheetVisibility(true); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void initAppBar() { + appBarLayout = bind.getRoot().findViewById(R.id.toolbar_fragment); + materialToolbar = bind.getRoot().findViewById(R.id.toolbar); + + activity.setSupportActionBar(materialToolbar); + Objects.requireNonNull(materialToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + + tabLayout = new TabLayout(requireContext()); + tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); + tabLayout.setTabMode(TabLayout.MODE_FIXED); + + appBarLayout.addView(tabLayout); + } + + private void initHomePager() { + HomePager pager = new HomePager(this); + + pager.addFragment(new HomeTabMusicFragment(), getString(R.string.home_section_music), R.drawable.ic_home); + + if (Preferences.isPodcastSectionVisible()) + pager.addFragment(new HomeTabPodcastFragment(), getString(R.string.home_section_podcast), R.drawable.ic_graphic_eq); + + if (Preferences.isRadioSectionVisible()) + pager.addFragment(new HomeTabRadioFragment(), getString(R.string.home_section_radio), R.drawable.ic_play_for_work); + + bind.homeViewPager.setAdapter(pager); + bind.homeViewPager.setOffscreenPageLimit(3); + bind.homeViewPager.setUserInputEnabled(false); + + new TabLayoutMediator(tabLayout, bind.homeViewPager, + (tab, position) -> { + tab.setText(pager.getPageTitle(position)); + // tab.setIcon(pager.getPageIcon(position)); + } + ).attach(); + + tabLayout.setVisibility(Preferences.isPodcastSectionVisible() || Preferences.isRadioSectionVisible() ? View.VISIBLE : View.GONE); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java new file mode 100644 index 0000000..7936b5b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabMusicFragment.java @@ -0,0 +1,1233 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.PopupMenu; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.PagerSnapHelper; +import androidx.recyclerview.widget.SnapHelper; +import androidx.viewpager2.widget.ViewPager2; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentHomeTabMusicBinding; +import com.cappielloantonio.tempo.helper.recyclerview.CustomLinearSnapHelper; +import com.cappielloantonio.tempo.helper.recyclerview.DotsIndicatorDecoration; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.interfaces.PlaylistCallback; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.model.HomeSector; +import com.cappielloantonio.tempo.service.DownloaderManager; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Share; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter; +import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter; +import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter; +import com.cappielloantonio.tempo.ui.adapter.ArtistHorizontalAdapter; +import com.cappielloantonio.tempo.ui.adapter.DiscoverSongAdapter; +import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter; +import com.cappielloantonio.tempo.ui.adapter.ShareHorizontalAdapter; +import com.cappielloantonio.tempo.ui.adapter.SimilarTrackAdapter; +import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; +import com.cappielloantonio.tempo.ui.adapter.YearAdapter; +import com.cappielloantonio.tempo.ui.dialog.HomeRearrangementDialog; +import com.cappielloantonio.tempo.ui.dialog.PlaylistEditorDialog; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.util.UIUtil; +import com.cappielloantonio.tempo.viewmodel.HomeViewModel; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; +import com.google.android.material.snackbar.Snackbar; +import com.google.common.util.concurrent.ListenableFuture; + +import androidx.media3.common.MediaItem; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@UnstableApi +public class HomeTabMusicFragment extends Fragment implements ClickCallback { + private static final String TAG = "HomeFragment"; + + private FragmentHomeTabMusicBinding bind; + private MainActivity activity; + private HomeViewModel homeViewModel; + private PlaybackViewModel playbackViewModel; + + private DiscoverSongAdapter discoverSongAdapter; + private SimilarTrackAdapter similarMusicAdapter; + private ArtistAdapter radioArtistAdapter; + private ArtistAdapter bestOfArtistAdapter; + private SongHorizontalAdapter starredSongAdapter; + private SongHorizontalAdapter topSongAdapter; + private AlbumHorizontalAdapter starredAlbumAdapter; + private ArtistHorizontalAdapter starredArtistAdapter; + private AlbumAdapter recentlyAddedAlbumAdapter; + private AlbumAdapter recentlyPlayedAlbumAdapter; + private AlbumAdapter mostPlayedAlbumAdapter; + private AlbumHorizontalAdapter newReleasesAlbumAdapter; + private YearAdapter yearAdapter; + private PlaylistHorizontalAdapter playlistHorizontalAdapter; + private ShareHorizontalAdapter shareHorizontalAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentHomeTabMusicBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); + + init(); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initSyncStarredView(); + initSyncStarredAlbumsView(); + initSyncStarredArtistsView(); + initDiscoverSongSlideView(); + initSimilarSongView(); + initArtistRadio(); + initArtistBestOf(); + initStarredTracksView(); + initStarredAlbumsView(); + initStarredArtistsView(); + initMostPlayedAlbumView(); + initRecentPlayedAlbumView(); + initNewReleasesView(); + initYearSongView(); + initRecentAddedAlbumView(); + initTopSongsView(); + initPinnedPlaylistsView(); + initSharesView(); + initHomeReorganizer(); + + reorder(); + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observeStarredSongsPlayback(); + observeTopSongsPlayback(); + } + + @Override + public void onResume() { + super.onResume(); + refreshSharesView(); + if (topSongAdapter != null) setTopSongsMediaBrowserListenableFuture(); + if (starredSongAdapter != null) setStarredSongsMediaBrowserListenableFuture(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + bind.discoveryTextViewRefreshable.setOnLongClickListener(v -> { + homeViewModel.refreshDiscoverySongSample(getViewLifecycleOwner()); + return true; + }); + + bind.discoveryTextViewClickable.setOnClickListener(v -> { + homeViewModel.getRandomShuffleSample().observe(getViewLifecycleOwner(), songs -> { + MusicUtil.ratingFilter(songs); + + if (!songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + } + }); + }); + + bind.similarTracksTextViewRefreshable.setOnLongClickListener(v -> { + homeViewModel.refreshSimilarSongSample(getViewLifecycleOwner()); + return true; + }); + + bind.radioArtistTextViewRefreshable.setOnLongClickListener(v -> { + homeViewModel.refreshRadioArtistSample(getViewLifecycleOwner()); + return true; + }); + + bind.bestOfArtistTextViewRefreshable.setOnLongClickListener(v -> { + homeViewModel.refreshBestOfArtist(getViewLifecycleOwner()); + return true; + }); + + bind.starredTracksTextViewClickable.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.MEDIA_STARRED, Constants.MEDIA_STARRED); + activity.navController.navigate(R.id.action_homeFragment_to_songListPageFragment, bundle); + }); + + bind.starredAlbumsTextViewClickable.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.ALBUM_STARRED, Constants.ALBUM_STARRED); + activity.navController.navigate(R.id.action_homeFragment_to_albumListPageFragment, bundle); + }); + + bind.starredArtistsTextViewClickable.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.ARTIST_STARRED, Constants.ARTIST_STARRED); + activity.navController.navigate(R.id.action_homeFragment_to_artistListPageFragment, bundle); + }); + + bind.recentlyAddedAlbumsTextViewClickable.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.ALBUM_RECENTLY_ADDED, Constants.ALBUM_RECENTLY_ADDED); + activity.navController.navigate(R.id.action_homeFragment_to_albumListPageFragment, bundle); + }); + + bind.recentlyPlayedAlbumsTextViewClickable.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.ALBUM_RECENTLY_PLAYED, Constants.ALBUM_RECENTLY_PLAYED); + activity.navController.navigate(R.id.action_homeFragment_to_albumListPageFragment, bundle); + }); + + bind.mostPlayedAlbumsTextViewClickable.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.ALBUM_MOST_PLAYED, Constants.ALBUM_MOST_PLAYED); + activity.navController.navigate(R.id.action_homeFragment_to_albumListPageFragment, bundle); + }); + + bind.starredTracksTextViewRefreshable.setOnLongClickListener(v -> { + homeViewModel.refreshStarredTracks(getViewLifecycleOwner()); + return true; + }); + + bind.starredAlbumsTextViewRefreshable.setOnLongClickListener(v -> { + homeViewModel.refreshStarredAlbums(getViewLifecycleOwner()); + return true; + }); + + bind.starredArtistsTextViewRefreshable.setOnLongClickListener(v -> { + homeViewModel.refreshStarredArtists(getViewLifecycleOwner()); + return true; + }); + + bind.recentlyPlayedAlbumsTextViewRefreshable.setOnLongClickListener(v -> { + homeViewModel.refreshRecentlyPlayedAlbumList(getViewLifecycleOwner()); + return true; + }); + + bind.mostPlayedAlbumsTextViewRefreshable.setOnLongClickListener(v -> { + homeViewModel.refreshMostPlayedAlbums(getViewLifecycleOwner()); + return true; + }); + + bind.recentlyAddedAlbumsTextViewRefreshable.setOnLongClickListener(v -> { + homeViewModel.refreshMostRecentlyAddedAlbums(getViewLifecycleOwner()); + return true; + }); + + bind.sharesTextViewRefreshable.setOnLongClickListener(v -> { + homeViewModel.refreshShares(getViewLifecycleOwner()); + return true; + }); + + bind.gridTracksPreTextView.setOnClickListener(view -> showPopupMenu(view, R.menu.filter_top_songs_popup_menu)); + } + + private void initSyncStarredView() { + if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) { + homeViewModel.getAllStarredTracks().observeForever(new Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null) { + DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext()); + List toSync = new ArrayList<>(); + + for (Child song : songs) { + if (!manager.isDownloaded(song.getId())) { + toSync.add(song.getTitle()); + } + } + + if (!toSync.isEmpty()) { + bind.homeSyncStarredCard.setVisibility(View.VISIBLE); + bind.homeSyncStarredTracksToSync.setText(String.join(", ", toSync)); + } + } + + homeViewModel.getAllStarredTracks().removeObserver(this); + } + }); + } + + bind.homeSyncStarredCancel.setOnClickListener(v -> bind.homeSyncStarredCard.setVisibility(View.GONE)); + + bind.homeSyncStarredDownload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + homeViewModel.getAllStarredTracks().observeForever(new Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null) { + DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext()); + + for (Child song : songs) { + if (!manager.isDownloaded(song.getId())) { + manager.download(MappingUtil.mapDownload(song), new Download(song)); + } + } + } + + homeViewModel.getAllStarredTracks().removeObserver(this); + bind.homeSyncStarredCard.setVisibility(View.GONE); + } + }); + } + }); + } + + private void initSyncStarredAlbumsView() { + if (Preferences.isStarredAlbumsSyncEnabled()) { + homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List albums) { + if (albums != null && !albums.isEmpty()) { + checkIfAlbumsNeedSync(albums); + } + } + }); + } + + bind.homeSyncStarredAlbumsCancel.setOnClickListener(v -> { + bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE); + }); + + bind.homeSyncStarredAlbumsDownload.setOnClickListener(v -> { + homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List allSongs) { + if (allSongs != null && !allSongs.isEmpty()) { + DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext()); + int songsToDownload = 0; + + for (Child song : allSongs) { + if (!manager.isDownloaded(song.getId())) { + manager.download(MappingUtil.mapDownload(song), new Download(song)); + songsToDownload++; + } + } + + if (songsToDownload > 0) { + Toast.makeText(requireContext(), + getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload), + Toast.LENGTH_SHORT).show(); + } + } + + bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE); + } + }); + }); + } + + private void checkIfAlbumsNeedSync(List albums) { + homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List allSongs) { + if (allSongs != null) { + DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext()); + int songsToDownload = 0; + List albumsNeedingSync = new ArrayList<>(); + + for (AlbumID3 album : albums) { + boolean albumNeedsSync = false; + // Check if any songs from this album need downloading + for (Child song : allSongs) { + if (song.getAlbumId() != null && song.getAlbumId().equals(album.getId()) && + !manager.isDownloaded(song.getId())) { + songsToDownload++; + albumNeedsSync = true; + } + } + if (albumNeedsSync) { + albumsNeedingSync.add(album.getName()); + } + } + + if (songsToDownload > 0) { + bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE); + String message = getResources().getQuantityString( + R.plurals.home_sync_starred_albums_count, + albumsNeedingSync.size(), + albumsNeedingSync.size() + ); + bind.homeSyncStarredAlbumsToSync.setText(message); + } else { + bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE); + } + } + } + }); + } + + private void initSyncStarredArtistsView() { + if (Preferences.isStarredArtistsSyncEnabled()) { + homeViewModel.getStarredArtists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List artists) { + if (artists != null && !artists.isEmpty()) { + checkIfArtistsNeedSync(artists); + } + } + }); + } + + bind.homeSyncStarredArtistsCancel.setOnClickListener(v -> { + bind.homeSyncStarredArtistsCard.setVisibility(View.GONE); + }); + + bind.homeSyncStarredArtistsDownload.setOnClickListener(v -> { + homeViewModel.getAllStarredArtistSongs().observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List allSongs) { + if (allSongs != null && !allSongs.isEmpty()) { + DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext()); + int songsToDownload = 0; + + for (Child song : allSongs) { + if (!manager.isDownloaded(song.getId())) { + manager.download(MappingUtil.mapDownload(song), new Download(song)); + songsToDownload++; + } + } + + if (songsToDownload > 0) { + Toast.makeText(requireContext(), + getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload), + Toast.LENGTH_SHORT).show(); + } + } + + bind.homeSyncStarredArtistsCard.setVisibility(View.GONE); + } + }); + }); + } + + private void checkIfArtistsNeedSync(List artists) { + homeViewModel.getAllStarredArtistSongs().observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List allSongs) { + if (allSongs != null) { + DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext()); + int songsToDownload = 0; + List artistsNeedingSync = new ArrayList<>(); + + for (ArtistID3 artist : artists) { + boolean artistNeedsSync = false; + // Check if any songs from this artist need downloading + for (Child song : allSongs) { + if (song.getArtistId() != null && song.getArtistId().equals(artist.getId()) && + !manager.isDownloaded(song.getId())) { + songsToDownload++; + artistNeedsSync = true; + } + } + if (artistNeedsSync) { + artistsNeedingSync.add(artist.getName()); + } + } + + if (songsToDownload > 0) { + bind.homeSyncStarredArtistsCard.setVisibility(View.VISIBLE); + String message = getResources().getQuantityString( + R.plurals.home_sync_starred_artists_count, + artistsNeedingSync.size(), + artistsNeedingSync.size() + ); + bind.homeSyncStarredArtistsToSync.setText(message); + } else { + bind.homeSyncStarredArtistsCard.setVisibility(View.GONE); + } + } + } + }); + } + + private void initDiscoverSongSlideView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return; + + bind.discoverSongViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL); + + discoverSongAdapter = new DiscoverSongAdapter(this); + bind.discoverSongViewPager.setAdapter(discoverSongAdapter); + bind.discoverSongViewPager.setOffscreenPageLimit(1); + homeViewModel.getDiscoverSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> { + MusicUtil.ratingFilter(songs); + + if (songs == null) { + if (bind != null) bind.homeDiscoverSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homeDiscoverSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); + + discoverSongAdapter.setItems(songs); + } + }); + + setSlideViewOffset(bind.discoverSongViewPager, 20, 16); + } + + private void initSimilarSongView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_MADE_FOR_YOU)) return; + + bind.similarTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.similarTracksRecyclerView.setHasFixedSize(true); + + similarMusicAdapter = new SimilarTrackAdapter(this); + bind.similarTracksRecyclerView.setAdapter(similarMusicAdapter); + homeViewModel.getStarredTracksSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> { + MusicUtil.ratingFilter(songs); + + if (songs == null) { + if (bind != null) bind.homeSimilarTracksSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homeSimilarTracksSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); + + similarMusicAdapter.setItems(songs); + } + }); + + CustomLinearSnapHelper similarSongSnapHelper = new CustomLinearSnapHelper(); + similarSongSnapHelper.attachToRecyclerView(bind.similarTracksRecyclerView); + } + + private void initArtistBestOf() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_BEST_OF)) return; + + bind.bestOfArtistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.bestOfArtistRecyclerView.setHasFixedSize(true); + + bestOfArtistAdapter = new ArtistAdapter(this, false, true); + bind.bestOfArtistRecyclerView.setAdapter(bestOfArtistAdapter); + homeViewModel.getBestOfArtists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> { + if (artists == null) { + if (bind != null) bind.homeBestOfArtistSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homeBestOfArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); + + bestOfArtistAdapter.setItems(artists); + } + }); + + CustomLinearSnapHelper artistBestOfSnapHelper = new CustomLinearSnapHelper(); + artistBestOfSnapHelper.attachToRecyclerView(bind.bestOfArtistRecyclerView); + } + + private void initArtistRadio() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_RADIO_STATION)) return; + + bind.radioArtistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.radioArtistRecyclerView.setHasFixedSize(true); + + radioArtistAdapter = new ArtistAdapter(this, true, false); + bind.radioArtistRecyclerView.setAdapter(radioArtistAdapter); + homeViewModel.getStarredArtistsSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> { + if (artists == null) { + if (bind != null) bind.homeRadioArtistSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homeRadioArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); + if (bind != null) + bind.afterRadioArtistDivider.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); + + radioArtistAdapter.setItems(artists); + } + }); + + CustomLinearSnapHelper artistRadioSnapHelper = new CustomLinearSnapHelper(); + artistRadioSnapHelper.attachToRecyclerView(bind.radioArtistRecyclerView); + } + + private void initTopSongsView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_TOP_SONGS)) return; + + bind.topSongsRecyclerView.setHasFixedSize(true); + + topSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null); + bind.topSongsRecyclerView.setAdapter(topSongAdapter); + setTopSongsMediaBrowserListenableFuture(); + reapplyTopSongsPlayback(); + homeViewModel.getChronologySample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> { + if (chronologies == null || chronologies.isEmpty()) { + if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE); + if (bind != null) bind.afterGridDivider.setVisibility(View.GONE); + } else { + if (bind != null) bind.homeGridTracksSector.setVisibility(View.VISIBLE); + if (bind != null) bind.afterGridDivider.setVisibility(View.VISIBLE); + if (bind != null) + bind.topSongsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(chronologies.size(), 5), GridLayoutManager.HORIZONTAL, false)); + + List topSongs = chronologies.stream() + .map(cronologia -> (Child) cronologia) + .collect(Collectors.toList()); + + topSongAdapter.setItems(topSongs); + reapplyTopSongsPlayback(); + } + }); + + SnapHelper topTrackSnapHelper = new PagerSnapHelper(); + topTrackSnapHelper.attachToRecyclerView(bind.topSongsRecyclerView); + + bind.topSongsRecyclerView.addItemDecoration( + new DotsIndicatorDecoration( + getResources().getDimensionPixelSize(R.dimen.radius), + getResources().getDimensionPixelSize(R.dimen.radius) * 4, + getResources().getDimensionPixelSize(R.dimen.dots_height), + requireContext().getResources().getColor(R.color.titleTextColor, null), + requireContext().getResources().getColor(R.color.titleTextColor, null)) + ); + } + + private void initStarredTracksView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_STARRED_TRACKS)) return; + + bind.starredTracksRecyclerView.setHasFixedSize(true); + + starredSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null); + bind.starredTracksRecyclerView.setAdapter(starredSongAdapter); + setStarredSongsMediaBrowserListenableFuture(); + reapplyStarredSongsPlayback(); + homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> { + if (songs == null) { + if (bind != null) bind.starredTracksSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.starredTracksSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); + if (bind != null) + bind.starredTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(songs.size(), 5), GridLayoutManager.HORIZONTAL, false)); + + starredSongAdapter.setItems(songs); + reapplyStarredSongsPlayback(); + } + }); + + SnapHelper starredTrackSnapHelper = new PagerSnapHelper(); + starredTrackSnapHelper.attachToRecyclerView(bind.starredTracksRecyclerView); + + bind.starredTracksRecyclerView.addItemDecoration( + new DotsIndicatorDecoration( + getResources().getDimensionPixelSize(R.dimen.radius), + getResources().getDimensionPixelSize(R.dimen.radius) * 4, + getResources().getDimensionPixelSize(R.dimen.dots_height), + requireContext().getResources().getColor(R.color.titleTextColor, null), + requireContext().getResources().getColor(R.color.titleTextColor, null)) + ); + } + + private void initStarredAlbumsView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_STARRED_ALBUMS)) return; + + bind.starredAlbumsRecyclerView.setHasFixedSize(true); + + starredAlbumAdapter = new AlbumHorizontalAdapter(this, false); + bind.starredAlbumsRecyclerView.setAdapter(starredAlbumAdapter); + homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { + if (albums == null) { + if (bind != null) bind.starredAlbumsSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.starredAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); + if (bind != null) + bind.starredAlbumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(albums.size(), 5), GridLayoutManager.HORIZONTAL, false)); + + starredAlbumAdapter.setItems(albums); + } + }); + + SnapHelper starredAlbumSnapHelper = new PagerSnapHelper(); + starredAlbumSnapHelper.attachToRecyclerView(bind.starredAlbumsRecyclerView); + + bind.starredAlbumsRecyclerView.addItemDecoration( + new DotsIndicatorDecoration( + getResources().getDimensionPixelSize(R.dimen.radius), + getResources().getDimensionPixelSize(R.dimen.radius) * 4, + getResources().getDimensionPixelSize(R.dimen.dots_height), + requireContext().getResources().getColor(R.color.titleTextColor, null), + requireContext().getResources().getColor(R.color.titleTextColor, null)) + ); + } + + private void initStarredArtistsView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_STARRED_ARTISTS)) return; + + bind.starredArtistsRecyclerView.setHasFixedSize(true); + + starredArtistAdapter = new ArtistHorizontalAdapter(this); + bind.starredArtistsRecyclerView.setAdapter(starredArtistAdapter); + homeViewModel.getStarredArtists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> { + if (artists == null) { + if (bind != null) bind.starredArtistsSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.starredArtistsSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); + if (bind != null) + bind.afterFavoritesDivider.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); + if (bind != null) + bind.starredArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(artists.size(), 5), GridLayoutManager.HORIZONTAL, false)); + + starredArtistAdapter.setItems(artists); + } + }); + + SnapHelper starredArtistSnapHelper = new PagerSnapHelper(); + starredArtistSnapHelper.attachToRecyclerView(bind.starredArtistsRecyclerView); + + bind.starredArtistsRecyclerView.addItemDecoration( + new DotsIndicatorDecoration( + getResources().getDimensionPixelSize(R.dimen.radius), + getResources().getDimensionPixelSize(R.dimen.radius) * 4, + getResources().getDimensionPixelSize(R.dimen.dots_height), + requireContext().getResources().getColor(R.color.titleTextColor, null), + requireContext().getResources().getColor(R.color.titleTextColor, null)) + ); + } + + private void initNewReleasesView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_NEW_RELEASES)) return; + + bind.newReleasesRecyclerView.setHasFixedSize(true); + + newReleasesAlbumAdapter = new AlbumHorizontalAdapter(this, false); + bind.newReleasesRecyclerView.setAdapter(newReleasesAlbumAdapter); + homeViewModel.getRecentlyReleasedAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { + if (albums == null) { + if (bind != null) bind.homeNewReleasesSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homeNewReleasesSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); + if (bind != null) + bind.newReleasesRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(albums.size(), 5), GridLayoutManager.HORIZONTAL, false)); + + newReleasesAlbumAdapter.setItems(albums); + } + }); + + SnapHelper newReleasesSnapHelper = new PagerSnapHelper(); + newReleasesSnapHelper.attachToRecyclerView(bind.newReleasesRecyclerView); + + bind.newReleasesRecyclerView.addItemDecoration( + new DotsIndicatorDecoration( + getResources().getDimensionPixelSize(R.dimen.radius), + getResources().getDimensionPixelSize(R.dimen.radius) * 4, + getResources().getDimensionPixelSize(R.dimen.dots_height), + requireContext().getResources().getColor(R.color.titleTextColor, null), + requireContext().getResources().getColor(R.color.titleTextColor, null)) + ); + } + + private void initYearSongView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_FLASHBACK)) return; + + bind.yearsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.yearsRecyclerView.setHasFixedSize(true); + + yearAdapter = new YearAdapter(this); + bind.yearsRecyclerView.setAdapter(yearAdapter); + homeViewModel.getYearList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), years -> { + if (years == null) { + if (bind != null) bind.homeFlashbackSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homeFlashbackSector.setVisibility(!years.isEmpty() ? View.VISIBLE : View.GONE); + + yearAdapter.setItems(years); + } + }); + + CustomLinearSnapHelper yearSnapHelper = new CustomLinearSnapHelper(); + yearSnapHelper.attachToRecyclerView(bind.yearsRecyclerView); + } + + private void initMostPlayedAlbumView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_MOST_PLAYED)) return; + + bind.mostPlayedAlbumsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.mostPlayedAlbumsRecyclerView.setHasFixedSize(true); + + mostPlayedAlbumAdapter = new AlbumAdapter(this); + bind.mostPlayedAlbumsRecyclerView.setAdapter(mostPlayedAlbumAdapter); + homeViewModel.getMostPlayedAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { + if (albums == null) { + if (bind != null) bind.homeMostPlayedAlbumsSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homeMostPlayedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); + + mostPlayedAlbumAdapter.setItems(albums); + } + }); + + CustomLinearSnapHelper mostPlayedAlbumSnapHelper = new CustomLinearSnapHelper(); + mostPlayedAlbumSnapHelper.attachToRecyclerView(bind.mostPlayedAlbumsRecyclerView); + } + + private void initRecentPlayedAlbumView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_LAST_PLAYED)) return; + + bind.recentlyPlayedAlbumsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.recentlyPlayedAlbumsRecyclerView.setHasFixedSize(true); + + recentlyPlayedAlbumAdapter = new AlbumAdapter(this); + bind.recentlyPlayedAlbumsRecyclerView.setAdapter(recentlyPlayedAlbumAdapter); + homeViewModel.getRecentlyPlayedAlbumList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { + if (albums == null) { + if (bind != null) bind.homeRecentlyPlayedAlbumsSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homeRecentlyPlayedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); + + recentlyPlayedAlbumAdapter.setItems(albums); + } + }); + + CustomLinearSnapHelper recentPlayedAlbumSnapHelper = new CustomLinearSnapHelper(); + recentPlayedAlbumSnapHelper.attachToRecyclerView(bind.recentlyPlayedAlbumsRecyclerView); + } + + private void initRecentAddedAlbumView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_RECENTLY_ADDED)) return; + + bind.recentlyAddedAlbumsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.recentlyAddedAlbumsRecyclerView.setHasFixedSize(true); + + recentlyAddedAlbumAdapter = new AlbumAdapter(this); + bind.recentlyAddedAlbumsRecyclerView.setAdapter(recentlyAddedAlbumAdapter); + homeViewModel.getMostRecentlyAddedAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { + if (albums == null) { + if (bind != null) bind.homeRecentlyAddedAlbumsSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homeRecentlyAddedAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); + + recentlyAddedAlbumAdapter.setItems(albums); + } + }); + + CustomLinearSnapHelper recentAddedAlbumSnapHelper = new CustomLinearSnapHelper(); + recentAddedAlbumSnapHelper.attachToRecyclerView(bind.recentlyAddedAlbumsRecyclerView); + } + + private void initPinnedPlaylistsView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_PINNED_PLAYLISTS)) return; + + bind.pinnedPlaylistsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.pinnedPlaylistsRecyclerView.setHasFixedSize(true); + + playlistHorizontalAdapter = new PlaylistHorizontalAdapter(this); + bind.pinnedPlaylistsRecyclerView.setAdapter(playlistHorizontalAdapter); + homeViewModel.getPinnedPlaylists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), playlists -> { + if (playlists == null) { + if (bind != null) bind.pinnedPlaylistsSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.pinnedPlaylistsSector.setVisibility(!playlists.isEmpty() ? View.VISIBLE : View.GONE); + + playlistHorizontalAdapter.setItems(playlists); + } + }); + } + + private void initSharesView() { + if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_SHARED)) return; + + bind.sharesRecyclerView.setHasFixedSize(true); + + shareHorizontalAdapter = new ShareHorizontalAdapter(this); + bind.sharesRecyclerView.setAdapter(shareHorizontalAdapter); + if (Preferences.isSharingEnabled()) { + homeViewModel.getShares(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), shares -> { + if (shares == null) { + if (bind != null) bind.sharesSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.sharesSector.setVisibility(!shares.isEmpty() ? View.VISIBLE : View.GONE); + if (bind != null) + bind.sharesRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(shares.size(), 10), GridLayoutManager.HORIZONTAL, false)); + + shareHorizontalAdapter.setItems(shares); + } + }); + } + + SnapHelper starredTrackSnapHelper = new PagerSnapHelper(); + starredTrackSnapHelper.attachToRecyclerView(bind.sharesRecyclerView); + + bind.sharesRecyclerView.addItemDecoration( + new DotsIndicatorDecoration( + getResources().getDimensionPixelSize(R.dimen.radius), + getResources().getDimensionPixelSize(R.dimen.radius) * 4, + getResources().getDimensionPixelSize(R.dimen.dots_height), + requireContext().getResources().getColor(R.color.titleTextColor, null), + requireContext().getResources().getColor(R.color.titleTextColor, null)) + ); + } + + private void initHomeReorganizer() { + final Handler handler = new Handler(); + final Runnable runnable = () -> { + if (bind != null) bind.homeSectorRearrangementButton.setVisibility(View.VISIBLE); + }; + handler.postDelayed(runnable, 5000); + + bind.homeSectorRearrangementButton.setOnClickListener(v -> { + HomeRearrangementDialog dialog = new HomeRearrangementDialog(); + dialog.show(requireActivity().getSupportFragmentManager(), null); + }); + } + + private void refreshSharesView() { + final Handler handler = new Handler(); + final Runnable runnable = () -> { + if (getView() != null && bind != null && Preferences.isSharingEnabled()) { + homeViewModel.refreshShares(getViewLifecycleOwner()); + } + }; + handler.postDelayed(runnable, 100); + } + + private void setSlideViewOffset(ViewPager2 viewPager, float pageOffset, float pageMargin) { + viewPager.setPageTransformer((page, position) -> { + float myOffset = position * -(2 * pageOffset + pageMargin); + if (viewPager.getOrientation() == ViewPager2.ORIENTATION_HORIZONTAL) { + if (ViewCompat.getLayoutDirection(viewPager) == ViewCompat.LAYOUT_DIRECTION_RTL) { + page.setTranslationX(-myOffset); + } else { + page.setTranslationX(myOffset); + } + } else { + page.setTranslationY(myOffset); + } + }); + } + + public void reorder() { + if (bind != null && homeViewModel.getHomeSectorList() != null) { + bind.homeLinearLayoutContainer.removeAllViews(); + + for (HomeSector sector : homeViewModel.getHomeSectorList()) { + if (!sector.isVisible()) continue; + + switch (sector.getId()) { + case Constants.HOME_SECTOR_DISCOVERY: + bind.homeLinearLayoutContainer.addView(bind.homeDiscoverSector); + break; + case Constants.HOME_SECTOR_MADE_FOR_YOU: + bind.homeLinearLayoutContainer.addView(bind.homeSimilarTracksSector); + break; + case Constants.HOME_SECTOR_BEST_OF: + bind.homeLinearLayoutContainer.addView(bind.homeBestOfArtistSector); + break; + case Constants.HOME_SECTOR_RADIO_STATION: + bind.homeLinearLayoutContainer.addView(bind.homeRadioArtistSector); + break; + case Constants.HOME_SECTOR_TOP_SONGS: + bind.homeLinearLayoutContainer.addView(bind.homeGridTracksSector); + break; + case Constants.HOME_SECTOR_STARRED_TRACKS: + bind.homeLinearLayoutContainer.addView(bind.starredTracksSector); + break; + case Constants.HOME_SECTOR_STARRED_ALBUMS: + bind.homeLinearLayoutContainer.addView(bind.starredAlbumsSector); + break; + case Constants.HOME_SECTOR_STARRED_ARTISTS: + bind.homeLinearLayoutContainer.addView(bind.starredArtistsSector); + break; + case Constants.HOME_SECTOR_NEW_RELEASES: + bind.homeLinearLayoutContainer.addView(bind.homeNewReleasesSector); + break; + case Constants.HOME_SECTOR_FLASHBACK: + bind.homeLinearLayoutContainer.addView(bind.homeFlashbackSector); + break; + case Constants.HOME_SECTOR_MOST_PLAYED: + bind.homeLinearLayoutContainer.addView(bind.homeMostPlayedAlbumsSector); + break; + case Constants.HOME_SECTOR_LAST_PLAYED: + bind.homeLinearLayoutContainer.addView(bind.homeRecentlyPlayedAlbumsSector); + break; + case Constants.HOME_SECTOR_RECENTLY_ADDED: + bind.homeLinearLayoutContainer.addView(bind.homeRecentlyAddedAlbumsSector); + break; + case Constants.HOME_SECTOR_PINNED_PLAYLISTS: + bind.homeLinearLayoutContainer.addView(bind.pinnedPlaylistsSector); + break; + case Constants.HOME_SECTOR_SHARED: + bind.homeLinearLayoutContainer.addView(bind.sharesSector); + break; + } + } + + bind.homeLinearLayoutContainer.addView(bind.homeSectorRearrangementButton); + } + } + + private void showPopupMenu(View view, int menuResource) { + PopupMenu popup = new PopupMenu(requireContext(), view); + popup.getMenuInflater().inflate(menuResource, popup.getMenu()); + + popup.setOnMenuItemClickListener(menuItem -> { + if (menuItem.getItemId() == R.id.menu_last_week_name) { + homeViewModel.changeChronologyPeriod(getViewLifecycleOwner(), 0); + bind.gridTracksPreTextView.setText(getString(R.string.home_title_last_week)); + return true; + } else if (menuItem.getItemId() == R.id.menu_last_month_name) { + homeViewModel.changeChronologyPeriod(getViewLifecycleOwner(), 1); + bind.gridTracksPreTextView.setText(getString(R.string.home_title_last_month)); + return true; + } else if (menuItem.getItemId() == R.id.menu_last_year_name) { + homeViewModel.changeChronologyPeriod(getViewLifecycleOwner(), 2); + bind.gridTracksPreTextView.setText(getString(R.string.home_title_last_year)); + return true; + } + + return false; + }); + + popup.show(); + } + + private void refreshPlaylistView() { + final Handler handler = new Handler(); + + final Runnable runnable = () -> { + if (getView() != null && bind != null && homeViewModel != null) + homeViewModel.getPinnedPlaylists(getViewLifecycleOwner()); + }; + + handler.postDelayed(runnable, 100); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onMediaClick(Bundle bundle) { + if (bundle.containsKey(Constants.MEDIA_MIX)) { + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelable(Constants.TRACK_OBJECT)); + activity.setBottomSheetInPeek(true); + + if (mediaBrowserListenableFuture != null) { + homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.TRACK_OBJECT)).observe(getViewLifecycleOwner(), songs -> { + MusicUtil.ratingFilter(songs); + + if (songs != null && !songs.isEmpty()) { + MediaManager.enqueue(mediaBrowserListenableFuture, songs, true); + } + }); + } + } else if (bundle.containsKey(Constants.MEDIA_CHRONOLOGY)) { + List media = bundle.getParcelableArrayList(Constants.TRACKS_OBJECT); + MediaManager.startQueue(mediaBrowserListenableFuture, media, bundle.getInt(Constants.ITEM_POSITION)); + activity.setBottomSheetInPeek(true); + } else { + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); + activity.setBottomSheetInPeek(true); + } + topSongAdapter.notifyDataSetChanged(); + starredSongAdapter.notifyDataSetChanged(); + } + + @Override + public void onMediaLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); + } + + @Override + public void onAlbumClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle); + } + + @Override + public void onAlbumLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumBottomSheetDialog, bundle); + } + + @Override + public void onArtistClick(Bundle bundle) { + if (bundle.containsKey(Constants.MEDIA_MIX) && bundle.getBoolean(Constants.MEDIA_MIX)) { + Snackbar.make(requireView(), R.string.artist_adapter_radio_station_starting, Snackbar.LENGTH_LONG) + .setAnchorView(activity.bind.playerBottomSheet) + .show(); + + if (mediaBrowserListenableFuture != null) { + homeViewModel.getArtistInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.ARTIST_OBJECT)).observe(getViewLifecycleOwner(), songs -> { + MusicUtil.ratingFilter(songs); + + if (!songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + } + }); + } + } else if (bundle.containsKey(Constants.MEDIA_BEST_OF) && bundle.getBoolean(Constants.MEDIA_BEST_OF)) { + if (mediaBrowserListenableFuture != null) { + homeViewModel.getArtistBestOf(getViewLifecycleOwner(), bundle.getParcelable(Constants.ARTIST_OBJECT)).observe(getViewLifecycleOwner(), songs -> { + MusicUtil.ratingFilter(songs); + + if (!songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + } + }); + } + } else { + Navigation.findNavController(requireView()).navigate(R.id.artistPageFragment, bundle); + } + } + + @Override + public void onArtistLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); + } + + @Override + public void onYearClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songListPageFragment, bundle); + } + + @Override + public void onShareClick(Bundle bundle) { + Share share = bundle.getParcelable(Constants.SHARE_OBJECT); + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(share.getUrl())).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + @Override + public void onPlaylistClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.playlistPageFragment, bundle); + } + + @Override + public void onPlaylistLongClick(Bundle bundle) { + PlaylistEditorDialog dialog = new PlaylistEditorDialog(new PlaylistCallback() { + @Override + public void onDismiss() { + refreshPlaylistView(); + } + }); + + dialog.setArguments(bundle); + dialog.show(activity.getSupportFragmentManager(), null); + } + + @Override + public void onShareLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.shareBottomSheetDialog, bundle); + } + + private void observeStarredSongsPlayback() { + playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> { + if (starredSongAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + starredSongAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (starredSongAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + starredSongAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void observeTopSongsPlayback() { + playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> { + if (topSongAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + topSongAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (topSongAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + topSongAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyStarredSongsPlayback() { + if (starredSongAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + starredSongAdapter.setPlaybackState(id, playing != null && playing); + } + } + + private void reapplyTopSongsPlayback() { + if (topSongAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + topSongAdapter.setPlaybackState(id, playing != null && playing); + } + } + + private void setTopSongsMediaBrowserListenableFuture() { + topSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + } + + private void setStarredSongsMediaBrowserListenableFuture() { + starredSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabPodcastFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabPodcastFragment.java new file mode 100644 index 0000000..f1c42e1 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabPodcastFragment.java @@ -0,0 +1,188 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentHomeTabPodcastBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.interfaces.PodcastCallback; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.PodcastChannelHorizontalAdapter; +import com.cappielloantonio.tempo.ui.adapter.PodcastEpisodeAdapter; +import com.cappielloantonio.tempo.ui.dialog.PodcastChannelEditorDialog; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.util.UIUtil; +import com.cappielloantonio.tempo.viewmodel.PodcastViewModel; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.Objects; +import java.util.stream.Collectors; + +@UnstableApi +public class HomeTabPodcastFragment extends Fragment implements ClickCallback, PodcastCallback { + private static final String TAG = "HomeTabPodcastFragment"; + + private FragmentHomeTabPodcastBinding bind; + private MainActivity activity; + private PodcastViewModel podcastViewModel; + + private PodcastEpisodeAdapter podcastEpisodeAdapter; + private PodcastChannelHorizontalAdapter podcastChannelHorizontalAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentHomeTabPodcastBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + podcastViewModel = new ViewModelProvider(requireActivity()).get(PodcastViewModel.class); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + init(); + initPodcastView(); + initNewestPodcastsView(); + initPodcastChannelsView(); + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + bind.podcastChannelsPreTextView.setOnClickListener(v -> { + PodcastChannelEditorDialog dialog = new PodcastChannelEditorDialog(this); + dialog.show(activity.getSupportFragmentManager(), null); + }); + + bind.podcastChannelsTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_homeFragment_to_podcastChannelCatalogueFragment)); + bind.hideSectionButton.setOnClickListener(v -> Preferences.setPodcastSectionHidden()); + } + + private void initPodcastView() { + podcastViewModel.getPodcastChannels(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), podcastChannels -> { + if (podcastChannels == null) { + if (bind != null) bind.homePodcastChannelsSector.setVisibility(View.GONE); + if (bind != null) bind.emptyPodcastLayout.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homePodcastChannelsSector.setVisibility(!podcastChannels.isEmpty() ? View.VISIBLE : View.GONE); + if (bind != null) + bind.emptyPodcastLayout.setVisibility(podcastChannels.isEmpty() ? View.VISIBLE : View.GONE); + } + }); + } + + private void initPodcastChannelsView() { + bind.podcastChannelsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + + podcastChannelHorizontalAdapter = new PodcastChannelHorizontalAdapter(this); + bind.podcastChannelsRecyclerView.setAdapter(podcastChannelHorizontalAdapter); + podcastViewModel.getPodcastChannels(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), podcastChannels -> { + if (podcastChannels == null) { + if (bind != null) bind.homePodcastChannelsSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homePodcastChannelsSector.setVisibility(!podcastChannels.isEmpty() ? View.VISIBLE : View.GONE); + + podcastChannelHorizontalAdapter.setItems(podcastChannels); + } + }); + } + + private void initNewestPodcastsView() { + bind.newestPodcastsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.newestPodcastsRecyclerView.addItemDecoration(UIUtil.getDividerItemDecoration(requireContext())); + + podcastEpisodeAdapter = new PodcastEpisodeAdapter(this); + bind.newestPodcastsRecyclerView.setAdapter(podcastEpisodeAdapter); + podcastViewModel.getNewestPodcastEpisodes(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), podcastEpisodes -> { + if (podcastEpisodes == null) { + if (bind != null) bind.homeNewestPodcastsSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homeNewestPodcastsSector.setVisibility(!podcastEpisodes.isEmpty() ? View.VISIBLE : View.GONE); + + podcastEpisodeAdapter.setItems(podcastEpisodes.stream().filter(podcastEpisode -> Objects.equals(podcastEpisode.getStatus(), "completed")).collect(Collectors.toList())); + } + }); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onPodcastEpisodeClick(Bundle bundle) { + MediaManager.startPodcast(mediaBrowserListenableFuture, bundle.getParcelable(Constants.PODCAST_OBJECT)); + activity.setBottomSheetInPeek(true); + } + + @Override + public void onPodcastEpisodeLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.podcastEpisodeBottomSheetDialog, bundle); + } + + @Override + public void onPodcastChannelClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.podcastChannelPageFragment, bundle); + } + + @Override + public void onPodcastChannelLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.podcastChannelBottomSheetDialog, bundle); + } + + @Override + public void onDismiss() { + new Handler().postDelayed(() -> { + if (podcastViewModel != null) podcastViewModel.refreshPodcastChannels(getViewLifecycleOwner()); + if (podcastViewModel != null) podcastViewModel.refreshNewestPodcastEpisodes(getViewLifecycleOwner()); + }, 1000); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabRadioFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabRadioFragment.java new file mode 100644 index 0000000..f9174c3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeTabRadioFragment.java @@ -0,0 +1,151 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.databinding.FragmentHomeTabRadioBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.interfaces.RadioCallback; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.InternetRadioStationAdapter; +import com.cappielloantonio.tempo.ui.dialog.RadioEditorDialog; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.RadioViewModel; +import com.google.common.util.concurrent.ListenableFuture; + +@UnstableApi +public class HomeTabRadioFragment extends Fragment implements ClickCallback, RadioCallback { + private static final String TAG = "HomeTabRadioFragment"; + + private FragmentHomeTabRadioBinding bind; + private MainActivity activity; + private RadioViewModel radioViewModel; + + private InternetRadioStationAdapter internetRadioStationAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentHomeTabRadioBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + radioViewModel = new ViewModelProvider(requireActivity()).get(RadioViewModel.class); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + init(); + initRadioStationView(); + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + bind.internetRadioStationPreTextView.setOnClickListener(v -> { + RadioEditorDialog dialog = new RadioEditorDialog(this); + dialog.show(activity.getSupportFragmentManager(), null); + }); + + bind.internetRadioStationTitleTextView.setOnLongClickListener(v -> { + radioViewModel.getInternetRadioStations(getViewLifecycleOwner()); + return true; + }); + + bind.hideSectionButton.setOnClickListener(v -> Preferences.setRadioSectionHidden()); + } + + private void initRadioStationView() { + bind.internetRadioStationRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.internetRadioStationRecyclerView.setHasFixedSize(true); + + internetRadioStationAdapter = new InternetRadioStationAdapter(this); + bind.internetRadioStationRecyclerView.setAdapter(internetRadioStationAdapter); + radioViewModel.getInternetRadioStations(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), internetRadioStations -> { + if (internetRadioStations == null) { + if (bind != null) bind.homeRadioStationSector.setVisibility(View.GONE); + if (bind != null) bind.emptyRadioStationLayout.setVisibility(View.GONE); + } else { + if (bind != null) + bind.homeRadioStationSector.setVisibility(!internetRadioStations.isEmpty() ? View.VISIBLE : View.GONE); + if (bind != null) + bind.emptyRadioStationLayout.setVisibility(internetRadioStations.isEmpty() ? View.VISIBLE : View.GONE); + + internetRadioStationAdapter.setItems(internetRadioStations); + } + }); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onInternetRadioStationClick(Bundle bundle) { + MediaManager.startRadio(mediaBrowserListenableFuture, bundle.getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT)); + activity.setBottomSheetInPeek(true); + } + + @Override + public void onInternetRadioStationLongClick(Bundle bundle) { + RadioEditorDialog dialog = new RadioEditorDialog(new RadioCallback() { + @Override + public void onDismiss() { + radioViewModel.getInternetRadioStations(getViewLifecycleOwner()); + } + }); + dialog.setArguments(bundle); + dialog.show(activity.getSupportFragmentManager(), null); + } + + @Override + public void onDismiss() { + new Handler().postDelayed(() -> { + if (radioViewModel != null) + radioViewModel.refreshInternetRadioStations(getViewLifecycleOwner()); + }, 1000); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/IndexFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/IndexFragment.java new file mode 100644 index 0000000..97fd580 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/IndexFragment.java @@ -0,0 +1,110 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentIndexBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.MusicFolder; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.MusicIndexAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.IndexUtil; +import com.cappielloantonio.tempo.viewmodel.IndexViewModel; + +@UnstableApi +public class IndexFragment extends Fragment implements ClickCallback { + private static final String TAG = "IndexFragment"; + + private FragmentIndexBinding bind; + private MainActivity activity; + private IndexViewModel indexViewModel; + + private MusicIndexAdapter musicIndexAdapter; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentIndexBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + indexViewModel = new ViewModelProvider(requireActivity()).get(IndexViewModel.class); + + initAppBar(); + initDirectoryListView(); + init(); + + return view; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + MusicFolder musicFolder = getArguments().getParcelable(Constants.MUSIC_FOLDER_OBJECT); + + if (musicFolder != null) { + indexViewModel.setMusicFolder(musicFolder); + bind.indexTitleLabel.setText(musicFolder.getName()); + } + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + if (bind != null) + bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp()); + + if (bind != null) + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.indexInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(indexViewModel.getMusicFolderName()); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + } + + private void initDirectoryListView() { + MusicFolder musicFolder = getArguments().getParcelable(Constants.MUSIC_FOLDER_OBJECT); + + bind.indexRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.indexRecyclerView.setHasFixedSize(true); + + musicIndexAdapter = new MusicIndexAdapter(this); + bind.indexRecyclerView.setAdapter(musicIndexAdapter); + + indexViewModel.getIndexes(musicFolder != null ? musicFolder.getId() : null).observe(getViewLifecycleOwner(), indexes -> { + if (indexes != null) { + musicIndexAdapter.setItems(IndexUtil.getArtist(indexes)); + } + }); + + bind.fastScrollbar.setRecyclerView(bind.indexRecyclerView); + bind.fastScrollbar.setViewsToUse(R.layout.layout_fast_scrollbar, R.id.fastscroller_bubble, R.id.fastscroller_handle); + } + + @Override + public void onMusicIndexClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.directoryFragment, bundle); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LandingFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LandingFragment.java new file mode 100644 index 0000000..b9ca968 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LandingFragment.java @@ -0,0 +1,17 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.fragment.app.Fragment; + +import com.cappielloantonio.tempo.R; + +public class LandingFragment extends Fragment { + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_landing, container, false); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java new file mode 100644 index 0000000..711b1c6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java @@ -0,0 +1,295 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentLibraryBinding; +import com.cappielloantonio.tempo.helper.recyclerview.CustomLinearSnapHelper; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.interfaces.PlaylistCallback; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter; +import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter; +import com.cappielloantonio.tempo.ui.adapter.GenreAdapter; +import com.cappielloantonio.tempo.ui.adapter.MusicFolderAdapter; +import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter; +import com.cappielloantonio.tempo.ui.dialog.PlaylistEditorDialog; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.LibraryViewModel; +import com.google.android.material.appbar.MaterialToolbar; + +import java.util.Objects; + +@UnstableApi +public class LibraryFragment extends Fragment implements ClickCallback { + private static final String TAG = "LibraryFragment"; + + private FragmentLibraryBinding bind; + private MainActivity activity; + private LibraryViewModel libraryViewModel; + + private MusicFolderAdapter musicFolderAdapter; + private AlbumAdapter albumAdapter; + private ArtistAdapter artistAdapter; + private GenreAdapter genreAdapter; + private PlaylistHorizontalAdapter playlistHorizontalAdapter; + + private MaterialToolbar materialToolbar; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentLibraryBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + libraryViewModel = new ViewModelProvider(requireActivity()).get(LibraryViewModel.class); + + init(); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initAppBar(); + initMusicFolderView(); + initAlbumView(); + initArtistView(); + initGenreView(); + initPlaylistView(); + } + + @Override + public void onStart() { + super.onStart(); + activity.setBottomNavigationBarVisibility(true); + } + + @Override + public void onResume() { + super.onResume(); + refreshPlaylistView(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + bind.albumCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_albumCatalogueFragment)); + bind.artistCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_artistCatalogueFragment)); + bind.genreCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_genreCatalogueFragment)); + bind.playlistCatalogueTextViewClickable.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putString(Constants.PLAYLIST_ALL, Constants.PLAYLIST_ALL); + activity.navController.navigate(R.id.action_libraryFragment_to_playlistCatalogueFragment, bundle); + }); + + bind.albumCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshAlbumSample(getViewLifecycleOwner()); + return true; + }); + bind.artistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshArtistSample(getViewLifecycleOwner()); + return true; + }); + bind.genreCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshGenreSample(getViewLifecycleOwner()); + return true; + }); + bind.playlistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> { + libraryViewModel.refreshPlaylistSample(getViewLifecycleOwner()); + return true; + }); + } + + private void initAppBar() { + materialToolbar = bind.getRoot().findViewById(R.id.toolbar); + + activity.setSupportActionBar(materialToolbar); + Objects.requireNonNull(materialToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + } + + private void initMusicFolderView() { + if (!Preferences.isMusicDirectorySectionVisible()) { + bind.libraryMusicFolderSector.setVisibility(View.GONE); + return; + } + + bind.musicFolderRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.musicFolderRecyclerView.setHasFixedSize(true); + + musicFolderAdapter = new MusicFolderAdapter(this); + bind.musicFolderRecyclerView.setAdapter(musicFolderAdapter); + libraryViewModel.getMusicFolders(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), musicFolders -> { + if (musicFolders == null) { + if (bind != null) bind.libraryMusicFolderSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.libraryMusicFolderSector.setVisibility(!musicFolders.isEmpty() ? View.VISIBLE : View.GONE); + + musicFolderAdapter.setItems(musicFolders); + } + }); + } + + private void initAlbumView() { + bind.albumRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.albumRecyclerView.setHasFixedSize(true); + + albumAdapter = new AlbumAdapter(this); + bind.albumRecyclerView.setAdapter(albumAdapter); + libraryViewModel.getAlbumSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { + if (albums == null) { + if (bind != null) bind.libraryAlbumSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.libraryAlbumSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); + + albumAdapter.setItems(albums); + } + }); + + CustomLinearSnapHelper albumSnapHelper = new CustomLinearSnapHelper(); + albumSnapHelper.attachToRecyclerView(bind.albumRecyclerView); + } + + private void initArtistView() { + bind.artistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.artistRecyclerView.setHasFixedSize(true); + + artistAdapter = new ArtistAdapter(this, false, false); + bind.artistRecyclerView.setAdapter(artistAdapter); + libraryViewModel.getArtistSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> { + if (artists == null) { + if (bind != null) bind.libraryArtistSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.libraryArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); + + artistAdapter.setItems(artists); + } + }); + + CustomLinearSnapHelper artistSnapHelper = new CustomLinearSnapHelper(); + artistSnapHelper.attachToRecyclerView(bind.artistRecyclerView); + } + + private void initGenreView() { + bind.genreRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3, GridLayoutManager.HORIZONTAL, false)); + bind.genreRecyclerView.setHasFixedSize(true); + + genreAdapter = new GenreAdapter(this); + bind.genreRecyclerView.setAdapter(genreAdapter); + + libraryViewModel.getGenreSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), genres -> { + if (genres == null) { + if (bind != null) bind.libraryGenresSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.libraryGenresSector.setVisibility(!genres.isEmpty() ? View.VISIBLE : View.GONE); + + genreAdapter.setItems(genres); + } + }); + + CustomLinearSnapHelper genreSnapHelper = new CustomLinearSnapHelper(); + genreSnapHelper.attachToRecyclerView(bind.genreRecyclerView); + } + + private void initPlaylistView() { + bind.playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.playlistRecyclerView.setHasFixedSize(true); + + playlistHorizontalAdapter = new PlaylistHorizontalAdapter(this); + bind.playlistRecyclerView.setAdapter(playlistHorizontalAdapter); + libraryViewModel.getPlaylistSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), playlists -> { + if (playlists == null) { + if (bind != null) bind.libraryPlaylistSector.setVisibility(View.GONE); + } else { + if (bind != null) + bind.libraryPlaylistSector.setVisibility(!playlists.isEmpty() ? View.VISIBLE : View.GONE); + + playlistHorizontalAdapter.setItems(playlists); + } + }); + } + + private void refreshPlaylistView() { + final Handler handler = new Handler(); + + final Runnable runnable = () -> { + if (getView() != null && bind != null && libraryViewModel != null) + libraryViewModel.refreshPlaylistSample(getViewLifecycleOwner()); + }; + + handler.postDelayed(runnable, 100); + } + + @Override + public void onAlbumClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle); + } + + @Override + public void onAlbumLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumBottomSheetDialog, bundle); + } + + @Override + public void onArtistClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistPageFragment, bundle); + } + + @Override + public void onArtistLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); + } + + @Override + public void onGenreClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songListPageFragment, bundle); + } + + @Override + public void onPlaylistClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.playlistPageFragment, bundle); + } + + @Override + public void onPlaylistLongClick(Bundle bundle) { + PlaylistEditorDialog dialog = new PlaylistEditorDialog(new PlaylistCallback() { + @Override + public void onDismiss() { + refreshPlaylistView(); + } + }); + + dialog.setArguments(bundle); + dialog.show(activity.getSupportFragmentManager(), null); + } + + @Override + public void onMusicFolderClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.indexFragment, bundle); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LoginFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LoginFragment.java new file mode 100644 index 0000000..d5bb0fa --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LoginFragment.java @@ -0,0 +1,167 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.ui.adapter.ServerAdapter; +import com.cappielloantonio.tempo.databinding.FragmentLoginBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.interfaces.SystemCallback; +import com.cappielloantonio.tempo.model.Server; +import com.cappielloantonio.tempo.repository.SystemRepository; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.dialog.ServerSignupDialog; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.LoginViewModel; + +@UnstableApi +public class LoginFragment extends Fragment implements ClickCallback { + private static final String TAG = "LoginFragment"; + + private FragmentLoginBinding bind; + private MainActivity activity; + private LoginViewModel loginViewModel; + + private ServerAdapter serverAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.login_page_menu, menu); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + loginViewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class); + bind = FragmentLoginBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + + initAppBar(); + initServerListView(); + + return view; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.serverInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(R.string.login_title); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + } + + private void initServerListView() { + bind.serverListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.serverListRecyclerView.setHasFixedSize(true); + + serverAdapter = new ServerAdapter(this); + bind.serverListRecyclerView.setAdapter(serverAdapter); + loginViewModel.getServerList().observe(getViewLifecycleOwner(), servers -> { + if (!servers.isEmpty()) { + if (bind != null) bind.noServerAddedTextView.setVisibility(View.GONE); + if (bind != null) bind.serverListRecyclerView.setVisibility(View.VISIBLE); + serverAdapter.setItems(servers); + } else { + if (bind != null) bind.noServerAddedTextView.setVisibility(View.VISIBLE); + if (bind != null) bind.serverListRecyclerView.setVisibility(View.GONE); + } + }); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_add) { + ServerSignupDialog dialog = new ServerSignupDialog(); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + } + + return false; + } + + @Override + public void onServerClick(Bundle bundle) { + Server server = bundle.getParcelable("server_object"); + saveServerPreference(server.getServerId(), server.getAddress(), server.getLocalAddress(), server.getUsername(), server.getPassword(), server.isLowSecurity()); + + SystemRepository systemRepository = new SystemRepository(); + systemRepository.checkUserCredential(new SystemCallback() { + @Override + public void onError(Exception exception) { + Preferences.switchInUseServerAddress(); + resetServerPreference(); + Toast.makeText(requireContext(), exception.getMessage(), Toast.LENGTH_SHORT).show(); + } + + @Override + public void onSuccess(String password, String token, String salt) { + activity.goFromLogin(); + } + }); + } + + @Override + public void onServerLongClick(Bundle bundle) { + ServerSignupDialog dialog = new ServerSignupDialog(); + dialog.setArguments(bundle); + dialog.show(activity.getSupportFragmentManager(), null); + } + + private void saveServerPreference(String serverId, String server, String localAddress, String user, String password, boolean isLowSecurity) { + Preferences.setServerId(serverId); + Preferences.setServer(server); + Preferences.setLocalAddress(localAddress); + Preferences.setUser(user); + Preferences.setPassword(password); + Preferences.setLowSecurity(isLowSecurity); + + App.getSubsonicClientInstance(true); + } + + private void resetServerPreference() { + Preferences.setServerId(null); + Preferences.setServer(null); + Preferences.setUser(null); + Preferences.setPassword(null); + Preferences.setToken(null); + Preferences.setSalt(null); + Preferences.setLowSecurity(false); + + App.getSubsonicClientInstance(true); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java new file mode 100644 index 0000000..e2bca34 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java @@ -0,0 +1,334 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.Player; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.MediaController; +import androidx.media3.session.SessionToken; +import androidx.viewpager2.widget.ViewPager2; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentPlayerBottomSheetBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.PlayQueue; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerVerticalPager; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; +import com.google.android.material.elevation.SurfaceColors; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.Objects; +import java.util.stream.IntStream; + +@OptIn(markerClass = UnstableApi.class) +public class PlayerBottomSheetFragment extends Fragment { + private FragmentPlayerBottomSheetBinding bind; + + private PlayerBottomSheetViewModel playerBottomSheetViewModel; + private ListenableFuture mediaBrowserListenableFuture; + + private Handler progressBarHandler; + private Runnable progressBarRunnable; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + bind = FragmentPlayerBottomSheetBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + + playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); + + customizeBottomSheetBackground(); + customizeBottomSheetAction(); + initViewPager(); + setHeaderBookmarksButton(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + bindMediaController(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void customizeBottomSheetBackground() { + bind.playerHeaderLayout.getRoot().setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8)); + } + + private void customizeBottomSheetAction() { + bind.playerHeaderLayout.getRoot().setOnClickListener(view -> ((MainActivity) requireActivity()).expandBottomSheet()); + } + + private void initViewPager() { + bind.playerBodyLayout.playerBodyBottomSheetViewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL); + bind.playerBodyLayout.playerBodyBottomSheetViewPager.setAdapter(new PlayerControllerVerticalPager(this)); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaController.releaseFuture(mediaBrowserListenableFuture); + } + + private void bindMediaController() { + mediaBrowserListenableFuture.addListener(() -> { + try { + MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); + + mediaBrowser.setShuffleModeEnabled(Preferences.isShuffleModeEnabled()); + mediaBrowser.setRepeatMode(Preferences.getRepeatMode()); + + setMediaControllerListener(mediaBrowser); + } catch (Exception e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + + private void setMediaControllerListener(MediaBrowser mediaBrowser) { + defineProgressBarHandler(mediaBrowser); + setMediaControllerUI(mediaBrowser); + setMetadata(mediaBrowser.getMediaMetadata()); + setContentDuration(mediaBrowser.getContentDuration()); + setPlayingState(mediaBrowser.isPlaying()); + setHeaderMediaController(); + setHeaderNextButtonState(mediaBrowser.hasNextMediaItem()); + + mediaBrowser.addListener(new Player.Listener() { + @Override + public void onMediaMetadataChanged(@NonNull MediaMetadata mediaMetadata) { + setMediaControllerUI(mediaBrowser); + setMetadata(mediaMetadata); + setContentDuration(mediaBrowser.getContentDuration()); + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + setPlayingState(isPlaying); + } + + @Override + public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { + Player.Listener.super.onSkipSilenceEnabledChanged(skipSilenceEnabled); + } + + @Override + public void onEvents(Player player, Player.Events events) { + setHeaderNextButtonState(mediaBrowser.hasNextMediaItem()); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + Preferences.setShuffleModeEnabled(shuffleModeEnabled); + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + Preferences.setRepeatMode(repeatMode); + } + }); + } + + private void setMetadata(MediaMetadata mediaMetadata) { + if (mediaMetadata.extras != null) { + playerBottomSheetViewModel.setLiveMedia(getViewLifecycleOwner(), mediaMetadata.extras.getString("type"), mediaMetadata.extras.getString("id")); + playerBottomSheetViewModel.setLiveAlbum(getViewLifecycleOwner(), mediaMetadata.extras.getString("type"), mediaMetadata.extras.getString("albumId")); + playerBottomSheetViewModel.setLiveArtist(getViewLifecycleOwner(), mediaMetadata.extras.getString("type"), mediaMetadata.extras.getString("artistId")); + playerBottomSheetViewModel.setLiveDescription(mediaMetadata.extras.getString("description", null)); + + bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mediaMetadata.extras.getString("title")); + bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText( + mediaMetadata.artist != null + ? mediaMetadata.artist + : Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) + ? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder)) + : ""); + + CustomGlideRequest.Builder + .from(requireContext(), mediaMetadata.extras.getString("coverArtId"), CustomGlideRequest.ResourceType.Song) + .build() + .into(bind.playerHeaderLayout.playerHeaderMediaCoverImage); + + bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(mediaMetadata.extras.getString("title") != null && !Objects.equals(mediaMetadata.extras.getString("title"), "") ? View.VISIBLE : View.GONE); + bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility( + (mediaMetadata.extras.getString("artist") != null && !Objects.equals(mediaMetadata.extras.getString("artist"), "")) + || (Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null) + ? View.VISIBLE + : View.GONE); + } + } + + + private void setMediaControllerUI(MediaBrowser mediaBrowser) { + if (mediaBrowser.getMediaMetadata().extras != null) { + switch (mediaBrowser.getMediaMetadata().extras.getString("type", Constants.MEDIA_TYPE_MUSIC)) { + case Constants.MEDIA_TYPE_PODCAST: + bind.playerHeaderLayout.playerHeaderFastForwardMediaButton.setVisibility(View.VISIBLE); + bind.playerHeaderLayout.playerHeaderRewindMediaButton.setVisibility(View.VISIBLE); + bind.playerHeaderLayout.playerHeaderNextMediaButton.setVisibility(View.GONE); + break; + case Constants.MEDIA_TYPE_MUSIC: + default: + bind.playerHeaderLayout.playerHeaderFastForwardMediaButton.setVisibility(View.GONE); + bind.playerHeaderLayout.playerHeaderRewindMediaButton.setVisibility(View.GONE); + bind.playerHeaderLayout.playerHeaderNextMediaButton.setVisibility(View.VISIBLE); + break; + } + } + } + + private void setContentDuration(long duration) { + bind.playerHeaderLayout.playerHeaderSeekBar.setMax((int) (duration / 1000)); + } + + private void setProgress(MediaBrowser mediaBrowser) { + if (bind != null) + bind.playerHeaderLayout.playerHeaderSeekBar.setProgress((int) (mediaBrowser.getCurrentPosition() / 1000), true); + } + + private void setPlayingState(boolean isPlaying) { + bind.playerHeaderLayout.playerHeaderButton.setChecked(isPlaying); + runProgressBarHandler(isPlaying); + } + + private void setHeaderMediaController() { + bind.playerHeaderLayout.playerHeaderButton.setOnClickListener(view -> bind.getRoot().findViewById(R.id.exo_play_pause).performClick()); + bind.playerHeaderLayout.playerHeaderNextMediaButton.setOnClickListener(view -> bind.getRoot().findViewById(R.id.exo_next).performClick()); + bind.playerHeaderLayout.playerHeaderRewindMediaButton.setOnClickListener(view -> bind.getRoot().findViewById(R.id.exo_rew).performClick()); + bind.playerHeaderLayout.playerHeaderFastForwardMediaButton.setOnClickListener(view -> bind.getRoot().findViewById(R.id.exo_ffwd).performClick()); + } + + private void setHeaderNextButtonState(boolean isEnabled) { + bind.playerHeaderLayout.playerHeaderNextMediaButton.setEnabled(isEnabled); + bind.playerHeaderLayout.playerHeaderNextMediaButton.setAlpha(isEnabled ? (float) 1.0 : (float) 0.3); + } + + public View getPlayerHeader() { + return requireView().findViewById(R.id.player_header_layout); + } + + public void goBackToFirstPage() { + bind.playerBodyLayout.playerBodyBottomSheetViewPager.setCurrentItem(0, false); + goToControllerPage(); + } + + public void goToControllerPage() { + PlayerControllerVerticalPager playerControllerVerticalPager = (PlayerControllerVerticalPager) bind.playerBodyLayout.playerBodyBottomSheetViewPager.getAdapter(); + if (playerControllerVerticalPager != null) { + PlayerControllerFragment playerControllerFragment = (PlayerControllerFragment) playerControllerVerticalPager.getRegisteredFragment(0); + if (playerControllerFragment != null) { + playerControllerFragment.goToControllerPage(); + } + } + } + + public void goToLyricsPage() { + PlayerControllerVerticalPager playerControllerVerticalPager = (PlayerControllerVerticalPager) bind.playerBodyLayout.playerBodyBottomSheetViewPager.getAdapter(); + if (playerControllerVerticalPager != null) { + PlayerControllerFragment playerControllerFragment = (PlayerControllerFragment) playerControllerVerticalPager.getRegisteredFragment(0); + if (playerControllerFragment != null) { + playerControllerFragment.goToLyricsPage(); + } + } + } + + public void goToQueuePage() { + bind.playerBodyLayout.playerBodyBottomSheetViewPager.setCurrentItem(1, true); + } + + public void setPlayerControllerVerticalPagerDraggableState(Boolean isDraggable) { + ViewPager2 playerControllerVerticalPager = (ViewPager2) bind.playerBodyLayout.playerBodyBottomSheetViewPager; + playerControllerVerticalPager.setUserInputEnabled(isDraggable); + } + + private void defineProgressBarHandler(MediaBrowser mediaBrowser) { + progressBarHandler = new Handler(); + progressBarRunnable = () -> { + setProgress(mediaBrowser); + progressBarHandler.postDelayed(progressBarRunnable, 1000); + }; + } + + private void runProgressBarHandler(boolean isPlaying) { + if (isPlaying) { + progressBarHandler.postDelayed(progressBarRunnable, 1000); + } else { + progressBarHandler.removeCallbacks(progressBarRunnable); + } + } + + private void setHeaderBookmarksButton() { + if (Preferences.isSyncronizationEnabled()) { + playerBottomSheetViewModel.getPlayQueue().observeForever(new Observer() { + @Override + public void onChanged(PlayQueue playQueue) { + playerBottomSheetViewModel.getPlayQueue().removeObserver(this); + + if (bind == null) return; + + if (playQueue != null && !playQueue.getEntries().isEmpty()) { + int index = IntStream.range(0, playQueue.getEntries().size()).filter(ix -> playQueue.getEntries().get(ix).getId().equals(playQueue.getCurrent())).findFirst().orElse(-1); + + if (index != -1) { + bind.playerHeaderLayout.playerHeaderBookmarkMediaButton.setVisibility(View.VISIBLE); + bind.playerHeaderLayout.playerHeaderBookmarkMediaButton.setOnClickListener(v -> { + MediaManager.startQueue(mediaBrowserListenableFuture, playQueue.getEntries(), index); + bind.playerHeaderLayout.playerHeaderBookmarkMediaButton.setVisibility(View.GONE); + }); + } + } else { + bind.playerHeaderLayout.playerHeaderBookmarkMediaButton.setVisibility(View.GONE); + bind.playerHeaderLayout.playerHeaderBookmarkMediaButton.setOnClickListener(null); + } + } + }); + + bind.playerHeaderLayout.playerHeaderBookmarkMediaButton.setOnLongClickListener(v -> { + bind.playerHeaderLayout.playerHeaderBookmarkMediaButton.setVisibility(View.GONE); + return true; + }); + + new Handler().postDelayed(() -> { + if (bind != null) + bind.playerHeaderLayout.playerHeaderBookmarkMediaButton.setVisibility(View.GONE); + }, Preferences.getSyncCountdownTimer() * 1000L); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java new file mode 100644 index 0000000..e3155b5 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java @@ -0,0 +1,668 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.RatingBar; +import android.widget.TextView; +import android.widget.ToggleButton; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.PlaybackParameters; +import androidx.media3.common.Player; +import androidx.media3.common.util.RepeatModeUtil; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.NavController; +import androidx.navigation.NavOptions; +import androidx.navigation.fragment.NavHostFragment; +import androidx.viewpager2.widget.ViewPager2; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBinding; +import com.cappielloantonio.tempo.service.EqualizerManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.dialog.RatingDialog; +import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog; +import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager; +import com.cappielloantonio.tempo.util.AssetLinkUtil; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; +import com.cappielloantonio.tempo.viewmodel.RatingViewModel; +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.android.material.elevation.SurfaceColors; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@UnstableApi +public class PlayerControllerFragment extends Fragment { + private static final String TAG = "PlayerCoverFragment"; + + private InnerFragmentPlayerControllerBinding bind; + private ViewPager2 playerMediaCoverViewPager; + private ToggleButton buttonFavorite; + private RatingViewModel ratingViewModel; + private RatingBar songRatingBar; + private TextView playerMediaTitleLabel; + private TextView playerArtistNameLabel; + private Button playbackSpeedButton; + private ToggleButton skipSilenceToggleButton; + private Chip playerMediaExtension; + private TextView playerMediaBitrate; + private ConstraintLayout playerQuickActionView; + private ImageButton playerOpenQueueButton; + private ImageButton playerTrackInfo; + private LinearLayout ratingContainer; + private ImageButton equalizerButton; + private ChipGroup assetLinkChipGroup; + private Chip playerSongLinkChip; + private Chip playerAlbumLinkChip; + private Chip playerArtistLinkChip; + + private MainActivity activity; + private PlayerBottomSheetViewModel playerBottomSheetViewModel; + private ListenableFuture mediaBrowserListenableFuture; + + private MediaService.LocalBinder mediaServiceBinder; + private boolean isServiceBound = false; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = InnerFragmentPlayerControllerBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + + playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); + ratingViewModel = new ViewModelProvider(requireActivity()).get(RatingViewModel.class); + + init(); + initQuickActionView(); + initCoverLyricsSlideView(); + initMediaListenable(); + initMediaLabelButton(); + initArtistLabelButton(); + initEqualizerButton(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + initializeBrowser(); + bindMediaController(); + } + + @Override + public void onStop() { + releaseBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + playerMediaCoverViewPager = bind.getRoot().findViewById(R.id.player_media_cover_view_pager); + buttonFavorite = bind.getRoot().findViewById(R.id.button_favorite); + playerMediaTitleLabel = bind.getRoot().findViewById(R.id.player_media_title_label); + playerArtistNameLabel = bind.getRoot().findViewById(R.id.player_artist_name_label); + playbackSpeedButton = bind.getRoot().findViewById(R.id.player_playback_speed_button); + skipSilenceToggleButton = bind.getRoot().findViewById(R.id.player_skip_silence_toggle_button); + playerMediaExtension = bind.getRoot().findViewById(R.id.player_media_extension); + playerMediaBitrate = bind.getRoot().findViewById(R.id.player_media_bitrate); + playerQuickActionView = bind.getRoot().findViewById(R.id.player_quick_action_view); + playerOpenQueueButton = bind.getRoot().findViewById(R.id.player_open_queue_button); + playerTrackInfo = bind.getRoot().findViewById(R.id.player_info_track); + songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar); + ratingContainer = bind.getRoot().findViewById(R.id.rating_container); + equalizerButton = bind.getRoot().findViewById(R.id.player_open_equalizer_button); + assetLinkChipGroup = bind.getRoot().findViewById(R.id.asset_link_chip_group); + playerSongLinkChip = bind.getRoot().findViewById(R.id.asset_link_song_chip); + playerAlbumLinkChip = bind.getRoot().findViewById(R.id.asset_link_album_chip); + playerArtistLinkChip = bind.getRoot().findViewById(R.id.asset_link_artist_chip); + checkAndSetRatingContainerVisibility(); + } + + private void initQuickActionView() { + playerQuickActionView.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8)); + + playerOpenQueueButton.setOnClickListener(view -> { + PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) requireActivity().getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet"); + if (playerBottomSheetFragment != null) { + playerBottomSheetFragment.goToQueuePage(); + } + }); + } + + private void initializeBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + private void bindMediaController() { + mediaBrowserListenableFuture.addListener(() -> { + try { + MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); + + bind.nowPlayingMediaControllerView.setPlayer(mediaBrowser); + mediaBrowser.setShuffleModeEnabled(Preferences.isShuffleModeEnabled()); + mediaBrowser.setRepeatMode(Preferences.getRepeatMode()); + setMediaControllerListener(mediaBrowser); + } catch (Exception e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + + private void setMediaControllerListener(MediaBrowser mediaBrowser) { + setMediaControllerUI(mediaBrowser); + setMetadata(mediaBrowser.getMediaMetadata()); + setMediaInfo(mediaBrowser.getMediaMetadata()); + + mediaBrowser.addListener(new Player.Listener() { + @Override + public void onMediaMetadataChanged(@NonNull MediaMetadata mediaMetadata) { + setMediaControllerUI(mediaBrowser); + setMetadata(mediaMetadata); + setMediaInfo(mediaMetadata); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + Preferences.setShuffleModeEnabled(shuffleModeEnabled); + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + Preferences.setRepeatMode(repeatMode); + } + }); + } + + private void setMetadata(MediaMetadata mediaMetadata) { + playerMediaTitleLabel.setText(String.valueOf(mediaMetadata.title)); + playerArtistNameLabel.setText( + mediaMetadata.artist != null + ? String.valueOf(mediaMetadata.artist) + : mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) + ? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder)) + : ""); + + playerMediaTitleLabel.setSelected(true); + playerArtistNameLabel.setSelected(true); + + playerMediaTitleLabel.setVisibility(mediaMetadata.title != null && !Objects.equals(mediaMetadata.title, "") ? View.VISIBLE : View.GONE); + playerArtistNameLabel.setVisibility( + (mediaMetadata.artist != null && !Objects.equals(mediaMetadata.artist, "")) + || mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null + ? View.VISIBLE + : View.GONE); + + updateAssetLinkChips(mediaMetadata); + } + + private void setMediaInfo(MediaMetadata mediaMetadata) { + if (mediaMetadata.extras != null) { + String extension = mediaMetadata.extras.getString("suffix", getString(R.string.player_unknown_format)); + String bitrate = mediaMetadata.extras.getInt("bitrate", 0) != 0 ? mediaMetadata.extras.getInt("bitrate", 0) + "kbps" : "Original"; + String samplingRate = mediaMetadata.extras.getInt("samplingRate", 0) != 0 ? new DecimalFormat("0.#").format(mediaMetadata.extras.getInt("samplingRate", 0) / 1000.0) + "kHz" : ""; + String bitDepth = mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + "b" : ""; + + playerMediaExtension.setText(extension); + + if (bitrate.equals("Original")) { + playerMediaBitrate.setVisibility(View.GONE); + } else { + List mediaQualityItems = new ArrayList<>(); + + if (!bitrate.trim().isEmpty()) mediaQualityItems.add(bitrate); + if (!bitDepth.trim().isEmpty()) mediaQualityItems.add(bitDepth); + if (!samplingRate.trim().isEmpty()) mediaQualityItems.add(samplingRate); + + String mediaQuality = TextUtils.join(" • ", mediaQualityItems); + playerMediaBitrate.setVisibility(View.VISIBLE); + playerMediaBitrate.setText(mediaQuality); + } + } + + boolean isTranscodingExtension = !MusicUtil.getTranscodingFormatPreference().equals("raw"); + boolean isTranscodingBitrate = !MusicUtil.getBitratePreference().equals("0"); + + if (isTranscodingExtension || isTranscodingBitrate) { + playerMediaExtension.setText(MusicUtil.getTranscodingFormatPreference() + " (" + getString(R.string.player_transcoding) + ")"); + playerMediaBitrate.setText(!MusicUtil.getBitratePreference().equals("0") ? MusicUtil.getBitratePreference() + "kbps" : getString(R.string.player_transcoding_requested)); + } + + playerTrackInfo.setOnClickListener(view -> { + TrackInfoDialog dialog = new TrackInfoDialog(mediaMetadata); + dialog.show(activity.getSupportFragmentManager(), null); + }); + } + + private void updateAssetLinkChips(MediaMetadata mediaMetadata) { + if (assetLinkChipGroup == null) return; + String mediaType = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type", Constants.MEDIA_TYPE_MUSIC) : Constants.MEDIA_TYPE_MUSIC; + if (!Constants.MEDIA_TYPE_MUSIC.equals(mediaType)) { + clearAssetLinkChip(playerSongLinkChip); + clearAssetLinkChip(playerAlbumLinkChip); + clearAssetLinkChip(playerArtistLinkChip); + syncAssetLinkGroupVisibility(); + return; + } + + String songId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("id") : null; + String albumId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("albumId") : null; + String artistId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("artistId") : null; + + AssetLinkUtil.AssetLink songLink = bindAssetLinkChip(playerSongLinkChip, AssetLinkUtil.TYPE_SONG, songId); + AssetLinkUtil.AssetLink albumLink = bindAssetLinkChip(playerAlbumLinkChip, AssetLinkUtil.TYPE_ALBUM, albumId); + AssetLinkUtil.AssetLink artistLink = bindAssetLinkChip(playerArtistLinkChip, AssetLinkUtil.TYPE_ARTIST, artistId); + bindAssetLinkView(playerMediaTitleLabel, songLink); + bindAssetLinkView(playerArtistNameLabel, artistLink != null ? artistLink : songLink); + bindAssetLinkView(playerMediaCoverViewPager, songLink); + syncAssetLinkGroupVisibility(); + } + + private AssetLinkUtil.AssetLink bindAssetLinkChip(Chip chip, String type, String id) { + if (chip == null) return null; + if (TextUtils.isEmpty(id)) { + clearAssetLinkChip(chip); + return null; + } + + String label = getString(AssetLinkUtil.getLabelRes(type)); + AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.buildAssetLink(type, id); + if (assetLink == null) { + clearAssetLinkChip(chip); + return null; + } + + chip.setText(getString(R.string.asset_link_chip_text, label, assetLink.id)); + chip.setVisibility(View.VISIBLE); + + chip.setOnClickListener(v -> { + if (assetLink != null) { + activity.openAssetLink(assetLink); + } + }); + + chip.setOnLongClickListener(v -> { + if (assetLink != null) { + AssetLinkUtil.copyToClipboard(requireContext(), assetLink); + Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show(); + } + return true; + }); + + return assetLink; + } + + private void clearAssetLinkChip(Chip chip) { + if (chip == null) return; + chip.setVisibility(View.GONE); + chip.setText(""); + chip.setOnClickListener(null); + chip.setOnLongClickListener(null); + } + + private void bindAssetLinkView(View view, AssetLinkUtil.AssetLink assetLink) { + if (view == null) return; + if (assetLink == null) { + AssetLinkUtil.clearLinkAppearance(view); + view.setOnClickListener(null); + view.setOnLongClickListener(null); + view.setClickable(false); + view.setLongClickable(false); + return; + } + + view.setClickable(true); + view.setLongClickable(true); + AssetLinkUtil.applyLinkAppearance(view); + view.setOnClickListener(v -> { + boolean collapse = !AssetLinkUtil.TYPE_SONG.equals(assetLink.type); + activity.openAssetLink(assetLink, collapse); + }); + view.setOnLongClickListener(v -> { + AssetLinkUtil.copyToClipboard(requireContext(), assetLink); + Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show(); + return true; + }); + } + + private void syncAssetLinkGroupVisibility() { + if (assetLinkChipGroup == null) return; + boolean hasVisible = false; + for (int i = 0; i < assetLinkChipGroup.getChildCount(); i++) { + View child = assetLinkChipGroup.getChildAt(i); + if (child.getVisibility() == View.VISIBLE) { + hasVisible = true; + break; + } + } + assetLinkChipGroup.setVisibility(hasVisible ? View.VISIBLE : View.GONE); + } + + private void setMediaControllerUI(MediaBrowser mediaBrowser) { + initPlaybackSpeedButton(mediaBrowser); + + if (mediaBrowser.getMediaMetadata().extras != null) { + switch (mediaBrowser.getMediaMetadata().extras.getString("type", Constants.MEDIA_TYPE_MUSIC)) { + case Constants.MEDIA_TYPE_PODCAST: + bind.getRoot().setShowShuffleButton(false); + bind.getRoot().setShowRewindButton(true); + bind.getRoot().setShowPreviousButton(false); + bind.getRoot().setShowNextButton(false); + bind.getRoot().setShowFastForwardButton(true); + bind.getRoot().setRepeatToggleModes(RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE); + bind.getRoot().findViewById(R.id.player_playback_speed_button).setVisibility(View.VISIBLE); + bind.getRoot().findViewById(R.id.player_skip_silence_toggle_button).setVisibility(View.VISIBLE); + bind.getRoot().findViewById(R.id.button_favorite).setVisibility(View.GONE); + setPlaybackParameters(mediaBrowser); + break; + case Constants.MEDIA_TYPE_RADIO: + bind.getRoot().setShowShuffleButton(false); + bind.getRoot().setShowRewindButton(false); + bind.getRoot().setShowPreviousButton(false); + bind.getRoot().setShowNextButton(false); + bind.getRoot().setShowFastForwardButton(false); + bind.getRoot().setRepeatToggleModes(RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE); + bind.getRoot().findViewById(R.id.player_playback_speed_button).setVisibility(View.GONE); + bind.getRoot().findViewById(R.id.player_skip_silence_toggle_button).setVisibility(View.GONE); + bind.getRoot().findViewById(R.id.button_favorite).setVisibility(View.GONE); + setPlaybackParameters(mediaBrowser); + break; + case Constants.MEDIA_TYPE_MUSIC: + default: + bind.getRoot().setShowShuffleButton(true); + bind.getRoot().setShowRewindButton(false); + bind.getRoot().setShowPreviousButton(true); + bind.getRoot().setShowNextButton(true); + bind.getRoot().setShowFastForwardButton(false); + bind.getRoot().setRepeatToggleModes(RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL | RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE); + bind.getRoot().findViewById(R.id.player_playback_speed_button).setVisibility(View.GONE); + bind.getRoot().findViewById(R.id.player_skip_silence_toggle_button).setVisibility(View.GONE); + bind.getRoot().findViewById(R.id.button_favorite).setVisibility(View.VISIBLE); + resetPlaybackParameters(mediaBrowser); + break; + } + } + } + + private void initCoverLyricsSlideView() { + playerMediaCoverViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL); + playerMediaCoverViewPager.setAdapter(new PlayerControllerHorizontalPager(this)); + + playerMediaCoverViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + + PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) requireActivity().getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet"); + + if (position == 0) { + activity.setBottomSheetDraggableState(true); + + if (playerBottomSheetFragment != null) { + playerBottomSheetFragment.setPlayerControllerVerticalPagerDraggableState(true); + } + } else if (position == 1) { + activity.setBottomSheetDraggableState(false); + + if (playerBottomSheetFragment != null) { + playerBottomSheetFragment.setPlayerControllerVerticalPagerDraggableState(false); + } + } + } + }); + } + + private void initMediaListenable() { + playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), media -> { + if (media != null) { + ratingViewModel.setSong(media); + buttonFavorite.setChecked(media.getStarred() != null); + buttonFavorite.setOnClickListener(v -> playerBottomSheetViewModel.setFavorite(requireContext(), media)); + buttonFavorite.setOnLongClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.TRACK_OBJECT, media); + + RatingDialog dialog = new RatingDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + + + return true; + }); + + Integer currentRating = media.getUserRating(); + + if (currentRating != null) { + songRatingBar.setRating(currentRating); + } else { + songRatingBar.setRating(0); + } + + songRatingBar.setOnRatingBarChangeListener(new RatingBar.OnRatingBarChangeListener() { + @Override + public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) { + if (fromUser) { + ratingViewModel.rate((int) rating); + media.setUserRating((int) rating); + } + } + }); + + + if (getActivity() != null) { + playerBottomSheetViewModel.refreshMediaInfo(requireActivity(), media); + } + } + }); + } + + private void initMediaLabelButton() { + playerBottomSheetViewModel.getLiveAlbum().observe(getViewLifecycleOwner(), album -> { + if (album != null) { + playerMediaTitleLabel.setOnClickListener(view -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, album); + NavHostFragment.findNavController(this).navigate(R.id.albumPageFragment, bundle); + activity.collapseBottomSheetDelayed(); + }); + } + }); + } + + private void initArtistLabelButton() { + playerBottomSheetViewModel.getLiveArtist().observe(getViewLifecycleOwner(), artist -> { + if (artist != null) { + playerArtistNameLabel.setOnClickListener(view -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artist); + NavHostFragment.findNavController(this).navigate(R.id.artistPageFragment, bundle); + activity.collapseBottomSheetDelayed(); + }); + } + }); + } + + private void initPlaybackSpeedButton(MediaBrowser mediaBrowser) { + playbackSpeedButton.setOnClickListener(view -> { + float currentSpeed = Preferences.getPlaybackSpeed(); + + if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_080) { + mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100)); + playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_100)); + Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_100); + } else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_100) { + mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_125)); + playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_125)); + Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_125); + } else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_125) { + mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_150)); + playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_150)); + Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_150); + } else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_150) { + mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_175)); + playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_175)); + Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_175); + } else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_175) { + mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_200)); + playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_200)); + Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_200); + } else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_200) { + mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_080)); + playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_080)); + Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_080); + } + }); + + skipSilenceToggleButton.setOnClickListener(view -> { + Preferences.setSkipSilenceMode(!skipSilenceToggleButton.isChecked()); + }); + } + + private void initEqualizerButton() { + equalizerButton.setOnClickListener(v -> { + NavController navController = NavHostFragment.findNavController(this); + NavOptions navOptions = new NavOptions.Builder() + .setLaunchSingleTop(true) + .setPopUpTo(R.id.equalizerFragment, true) + .build(); + navController.navigate(R.id.equalizerFragment, null, navOptions); + if (activity != null) activity.collapseBottomSheetDelayed(); + }); + } + + public void goToControllerPage() { + playerMediaCoverViewPager.setCurrentItem(0, false); + } + + public void goToLyricsPage() { + playerMediaCoverViewPager.setCurrentItem(1, true); + } + + private void checkAndSetRatingContainerVisibility() { + if (ratingContainer == null) return; + + if (Preferences.showItemStarRating()) { + ratingContainer.setVisibility(View.VISIBLE); + } + else { + ratingContainer.setVisibility(View.GONE); + } + } + + private void setPlaybackParameters(MediaBrowser mediaBrowser) { + Button playbackSpeedButton = bind.getRoot().findViewById(R.id.player_playback_speed_button); + float currentSpeed = Preferences.getPlaybackSpeed(); + boolean skipSilence = Preferences.isSkipSilenceMode(); + + mediaBrowser.setPlaybackParameters(new PlaybackParameters(currentSpeed)); + playbackSpeedButton.setText(getString(R.string.player_playback_speed, currentSpeed)); + + // TODO Skippare il silenzio + skipSilenceToggleButton.setChecked(skipSilence); + } + + private void resetPlaybackParameters(MediaBrowser mediaBrowser) { + mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100)); + // TODO Resettare lo skip del silenzio + } + + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mediaServiceBinder = (MediaService.LocalBinder) service; + isServiceBound = true; + checkEqualizerBands(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mediaServiceBinder = null; + isServiceBound = false; + } + }; + + private void bindMediaService() { + Intent intent = new Intent(requireActivity(), MediaService.class); + intent.setAction(MediaService.ACTION_BIND_EQUALIZER); + requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + isServiceBound = true; + } + + private void checkEqualizerBands() { + if (mediaServiceBinder != null) { + EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager(); + short numBands = eqManager.getNumberOfBands(); + + if (equalizerButton != null) { + if (numBands == 0) { + equalizerButton.setVisibility(View.GONE); + + ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) playerOpenQueueButton.getLayoutParams(); + params.startToEnd = ConstraintLayout.LayoutParams.UNSET; + params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID; + playerOpenQueueButton.setLayoutParams(params); + } else { + equalizerButton.setVisibility(View.VISIBLE); + + ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) playerOpenQueueButton.getLayoutParams(); + params.startToStart = ConstraintLayout.LayoutParams.UNSET; + params.startToEnd = R.id.player_open_equalizer_button; + playerOpenQueueButton.setLayoutParams(params); + } + } + } + } + + @Override + public void onResume() { + super.onResume(); + bindMediaService(); + } + + @Override + public void onPause() { + super.onPause(); + if (isServiceBound) { + requireActivity().unbindService(serviceConnection); + isServiceBound = false; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java new file mode 100644 index 0000000..2d73847 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerCoverFragment.java @@ -0,0 +1,203 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.os.Bundle; +import android.os.Handler; +import android.transition.Fade; +import android.transition.Transition; +import android.transition.TransitionManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import java.util.ArrayList; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.Player; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerCoverBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.util.ExternalAudioWriter; +import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.google.android.material.snackbar.Snackbar; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +@UnstableApi +public class PlayerCoverFragment extends Fragment { + private PlayerBottomSheetViewModel playerBottomSheetViewModel; + private InnerFragmentPlayerCoverBinding bind; + private ListenableFuture mediaBrowserListenableFuture; + + private final Handler handler = new Handler(); + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + bind = InnerFragmentPlayerCoverBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + + playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); + + initOverlay(); + initInnerButton(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + initializeBrowser(); + bindMediaController(); + toggleOverlayVisibility(false); + } + + @Override + public void onStop() { + releaseBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void initTapButtonHideTransition() { + bind.nowPlayingTapButton.setVisibility(View.VISIBLE); + + handler.removeCallbacksAndMessages(null); + + final Runnable runnable = () -> { + if (bind != null) bind.nowPlayingTapButton.setVisibility(View.GONE); + }; + + handler.postDelayed(runnable, 10000); + } + + private void initOverlay() { + bind.nowPlayingSongCoverImageView.setOnClickListener(view -> toggleOverlayVisibility(true)); + bind.nowPlayingSongCoverButtonGroup.setOnClickListener(view -> toggleOverlayVisibility(false)); + bind.nowPlayingTapButton.setOnClickListener(view -> toggleOverlayVisibility(true)); + } + + private void toggleOverlayVisibility(boolean isVisible) { + Transition transition = new Fade(); + transition.setDuration(200); + transition.addTarget(bind.nowPlayingSongCoverButtonGroup); + + TransitionManager.beginDelayedTransition(bind.getRoot(), transition); + bind.nowPlayingSongCoverButtonGroup.setVisibility(isVisible ? View.VISIBLE : View.GONE); + bind.nowPlayingTapButton.setVisibility(isVisible ? View.GONE : View.VISIBLE); + + bind.innerButtonBottomRight.setVisibility(Preferences.isSyncronizationEnabled() ? View.VISIBLE : View.GONE); + bind.innerButtonBottomRightAlternative.setVisibility(Preferences.isSyncronizationEnabled() ? View.GONE : View.VISIBLE); + + if (!isVisible) initTapButtonHideTransition(); + } + + private void initInnerButton() { + playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), song -> { + if (song != null && bind != null) { + bind.innerButtonTopLeft.setOnClickListener(view -> { + if (Preferences.getDownloadDirectoryUri() == null) { + DownloadUtil.getDownloadTracker(requireContext()).download( + MappingUtil.mapDownload(song), + new Download(song) + ); + } else { + ExternalAudioWriter.downloadToUserDirectory(requireContext(), song); + } + }); + + bind.innerButtonTopRight.setOnClickListener(view -> { + ArrayList tracks = new ArrayList<>(); + tracks.add(song); + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, tracks); + + PlaylistChooserDialog dialog = new PlaylistChooserDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + } + ); + + bind.innerButtonBottomLeft.setOnClickListener(view -> { + playerBottomSheetViewModel.getMediaInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), media -> { + MediaManager.enqueue(mediaBrowserListenableFuture, media, true); + }); + }); + + bind.innerButtonBottomRight.setOnClickListener(view -> { + if (playerBottomSheetViewModel.savePlayQueue()) { + Snackbar.make(requireView(), R.string.player_queue_save_queue_success, Snackbar.LENGTH_LONG).show(); + } + }); + + bind.innerButtonBottomRightAlternative.setOnClickListener(view -> { + if (getActivity() != null) { + PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) requireActivity().getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet"); + if (playerBottomSheetFragment != null) { + playerBottomSheetFragment.goToLyricsPage(); + } + } + }); + } + }); + } + + private void initializeBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + private void bindMediaController() { + mediaBrowserListenableFuture.addListener(() -> { + try { + MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); + setMediaBrowserListener(mediaBrowser); + } catch (Exception exception) { + exception.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + + private void setMediaBrowserListener(MediaBrowser mediaBrowser) { + setCover(mediaBrowser.getMediaMetadata()); + + mediaBrowser.addListener(new Player.Listener() { + @Override + public void onMediaMetadataChanged(@NonNull MediaMetadata mediaMetadata) { + setCover(mediaMetadata); + toggleOverlayVisibility(false); + } + }); + } + + private void setCover(MediaMetadata mediaMetadata) { + CustomGlideRequest.Builder + .from(requireContext(), mediaMetadata.extras != null ? mediaMetadata.extras.getString("coverArtId") : null, CustomGlideRequest.ResourceType.Song) + .build() + .into(bind.nowPlayingSongCoverImageView); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java new file mode 100644 index 0000000..24a1abc --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerLyricsFragment.java @@ -0,0 +1,365 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.os.Bundle; +import android.os.Handler; +import android.text.Layout; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerLyricsBinding; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.Line; +import com.cappielloantonio.tempo.subsonic.models.LyricsList; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.android.material.button.MaterialButton; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.List; + + +@OptIn(markerClass = UnstableApi.class) +public class PlayerLyricsFragment extends Fragment { + private static final String TAG = "PlayerLyricsFragment"; + + private InnerFragmentPlayerLyricsBinding bind; + private PlayerBottomSheetViewModel playerBottomSheetViewModel; + private ListenableFuture mediaBrowserListenableFuture; + private MediaBrowser mediaBrowser; + private Handler syncLyricsHandler; + private Runnable syncLyricsRunnable; + private String currentLyrics; + private LyricsList currentLyricsList; + private String currentDescription; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + bind = InnerFragmentPlayerLyricsBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + + playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); + + initOverlay(); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + initPanelContent(); + observeDownloadState(); + } + + @Override + public void onStart() { + super.onStart(); + initializeBrowser(); + + } + + @Override + public void onResume() { + super.onResume(); + bindMediaController(); + requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + @Override + public void onPause() { + super.onPause(); + releaseHandler(); + if (!Preferences.isDisplayAlwaysOn()) { + requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + + @Override + public void onStop() { + releaseBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + currentLyrics = null; + currentLyricsList = null; + currentDescription = null; + } + + private void initOverlay() { + bind.syncLyricsTapButton.setOnClickListener(view -> { + playerBottomSheetViewModel.changeSyncLyricsState(); + }); + + bind.downloadLyricsButton.setOnClickListener(view -> { + boolean saved = playerBottomSheetViewModel.downloadCurrentLyrics(); + if (getContext() != null) { + Toast.makeText( + requireContext(), + saved ? R.string.player_lyrics_download_success : R.string.player_lyrics_download_failure, + Toast.LENGTH_SHORT + ).show(); + } + }); + } + + private void initializeBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseHandler() { + if (syncLyricsHandler != null) { + syncLyricsHandler.removeCallbacks(syncLyricsRunnable); + syncLyricsHandler = null; + } + } + + private void releaseBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + private void bindMediaController() { + mediaBrowserListenableFuture.addListener(() -> { + try { + mediaBrowser = mediaBrowserListenableFuture.get(); + defineProgressHandler(); + } catch (Exception e) { + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + + private void initPanelContent() { + playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> { + currentLyrics = lyrics; + updatePanelContent(); + }); + + playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> { + currentLyricsList = lyricsList; + updatePanelContent(); + }); + + playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> { + currentDescription = description; + updatePanelContent(); + }); + } + + private void observeDownloadState() { + playerBottomSheetViewModel.getLyricsCachedState().observe(getViewLifecycleOwner(), cached -> { + if (bind != null) { + MaterialButton downloadButton = (MaterialButton) bind.downloadLyricsButton; + if (cached != null && cached) { + downloadButton.setIconResource(R.drawable.ic_done); + downloadButton.setContentDescription(getString(R.string.player_lyrics_downloaded_content_description)); + } else { + downloadButton.setIconResource(R.drawable.ic_download); + downloadButton.setContentDescription(getString(R.string.player_lyrics_download_content_description)); + } + } + }); + } + + private void updatePanelContent() { + if (bind == null) { + return; + } + + bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0); + + if (hasStructuredLyrics(currentLyricsList)) { + setSyncLirics(currentLyricsList); + bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); + bind.emptyDescriptionImageView.setVisibility(View.GONE); + bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); + bind.syncLyricsTapButton.setVisibility(View.VISIBLE); + bind.downloadLyricsButton.setVisibility(View.VISIBLE); + bind.downloadLyricsButton.setEnabled(true); + } else if (hasText(currentLyrics)) { + bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentLyrics)); + bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); + bind.emptyDescriptionImageView.setVisibility(View.GONE); + bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); + bind.syncLyricsTapButton.setVisibility(View.GONE); + bind.downloadLyricsButton.setVisibility(View.VISIBLE); + bind.downloadLyricsButton.setEnabled(true); + } else if (hasText(currentDescription)) { + bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentDescription)); + bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE); + bind.emptyDescriptionImageView.setVisibility(View.GONE); + bind.titleEmptyDescriptionLabel.setVisibility(View.GONE); + bind.syncLyricsTapButton.setVisibility(View.GONE); + bind.downloadLyricsButton.setVisibility(View.GONE); + bind.downloadLyricsButton.setEnabled(false); + } else { + bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE); + bind.emptyDescriptionImageView.setVisibility(View.VISIBLE); + bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE); + bind.syncLyricsTapButton.setVisibility(View.GONE); + bind.downloadLyricsButton.setVisibility(View.GONE); + bind.downloadLyricsButton.setEnabled(false); + } + } + + private boolean hasText(String value) { + return value != null && !value.trim().isEmpty(); + } + + private boolean hasStructuredLyrics(LyricsList lyricsList) { + return lyricsList != null + && lyricsList.getStructuredLyrics() != null + && !lyricsList.getStructuredLyrics().isEmpty() + && lyricsList.getStructuredLyrics().get(0) != null + && lyricsList.getStructuredLyrics().get(0).getLine() != null + && !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty(); + } + + @SuppressLint("DefaultLocale") + private void setSyncLirics(LyricsList lyricsList) { + if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) { + StringBuilder lyricsBuilder = new StringBuilder(); + List lines = lyricsList.getStructuredLyrics().get(0).getLine(); + + if (lines != null) { + for (Line line : lines) { + lyricsBuilder.append(line.getValue().trim()).append("\n"); + } + } + + bind.nowPlayingSongLyricsTextView.setText(lyricsBuilder.toString()); + } + } + + private void defineProgressHandler() { + playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> { + if (!hasStructuredLyrics(lyricsList)) { + releaseHandler(); + return; + } + + if (!lyricsList.getStructuredLyrics().get(0).getSynced()) { + releaseHandler(); + return; + } + + syncLyricsHandler = new Handler(); + syncLyricsRunnable = () -> { + if (syncLyricsHandler != null) { + if (bind != null) { + displaySyncedLyrics(); + } + + syncLyricsHandler.postDelayed(syncLyricsRunnable, 250); + } + }; + + syncLyricsHandler.postDelayed(syncLyricsRunnable, 250); + }); + } + + private void displaySyncedLyrics() { + LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue(); + int timestamp = (int) (mediaBrowser.getCurrentPosition()); + + if (hasStructuredLyrics(lyricsList)) { + StringBuilder lyricsBuilder = new StringBuilder(); + List lines = lyricsList.getStructuredLyrics().get(0).getLine(); + + if (lines == null || lines.isEmpty()) return; + + for (Line line : lines) { + lyricsBuilder.append(line.getValue().trim()).append("\n"); + } + + Line toHighlight = lines.stream().filter(line -> line != null && line.getStart() != null && line.getStart() < timestamp).reduce((first, second) -> second).orElse(null); + + if (toHighlight != null) { + String lyrics = lyricsBuilder.toString(); + Spannable spannableString = new SpannableString(lyrics); + + int startingPosition = getStartPosition(lines, toHighlight); + int endingPosition = startingPosition + toHighlight.getValue().length(); + + spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null)), 0, lyrics.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.lyricsTextColor, null)), startingPosition, endingPosition, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + bind.nowPlayingSongLyricsTextView.setText(spannableString); + + if (playerBottomSheetViewModel.getSyncLyricsState()) { + bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(lines, toHighlight)); + } + } + } + } + + private int getStartPosition(List lines, Line toHighlight) { + int start = 0; + + for (Line line : lines) { + if (line != toHighlight) { + start = start + line.getValue().length() + 1; + } else { + break; + } + } + + return start; + } + + private int getLineCount(List lines, Line toHighlight) { + int start = 0; + + for (Line line : lines) { + if (line != toHighlight) { + bind.tempLyricsLineTextView.setText(line.getValue()); + start = start + bind.tempLyricsLineTextView.getLineCount(); + } else { + break; + } + } + + return start; + } + + private int getScroll(List lines, Line toHighlight) { + int startIndex = getStartPosition(lines, toHighlight); + Layout layout = bind.nowPlayingSongLyricsTextView.getLayout(); + if (layout == null) return 0; + + int line = layout.getLineForOffset(startIndex); + int lineTop = layout.getLineTop(line); + int lineBottom = layout.getLineBottom(line); + int lineCenter = (lineTop + lineBottom) / 2; + + int scrollViewHeight = bind.nowPlayingSongLyricsSrollView.getHeight(); + int scroll = lineCenter - scrollViewHeight / 2; + + return Math.max(scroll, 0); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java new file mode 100644 index 0000000..8c6e052 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java @@ -0,0 +1,258 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerQueueBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; +import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.stream.Collectors; + +@UnstableApi +public class PlayerQueueFragment extends Fragment implements ClickCallback { + private static final String TAG = "PlayerQueueFragment"; + + private InnerFragmentPlayerQueueBinding bind; + + private PlayerBottomSheetViewModel playerBottomSheetViewModel; + private PlaybackViewModel playbackViewModel; + private ListenableFuture mediaBrowserListenableFuture; + + private PlayerSongQueueAdapter playerSongQueueAdapter; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + bind = InnerFragmentPlayerQueueBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + + playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); + + initQueueRecyclerView(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + initializeBrowser(); + bindMediaController(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); + } + + @Override + public void onResume() { + super.onResume(); + setMediaBrowserListenableFuture(); + updateNowPlayingItem(); + try { + long position = mediaBrowserListenableFuture.get().getCurrentMediaItemIndex(); + bind.playerQueueRecyclerView.scrollToPosition((int) position); + } catch (Exception e) { + Log.e("PlayerQueueFragment", "Failed to get mediaBrowserListenableFuture in onResume", e); + } + } + + @Override + public void onStop() { + releaseBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void initializeBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + private void bindMediaController() { + mediaBrowserListenableFuture.addListener(() -> { + try { + MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); + initShuffleButton(mediaBrowser); + initCleanButton(mediaBrowser); + } catch (Exception exception) { + exception.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + + private void setMediaBrowserListenableFuture() { + playerSongQueueAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + } + + private void initQueueRecyclerView() { + bind.playerQueueRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.playerQueueRecyclerView.setHasFixedSize(true); + + playerSongQueueAdapter = new PlayerSongQueueAdapter(this); + bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter); + reapplyPlayback(); + + playerBottomSheetViewModel.getQueueSong().observe(getViewLifecycleOwner(), queue -> { + if (queue != null) { + playerSongQueueAdapter.setItems(queue.stream().map(item -> (Child) item).collect(Collectors.toList())); + reapplyPlayback(); + } + }); + + new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT) { + int originalPosition = -1; + int fromPosition = -1; + int toPosition = -1; + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { + if (originalPosition == -1) { + originalPosition = viewHolder.getBindingAdapterPosition(); + } + + fromPosition = viewHolder.getBindingAdapterPosition(); + toPosition = target.getBindingAdapterPosition(); + + /* + * Per spostare un elemento nella coda devo: + * - Spostare graficamente la traccia da una posizione all'altra con Collections.swap() + * - Spostare nel db la traccia, tramite QueueRepository + * - Notificare il Service dell'avvenuto spostamento con MusicPlayerRemote.moveSong() + * + * In onMove prendo la posizione di inizio e fine, ma solo al rilascio dell'elemento procedo allo spostamento + * In questo modo evito che ad ogni cambio di posizione vada a riscrivere nel db + * Al rilascio dell'elemento chiamo il metodo clearView() + */ + + Collections.swap(playerSongQueueAdapter.getItems(), fromPosition, toPosition); + recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition); + + return false; + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + + if (originalPosition != -1 && fromPosition != -1 && toPosition != -1) { + MediaManager.swap(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), originalPosition, toPosition); + } + + originalPosition = -1; + fromPosition = -1; + toPosition = -1; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + MediaManager.remove(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), viewHolder.getBindingAdapterPosition()); + viewHolder.getBindingAdapter().notifyDataSetChanged(); + } + }).attachToRecyclerView(bind.playerQueueRecyclerView); + } + + private void initShuffleButton(MediaBrowser mediaBrowser) { + bind.playerShuffleQueueFab.setOnClickListener(view -> { + int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1; + int endPosition = playerSongQueueAdapter.getItems().size() - 1; + + if (startPosition < endPosition) { + ArrayList pool = new ArrayList<>(); + + for (int i = startPosition; i <= endPosition; i++) { + pool.add(i); + } + + while (pool.size() >= 2) { + int fromPosition = (int) (Math.random() * (pool.size())); + int positionA = pool.get(fromPosition); + pool.remove(fromPosition); + + int toPosition = (int) (Math.random() * (pool.size())); + int positionB = pool.get(toPosition); + pool.remove(toPosition); + + Collections.swap(playerSongQueueAdapter.getItems(), positionA, positionB); + bind.playerQueueRecyclerView.getAdapter().notifyItemMoved(positionA, positionB); + } + + MediaManager.shuffle(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition); + } + }); + } + + private void initCleanButton(MediaBrowser mediaBrowser) { + bind.playerCleanQueueButton.setOnClickListener(view -> { + int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1; + int endPosition = playerSongQueueAdapter.getItems().size(); + + MediaManager.removeRange(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition); + bind.playerQueueRecyclerView.getAdapter().notifyItemRangeRemoved(startPosition, endPosition); + }); + } + + private void updateNowPlayingItem() { + playerSongQueueAdapter.notifyDataSetChanged(); + } + + @Override + public void onMediaClick(Bundle bundle) { + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); + } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> { + if (playerSongQueueAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + playerSongQueueAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (playerSongQueueAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + playerSongQueueAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (playerSongQueueAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + playerSongQueueAdapter.setPlaybackState(id, playing != null && playing); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistCatalogueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistCatalogueFragment.java new file mode 100644 index 0000000..17bd58c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistCatalogueFragment.java @@ -0,0 +1,186 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.PopupMenu; +import android.widget.SearchView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentPlaylistCatalogueBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter; +import com.cappielloantonio.tempo.ui.dialog.PlaylistEditorDialog; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.PlaylistCatalogueViewModel; + +@UnstableApi +public class PlaylistCatalogueFragment extends Fragment implements ClickCallback { + private FragmentPlaylistCatalogueBinding bind; + private MainActivity activity; + private PlaylistCatalogueViewModel playlistCatalogueViewModel; + + private PlaylistHorizontalAdapter playlistHorizontalAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentPlaylistCatalogueBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + playlistCatalogueViewModel = new ViewModelProvider(requireActivity()).get(PlaylistCatalogueViewModel.class); + + init(); + initAppBar(); + initPlaylistCatalogueView(); + + return view; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + if (requireArguments().getString(Constants.PLAYLIST_ALL) != null) { + playlistCatalogueViewModel.setType(Constants.PLAYLIST_ALL); + } else if (requireArguments().getString(Constants.PLAYLIST_DOWNLOADED) != null) { + playlistCatalogueViewModel.setType(Constants.PLAYLIST_DOWNLOADED); + } + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + bind.toolbar.setNavigationOnClickListener(v -> { + hideKeyboard(v); + activity.navController.navigateUp(); + }); + + + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.albumInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(R.string.playlist_catalogue_title); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + } + + @SuppressLint("ClickableViewAccessibility") + private void initPlaylistCatalogueView() { + bind.playlistCatalogueRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.playlistCatalogueRecyclerView.setHasFixedSize(true); + + playlistHorizontalAdapter = new PlaylistHorizontalAdapter(this); + bind.playlistCatalogueRecyclerView.setAdapter(playlistHorizontalAdapter); + + if (getActivity() != null) { + playlistCatalogueViewModel.getPlaylistList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), playlists -> { + if (playlists != null) playlistHorizontalAdapter.setItems(playlists); + }); + } + + bind.playlistCatalogueRecyclerView.setOnTouchListener((v, event) -> { + hideKeyboard(v); + return false; + }); + + bind.playlistListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_playlist_popup_menu)); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.toolbar_menu, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + + SearchView searchView = (SearchView) searchItem.getActionView(); + searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + playlistHorizontalAdapter.getFilter().filter(newText); + return false; + } + }); + + searchView.setPadding(-32, 0, 0, 0); + } + + private void hideKeyboard(View view) { + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void showPopupMenu(View view, int menuResource) { + PopupMenu popup = new PopupMenu(requireContext(), view); + popup.getMenuInflater().inflate(menuResource, popup.getMenu()); + + popup.setOnMenuItemClickListener(menuItem -> { + if (menuItem.getItemId() == R.id.menu_playlist_sort_name) { + playlistHorizontalAdapter.sort(Constants.GENRE_ORDER_BY_NAME); + return true; + } else if (menuItem.getItemId() == R.id.menu_playlist_sort_random) { + playlistHorizontalAdapter.sort(Constants.GENRE_ORDER_BY_RANDOM); + return true; + } + + return false; + }); + + popup.show(); + } + + @Override + public void onPlaylistClick(Bundle bundle) { + bundle.putBoolean("is_offline", false); + Navigation.findNavController(requireView()).navigate(R.id.playlistPageFragment, bundle); + hideKeyboard(requireView()); + } + + @Override + public void onPlaylistLongClick(Bundle bundle) { + PlaylistEditorDialog dialog = new PlaylistEditorDialog(null); + dialog.setArguments(bundle); + dialog.show(activity.getSupportFragmentManager(), null); + hideKeyboard(requireView()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java new file mode 100644 index 0000000..d4cf6c0 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlaylistPageFragment.java @@ -0,0 +1,323 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.SearchView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentPlaylistPageBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.ExternalAudioWriter; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; +import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.Collections; +import java.util.Objects; +import java.util.stream.Collectors; + +@UnstableApi +public class PlaylistPageFragment extends Fragment implements ClickCallback { + private FragmentPlaylistPageBinding bind; + private MainActivity activity; + private PlaylistPageViewModel playlistPageViewModel; + private PlaybackViewModel playbackViewModel; + + private SongHorizontalAdapter songHorizontalAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.playlist_page_menu, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + + SearchView searchView = (SearchView) searchItem.getActionView(); + searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + songHorizontalAdapter.getFilter().filter(newText); + return false; + } + }); + + searchView.setPadding(-32, 0, 0, 0); + + initMenuOption(menu); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentPlaylistPageBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + playlistPageViewModel = new ViewModelProvider(requireActivity()).get(PlaylistPageViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); + + init(); + initAppBar(); + initMusicButton(); + initBackCover(); + initSongsView(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); + } + + @Override + public void onResume() { + super.onResume(); + if (songHorizontalAdapter != null) setMediaBrowserListenableFuture(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_download_playlist) { + playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> { + if (isVisible() && getActivity() != null) { + if (Preferences.getDownloadDirectoryUri() == null) { + DownloadUtil.getDownloadTracker(requireContext()).download( + MappingUtil.mapDownloads(songs), + songs.stream().map(child -> { + Download toDownload = new Download(child); + toDownload.setPlaylistId(playlistPageViewModel.getPlaylist().getId()); + toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName()); + return toDownload; + }).collect(Collectors.toList()) + ); + } else { + songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child)); + } + } + }); + return true; + } else if (item.getItemId() == R.id.action_pin_playlist) { + playlistPageViewModel.setPinned(true); + return true; + } else if (item.getItemId() == R.id.action_unpin_playlist) { + playlistPageViewModel.setPinned(false); + return true; + } + + return false; + } + + private void init() { + playlistPageViewModel.setPlaylist(requireArguments().getParcelable(Constants.PLAYLIST_OBJECT)); + } + + private void initMenuOption(Menu menu) { + playlistPageViewModel.isPinned(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), isPinned -> { + menu.findItem(R.id.action_unpin_playlist).setVisible(isPinned); + menu.findItem(R.id.action_pin_playlist).setVisible(!isPinned); + }); + } + + private void initAppBar() { + activity.setSupportActionBar(bind.animToolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + bind.animToolbar.setTitle(playlistPageViewModel.getPlaylist().getName()); + + bind.playlistNameLabel.setText(playlistPageViewModel.getPlaylist().getName()); + bind.playlistSongCountLabel.setText(getString(R.string.playlist_song_count, playlistPageViewModel.getPlaylist().getSongCount())); + bind.playlistDurationLabel.setText(getString(R.string.playlist_duration, MusicUtil.getReadableDurationString(playlistPageViewModel.getPlaylist().getDuration(), false))); + + bind.animToolbar.setNavigationOnClickListener(v -> { + hideKeyboard(v); + activity.navController.navigateUp(); + }); + + Objects.requireNonNull(bind.animToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); + } + + private void hideKeyboard(View view) { + InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void initMusicButton() { + playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> { + if (bind != null) { + bind.playlistPagePlayButton.setOnClickListener(v -> { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + }); + + bind.playlistPageShuffleButton.setOnClickListener(v -> { + Collections.shuffle(songs); + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + activity.setBottomSheetInPeek(true); + }); + } + }); + } + + private void initBackCover() { + playlistPageViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> { + if (bind != null && songs != null && !songs.isEmpty()) { + Collections.shuffle(songs); + + // Pic top-left + CustomGlideRequest.Builder + .from(requireContext(), !songs.isEmpty() ? songs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .transform(new GranularRoundedCorners(CustomGlideRequest.CORNER_RADIUS, 0, 0, 0)) + .into(bind.playlistCoverImageViewTopLeft); + + // Pic top-right + CustomGlideRequest.Builder + .from(requireContext(), songs.size() > 1 ? songs.get(1).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .transform(new GranularRoundedCorners(0, CustomGlideRequest.CORNER_RADIUS, 0, 0)) + .into(bind.playlistCoverImageViewTopRight); + + // Pic bottom-left + CustomGlideRequest.Builder + .from(requireContext(), songs.size() > 2 ? songs.get(2).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .transform(new GranularRoundedCorners(0, 0, 0, CustomGlideRequest.CORNER_RADIUS)) + .into(bind.playlistCoverImageViewBottomLeft); + + // Pic bottom-right + CustomGlideRequest.Builder + .from(requireContext(), songs.size() > 3 ? songs.get(3).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .transform(new GranularRoundedCorners(0, 0, CustomGlideRequest.CORNER_RADIUS, 0)) + .into(bind.playlistCoverImageViewBottomRight); + } + }); + } + + private void initSongsView() { + bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.songRecyclerView.setHasFixedSize(true); + + songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null); + bind.songRecyclerView.setAdapter(songHorizontalAdapter); + setMediaBrowserListenableFuture(); + reapplyPlayback(); + + playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> { + songHorizontalAdapter.setItems(songs); + reapplyPlayback(); + }); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onMediaClick(Bundle bundle) { + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); + activity.setBottomSheetInPeek(true); + } + + @Override + public void onMediaLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); + } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> { + if (songHorizontalAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + } + + private void setMediaBrowserListenableFuture() { + songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PodcastChannelCatalogueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PodcastChannelCatalogueFragment.java new file mode 100644 index 0000000..8cd6e88 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PodcastChannelCatalogueFragment.java @@ -0,0 +1,155 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.SearchView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentPodcastChannelCatalogueBinding; +import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.PodcastChannelCatalogueAdapter; +import com.cappielloantonio.tempo.viewmodel.PodcastChannelCatalogueViewModel; + +@OptIn(markerClass = UnstableApi.class) +public class PodcastChannelCatalogueFragment extends Fragment implements ClickCallback { + private static final String TAG = "PodcastChannelCatalogue"; + + private FragmentPodcastChannelCatalogueBinding bind; + private MainActivity activity; + private PodcastChannelCatalogueViewModel podcastChannelCatalogueViewModel; + + private PodcastChannelCatalogueAdapter podcastChannelCatalogueAdapter; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentPodcastChannelCatalogueBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + podcastChannelCatalogueViewModel = new ViewModelProvider(requireActivity()).get(PodcastChannelCatalogueViewModel.class); + + initAppBar(); + initPodcastChannelCatalogueView(); + + return view; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + bind.toolbar.setNavigationOnClickListener(v -> { + hideKeyboard(v); + activity.navController.navigateUp(); + }); + + + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.podcastChannelInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(R.string.podcast_channel_catalogue_title); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + } + + @SuppressLint("ClickableViewAccessibility") + private void initPodcastChannelCatalogueView() { + bind.podcastChannelCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2)); + bind.podcastChannelCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false)); + bind.podcastChannelCatalogueRecyclerView.setHasFixedSize(true); + + podcastChannelCatalogueAdapter = new PodcastChannelCatalogueAdapter(this); + podcastChannelCatalogueAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); + bind.podcastChannelCatalogueRecyclerView.setAdapter(podcastChannelCatalogueAdapter); + podcastChannelCatalogueViewModel.getPodcastChannels(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> { + if (albums != null) { + podcastChannelCatalogueAdapter.setItems(albums); + } + }); + + bind.podcastChannelCatalogueRecyclerView.setOnTouchListener((v, event) -> { + hideKeyboard(v); + return false; + }); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.toolbar_menu, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + + SearchView searchView = (SearchView) searchItem.getActionView(); + searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + podcastChannelCatalogueAdapter.getFilter().filter(newText); + return false; + } + }); + + searchView.setPadding(-32, 0, 0, 0); + } + + private void hideKeyboard(View view) { + InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + @Override + public void onPodcastChannelClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.podcastChannelPageFragment, bundle); + hideKeyboard(requireView()); + } + + @Override + public void onPodcastChannelLongClick(Bundle bundle) { + // Navigation.findNavController(requireView()).navigate(R.id.albumBottomSheetDialog, bundle); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PodcastChannelPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PodcastChannelPageFragment.java new file mode 100644 index 0000000..4e35e3c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PodcastChannelPageFragment.java @@ -0,0 +1,183 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.PopupMenu; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentPodcastChannelPageBinding; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.PodcastEpisodeAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.UIUtil; +import com.cappielloantonio.tempo.viewmodel.PodcastChannelPageViewModel; +import com.google.android.material.snackbar.Snackbar; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; + +@UnstableApi +public class PodcastChannelPageFragment extends Fragment implements ClickCallback { + private FragmentPodcastChannelPageBinding bind; + private MainActivity activity; + private PodcastChannelPageViewModel podcastChannelPageViewModel; + + private PodcastEpisodeAdapter podcastEpisodeAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentPodcastChannelPageBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + podcastChannelPageViewModel = new ViewModelProvider(requireActivity()).get(PodcastChannelPageViewModel.class); + + init(); + initAppBar(); + initPodcastChannelInfo(); + initPodcastChannelEpisodesView(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + podcastChannelPageViewModel.setPodcastChannel(requireArguments().getParcelable(Constants.PODCAST_CHANNEL_OBJECT)); + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + bind.toolbar.setTitle(podcastChannelPageViewModel.getPodcastChannel().getTitle()); + bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp()); + bind.toolbar.setTitle(podcastChannelPageViewModel.getPodcastChannel().getTitle()); + } + + private void initPodcastChannelInfo() { + String normalizePodcastChannelDescription = MusicUtil.forceReadableString(podcastChannelPageViewModel.getPodcastChannel().getDescription()); + + if (bind != null) { + bind.podcastChannelDescriptionTextView.setVisibility(!normalizePodcastChannelDescription.trim().isEmpty() ? View.VISIBLE : View.GONE); + bind.podcastChannelDescriptionTextView.setText(normalizePodcastChannelDescription); + bind.podcastEpisodesFilterImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.filter_podcast_episode_popup_menu)); + } + } + + private void initPodcastChannelEpisodesView() { + bind.podcastEpisodesRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.podcastEpisodesRecyclerView.addItemDecoration(UIUtil.getDividerItemDecoration(requireContext())); + + podcastEpisodeAdapter = new PodcastEpisodeAdapter(this); + bind.podcastEpisodesRecyclerView.setAdapter(podcastEpisodeAdapter); + podcastChannelPageViewModel.getPodcastChannelEpisodes().observe(getViewLifecycleOwner(), channels -> { + if (channels == null) { + if (bind != null) { + bind.podcastEpisodesRecyclerView.setVisibility(View.GONE); + } + } else { + if (bind != null) { + bind.podcastEpisodesRecyclerView.setVisibility(View.VISIBLE); + } + + if (!channels.isEmpty() && channels.get(0) != null && channels.get(0).getEpisodes() != null) { + List availableEpisode = channels.get(0).getEpisodes(); + + if (bind != null && availableEpisode != null) { + bind.podcastEpisodesRecyclerView.setVisibility(availableEpisode.isEmpty() ? View.GONE : View.VISIBLE); + podcastEpisodeAdapter.setItems(availableEpisode); + } + } + } + }); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + private void showPopupMenu(View view, int menuResource) { + PopupMenu popup = new PopupMenu(requireContext(), view); + popup.getMenuInflater().inflate(menuResource, popup.getMenu()); + + popup.setOnMenuItemClickListener(menuItem -> { + if (menuItem.getItemId() == R.id.menu_podcast_filter_download) { + podcastEpisodeAdapter.sort(Constants.PODCAST_FILTER_BY_DOWNLOAD); + return true; + } else if (menuItem.getItemId() == R.id.menu_podcast_filter_all) { + podcastEpisodeAdapter.sort(Constants.PODCAST_FILTER_BY_ALL); + return true; + } + + return false; + }); + + popup.show(); + } + + @Override + public void onPodcastEpisodeClick(Bundle bundle) { + MediaManager.startPodcast(mediaBrowserListenableFuture, bundle.getParcelable(Constants.PODCAST_OBJECT)); + activity.setBottomSheetInPeek(true); + } + + @Override + public void onPodcastEpisodeLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.podcastEpisodeBottomSheetDialog, bundle); + } + + @Override + public void onPodcastEpisodeAltClick(Bundle bundle) { + PodcastEpisode episode = bundle.getParcelable(Constants.PODCAST_OBJECT); + podcastChannelPageViewModel.requestPodcastEpisodeDownload(episode); + + Snackbar.make(requireView(), R.string.podcast_episode_download_request_snackbar, Snackbar.LENGTH_SHORT) + .setAnchorView(activity.bind.bottomNavigation) + .show(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SearchFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SearchFragment.java new file mode 100644 index 0000000..14efc14 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SearchFragment.java @@ -0,0 +1,330 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.content.ComponentName; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentSearchBinding; +import com.cappielloantonio.tempo.helper.recyclerview.CustomLinearSnapHelper; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter; +import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter; +import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; +import com.cappielloantonio.tempo.viewmodel.SearchViewModel; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.Collections; + +@UnstableApi +public class SearchFragment extends Fragment implements ClickCallback { + private static final String TAG = "SearchFragment"; + + private FragmentSearchBinding bind; + private MainActivity activity; + private SearchViewModel searchViewModel; + private PlaybackViewModel playbackViewModel; + + private ArtistAdapter artistAdapter; + private AlbumAdapter albumAdapter; + private SongHorizontalAdapter songHorizontalAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentSearchBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + searchViewModel = new ViewModelProvider(requireActivity()).get(SearchViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); + + initSearchResultView(); + initSearchView(); + inputFocus(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); + } + + @Override + public void onResume() { + super.onResume(); + if (songHorizontalAdapter != null) setMediaBrowserListenableFuture(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void initSearchResultView() { + // Artists + bind.searchResultArtistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.searchResultArtistRecyclerView.setHasFixedSize(true); + + artistAdapter = new ArtistAdapter(this, false, false); + bind.searchResultArtistRecyclerView.setAdapter(artistAdapter); + + CustomLinearSnapHelper artistSnapHelper = new CustomLinearSnapHelper(); + artistSnapHelper.attachToRecyclerView(bind.searchResultArtistRecyclerView); + + // Albums + bind.searchResultAlbumRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.searchResultAlbumRecyclerView.setHasFixedSize(true); + + albumAdapter = new AlbumAdapter(this); + bind.searchResultAlbumRecyclerView.setAdapter(albumAdapter); + + CustomLinearSnapHelper albumSnapHelper = new CustomLinearSnapHelper(); + albumSnapHelper.attachToRecyclerView(bind.searchResultAlbumRecyclerView); + + // Songs + bind.searchResultTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.searchResultTracksRecyclerView.setHasFixedSize(true); + + songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null); + setMediaBrowserListenableFuture(); + reapplyPlayback(); + + bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter); + } + + private void initSearchView() { + setRecentSuggestions(); + + bind.searchView + .getEditText() + .setOnEditorActionListener((textView, actionId, keyEvent) -> { + String query = bind.searchView.getText().toString(); + + if (isQueryValid(query)) { + search(query); + return true; + } + + return false; + }); + + bind.searchView + .getEditText() + .addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int start, int before, int count) { + if (start + count > 1) { + setSearchSuggestions(charSequence.toString()); + } else { + setRecentSuggestions(); + } + } + + @Override + public void afterTextChanged(Editable editable) { + + } + }); + } + + public void setRecentSuggestions() { + bind.searchViewSuggestionContainer.removeAllViews(); + + for (String suggestion : searchViewModel.getRecentSearchSuggestion()) { + View view = LayoutInflater.from(bind.searchViewSuggestionContainer.getContext()).inflate(R.layout.item_search_suggestion, bind.searchViewSuggestionContainer, false); + + ImageView leadingImageView = view.findViewById(R.id.search_suggestion_icon); + TextView titleView = view.findViewById(R.id.search_suggestion_title); + ImageView tailingImageView = view.findViewById(R.id.search_suggestion_delete_icon); + + leadingImageView.setImageDrawable(getResources().getDrawable(R.drawable.ic_history, null)); + titleView.setText(suggestion); + + view.setOnClickListener(v -> search(suggestion)); + + tailingImageView.setOnClickListener(v -> { + searchViewModel.deleteRecentSearch(suggestion); + setRecentSuggestions(); + }); + + bind.searchViewSuggestionContainer.addView(view); + } + } + + public void setSearchSuggestions(String query) { + searchViewModel.getSearchSuggestion(query).observe(getViewLifecycleOwner(), suggestions -> { + bind.searchViewSuggestionContainer.removeAllViews(); + + for (String suggestion : suggestions) { + View view = LayoutInflater.from(bind.searchViewSuggestionContainer.getContext()).inflate(R.layout.item_search_suggestion, bind.searchViewSuggestionContainer, false); + + ImageView leadingImageView = view.findViewById(R.id.search_suggestion_icon); + TextView titleView = view.findViewById(R.id.search_suggestion_title); + ImageView tailingImageView = view.findViewById(R.id.search_suggestion_delete_icon); + + leadingImageView.setImageDrawable(getResources().getDrawable(R.drawable.ic_search, null)); + titleView.setText(suggestion); + tailingImageView.setVisibility(View.GONE); + + view.setOnClickListener(v -> search(suggestion)); + + bind.searchViewSuggestionContainer.addView(view); + } + }); + } + + public void search(String query) { + searchViewModel.setQuery(query); + bind.searchBar.setText(query); + bind.searchView.hide(); + performSearch(query); + } + + private void performSearch(String query) { + searchViewModel.search3(query).observe(getViewLifecycleOwner(), result -> { + if (bind != null) { + if (result.getArtists() != null) { + bind.searchArtistSector.setVisibility(!result.getArtists().isEmpty() ? View.VISIBLE : View.GONE); + artistAdapter.setItems(result.getArtists()); + } else { + artistAdapter.setItems(Collections.emptyList()); + bind.searchArtistSector.setVisibility(View.GONE); + } + + if (result.getAlbums() != null) { + bind.searchAlbumSector.setVisibility(!result.getAlbums().isEmpty() ? View.VISIBLE : View.GONE); + albumAdapter.setItems(result.getAlbums()); + } else { + albumAdapter.setItems(Collections.emptyList()); + bind.searchAlbumSector.setVisibility(View.GONE); + } + + if (result.getSongs() != null) { + bind.searchSongSector.setVisibility(!result.getSongs().isEmpty() ? View.VISIBLE : View.GONE); + songHorizontalAdapter.setItems(result.getSongs()); + } else { + songHorizontalAdapter.setItems(Collections.emptyList()); + bind.searchSongSector.setVisibility(View.GONE); + } + } + }); + + bind.searchResultLayout.setVisibility(View.VISIBLE); + } + + private boolean isQueryValid(String query) { + return !query.equals("") && query.trim().length() > 1; + } + + private void inputFocus() { + bind.searchView.show(); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onMediaClick(Bundle bundle) { + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); + songHorizontalAdapter.notifyDataSetChanged(); + activity.setBottomSheetInPeek(true); + } + + @Override + public void onMediaLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); + } + + @Override + public void onAlbumClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle); + } + + @Override + public void onAlbumLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.albumBottomSheetDialog, bundle); + } + + @Override + public void onArtistClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistPageFragment, bundle); + } + + @Override + public void onArtistLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle); + } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> { + if (songHorizontalAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + } + + private void setMediaBrowserListenableFuture() { + songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java new file mode 100644 index 0000000..bbbdecc --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -0,0 +1,580 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.app.Activity; +import android.content.Context; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.audiofx.AudioEffect; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.os.LocaleListCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.NavController; +import androidx.navigation.NavOptions; +import androidx.navigation.fragment.NavHostFragment; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreference; + +import com.cappielloantonio.tempo.BuildConfig; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.helper.ThemeHelper; +import com.cappielloantonio.tempo.interfaces.DialogClickCallback; +import com.cappielloantonio.tempo.interfaces.ScanCallback; +import com.cappielloantonio.tempo.service.EqualizerManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog; +import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.util.UIUtil; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.viewmodel.SettingViewModel; + +import java.util.Locale; +import java.util.Map; + +@OptIn(markerClass = UnstableApi.class) +public class SettingsFragment extends PreferenceFragmentCompat { + private static final String TAG = "SettingsFragment"; + + private MainActivity activity; + private SettingViewModel settingViewModel; + + private ActivityResultLauncher equalizerResultLauncher; + private ActivityResultLauncher directoryPickerLauncher; + + private MediaService.LocalBinder mediaServiceBinder; + private boolean isServiceBound = false; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + equalizerResultLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> {} + ); + + if (!BuildConfig.FLAVOR.equals("tempus")) { + PreferenceCategory githubUpdateCategory = findPreference("settings_github_update_category_key"); + if (githubUpdateCategory != null) { + getPreferenceScreen().removePreference(githubUpdateCategory); + } + } + + directoryPickerLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK) { + Intent data = result.getData(); + if (data != null) { + Uri uri = data.getData(); + if (uri != null) { + requireContext().getContentResolver().takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ); + + Preferences.setDownloadDirectoryUri(uri.toString()); + ExternalAudioReader.refreshCache(); + Toast.makeText(requireContext(), R.string.settings_download_folder_set, Toast.LENGTH_SHORT).show(); + checkDownloadDirectory(); + } + } + } + }); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + View view = super.onCreateView(inflater, container, savedInstanceState); + settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class); + + if (view != null) { + getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom)); + } + + return view; + } + + @Override + public void onStart() { + super.onStart(); + activity.setBottomNavigationBarVisibility(false); + activity.setBottomSheetVisibility(false); + } + + @Override + public void onResume() { + super.onResume(); + + checkSystemEqualizer(); + checkCacheStorage(); + checkStorage(); + checkDownloadDirectory(); + + setStreamingCacheSize(); + setAppLanguage(); + setVersion(); + + actionLogout(); + actionScan(); + actionSyncStarredAlbums(); + actionSyncStarredTracks(); + actionSyncStarredArtists(); + actionChangeStreamingCacheStorage(); + actionChangeDownloadStorage(); + actionSetDownloadDirectory(); + actionDeleteDownloadStorage(); + actionKeepScreenOn(); + actionAutoDownloadLyrics(); + actionMiniPlayerHeart(); + + bindMediaService(); + actionAppEqualizer(); + } + + @Override + public void onStop() { + super.onStop(); + activity.setBottomSheetVisibility(true); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.global_preferences, rootKey); + ListPreference themePreference = findPreference(Preferences.THEME); + if (themePreference != null) { + themePreference.setOnPreferenceChangeListener( + (preference, newValue) -> { + String themeOption = (String) newValue; + ThemeHelper.applyTheme(themeOption); + return true; + }); + } + } + + private void checkSystemEqualizer() { + Preference equalizer = findPreference("system_equalizer"); + + if (equalizer == null) return; + + Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); + + if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) { + equalizer.setOnPreferenceClickListener(preference -> { + equalizerResultLauncher.launch(intent); + return true; + }); + } else { + equalizer.setVisible(false); + } + } + + private void checkCacheStorage() { + Preference storage = findPreference("streaming_cache_storage"); + + if (storage == null) return; + + try { + if (requireContext().getExternalFilesDirs(null)[1] == null) { + storage.setVisible(false); + } else { + storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button); + } + } catch (Exception exception) { + storage.setVisible(false); + } + } + + private void checkStorage() { + Preference storage = findPreference("download_storage"); + + if (storage == null) return; + + try { + if (requireContext().getExternalFilesDirs(null)[1] == null) { + storage.setVisible(false); + } else { + int pref = Preferences.getDownloadStoragePreference(); + if (pref == 0) { + storage.setSummary(R.string.download_storage_internal_dialog_negative_button); + } else if (pref == 1) { + storage.setSummary(R.string.download_storage_external_dialog_positive_button); + } else { + storage.setSummary(R.string.download_storage_directory_dialog_neutral_button); + } + } + } catch (Exception exception) { + storage.setVisible(false); + } + } + + private void checkDownloadDirectory() { + Preference storage = findPreference("download_storage"); + Preference directory = findPreference("set_download_directory"); + + if (directory == null) return; + + String current = Preferences.getDownloadDirectoryUri(); + if (current != null) { + if (storage != null) storage.setVisible(false); + directory.setVisible(true); + directory.setIcon(R.drawable.ic_close); + directory.setTitle(R.string.settings_clear_download_folder); + directory.setSummary(current); + } else { + if (storage != null) storage.setVisible(true); + if (Preferences.getDownloadStoragePreference() == 2) { + directory.setVisible(true); + directory.setIcon(R.drawable.ic_folder); + directory.setTitle(R.string.settings_set_download_folder); + directory.setSummary(R.string.settings_choose_download_folder); + } else { + directory.setVisible(false); + } + } + } + + private void setStreamingCacheSize() { + ListPreference streamingCachePreference = findPreference("streaming_cache_size"); + + if (streamingCachePreference != null) { + streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider() { + @Nullable + @Override + public CharSequence provideSummary(@NonNull ListPreference preference) { + CharSequence entry = preference.getEntry(); + + if (entry == null) return null; + + long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024); + + return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb)); + } + }); + } + } + + private void setAppLanguage() { + ListPreference localePref = (ListPreference) findPreference("language"); + + Map locales = UIUtil.getLangPreferenceDropdownEntries(requireContext()); + + CharSequence[] entries = locales.keySet().toArray(new CharSequence[locales.size()]); + CharSequence[] entryValues = locales.values().toArray(new CharSequence[locales.size()]); + + localePref.setEntries(entries); + localePref.setEntryValues(entryValues); + + String value = localePref.getValue(); + if ("default".equals(value)) { + localePref.setSummary(requireContext().getString(R.string.settings_system_language)); + } else { + localePref.setSummary(Locale.forLanguageTag(value).getDisplayName()); + } + + localePref.setOnPreferenceChangeListener((preference, newValue) -> { + if ("default".equals(newValue)) { + AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()); + preference.setSummary(requireContext().getString(R.string.settings_system_language)); + } else { + LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue); + AppCompatDelegate.setApplicationLocales(appLocale); + preference.setSummary(Locale.forLanguageTag((String) newValue).getDisplayName()); + } + return true; + }); + } + + private void setVersion() { + findPreference("version").setSummary(BuildConfig.VERSION_NAME); + } + + private void actionLogout() { + findPreference("logout").setOnPreferenceClickListener(preference -> { + activity.quit(); + return true; + }); + } + + private void actionScan() { + findPreference("scan_library").setOnPreferenceClickListener(preference -> { + settingViewModel.launchScan(new ScanCallback() { + @Override + public void onError(Exception exception) { + findPreference("scan_library").setSummary(exception.getMessage()); + } + + @Override + public void onSuccess(boolean isScanning, long count) { + findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); + if (isScanning) getScanStatus(); + } + }); + + return true; + }); + } + + private void actionSyncStarredTracks() { + findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredSyncDialog dialog = new StarredSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionSyncStarredAlbums() { + findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionSyncStarredArtists() { + findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionChangeStreamingCacheStorage() { + findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> { + StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() { + @Override + public void onPositiveClick() { + findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button); + } + + @Override + public void onNegativeClick() { + findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button); + } + }); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionChangeDownloadStorage() { + findPreference("download_storage").setOnPreferenceClickListener(preference -> { + DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() { + @Override + public void onPositiveClick() { + findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button); + checkDownloadDirectory(); + } + + @Override + public void onNegativeClick() { + findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button); + checkDownloadDirectory(); + } + + @Override + public void onNeutralClick() { + findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button); + checkDownloadDirectory(); + } + }); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionSetDownloadDirectory() { + Preference pref = findPreference("set_download_directory"); + if (pref != null) { + pref.setOnPreferenceClickListener(preference -> { + String current = Preferences.getDownloadDirectoryUri(); + + if (current != null) { + Preferences.setDownloadDirectoryUri(null); + Preferences.setDownloadStoragePreference(0); + ExternalAudioReader.refreshCache(); + Toast.makeText(requireContext(), R.string.settings_download_folder_cleared, Toast.LENGTH_SHORT).show(); + checkStorage(); + checkDownloadDirectory(); + } else { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + directoryPickerLauncher.launch(intent); + } + return true; + }); + } + } + + private void actionDeleteDownloadStorage() { + findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> { + DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog(); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionMiniPlayerHeart() { + SwitchPreference preference = findPreference("mini_shuffle_button_visibility"); + if (preference == null) { + return; + } + + preference.setChecked(Preferences.showShuffleInsteadOfHeart()); + preference.setOnPreferenceChangeListener((pref, newValue) -> { + if (newValue instanceof Boolean) { + Preferences.setShuffleInsteadOfHeart((Boolean) newValue); + } + return true; + }); + } + + private void actionAutoDownloadLyrics() { + SwitchPreference preference = findPreference("auto_download_lyrics"); + if (preference == null) { + return; + } + + preference.setChecked(Preferences.isAutoDownloadLyricsEnabled()); + preference.setOnPreferenceChangeListener((pref, newValue) -> { + if (newValue instanceof Boolean) { + Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue); + } + return true; + }); + } + + private void getScanStatus() { + settingViewModel.getScanStatus(new ScanCallback() { + @Override + public void onError(Exception exception) { + findPreference("scan_library").setSummary(exception.getMessage()); + } + + @Override + public void onSuccess(boolean isScanning, long count) { + findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); + if (isScanning) getScanStatus(); + } + }); + } + + private void actionKeepScreenOn() { + findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + return true; + }); + } + + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mediaServiceBinder = (MediaService.LocalBinder) service; + isServiceBound = true; + checkEqualizerBands(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mediaServiceBinder = null; + isServiceBound = false; + } + }; + + private void bindMediaService() { + Intent intent = new Intent(requireActivity(), MediaService.class); + intent.setAction(MediaService.ACTION_BIND_EQUALIZER); + requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + isServiceBound = true; + } + + private void checkEqualizerBands() { + if (mediaServiceBinder != null) { + EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager(); + short numBands = eqManager.getNumberOfBands(); + Preference appEqualizer = findPreference("app_equalizer"); + if (appEqualizer != null) { + appEqualizer.setVisible(numBands > 0); + } + } + } + + private void actionAppEqualizer() { + Preference appEqualizer = findPreference("app_equalizer"); + if (appEqualizer != null) { + appEqualizer.setOnPreferenceClickListener(preference -> { + NavController navController = NavHostFragment.findNavController(this); + NavOptions navOptions = new NavOptions.Builder() + .setLaunchSingleTop(true) + .setPopUpTo(R.id.equalizerFragment, true) + .build(); + activity.setBottomNavigationBarVisibility(true); + activity.setBottomSheetVisibility(true); + navController.navigate(R.id.equalizerFragment, null, navOptions); + return true; + }); + } + } + + @Override + public void onPause() { + super.onPause(); + if (isServiceBound) { + requireActivity().unbindService(serviceConnection); + isServiceBound = false; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SongListPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SongListPageFragment.java new file mode 100644 index 0000000..e0cad18 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SongListPageFragment.java @@ -0,0 +1,370 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.PopupMenu; +import android.widget.SearchView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentSongListPageBinding; +import com.cappielloantonio.tempo.helper.recyclerview.PaginationScrollListener; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; +import com.cappielloantonio.tempo.viewmodel.SongListPageViewModel; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.Collections; +import java.util.List; + +@UnstableApi +public class SongListPageFragment extends Fragment implements ClickCallback { + private static final String TAG = "SongListPageFragment"; + + private FragmentSongListPageBinding bind; + private MainActivity activity; + private SongListPageViewModel songListPageViewModel; + private PlaybackViewModel playbackViewModel; + + private SongHorizontalAdapter songHorizontalAdapter; + + private ListenableFuture mediaBrowserListenableFuture; + + private boolean isLoading = true; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + bind = FragmentSongListPageBinding.inflate(inflater, container, false); + View view = bind.getRoot(); + songListPageViewModel = new ViewModelProvider(requireActivity()).get(SongListPageViewModel.class); + playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); + + init(); + initAppBar(); + initButtons(); + initSongListView(); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + initializeMediaBrowser(); + + MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); + observePlayback(); + } + + @Override + public void onResume() { + super.onResume(); + setMediaBrowserListenableFuture(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void init() { + if (requireArguments().getString(Constants.MEDIA_RECENTLY_PLAYED) != null) { + songListPageViewModel.title = Constants.MEDIA_RECENTLY_PLAYED; + songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_recently_played); + bind.pageTitleLabel.setText(R.string.song_list_page_recently_played); + } else if (requireArguments().getString(Constants.MEDIA_MOST_PLAYED) != null) { + songListPageViewModel.title = Constants.MEDIA_MOST_PLAYED; + songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_most_played); + bind.pageTitleLabel.setText(R.string.song_list_page_most_played); + } else if (requireArguments().getString(Constants.MEDIA_RECENTLY_ADDED) != null) { + songListPageViewModel.title = Constants.MEDIA_RECENTLY_ADDED; + songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_recently_added); + bind.pageTitleLabel.setText(R.string.song_list_page_recently_added); + } else if (requireArguments().getString(Constants.MEDIA_BY_GENRE) != null) { + songListPageViewModel.title = Constants.MEDIA_BY_GENRE; + songListPageViewModel.genre = requireArguments().getParcelable(Constants.GENRE_OBJECT); + songListPageViewModel.toolbarTitle = songListPageViewModel.genre.getGenre(); + bind.pageTitleLabel.setText(songListPageViewModel.genre.getGenre()); + } else if (requireArguments().getString(Constants.MEDIA_BY_ARTIST) != null) { + songListPageViewModel.title = Constants.MEDIA_BY_ARTIST; + songListPageViewModel.artist = requireArguments().getParcelable(Constants.ARTIST_OBJECT); + songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_top, songListPageViewModel.artist.getName()); + bind.pageTitleLabel.setText(getString(R.string.song_list_page_top, songListPageViewModel.artist.getName())); + } else if (requireArguments().getString(Constants.MEDIA_BY_GENRES) != null) { + songListPageViewModel.title = Constants.MEDIA_BY_GENRES; + songListPageViewModel.filters = requireArguments().getStringArrayList("filters_list"); + songListPageViewModel.filterNames = requireArguments().getStringArrayList("filter_name_list"); + songListPageViewModel.toolbarTitle = songListPageViewModel.getFiltersTitle(); + bind.pageTitleLabel.setText(songListPageViewModel.getFiltersTitle()); + } else if (requireArguments().getString(Constants.MEDIA_BY_YEAR) != null) { + songListPageViewModel.title = Constants.MEDIA_BY_YEAR; + songListPageViewModel.year = requireArguments().getInt("year_object"); + songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_year, songListPageViewModel.year); + bind.pageTitleLabel.setText(getString(R.string.song_list_page_year, songListPageViewModel.year)); + } else if (requireArguments().getString(Constants.MEDIA_STARRED) != null) { + songListPageViewModel.title = Constants.MEDIA_STARRED; + songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_starred); + bind.pageTitleLabel.setText(R.string.song_list_page_starred); + } else if (requireArguments().getString(Constants.MEDIA_DOWNLOADED) != null) { + songListPageViewModel.title = Constants.MEDIA_DOWNLOADED; + songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_downloaded); + bind.pageTitleLabel.setText(getString(R.string.song_list_page_downloaded)); + } else if (requireArguments().getParcelable(Constants.ALBUM_OBJECT) != null) { + songListPageViewModel.album = requireArguments().getParcelable(Constants.ALBUM_OBJECT); + songListPageViewModel.title = Constants.MEDIA_FROM_ALBUM; + songListPageViewModel.toolbarTitle = songListPageViewModel.album.getName(); + bind.pageTitleLabel.setText(songListPageViewModel.album.getName()); + } + } + + private void initAppBar() { + activity.setSupportActionBar(bind.toolbar); + + if (activity.getSupportActionBar() != null) { + activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); + activity.getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + if (bind != null) + bind.toolbar.setNavigationOnClickListener(v -> { + hideKeyboard(v); + activity.navController.navigateUp(); + }); + + if (bind != null) + bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { + if ((bind.albumInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) { + bind.toolbar.setTitle(songListPageViewModel.toolbarTitle); + } else { + bind.toolbar.setTitle(R.string.empty_string); + } + }); + } + + private void initButtons() { + songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> { + if (bind != null) { + setSongListPageSorter(); + + bind.songListShuffleImageView.setOnClickListener(v -> { + Collections.shuffle(songs); + MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(500, songs.size())), 0); + activity.setBottomSheetInPeek(true); + }); + } + }); + } + + @SuppressLint("ClickableViewAccessibility") + private void initSongListView() { + bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.songListRecyclerView.setHasFixedSize(true); + + songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null); + bind.songListRecyclerView.setAdapter(songHorizontalAdapter); + setMediaBrowserListenableFuture(); + reapplyPlayback(); + songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> { + isLoading = false; + songHorizontalAdapter.setItems(songs); + reapplyPlayback(); + setSongListPageSubtitle(songs); + }); + + bind.songListRecyclerView.addOnScrollListener(new PaginationScrollListener((LinearLayoutManager) bind.songListRecyclerView.getLayoutManager()) { + @Override + protected void loadMoreItems() { + isLoading = true; + songListPageViewModel.getSongsByPage(getViewLifecycleOwner()); + } + + @Override + public boolean isLoading() { + return isLoading; + } + }); + + bind.songListRecyclerView.setOnTouchListener((v, event) -> { + hideKeyboard(v); + return false; + }); + + bind.songListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_song_popup_menu)); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.toolbar_menu, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + + SearchView searchView = (SearchView) searchItem.getActionView(); + searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + songHorizontalAdapter.getFilter().filter(newText); + return false; + } + }); + + searchView.setPadding(-32, 0, 0, 0); + } + + private void hideKeyboard(View view) { + InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void showPopupMenu(View view, int menuResource) { + PopupMenu popup = new PopupMenu(requireContext(), view); + popup.getMenuInflater().inflate(menuResource, popup.getMenu()); + + popup.setOnMenuItemClickListener(menuItem -> { + if (menuItem.getItemId() == R.id.menu_song_sort_name) { + songHorizontalAdapter.sort(Constants.MEDIA_BY_TITLE); + return true; + } else if (menuItem.getItemId() == R.id.menu_song_sort_most_recently_starred) { + songHorizontalAdapter.sort(Constants.MEDIA_MOST_RECENTLY_STARRED); + return true; + } else if (menuItem.getItemId() == R.id.menu_song_sort_least_recently_starred) { + songHorizontalAdapter.sort(Constants.MEDIA_LEAST_RECENTLY_STARRED); + return true; + } + + return false; + }); + + popup.show(); + } + + private void setSongListPageSubtitle(List children) { + switch (songListPageViewModel.title) { + case Constants.MEDIA_BY_GENRE: + bind.pageSubtitleLabel.setText(children.size() < songListPageViewModel.maxNumberByGenre ? + getString(R.string.generic_list_page_count, children.size()) : + getString(R.string.generic_list_page_count_unknown, songListPageViewModel.maxNumberByGenre) + ); + break; + case Constants.MEDIA_BY_YEAR: + bind.pageSubtitleLabel.setText(children.size() < songListPageViewModel.maxNumberByYear ? + getString(R.string.generic_list_page_count, children.size()) : + getString(R.string.generic_list_page_count_unknown, songListPageViewModel.maxNumberByYear) + ); + break; + case Constants.MEDIA_BY_ARTIST: + case Constants.MEDIA_BY_GENRES: + case Constants.MEDIA_STARRED: + bind.pageSubtitleLabel.setText(getString(R.string.generic_list_page_count, children.size())); + break; + } + } + + private void setSongListPageSorter() { + switch (songListPageViewModel.title) { + case Constants.MEDIA_BY_GENRE: + case Constants.MEDIA_BY_YEAR: + bind.songListSortImageView.setVisibility(View.GONE); + break; + case Constants.MEDIA_BY_ARTIST: + case Constants.MEDIA_BY_GENRES: + case Constants.MEDIA_STARRED: + bind.songListSortImageView.setVisibility(View.VISIBLE); + break; + } + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + @Override + public void onMediaClick(Bundle bundle) { + hideKeyboard(requireView()); + MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); + activity.setBottomSheetInPeek(true); + } + + @Override + public void onMediaLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); + } + + private void observePlayback() { + playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> { + if (songHorizontalAdapter != null) { + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + }); + } + + private void reapplyPlayback() { + if (songHorizontalAdapter != null) { + String id = playbackViewModel.getCurrentSongId().getValue(); + Boolean playing = playbackViewModel.getIsPlaying().getValue(); + songHorizontalAdapter.setPlaybackState(id, playing != null && playing); + } + } + + private void setMediaBrowserListenableFuture() { + songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java new file mode 100644 index 0000000..a6167ee --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -0,0 +1,294 @@ +package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ComponentName; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ToggleButton; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.fragment.NavHostFragment; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.MediaCallback; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.util.ExternalAudioWriter; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel; +import com.cappielloantonio.tempo.viewmodel.HomeViewModel; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.snackbar.Snackbar; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@UnstableApi +public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { + private HomeViewModel homeViewModel; + private AlbumBottomSheetViewModel albumBottomSheetViewModel; + private AlbumID3 album; + + private TextView removeAllTextView; + private List currentAlbumTracks = Collections.emptyList(); + private List currentAlbumMediaItems = Collections.emptyList(); + + private ListenableFuture mediaBrowserListenableFuture; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.bottom_sheet_album_dialog, container, false); + + album = this.requireArguments().getParcelable(Constants.ALBUM_OBJECT); + + homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class); + albumBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(AlbumBottomSheetViewModel.class); + albumBottomSheetViewModel.setAlbum(album); + + init(view); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateRemoveAllVisibility); + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + private void init(View view) { + ImageView coverAlbum = view.findViewById(R.id.album_cover_image_view); + CustomGlideRequest.Builder + .from(requireContext(), albumBottomSheetViewModel.getAlbum().getCoverArtId(), CustomGlideRequest.ResourceType.Album) + .build() + .into(coverAlbum); + + TextView titleAlbum = view.findViewById(R.id.album_title_text_view); + titleAlbum.setText(albumBottomSheetViewModel.getAlbum().getName()); + titleAlbum.setSelected(true); + + TextView artistAlbum = view.findViewById(R.id.album_artist_text_view); + artistAlbum.setText(albumBottomSheetViewModel.getAlbum().getArtist()); + + ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); + favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null); + favoriteToggle.setOnClickListener(v -> { + albumBottomSheetViewModel.setFavorite(requireContext()); + }); + + TextView playRadio = view.findViewById(R.id.play_radio_text_view); + playRadio.setOnClickListener(v -> { + AlbumRepository albumRepository = new AlbumRepository(); + albumRepository.getInstantMix(album, 20, new MediaCallback() { + @Override + public void onError(Exception exception) { + exception.printStackTrace(); + } + + @Override + public void onLoadMedia(List media) { + MusicUtil.ratingFilter((ArrayList) media); + + if (!media.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList) media, 0); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + } + + dismissBottomSheet(); + } + }); + }); + + TextView playRandom = view.findViewById(R.id.play_random_text_view); + playRandom.setOnClickListener(v -> { + AlbumRepository albumRepository = new AlbumRepository(); + albumRepository.getAlbumTracks(album.getId()).observe(getViewLifecycleOwner(), songs -> { + Collections.shuffle(songs); + + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + + dismissBottomSheet(); + }); + }); + + TextView playNext = view.findViewById(R.id.play_next_text_view); + playNext.setOnClickListener(v -> albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { + MediaManager.enqueue(mediaBrowserListenableFuture, songs, true); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + + dismissBottomSheet(); + })); + + TextView addToQueue = view.findViewById(R.id.add_to_queue_text_view); + addToQueue.setOnClickListener(v -> albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { + MediaManager.enqueue(mediaBrowserListenableFuture, songs, false); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + + dismissBottomSheet(); + })); + + TextView downloadAll = view.findViewById(R.id.download_all_text_view); + albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { + List mediaItems = MappingUtil.mapDownloads(songs); + List downloads = songs.stream().map(Download::new).collect(Collectors.toList()); + + downloadAll.setOnClickListener(v -> { + if (Preferences.getDownloadDirectoryUri() == null) { + DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads); + } else { + songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child)); + } + dismissBottomSheet(); + }); + }); + + TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view); + addToPlaylist.setOnClickListener(v -> { + albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs)); + + PlaylistChooserDialog dialog = new PlaylistChooserDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + + dismissBottomSheet(); + }); + }); + + removeAllTextView = view.findViewById(R.id.remove_all_text_view); + albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> { + currentAlbumTracks = songs != null ? songs : Collections.emptyList(); + currentAlbumMediaItems = MappingUtil.mapDownloads(currentAlbumTracks); + + removeAllTextView.setOnClickListener(v -> { + if (Preferences.getDownloadDirectoryUri() == null) { + List downloads = currentAlbumTracks.stream().map(Download::new).collect(Collectors.toList()); + DownloadUtil.getDownloadTracker(requireContext()).remove(currentAlbumMediaItems, downloads); + } else { + currentAlbumTracks.forEach(ExternalAudioReader::delete); + } + dismissBottomSheet(); + }); + updateRemoveAllVisibility(); + }); + + TextView goToArtist = view.findViewById(R.id.go_to_artist_text_view); + goToArtist.setOnClickListener(v -> albumBottomSheetViewModel.getArtist().observe(getViewLifecycleOwner(), artist -> { + if (artist != null) { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artist); + NavHostFragment.findNavController(this).navigate(R.id.artistPageFragment, bundle); + } else { + Toast.makeText(requireContext(), getString(R.string.album_error_retrieving_artist), Toast.LENGTH_SHORT).show(); + } + + dismissBottomSheet(); + })); + + TextView share = view.findViewById(R.id.share_text_view); + share.setOnClickListener(v -> albumBottomSheetViewModel.shareAlbum().observe(getViewLifecycleOwner(), sharedAlbum -> { + if (sharedAlbum != null) { + ClipboardManager clipboardManager = (ClipboardManager) requireActivity().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = ClipData.newPlainText(getString(R.string.app_name), sharedAlbum.getUrl()); + clipboardManager.setPrimaryClip(clipData); + refreshShares(); + dismissBottomSheet(); + } else { + Toast.makeText(requireContext(), getString(R.string.share_unsupported_error), Toast.LENGTH_SHORT).show(); + dismissBottomSheet(); + } + })); + + share.setVisibility(Preferences.isSharingEnabled() ? View.VISIBLE : View.GONE); + } + + @Override + public void onClick(View v) { + dismissBottomSheet(); + } + + private void dismissBottomSheet() { + dismiss(); + } + + private void updateRemoveAllVisibility() { + if (removeAllTextView == null) { + return; + } + + if (currentAlbumTracks == null || currentAlbumTracks.isEmpty()) { + removeAllTextView.setVisibility(View.GONE); + return; + } + + if (Preferences.getDownloadDirectoryUri() == null) { + List mediaItems = currentAlbumMediaItems; + if (mediaItems == null || mediaItems.isEmpty()) { + removeAllTextView.setVisibility(View.GONE); + } else if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) { + removeAllTextView.setVisibility(View.VISIBLE); + } else { + removeAllTextView.setVisibility(View.GONE); + } + } else { + boolean hasLocal = currentAlbumTracks.stream().anyMatch(song -> ExternalAudioReader.getUri(song) != null); + removeAllTextView.setVisibility(hasLocal ? View.VISIBLE : View.GONE); + } + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + private void refreshShares() { + homeViewModel.refreshShares(requireActivity()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java new file mode 100644 index 0000000..9ec9b54 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ArtistBottomSheetDialog.java @@ -0,0 +1,142 @@ +package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog; + +import android.content.ComponentName; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ToggleButton; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.viewmodel.ArtistBottomSheetViewModel; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.common.util.concurrent.ListenableFuture; + +@UnstableApi +public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { + private static final String TAG = "AlbumBottomSheetDialog"; + + private ArtistBottomSheetViewModel artistBottomSheetViewModel; + private ArtistID3 artist; + + private ListenableFuture mediaBrowserListenableFuture; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.bottom_sheet_artist_dialog, container, false); + + artist = this.requireArguments().getParcelable(Constants.ARTIST_OBJECT); + + artistBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(ArtistBottomSheetViewModel.class); + artistBottomSheetViewModel.setArtist(artist); + + init(view); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + // TODO Use the viewmodel as a conduit and avoid direct calls + private void init(View view) { + ImageView coverArtist = view.findViewById(R.id.artist_cover_image_view); + CustomGlideRequest.Builder + .from(requireContext(), artistBottomSheetViewModel.getArtist().getCoverArtId(), CustomGlideRequest.ResourceType.Artist) + .build() + .into(coverArtist); + + TextView nameArtist = view.findViewById(R.id.song_title_text_view); + nameArtist.setText(artistBottomSheetViewModel.getArtist().getName()); + nameArtist.setSelected(true); + + ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); + favoriteToggle.setChecked(artistBottomSheetViewModel.getArtist().getStarred() != null); + favoriteToggle.setOnClickListener(v -> { + artistBottomSheetViewModel.setFavorite(requireContext()); + }); + + TextView playRadio = view.findViewById(R.id.play_radio_text_view); + playRadio.setOnClickListener(v -> { + ArtistRepository artistRepository = new ArtistRepository(); + + artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), songs -> { + // navidrome may return null for this + if (songs == null) + return; + MusicUtil.ratingFilter(songs); + + if (!songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + } + + dismissBottomSheet(); + }); + }); + + TextView playRandom = view.findViewById(R.id.play_random_text_view); + playRandom.setOnClickListener(v -> { + ArtistRepository artistRepository = new ArtistRepository(); + artistRepository.getRandomSong(artist, 50).observe(getViewLifecycleOwner(), songs -> { + MusicUtil.ratingFilter(songs); + + if (!songs.isEmpty()) { + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + + dismissBottomSheet(); + } else { + Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show(); + } + + dismissBottomSheet(); + }); + }); + } + + @Override + public void onClick(View v) { + dismissBottomSheet(); + } + + private void dismissBottomSheet() { + dismiss(); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/DownloadedBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/DownloadedBottomSheetDialog.java new file mode 100644 index 0000000..4a72064 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/DownloadedBottomSheetDialog.java @@ -0,0 +1,150 @@ +package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog; + +import android.content.ComponentName; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +@UnstableApi +public class DownloadedBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { + private List songs; + private String groupTitle; + private String groupSubtitle; + + private ListenableFuture mediaBrowserListenableFuture; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.bottom_sheet_downloaded_dialog, container, false); + + songs = this.requireArguments().getParcelableArrayList(Constants.DOWNLOAD_GROUP); + groupTitle = this.requireArguments().getString(Constants.DOWNLOAD_GROUP_TITLE); + groupSubtitle = this.requireArguments().getString(Constants.DOWNLOAD_GROUP_SUBTITLE); + + initUI(view); + init(view); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + private void initUI(View view) { + TextView playRandom = view.findViewById(R.id.play_random_text_view); + playRandom.setVisibility(songs.size() > 1 ? View.VISIBLE : View.GONE); + + TextView remove = view.findViewById(R.id.remove_all_text_view); + remove.setText(songs.size() > 1 ? getText(R.string.downloaded_bottom_sheet_remove_all) : getText(R.string.downloaded_bottom_sheet_remove)); + } + + private void init(View view) { + ImageView coverAlbum = view.findViewById(R.id.group_cover_image_view); + CustomGlideRequest.Builder.from(requireContext(), songs.get(new Random().nextInt(songs.size())).getCoverArtId(), CustomGlideRequest.ResourceType.Unknown).build().into(coverAlbum); + + TextView groupTitleView = view.findViewById(R.id.group_title_text_view); + groupTitleView.setText(this.groupTitle); + groupTitleView.setSelected(true); + + TextView groupSubtitleView = view.findViewById(R.id.group_subtitle_text_view); + groupSubtitleView.setText(this.groupSubtitle); + groupSubtitleView.setSelected(true); + + TextView playRandom = view.findViewById(R.id.play_random_text_view); + playRandom.setOnClickListener(v -> { + Collections.shuffle(songs); + + MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + + dismissBottomSheet(); + }); + + TextView playNext = view.findViewById(R.id.play_next_text_view); + playNext.setOnClickListener(v -> { + MediaManager.enqueue(mediaBrowserListenableFuture, songs, true); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + + dismissBottomSheet(); + }); + + TextView addToQueue = view.findViewById(R.id.add_to_queue_text_view); + addToQueue.setOnClickListener(v -> { + MediaManager.enqueue(mediaBrowserListenableFuture, songs, false); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + + dismissBottomSheet(); + }); + + TextView removeAll = view.findViewById(R.id.remove_all_text_view); + removeAll.setOnClickListener(v -> { + if (Preferences.getDownloadDirectoryUri() == null) { + List mediaItems = MappingUtil.mapDownloads(songs); + List downloads = songs.stream().map(Download::new).collect(Collectors.toList()); + DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads); + } else { + songs.forEach(ExternalAudioReader::delete); + } + + dismissBottomSheet(); + }); + } + + @Override + public void onClick(View v) { + dismissBottomSheet(); + } + + private void dismissBottomSheet() { + dismiss(); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/PodcastChannelBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/PodcastChannelBottomSheetDialog.java new file mode 100644 index 0000000..e69cbca --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/PodcastChannelBottomSheetDialog.java @@ -0,0 +1,96 @@ +package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog; + +import android.content.ComponentName; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.PodcastChannel; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.viewmodel.PodcastChannelBottomSheetViewModel; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.common.util.concurrent.ListenableFuture; + +@UnstableApi +public class PodcastChannelBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { + private PodcastChannelBottomSheetViewModel podcastChannelBottomSheetViewModel; + private PodcastChannel podcastChannel; + + private ListenableFuture mediaBrowserListenableFuture; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.bottom_sheet_podcast_channel_dialog, container, false); + + podcastChannel = requireArguments().getParcelable(Constants.PODCAST_CHANNEL_OBJECT); + + podcastChannelBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PodcastChannelBottomSheetViewModel.class); + podcastChannelBottomSheetViewModel.setPodcastChannel(podcastChannel); + + init(view); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + private void init(View view) { + ImageView coverPodcast = view.findViewById(R.id.podcast_cover_image_view); + + CustomGlideRequest.Builder + .from(requireContext(), podcastChannelBottomSheetViewModel.getPodcastChannel().getCoverArtId(), CustomGlideRequest.ResourceType.Podcast) + .build() + .into(coverPodcast); + + TextView titlePodcast = view.findViewById(R.id.podcast_title_text_view); + titlePodcast.setText(podcastChannelBottomSheetViewModel.getPodcastChannel().getTitle()); + + TextView delete = view.findViewById(R.id.delete_text_view); + delete.setOnClickListener(v -> { + podcastChannelBottomSheetViewModel.deletePodcastChannel(); + dismissBottomSheet(); + }); + } + + @Override + public void onClick(View v) { + dismissBottomSheet(); + } + + private void dismissBottomSheet() { + dismiss(); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/PodcastEpisodeBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/PodcastEpisodeBottomSheetDialog.java new file mode 100644 index 0000000..5d2a463 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/PodcastEpisodeBottomSheetDialog.java @@ -0,0 +1,155 @@ +package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog; + +import android.content.ComponentName; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.viewmodel.PodcastEpisodeBottomSheetViewModel; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.common.util.concurrent.ListenableFuture; + +@UnstableApi +public class PodcastEpisodeBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { + private PodcastEpisodeBottomSheetViewModel podcastEpisodeBottomSheetViewModel; + private PodcastEpisode podcastEpisode; + + private ListenableFuture mediaBrowserListenableFuture; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.bottom_sheet_podcast_episode_dialog, container, false); + + podcastEpisode = requireArguments().getParcelable(Constants.PODCAST_OBJECT); + + podcastEpisodeBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PodcastEpisodeBottomSheetViewModel.class); + podcastEpisodeBottomSheetViewModel.setPodcastEpisode(podcastEpisode); + + init(view); + + return view; + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + private void init(View view) { + ImageView coverPodcast = view.findViewById(R.id.podcast_cover_image_view); + + CustomGlideRequest.Builder + .from(requireContext(), podcastEpisodeBottomSheetViewModel.getPodcastEpisode().getCoverArtId(), CustomGlideRequest.ResourceType.Podcast) + .build() + .into(coverPodcast); + + TextView titlePodcast = view.findViewById(R.id.podcast_title_text_view); + titlePodcast.setText(podcastEpisodeBottomSheetViewModel.getPodcastEpisode().getTitle()); + + titlePodcast.setSelected(true); + + TextView playNext = view.findViewById(R.id.play_next_text_view); + playNext.setOnClickListener(v -> { + // TODO + // MediaManager.enqueue(mediaBrowserListenableFuture, podcast, true); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + dismissBottomSheet(); + }); + + TextView addToQueue = view.findViewById(R.id.add_to_queue_text_view); + addToQueue.setOnClickListener(v -> { + // TODO + // MediaManager.enqueue(mediaBrowserListenableFuture, podcast, false); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + dismissBottomSheet(); + }); + + TextView download = view.findViewById(R.id.download_text_view); + download.setOnClickListener(v -> { + // TODO + /* DownloadUtil.getDownloadTracker(requireContext()).download( + MappingUtil.mapMediaItem(podcast, false), + MappingUtil.mapDownload(podcast, null, null) + ); */ + dismissBottomSheet(); + }); + + TextView remove = view.findViewById(R.id.remove_text_view); + remove.setOnClickListener(v -> { + // TODO + /* DownloadUtil.getDownloadTracker(requireContext()).remove( + MappingUtil.mapMediaItem(podcast, false), + MappingUtil.mapDownload(podcast, null, null) + ); */ + dismissBottomSheet(); + }); + + initDownloadUI(download, remove); + + TextView delete = view.findViewById(R.id.delete_text_view); + delete.setOnClickListener(v -> { + podcastEpisodeBottomSheetViewModel.deletePodcastEpisode(); + dismissBottomSheet(); + }); + + TextView goToChannel = view.findViewById(R.id.go_to_channel_text_view); + goToChannel.setOnClickListener(v -> { + Toast.makeText(requireContext(), "Open the channel", Toast.LENGTH_SHORT).show(); + dismissBottomSheet(); + }); + } + + @Override + public void onClick(View v) { + dismissBottomSheet(); + } + + private void dismissBottomSheet() { + dismiss(); + } + + private void initDownloadUI(TextView download, TextView remove) { + // TODO + /* if (DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(MappingUtil.mapMediaItem(podcast, false))) { + download.setVisibility(View.GONE); + remove.setVisibility(View.VISIBLE); + } else { + download.setVisibility(View.VISIBLE); + remove.setVisibility(View.GONE); + } */ + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ShareBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ShareBottomSheetDialog.java new file mode 100644 index 0000000..90612f6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/ShareBottomSheetDialog.java @@ -0,0 +1,120 @@ +package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.subsonic.models.Share; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.ui.dialog.ShareUpdateDialog; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.UIUtil; +import com.cappielloantonio.tempo.viewmodel.HomeViewModel; +import com.cappielloantonio.tempo.viewmodel.ShareBottomSheetViewModel; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; + +import java.util.List; + +@UnstableApi +public class ShareBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { + + private HomeViewModel homeViewModel; + private ShareBottomSheetViewModel shareBottomSheetViewModel; + private Share share; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.bottom_sheet_share_dialog, container, false); + + share = this.requireArguments().getParcelable(Constants.SHARE_OBJECT); + + homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class); + shareBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(ShareBottomSheetViewModel.class); + shareBottomSheetViewModel.setShare(share); + + init(view); + + return view; + } + + private void init(View view) { + ImageView shareCover = view.findViewById(R.id.share_cover_image_view); + + String coverArtId = null; + List entries = shareBottomSheetViewModel.getShare().getEntries(); + + if (entries != null && !entries.isEmpty()) { + coverArtId = entries.get(0).getCoverArtId(); + } + + CustomGlideRequest.Builder + .from(requireContext(), coverArtId, CustomGlideRequest.ResourceType.Unknown) + .build() + .into(shareCover); + + TextView shareTitle = view.findViewById(R.id.share_title_text_view); + shareTitle.setText(shareBottomSheetViewModel.getShare().getDescription()); + shareTitle.setSelected(true); + + TextView shareSubtitle = view.findViewById(R.id.share_subtitle_text_view); + shareSubtitle.setText(requireContext().getString(R.string.share_subtitle_item, UIUtil.getReadableDate(share.getExpires()))); + shareSubtitle.setSelected(true); + + TextView copyLink = view.findViewById(R.id.copy_link_text_view); + copyLink.setOnClickListener(v -> { + ClipboardManager clipboardManager = (ClipboardManager) requireActivity().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = ClipData.newPlainText(getString(R.string.app_name), shareBottomSheetViewModel.getShare().getUrl()); + clipboardManager.setPrimaryClip(clipData); + dismissBottomSheet(); + }); + + TextView updateShare = view.findViewById(R.id.update_share_preferences_text_view); + updateShare.setOnClickListener(v -> { + // refreshShares(); + showUpdateShareDialog(); + dismissBottomSheet(); + }); + + TextView deleteShare = view.findViewById(R.id.delete_share_text_view); + deleteShare.setOnClickListener(v -> { + deleteShare(); + refreshShares(); + dismissBottomSheet(); + }); + } + + @Override + public void onClick(View v) { + dismissBottomSheet(); + } + + private void dismissBottomSheet() { + dismiss(); + } + + private void showUpdateShareDialog() { + ShareUpdateDialog dialog = new ShareUpdateDialog(); + dialog.show(requireActivity().getSupportFragmentManager(), null); + } + + private void refreshShares() { + homeViewModel.refreshShares(getParentFragment()); + } + + private void deleteShare() { + shareBottomSheetViewModel.deleteShare(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java new file mode 100644 index 0000000..39ba439 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java @@ -0,0 +1,400 @@ +package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ComponentName; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ToggleButton; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; +import androidx.navigation.fragment.NavHostFragment; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog; +import com.cappielloantonio.tempo.ui.dialog.RatingDialog; +import com.cappielloantonio.tempo.util.AssetLinkUtil; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.viewmodel.HomeViewModel; +import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.common.util.concurrent.ListenableFuture; + +import android.content.Intent; +import androidx.media3.common.MediaItem; +import com.cappielloantonio.tempo.util.ExternalAudioWriter; + +import java.util.ArrayList; +import java.util.Collections; + +@UnstableApi +public class SongBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { + private HomeViewModel homeViewModel; + private SongBottomSheetViewModel songBottomSheetViewModel; + private Child song; + + private TextView downloadButton; + private TextView removeButton; + private ChipGroup assetLinkChipGroup; + private Chip songLinkChip; + private Chip albumLinkChip; + private Chip artistLinkChip; + private AssetLinkUtil.AssetLink currentSongLink; + private AssetLinkUtil.AssetLink currentAlbumLink; + private AssetLinkUtil.AssetLink currentArtistLink; + + private ListenableFuture mediaBrowserListenableFuture; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.bottom_sheet_song_dialog, container, false); + + song = requireArguments().getParcelable(Constants.TRACK_OBJECT); + + homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class); + songBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(SongBottomSheetViewModel.class); + songBottomSheetViewModel.setSong(song); + + init(view); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateDownloadButtons); + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + private void init(View view) { + ImageView coverSong = view.findViewById(R.id.song_cover_image_view); + CustomGlideRequest.Builder + .from(requireContext(), songBottomSheetViewModel.getSong().getCoverArtId(), CustomGlideRequest.ResourceType.Song) + .build() + .into(coverSong); + + TextView titleSong = view.findViewById(R.id.song_title_text_view); + titleSong.setText(songBottomSheetViewModel.getSong().getTitle()); + + titleSong.setSelected(true); + + TextView artistSong = view.findViewById(R.id.song_artist_text_view); + artistSong.setText(songBottomSheetViewModel.getSong().getArtist()); + + initAssetLinkChips(view); + bindAssetLinkView(coverSong, currentSongLink); + bindAssetLinkView(titleSong, currentSongLink); + bindAssetLinkView(artistSong, currentArtistLink != null ? currentArtistLink : currentSongLink); + + ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); + favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null); + favoriteToggle.setOnClickListener(v -> { + songBottomSheetViewModel.setFavorite(requireContext()); + }); + favoriteToggle.setOnLongClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.TRACK_OBJECT, song); + + RatingDialog dialog = new RatingDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + + dismissBottomSheet(); + return true; + }); + + TextView playRadio = view.findViewById(R.id.play_radio_text_view); + playRadio.setOnClickListener(v -> { + MediaManager.startQueue(mediaBrowserListenableFuture, song); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + + songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> { + MusicUtil.ratingFilter(songs); + + if (songs == null) { + dismissBottomSheet(); + return; + } + + if (!songs.isEmpty()) { + MediaManager.enqueue(mediaBrowserListenableFuture, songs, true); + dismissBottomSheet(); + } + }); + }); + + TextView playNext = view.findViewById(R.id.play_next_text_view); + playNext.setOnClickListener(v -> { + MediaManager.enqueue(mediaBrowserListenableFuture, song, true); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + dismissBottomSheet(); + }); + + TextView addToQueue = view.findViewById(R.id.add_to_queue_text_view); + addToQueue.setOnClickListener(v -> { + MediaManager.enqueue(mediaBrowserListenableFuture, song, false); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + dismissBottomSheet(); + }); + + TextView rate = view.findViewById(R.id.rate_text_view); + rate.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.TRACK_OBJECT, song); + + RatingDialog dialog = new RatingDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + + dismissBottomSheet(); + }); + + downloadButton = view.findViewById(R.id.download_text_view); + downloadButton.setOnClickListener(v -> { + if (Preferences.getDownloadDirectoryUri() == null) { + DownloadUtil.getDownloadTracker(requireContext()).download( + MappingUtil.mapDownload(song), + new Download(song) + ); + } else { + ExternalAudioWriter.downloadToUserDirectory(requireContext(), song); + } + dismissBottomSheet(); + }); + + removeButton = view.findViewById(R.id.remove_text_view); + removeButton.setOnClickListener(v -> { + if (Preferences.getDownloadDirectoryUri() == null) { + DownloadUtil.getDownloadTracker(requireContext()).remove( + MappingUtil.mapDownload(song), + new Download(song) + ); + } else { + ExternalAudioReader.delete(song); + } + dismissBottomSheet(); + }); + + updateDownloadButtons(); + + TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view); + addToPlaylist.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(Collections.singletonList(song))); + + PlaylistChooserDialog dialog = new PlaylistChooserDialog(); + dialog.setArguments(bundle); + dialog.show(requireActivity().getSupportFragmentManager(), null); + + dismissBottomSheet(); + }); + + TextView goToAlbum = view.findViewById(R.id.go_to_album_text_view); + goToAlbum.setOnClickListener(v -> songBottomSheetViewModel.getAlbum().observe(getViewLifecycleOwner(), album -> { + if (album != null) { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, album); + NavHostFragment.findNavController(this).navigate(R.id.albumPageFragment, bundle); + } else + Toast.makeText(requireContext(), getString(R.string.song_bottom_sheet_error_retrieving_album), Toast.LENGTH_SHORT).show(); + + dismissBottomSheet(); + })); + + goToAlbum.setVisibility(songBottomSheetViewModel.getSong().getAlbumId() != null ? View.VISIBLE : View.GONE); + + TextView goToArtist = view.findViewById(R.id.go_to_artist_text_view); + goToArtist.setOnClickListener(v -> songBottomSheetViewModel.getArtist().observe(getViewLifecycleOwner(), artist -> { + if (artist != null) { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artist); + NavHostFragment.findNavController(this).navigate(R.id.artistPageFragment, bundle); + } else + Toast.makeText(requireContext(), getString(R.string.song_bottom_sheet_error_retrieving_artist), Toast.LENGTH_SHORT).show(); + + dismissBottomSheet(); + })); + + goToArtist.setVisibility(songBottomSheetViewModel.getSong().getArtistId() != null ? View.VISIBLE : View.GONE); + + TextView share = view.findViewById(R.id.share_text_view); + share.setOnClickListener(v -> songBottomSheetViewModel.shareTrack().observe(getViewLifecycleOwner(), sharedTrack -> { + if (sharedTrack != null) { + ClipboardManager clipboardManager = (ClipboardManager) requireActivity().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = ClipData.newPlainText(getString(R.string.app_name), sharedTrack.getUrl()); + clipboardManager.setPrimaryClip(clipData); + refreshShares(); + dismissBottomSheet(); + } else { + Toast.makeText(requireContext(), getString(R.string.share_unsupported_error), Toast.LENGTH_SHORT).show(); + dismissBottomSheet(); + } + })); + + share.setVisibility(Preferences.isSharingEnabled() ? View.VISIBLE : View.GONE); + } + + @Override + public void onClick(View v) { + dismissBottomSheet(); + } + + private void dismissBottomSheet() { + dismiss(); + } + + private void updateDownloadButtons() { + if (downloadButton == null || removeButton == null) { + return; + } + + if (Preferences.getDownloadDirectoryUri() == null) { + boolean downloaded = DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId()); + downloadButton.setVisibility(downloaded ? View.GONE : View.VISIBLE); + removeButton.setVisibility(downloaded ? View.VISIBLE : View.GONE); + } else { + boolean hasLocal = ExternalAudioReader.getUri(song) != null; + downloadButton.setVisibility(hasLocal ? View.GONE : View.VISIBLE); + removeButton.setVisibility(hasLocal ? View.VISIBLE : View.GONE); + } + } + + private void initAssetLinkChips(View root) { + assetLinkChipGroup = root.findViewById(R.id.asset_link_chip_group); + songLinkChip = root.findViewById(R.id.asset_link_song_chip); + albumLinkChip = root.findViewById(R.id.asset_link_album_chip); + artistLinkChip = root.findViewById(R.id.asset_link_artist_chip); + + currentSongLink = bindAssetLinkChip(songLinkChip, AssetLinkUtil.TYPE_SONG, song.getId()); + currentAlbumLink = bindAssetLinkChip(albumLinkChip, AssetLinkUtil.TYPE_ALBUM, song.getAlbumId()); + currentArtistLink = bindAssetLinkChip(artistLinkChip, AssetLinkUtil.TYPE_ARTIST, song.getArtistId()); + syncAssetLinkGroupVisibility(); + } + + private AssetLinkUtil.AssetLink bindAssetLinkChip(@Nullable Chip chip, String type, @Nullable String id) { + if (chip == null) return null; + if (id == null || id.isEmpty()) { + clearAssetLinkChip(chip); + return null; + } + + String label = getString(AssetLinkUtil.getLabelRes(type)); + AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.buildAssetLink(type, id); + if (assetLink == null) { + clearAssetLinkChip(chip); + return null; + } + + chip.setText(getString(R.string.asset_link_chip_text, label, assetLink.id)); + chip.setVisibility(View.VISIBLE); + + chip.setOnClickListener(v -> { + if (assetLink != null) { + ((MainActivity) requireActivity()).openAssetLink(assetLink); + } + }); + + chip.setOnLongClickListener(v -> { + if (assetLink != null) { + AssetLinkUtil.copyToClipboard(requireContext(), assetLink); + Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show(); + } + return true; + }); + + return assetLink; + } + + private void clearAssetLinkChip(@Nullable Chip chip) { + if (chip == null) return; + chip.setVisibility(View.GONE); + chip.setText(""); + chip.setOnClickListener(null); + chip.setOnLongClickListener(null); + } + + private void syncAssetLinkGroupVisibility() { + if (assetLinkChipGroup == null) return; + boolean hasVisible = false; + for (int i = 0; i < assetLinkChipGroup.getChildCount(); i++) { + View child = assetLinkChipGroup.getChildAt(i); + if (child.getVisibility() == View.VISIBLE) { + hasVisible = true; + break; + } + } + assetLinkChipGroup.setVisibility(hasVisible ? View.VISIBLE : View.GONE); + } + + private void bindAssetLinkView(@Nullable View view, @Nullable AssetLinkUtil.AssetLink assetLink) { + if (view == null) return; + if (assetLink == null) { + AssetLinkUtil.clearLinkAppearance(view); + view.setOnClickListener(null); + view.setOnLongClickListener(null); + view.setClickable(false); + view.setLongClickable(false); + return; + } + + view.setClickable(true); + view.setLongClickable(true); + AssetLinkUtil.applyLinkAppearance(view); + view.setOnClickListener(v -> ((MainActivity) requireActivity()).openAssetLink(assetLink, !AssetLinkUtil.TYPE_SONG.equals(assetLink.type))); + view.setOnLongClickListener(v -> { + AssetLinkUtil.copyToClipboard(requireContext(), assetLink); + Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show(); + return true; + }); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + + private void refreshShares() { + homeViewModel.refreshShares(requireActivity()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/pager/HomePager.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/pager/HomePager.java new file mode 100644 index 0000000..ecc79db --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/pager/HomePager.java @@ -0,0 +1,48 @@ +package com.cappielloantonio.tempo.ui.fragment.pager; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.Fragment; +import androidx.media3.common.util.UnstableApi; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import java.util.ArrayList; +import java.util.List; + +@OptIn(markerClass = UnstableApi.class) +public class HomePager extends FragmentStateAdapter { + private static final String TAG = "HomePager"; + + private final List fragments = new ArrayList<>(); + private final List titles = new ArrayList<>(); + private final List icons = new ArrayList<>(); + + public HomePager(@NonNull Fragment fragment) { + super(fragment); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + return fragments.get(position); + } + + @Override + public int getItemCount() { + return fragments.size(); + } + + public void addFragment(Fragment fragment, String title, int drawable) { + fragments.add(fragment); + titles.add(title); + icons.add(drawable); + } + + public String getPageTitle(int position) { + return titles.get(position); + } + + public Integer getPageIcon(int position) { + return icons.get(position); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/pager/PlayerControllerHorizontalPager.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/pager/PlayerControllerHorizontalPager.java new file mode 100644 index 0000000..4487a29 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/pager/PlayerControllerHorizontalPager.java @@ -0,0 +1,37 @@ +package com.cappielloantonio.tempo.ui.fragment.pager; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.Fragment; +import androidx.media3.common.util.UnstableApi; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import com.cappielloantonio.tempo.ui.fragment.PlayerCoverFragment; +import com.cappielloantonio.tempo.ui.fragment.PlayerLyricsFragment; + +@OptIn(markerClass = UnstableApi.class) +public class PlayerControllerHorizontalPager extends FragmentStateAdapter { + private static final String TAG = "PlayerControllerHorizontalPager"; + + public PlayerControllerHorizontalPager(@NonNull Fragment fragment) { + super(fragment); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + switch (position) { + case 0: + return new PlayerCoverFragment(); + case 1: + return new PlayerLyricsFragment(); + } + + return new PlayerCoverFragment(); + } + + @Override + public int getItemCount() { + return 2; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/pager/PlayerControllerVerticalPager.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/pager/PlayerControllerVerticalPager.java new file mode 100644 index 0000000..cc21b95 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/pager/PlayerControllerVerticalPager.java @@ -0,0 +1,51 @@ +package com.cappielloantonio.tempo.ui.fragment.pager; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.Fragment; +import androidx.media3.common.util.UnstableApi; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import com.cappielloantonio.tempo.ui.fragment.PlayerControllerFragment; +import com.cappielloantonio.tempo.ui.fragment.PlayerQueueFragment; + +import java.util.HashMap; + +@OptIn(markerClass = UnstableApi.class) +public class PlayerControllerVerticalPager extends FragmentStateAdapter { + private final HashMap maps; + + public PlayerControllerVerticalPager(@NonNull Fragment fragment) { + super(fragment); + + this.maps = new HashMap<>(); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + switch (position) { + case 0: + Fragment playerControllerFragment = new PlayerControllerFragment(); + maps.put(position, playerControllerFragment); + return playerControllerFragment; + case 1: + Fragment playerQueueFragment = new PlayerQueueFragment(); + maps.put(position, playerQueueFragment); + return playerQueueFragment; + } + + Fragment playerControllerFragment = new PlayerControllerFragment(); + maps.put(position, playerControllerFragment); + return playerControllerFragment; + } + + @Override + public int getItemCount() { + return 2; + } + + public Fragment getRegisteredFragment(int position) { + return maps.get(position); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkNavigator.java b/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkNavigator.java new file mode 100644 index 0000000..9d3ba96 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkNavigator.java @@ -0,0 +1,188 @@ +package com.cappielloantonio.tempo.util; + +import android.os.Bundle; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavController; +import androidx.navigation.NavOptions; + +import com.cappielloantonio.tempo.BuildConfig; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.PlaylistRepository; +import com.cappielloantonio.tempo.repository.SongRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Playlist; +import com.cappielloantonio.tempo.subsonic.models.Genre; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog.SongBottomSheetDialog; +import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel; + +public final class AssetLinkNavigator { + private final MainActivity activity; + private final SongRepository songRepository = new SongRepository(); + private final AlbumRepository albumRepository = new AlbumRepository(); + private final ArtistRepository artistRepository = new ArtistRepository(); + private final PlaylistRepository playlistRepository = new PlaylistRepository(); + + public AssetLinkNavigator(@NonNull MainActivity activity) { + this.activity = activity; + } + + public void open(@Nullable AssetLinkUtil.AssetLink assetLink) { + if (assetLink == null) { + return; + } + switch (assetLink.type) { + case AssetLinkUtil.TYPE_SONG: + openSong(assetLink.id); + break; + case AssetLinkUtil.TYPE_ALBUM: + openAlbum(assetLink.id); + break; + case AssetLinkUtil.TYPE_ARTIST: + openArtist(assetLink.id); + break; + case AssetLinkUtil.TYPE_PLAYLIST: + openPlaylist(assetLink.id); + break; + case AssetLinkUtil.TYPE_GENRE: + openGenre(assetLink.id); + break; + case AssetLinkUtil.TYPE_YEAR: + openYear(assetLink.id); + break; + default: + Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show(); + break; + } + } + + private void openSong(@NonNull String id) { + MutableLiveData liveData = songRepository.getSong(id); + Observer observer = new Observer() { + @Override + public void onChanged(Child child) { + liveData.removeObserver(this); + if (child == null) { + Toast.makeText(activity, R.string.asset_link_error_song, Toast.LENGTH_SHORT).show(); + return; + } + SongBottomSheetViewModel viewModel = new ViewModelProvider(activity).get(SongBottomSheetViewModel.class); + viewModel.setSong(child); + SongBottomSheetDialog dialog = new SongBottomSheetDialog(); + Bundle args = new Bundle(); + args.putParcelable(Constants.TRACK_OBJECT, child); + dialog.setArguments(args); + dialog.show(activity.getSupportFragmentManager(), null); + } + }; + liveData.observe(activity, observer); + } + + private void openAlbum(@NonNull String id) { + MutableLiveData liveData = albumRepository.getAlbum(id); + Observer observer = new Observer() { + @Override + public void onChanged(AlbumID3 album) { + liveData.removeObserver(this); + if (album == null) { + Toast.makeText(activity, R.string.asset_link_error_album, Toast.LENGTH_SHORT).show(); + return; + } + Bundle args = new Bundle(); + args.putParcelable(Constants.ALBUM_OBJECT, album); + navigateSafely(R.id.albumPageFragment, args); + } + }; + liveData.observe(activity, observer); + } + + private void openArtist(@NonNull String id) { + MutableLiveData liveData = artistRepository.getArtist(id); + Observer observer = new Observer() { + @Override + public void onChanged(ArtistID3 artist) { + liveData.removeObserver(this); + if (artist == null) { + Toast.makeText(activity, R.string.asset_link_error_artist, Toast.LENGTH_SHORT).show(); + return; + } + Bundle args = new Bundle(); + args.putParcelable(Constants.ARTIST_OBJECT, artist); + navigateSafely(R.id.artistPageFragment, args); + } + }; + liveData.observe(activity, observer); + } + + private void openPlaylist(@NonNull String id) { + MutableLiveData liveData = playlistRepository.getPlaylist(id); + Observer observer = new Observer() { + @Override + public void onChanged(Playlist playlist) { + liveData.removeObserver(this); + if (playlist == null) { + Toast.makeText(activity, R.string.asset_link_error_playlist, Toast.LENGTH_SHORT).show(); + return; + } + Bundle args = new Bundle(); + args.putParcelable(Constants.PLAYLIST_OBJECT, playlist); + navigateSafely(R.id.playlistPageFragment, args); + } + }; + liveData.observe(activity, observer); + } + + private void openGenre(@NonNull String genreName) { + String trimmed = genreName.trim(); + if (trimmed.isEmpty()) { + Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show(); + return; + } + + Genre genre = new Genre(); + genre.setGenre(trimmed); + genre.setSongCount(0); + genre.setAlbumCount(0); + Bundle args = new Bundle(); + args.putParcelable(Constants.GENRE_OBJECT, genre); + args.putString(Constants.MEDIA_BY_GENRE, Constants.MEDIA_BY_GENRE); + navigateSafely(R.id.songListPageFragment, args); + } + + private void openYear(@NonNull String yearValue) { + try { + int year = Integer.parseInt(yearValue.trim()); + Bundle args = new Bundle(); + args.putInt("year_object", year); + args.putString(Constants.MEDIA_BY_YEAR, Constants.MEDIA_BY_YEAR); + navigateSafely(R.id.songListPageFragment, args); + } catch (NumberFormatException ex) { + Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show(); + } + } + + private void navigateSafely(int destinationId, @Nullable Bundle args) { + activity.runOnUiThread(() -> { + NavController navController = activity.navController; + if (navController == null) { + return; + } + if (navController.getCurrentDestination() != null + && navController.getCurrentDestination().getId() == destinationId) { + navController.navigate(destinationId, args, new NavOptions.Builder().setLaunchSingleTop(true).build()); + } else { + navController.navigate(destinationId, args); + } + }); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkUtil.java new file mode 100644 index 0000000..1609a88 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkUtil.java @@ -0,0 +1,188 @@ +package com.cappielloantonio.tempo.util; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; + +import com.cappielloantonio.tempo.R; + +import java.util.Objects; + +import com.google.android.material.color.MaterialColors; + +public final class AssetLinkUtil { + public static final String SCHEME = "tempo"; + public static final String HOST_ASSET = "asset"; + + public static final String TYPE_SONG = "song"; + public static final String TYPE_ALBUM = "album"; + public static final String TYPE_ARTIST = "artist"; + public static final String TYPE_PLAYLIST = "playlist"; + public static final String TYPE_GENRE = "genre"; + public static final String TYPE_YEAR = "year"; + + private AssetLinkUtil() { + } + + @Nullable + public static AssetLink parse(@Nullable Intent intent) { + if (intent == null) return null; + return parse(intent.getData()); + } + + @Nullable + public static AssetLink parse(@Nullable Uri uri) { + if (uri == null) { + return null; + } + + if (!SCHEME.equalsIgnoreCase(uri.getScheme())) { + return null; + } + + String host = uri.getHost(); + if (!HOST_ASSET.equalsIgnoreCase(host)) { + return null; + } + + if (uri.getPathSegments().size() < 2) { + return null; + } + + String type = uri.getPathSegments().get(0); + String id = uri.getPathSegments().get(1); + if (TextUtils.isEmpty(type) || TextUtils.isEmpty(id)) { + return null; + } + + if (!isSupportedType(type)) { + return null; + } + + return new AssetLink(type, id, uri); + } + + public static boolean isSupportedType(@Nullable String type) { + if (type == null) return false; + switch (type) { + case TYPE_SONG: + case TYPE_ALBUM: + case TYPE_ARTIST: + case TYPE_PLAYLIST: + case TYPE_GENRE: + case TYPE_YEAR: + return true; + default: + return false; + } + } + + @NonNull + public static Uri buildUri(@NonNull String type, @NonNull String id) { + return new Uri.Builder() + .scheme(SCHEME) + .authority(HOST_ASSET) + .appendPath(type) + .appendPath(id) + .build(); + } + + @Nullable + public static String buildLink(@Nullable String type, @Nullable String id) { + if (TextUtils.isEmpty(type) || TextUtils.isEmpty(id) || !isSupportedType(type)) { + return null; + } + return buildUri(Objects.requireNonNull(type), Objects.requireNonNull(id)).toString(); + } + + @Nullable + public static AssetLink buildAssetLink(@Nullable String type, @Nullable String id) { + String link = buildLink(type, id); + return parseLinkString(link); + } + + @Nullable + public static AssetLink parseLinkString(@Nullable String link) { + if (TextUtils.isEmpty(link)) { + return null; + } + return parse(Uri.parse(link)); + } + + public static void copyToClipboard(@NonNull Context context, @NonNull AssetLink assetLink) { + ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboardManager == null) { + return; + } + ClipData clipData = ClipData.newPlainText(context.getString(R.string.asset_link_clipboard_label), assetLink.uri.toString()); + clipboardManager.setPrimaryClip(clipData); + } + + @StringRes + public static int getLabelRes(@NonNull String type) { + switch (type) { + case TYPE_SONG: + return R.string.asset_link_label_song; + case TYPE_ALBUM: + return R.string.asset_link_label_album; + case TYPE_ARTIST: + return R.string.asset_link_label_artist; + case TYPE_PLAYLIST: + return R.string.asset_link_label_playlist; + case TYPE_GENRE: + return R.string.asset_link_label_genre; + case TYPE_YEAR: + return R.string.asset_link_label_year; + default: + return R.string.asset_link_label_unknown; + } + } + + public static void applyLinkAppearance(@NonNull View view) { + if (view instanceof TextView) { + TextView textView = (TextView) view; + if (textView.getTag(R.id.tag_link_original_color) == null) { + textView.setTag(R.id.tag_link_original_color, textView.getCurrentTextColor()); + } + int accent = MaterialColors.getColor(view, com.google.android.material.R.attr.colorPrimary, + ContextCompat.getColor(view.getContext(), android.R.color.holo_blue_light)); + textView.setTextColor(accent); + } + } + + public static void clearLinkAppearance(@NonNull View view) { + if (view instanceof TextView) { + TextView textView = (TextView) view; + Object original = textView.getTag(R.id.tag_link_original_color); + if (original instanceof Integer) { + textView.setTextColor((Integer) original); + } else { + int defaultColor = MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnSurface, + ContextCompat.getColor(view.getContext(), android.R.color.primary_text_light)); + textView.setTextColor(defaultColor); + } + } + } + + public static final class AssetLink { + public final String type; + public final String id; + public final Uri uri; + + AssetLink(@NonNull String type, @NonNull String id, @NonNull Uri uri) { + this.type = type; + this.id = id; + this.uri = uri; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt new file mode 100644 index 0000000..c6a4e3a --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -0,0 +1,136 @@ +package com.cappielloantonio.tempo.util + +object Constants { + const val SHARED_PREF_KEY = "play-shared-preferences" + + const val ITEM_POSITION = "ITEM_POSITION" + + const val TRACK_OBJECT = "TRACK_OBJECT" + const val TRACKS_OBJECT = "TRACKS_OBJECT" + const val ALBUM_OBJECT = "ALBUM_OBJECT" + const val ARTIST_OBJECT = "ARTIST_OBJECT" + const val GENRE_OBJECT = "GENRE_OBJECT" + const val PLAYLIST_OBJECT = "PLAYLIST_OBJECT" + const val PODCAST_OBJECT = "PODCAST_OBJECT" + const val PODCAST_CHANNEL_OBJECT = "PODCAST_CHANNEL_OBJECT" + const val INTERNET_RADIO_STATION_OBJECT = "INTERNET_RADIO_STATION_OBJECT" + const val MUSIC_FOLDER_OBJECT = "MUSIC_FOLDER_OBJECT" + const val MUSIC_DIRECTORY_OBJECT = "MUSIC_DIRECTORY_OBJECT" + const val MUSIC_INDEX_OBJECT = "MUSIC_DIRECTORY_OBJECT" + const val MUSIC_DIRECTORY_ID = "MUSIC_DIRECTORY_ID" + + const val ALBUM_RECENTLY_PLAYED = "ALBUM_RECENTLY_PLAYED" + const val ALBUM_MOST_PLAYED = "ALBUM_MOST_PLAYED" + const val ALBUM_RECENTLY_ADDED = "ALBUM_RECENTLY_ADDED" + const val ALBUM_DOWNLOADED = "ALBUM_DOWNLOADED" + const val ALBUM_STARRED = "ALBUM_STARRED" + const val ALBUM_FROM_ARTIST = "ALBUM_FROM_ARTIST" + const val ALBUM_NEW_RELEASES = "ALBUM_NEW_RELEASES" + const val ALBUM_ORDER_BY_NAME = "ALBUM_ORDER_BY_NAME" + const val ALBUM_ORDER_BY_ARTIST = "ALBUM_ORDER_BY_ARTIST" + const val ALBUM_ORDER_BY_YEAR = "ALBUM_ORDER_BY_YEAR" + const val ALBUM_ORDER_BY_RANDOM = "ALBUM_ORDER_BY_RANDOM" + const val ALBUM_ORDER_BY_RECENTLY_ADDED = "ALBUM_ORDER_BY_RECENTLY_ADDED" + const val ALBUM_ORDER_BY_RECENTLY_PLAYED = "ALBUM_ORDER_BY_RECENTLY_PLAYED" + const val ALBUM_ORDER_BY_MOST_PLAYED = "ALBUM_ORDER_BY_MOST_PLAYED" + const val ALBUM_ORDER_BY_MOST_RECENTLY_STARRED = "ALBUM_ORDER_BY_MOST_RECENTLY_STARRED" + const val ALBUM_ORDER_BY_LEAST_RECENTLY_STARRED = "ALBUM_ORDER_BY_LEAST_RECENTLY_STARRED" + + const val ARTIST_DOWNLOADED = "ARTIST_DOWNLOADED" + const val ARTIST_STARRED = "ARTIST_STARRED" + const val ARTIST_ORDER_BY_NAME = "ARTIST_ORDER_BY_NAME" + const val ARTIST_ORDER_BY_RANDOM = "ARTIST_ORDER_BY_RANDOM" + const val ARTIST_ORDER_BY_ALBUM_COUNT = "ARTIST_ORDER_BY_ALBUM_COUNT" + const val ARTIST_ORDER_BY_MOST_RECENTLY_STARRED = "ARTIST_ORDER_BY_MOST_RECENTLY_STARRED" + const val ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED = "ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED" + + const val GENRE_ORDER_BY_NAME = "GENRE_ORDER_BY_NAME" + const val GENRE_ORDER_BY_RANDOM = "GENRE_ORDER_BY_RANDOM" + + const val PLAYLIST_ALL = "ALL" + const val PLAYLIST_DOWNLOADED = "DOWNLOADED" + const val PLAYLIST_ORDER_BY_NAME = "ORDER_BY_NAME" + const val PLAYLIST_ORDER_BY_RANDOM = "ORDER_BY_RANDOM" + + const val PODCAST_FILTER_BY_DOWNLOAD = "PODCAST_FILTER_BY_DOWNLOAD" + const val PODCAST_FILTER_BY_ALL = "PODCAST_FILTER_BY_ALL" + + const val MEDIA_TYPE_MUSIC = "music" + const val MEDIA_TYPE_PODCAST = "podcast" + const val MEDIA_TYPE_AUDIOBOOK = "audiobook" + const val MEDIA_TYPE_VIDEO = "video" + const val MEDIA_TYPE_RADIO = "radio" + + const val MEDIA_PLAYBACK_SPEED_080 = 0.8f + const val MEDIA_PLAYBACK_SPEED_100 = 1.0f + const val MEDIA_PLAYBACK_SPEED_125 = 1.25f + const val MEDIA_PLAYBACK_SPEED_150 = 1.50f + const val MEDIA_PLAYBACK_SPEED_175 = 1.75f + const val MEDIA_PLAYBACK_SPEED_200 = 2.0f + + const val MEDIA_RECENTLY_PLAYED = "MEDIA_RECENTLY_PLAYED" + const val MEDIA_MOST_PLAYED = "MEDIA_MOST_PLAYED" + const val MEDIA_RECENTLY_ADDED = "MEDIA_RECENTLY_ADDED" + const val MEDIA_BY_GENRE = "MEDIA_BY_GENRE" + const val MEDIA_BY_GENRES = "MEDIA_BY_GENRES" + const val MEDIA_BY_ARTIST = "MEDIA_BY_ARTIST" + const val MEDIA_BY_YEAR = "MEDIA_BY_YEAR" + const val MEDIA_STARRED = "MEDIA_STARRED" + const val MEDIA_DOWNLOADED = "MEDIA_DOWNLOADED" + const val MEDIA_FROM_ALBUM = "MEDIA_FROM_ALBUM" + const val MEDIA_MIX = "MEDIA_MIX" + const val MEDIA_CHRONOLOGY = "MEDIA_CHRONOLOGY" + const val MEDIA_BEST_OF = "MEDIA_BEST_OF" + const val MEDIA_BY_TITLE = "MEDIA_BY_TITLE" + const val MEDIA_MOST_RECENTLY_STARRED = "MEDIA_MOST_RECENTLY_STARRED" + const val MEDIA_LEAST_RECENTLY_STARRED = "MEDIA_LEAST_RECENTLY_STARRED" + + const val DOWNLOAD_URI = "rest/download" + const val ACTION_PLAY_EXTERNAL_DOWNLOAD = "com.cappielloantonio.tempo.action.PLAY_EXTERNAL_DOWNLOAD" + const val EXTRA_DOWNLOAD_URI = "EXTRA_DOWNLOAD_URI" + const val EXTRA_DOWNLOAD_MEDIA_ID = "EXTRA_DOWNLOAD_MEDIA_ID" + const val EXTRA_DOWNLOAD_TITLE = "EXTRA_DOWNLOAD_TITLE" + const val EXTRA_DOWNLOAD_ARTIST = "EXTRA_DOWNLOAD_ARTIST" + const val EXTRA_DOWNLOAD_ALBUM = "EXTRA_DOWNLOAD_ALBUM" + const val EXTRA_DOWNLOAD_DURATION = "EXTRA_DOWNLOAD_DURATION" + + const val DOWNLOAD_TYPE_TRACK = "download_type_track" + const val DOWNLOAD_TYPE_ALBUM = "download_type_album" + const val DOWNLOAD_TYPE_ARTIST = "download_type_artist" + const val DOWNLOAD_TYPE_GENRE = "download_type_genre" + const val DOWNLOAD_TYPE_YEAR = "download_type_year" + + const val DOWNLOAD_GROUP = "download_group" + const val DOWNLOAD_GROUP_TITLE = "download_group_title" + const val DOWNLOAD_GROUP_SUBTITLE = "download_group_subtitle" + + const val SHARE_OBJECT = "share_object" + + const val PLAYABLE_MEDIA_LIMIT = 100 + const val PRE_PLAYABLE_MEDIA = 15 + + const val HOME_SECTOR_DISCOVERY = "HOME_SECTOR_DISCOVERY" + const val HOME_SECTOR_MADE_FOR_YOU = "HOME_SECTOR_MADE_FOR_YOU" + const val HOME_SECTOR_BEST_OF = "HOME_SECTOR_BEST_OF" + const val HOME_SECTOR_RADIO_STATION = "HOME_SECTOR_RADIO_STATION" + const val HOME_SECTOR_TOP_SONGS = "HOME_SECTOR_TOP_SONGS" + const val HOME_SECTOR_STARRED_TRACKS = "HOME_SECTOR_STARRED_TRACKS" + const val HOME_SECTOR_STARRED_ALBUMS = "HOME_SECTOR_STARRED_ALBUMS" + const val HOME_SECTOR_STARRED_ARTISTS = "HOME_SECTOR_STARRED_ARTISTS" + const val HOME_SECTOR_NEW_RELEASES = "HOME_SECTOR_NEW_RELEASES" + const val HOME_SECTOR_FLASHBACK = "HOME_SECTOR_FLASHBACK" + const val HOME_SECTOR_MOST_PLAYED = "HOME_SECTOR_MOST_PLAYED" + const val HOME_SECTOR_LAST_PLAYED = "HOME_SECTOR_LAST_PLAYED" + const val HOME_SECTOR_RECENTLY_ADDED = "HOME_SECTOR_RECENTLY_ADDED" + const val HOME_SECTOR_PINNED_PLAYLISTS = "HOME_SECTOR_PINNED_PLAYLISTS" + const val HOME_SECTOR_SHARED = "HOME_SECTOR_SHARED" + + const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON = "android.media3.session.demo.SHUFFLE_ON" + const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = "android.media3.session.demo.SHUFFLE_OFF" + const val CUSTOM_COMMAND_TOGGLE_HEART_ON = "android.media3.session.demo.HEART_ON" + const val CUSTOM_COMMAND_TOGGLE_HEART_OFF = "android.media3.session.demo.HEART_OFF" + const val CUSTOM_COMMAND_TOGGLE_HEART_LOADING = "android.media3.session.demo.HEART_LOADING" + const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF" + const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = "android.media3.session.demo.REPEAT_ONE" + const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL" +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java new file mode 100644 index 0000000..6df73eb --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java @@ -0,0 +1,259 @@ +package com.cappielloantonio.tempo.util; + +import android.app.Notification; +import android.content.Context; + +import androidx.core.app.NotificationCompat; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.database.DatabaseProvider; +import androidx.media3.database.StandaloneDatabaseProvider; +import androidx.media3.datasource.DataSource; +import androidx.media3.datasource.DataSpec; +import androidx.media3.datasource.DefaultDataSource; +import androidx.media3.datasource.DefaultHttpDataSource; +import androidx.media3.datasource.ResolvingDataSource; +import androidx.media3.datasource.cache.Cache; +import androidx.media3.datasource.cache.CacheDataSource; +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor; +import androidx.media3.datasource.cache.NoOpCacheEvictor; +import androidx.media3.datasource.cache.SimpleCache; +import androidx.media3.exoplayer.DefaultRenderersFactory; +import androidx.media3.exoplayer.RenderersFactory; +import androidx.media3.exoplayer.offline.DownloadManager; +import androidx.media3.exoplayer.offline.DownloadNotificationHelper; + +import com.cappielloantonio.tempo.service.DownloaderManager; + +import java.io.File; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.util.ArrayList; +import java.util.concurrent.Executors; + +@UnstableApi +public final class DownloadUtil { + + public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"; + public static final String DOWNLOAD_NOTIFICATION_SUCCESSFUL_GROUP = "com.cappielloantonio.tempo.SuccessfulDownload"; + public static final String DOWNLOAD_NOTIFICATION_FAILED_GROUP = "com.cappielloantonio.tempo.FailedDownload"; + + private static final String STREAMING_CACHE_CONTENT_DIRECTORY = "streaming_cache"; + private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; + + private static DataSource.Factory dataSourceFactory; + private static DataSource.Factory httpDataSourceFactory; + private static DatabaseProvider databaseProvider; + private static File streamingCacheDirectory; + private static File downloadDirectory; + private static Cache downloadCache; + private static SimpleCache streamingCache; + private static DownloadManager downloadManager; + private static DownloaderManager downloaderManager; + private static DownloadNotificationHelper downloadNotificationHelper; + + public static boolean useExtensionRenderers() { + return true; + } + + public static RenderersFactory buildRenderersFactory(Context context, boolean preferExtensionRenderer) { + @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = + useExtensionRenderers() + ? (preferExtensionRenderer ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; + + return new DefaultRenderersFactory(context.getApplicationContext()).setExtensionRendererMode(extensionRendererMode); + } + + public static synchronized DataSource.Factory getHttpDataSourceFactory() { + if (httpDataSourceFactory == null) { + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); + CookieHandler.setDefault(cookieManager); + httpDataSourceFactory = new DefaultHttpDataSource + .Factory() + .setAllowCrossProtocolRedirects(true); + } + + return httpDataSourceFactory; + } + + public static synchronized DataSource.Factory getUpstreamDataSourceFactory(Context context) { + DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory()); + dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context)); + return dataSourceFactory; + } + + public static synchronized DataSource.Factory getCacheDataSourceFactory(Context context) { + CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory() + .setCache(getStreamingCache(context)) + .setUpstreamDataSourceFactory(getUpstreamDataSourceFactory(context)); + + ResolvingDataSource.Factory resolvingFactory = new ResolvingDataSource.Factory( + new StreamingCacheDataSource.Factory(streamCacheFactory), + dataSpec -> { + DataSpec.Builder builder = dataSpec.buildUpon(); + builder.setFlags(dataSpec.flags & ~DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN); + return builder.build(); + } + ); + dataSourceFactory = buildReadOnlyCacheDataSource(resolvingFactory, getDownloadCache(context)); + return dataSourceFactory; + } + + public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(Context context) { + if (downloadNotificationHelper == null) { + downloadNotificationHelper = new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID); + } + + return downloadNotificationHelper; + } + + public static synchronized DownloadManager getDownloadManager(Context context) { + ensureDownloadManagerInitialized(context); + return downloadManager; + } + + public static synchronized DownloaderManager getDownloadTracker(Context context) { + ensureDownloadManagerInitialized(context); + return downloaderManager; + } + + private static synchronized Cache getDownloadCache(Context context) { + if (downloadCache == null) { + File downloadContentDirectory = new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY); + downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context)); + } + + return downloadCache; + } + + private static synchronized SimpleCache getStreamingCache(Context context) { + if (streamingCache == null) { + File streamingCacheDirectory = new File(getStreamingCacheDirectory(context), STREAMING_CACHE_CONTENT_DIRECTORY); + + streamingCache = new SimpleCache( + streamingCacheDirectory, + new LeastRecentlyUsedCacheEvictor(Preferences.getStreamingCacheSize() * 1024 * 1024), + getDatabaseProvider(context) + ); + } + + return streamingCache; + } + + private static synchronized void ensureDownloadManagerInitialized(Context context) { + if (downloadManager == null) { + downloadManager = new DownloadManager( + context, + getDatabaseProvider(context), + getDownloadCache(context), + getHttpDataSourceFactory(), + Executors.newFixedThreadPool(6) + ); + + downloaderManager = new DownloaderManager(context, getHttpDataSourceFactory(), downloadManager); + } + } + + private static synchronized DatabaseProvider getDatabaseProvider(Context context) { + if (databaseProvider == null) { + databaseProvider = new StandaloneDatabaseProvider(context); + } + + return databaseProvider; + } + + private static synchronized File getStreamingCacheDirectory(Context context) { + if (streamingCacheDirectory == null) { + if (Preferences.getStreamingCacheStoragePreference() == 0) { + streamingCacheDirectory = context.getExternalFilesDirs(null)[0]; + if (streamingCacheDirectory == null) { + streamingCacheDirectory = context.getFilesDir(); + } + } else { + try { + streamingCacheDirectory = context.getExternalFilesDirs(null)[1]; + } catch (Exception exception) { + streamingCacheDirectory = context.getExternalFilesDirs(null)[0]; + Preferences.setStreamingCacheStoragePreference(0); + } + + } + } + + return streamingCacheDirectory; + } + + private static synchronized File getDownloadDirectory(Context context) { + if (downloadDirectory == null) { + int pref = Preferences.getDownloadStoragePreference(); + if (pref == 0) { + downloadDirectory = context.getExternalFilesDirs(null)[0]; + if (downloadDirectory == null) { + downloadDirectory = context.getFilesDir(); + } + } else if (pref == 1) { + try { + downloadDirectory = context.getExternalFilesDirs(null)[1]; + } catch (Exception exception) { + downloadDirectory = context.getExternalFilesDirs(null)[0]; + Preferences.setDownloadStoragePreference(0); + } + } else { + downloadDirectory = context.getExternalFilesDirs(null)[0]; + } + } + + return downloadDirectory; + } + + private static CacheDataSource.Factory buildReadOnlyCacheDataSource(DataSource.Factory upstreamFactory, Cache cache) { + return new CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(upstreamFactory) + .setCacheWriteDataSinkFactory(null) + .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR); + } + + public static synchronized void eraseDownloadFolder(Context context) { + File directory = getDownloadDirectory(context); + + ArrayList files = listFiles(directory, new ArrayList<>()); + + for (File file : files) { + file.delete(); + } + } + + private static synchronized ArrayList listFiles(File directory, ArrayList files) { + if (directory.isDirectory()) { + File[] list = directory.listFiles(); + + if (list != null) { + for (File file : list) { + if (file.isFile() && file.getName().toLowerCase().endsWith(".exo")) { + files.add(file); + } else if (file.isDirectory()) { + listFiles(file, files); + } + } + } + } + + return files; + } + + public static synchronized long getStreamingCacheSize(Context context) { + return getStreamingCache(context).getCacheSpace(); + } + + public static Notification buildGroupSummaryNotification(Context context, String channelId, String groupId, int icon, String title) { + return new NotificationCompat.Builder(context, channelId) + .setContentTitle(title) + .setSmallIcon(icon) + .setGroup(groupId) + .setGroupSummary(true) + .build(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt b/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt new file mode 100644 index 0000000..31dc172 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/DynamicMediaSourceFactory.kt @@ -0,0 +1,69 @@ +package com.cappielloantonio.tempo.util + +import android.content.Context +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource +import androidx.media3.exoplayer.drm.DrmSessionManagerProvider +import androidx.media3.exoplayer.hls.HlsMediaSource +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy +import androidx.media3.extractor.DefaultExtractorsFactory +import androidx.media3.extractor.ExtractorsFactory + +@UnstableApi +class DynamicMediaSourceFactory( + private val context: Context +) : MediaSource.Factory { + + override fun createMediaSource(mediaItem: MediaItem): MediaSource { + val mediaType: String? = mediaItem.mediaMetadata.extras?.getString("type", "") + + val streamingCacheSize = Preferences.getStreamingCacheSize() + val bypassCache = mediaType == Constants.MEDIA_TYPE_RADIO + + val useUpstream = when { + streamingCacheSize.toInt() == 0 -> true + streamingCacheSize > 0 && bypassCache -> true + streamingCacheSize > 0 && !bypassCache -> false + else -> true + } + + val dataSourceFactory: DataSource.Factory = if (useUpstream) { + DownloadUtil.getUpstreamDataSourceFactory(context) + } else { + DownloadUtil.getCacheDataSourceFactory(context) + } + + return when { + mediaItem.localConfiguration?.mimeType == MimeTypes.APPLICATION_M3U8 || + mediaItem.localConfiguration?.uri?.lastPathSegment?.endsWith(".m3u8", ignoreCase = true) == true -> { + HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem) + } + + else -> { + val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory() + ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) + .createMediaSource(mediaItem) + } + } + } + + override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory { + TODO("Not yet implemented") + } + + override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory { + TODO("Not yet implemented") + } + + override fun getSupportedTypes(): IntArray { + return intArrayOf( + C.CONTENT_TYPE_HLS, + C.CONTENT_TYPE_OTHER + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioReader.java b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioReader.java new file mode 100644 index 0000000..b8679f1 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioReader.java @@ -0,0 +1,244 @@ +package com.cappielloantonio.tempo.util; + +import android.net.Uri; +import android.os.Looper; +import android.os.SystemClock; + +import androidx.documentfile.provider.DocumentFile; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; + +import java.text.Normalizer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ExternalAudioReader { + + private static final Map cache = new ConcurrentHashMap<>(); + private static final Object LOCK = new Object(); + private static final ExecutorService REFRESH_EXECUTOR = Executors.newSingleThreadExecutor(); + private static final MutableLiveData refreshEvents = new MutableLiveData<>(); + + private static volatile String cachedDirUri; + private static volatile boolean refreshInProgress = false; + private static volatile boolean refreshQueued = false; + + private static String sanitizeFileName(String name) { + String sanitized = name.replaceAll("[\\/:*?\\\"<>|]", "_"); + sanitized = sanitized.replaceAll("\\s+", " ").trim(); + return sanitized; + } + + private static String normalizeForComparison(String name) { + String s = sanitizeFileName(name); + s = Normalizer.normalize(s, Normalizer.Form.NFKD); + s = s.replaceAll("\\p{InCombiningDiacriticalMarks}+", ""); + return s.toLowerCase(Locale.ROOT); + } + + private static void ensureCache() { + String uriString = Preferences.getDownloadDirectoryUri(); + if (uriString == null) { + synchronized (LOCK) { + cache.clear(); + cachedDirUri = null; + } + ExternalDownloadMetadataStore.clear(); + return; + } + + if (uriString.equals(cachedDirUri)) { + return; + } + + boolean runSynchronously = false; + synchronized (LOCK) { + if (refreshInProgress) { + return; + } + + if (Looper.myLooper() == Looper.getMainLooper()) { + scheduleRefreshLocked(); + return; + } + + refreshInProgress = true; + runSynchronously = true; + } + + if (runSynchronously) { + try { + rebuildCache(); + } finally { + onRefreshFinished(); + } + } + } + + public static void refreshCache() { + refreshCacheAsync(); + } + + public static void refreshCacheAsync() { + synchronized (LOCK) { + cachedDirUri = null; + cache.clear(); + } + requestRefresh(); + } + + public static LiveData getRefreshEvents() { + return refreshEvents; + } + + private static String buildKey(String artist, String title, String album) { + String name = artist != null && !artist.isEmpty() ? artist + " - " + title : title; + if (album != null && !album.isEmpty()) name += " (" + album + ")"; + return normalizeForComparison(name); + } + + private static Uri findUri(String artist, String title, String album) { + ensureCache(); + if (cachedDirUri == null) return null; + + DocumentFile file = cache.get(buildKey(artist, title, album)); + return file != null && file.exists() ? file.getUri() : null; + } + + public static Uri getUri(Child media) { + return findUri(media.getArtist(), media.getTitle(), media.getAlbum()); + } + + public static Uri getUri(PodcastEpisode episode) { + return findUri(episode.getArtist(), episode.getTitle(), episode.getAlbum()); + } + + public static synchronized void removeMetadata(Child media) { + if (media == null) { + return; + } + + String key = buildKey(media.getArtist(), media.getTitle(), media.getAlbum()); + cache.remove(key); + ExternalDownloadMetadataStore.remove(key); + } + + public static boolean delete(Child media) { + ensureCache(); + if (cachedDirUri == null) return false; + + String key = buildKey(media.getArtist(), media.getTitle(), media.getAlbum()); + DocumentFile file = cache.get(key); + boolean deleted = false; + if (file != null && file.exists()) { + deleted = file.delete(); + } + if (deleted) { + cache.remove(key); + ExternalDownloadMetadataStore.remove(key); + } + return deleted; + } + + private static void requestRefresh() { + synchronized (LOCK) { + scheduleRefreshLocked(); + } + } + + private static void scheduleRefreshLocked() { + if (refreshInProgress) { + refreshQueued = true; + return; + } + + refreshInProgress = true; + REFRESH_EXECUTOR.execute(() -> { + try { + rebuildCache(); + } finally { + onRefreshFinished(); + } + }); + } + + private static void rebuildCache() { + String uriString = Preferences.getDownloadDirectoryUri(); + if (uriString == null) { + synchronized (LOCK) { + cache.clear(); + cachedDirUri = null; + } + ExternalDownloadMetadataStore.clear(); + return; + } + + DocumentFile directory = DocumentFile.fromTreeUri(App.getContext(), Uri.parse(uriString)); + Map expectedSizes = ExternalDownloadMetadataStore.snapshot(); + Set verifiedKeys = new HashSet<>(); + Map newEntries = new HashMap<>(); + + if (directory != null && directory.canRead()) { + for (DocumentFile file : directory.listFiles()) { + if (file == null || file.isDirectory()) continue; + String existing = file.getName(); + if (existing == null) continue; + + String base = existing.replaceFirst("\\.[^\\.]+$", ""); + String key = normalizeForComparison(base); + Long expected = expectedSizes.get(key); + long actualLength = file.length(); + + if (expected != null && expected > 0 && actualLength == expected) { + newEntries.put(key, file); + verifiedKeys.add(key); + } else { + ExternalDownloadMetadataStore.remove(key); + } + } + } + + if (!expectedSizes.isEmpty()) { + if (verifiedKeys.isEmpty()) { + ExternalDownloadMetadataStore.clear(); + } else { + for (String key : expectedSizes.keySet()) { + if (!verifiedKeys.contains(key)) { + ExternalDownloadMetadataStore.remove(key); + } + } + } + } + + synchronized (LOCK) { + cache.clear(); + cache.putAll(newEntries); + cachedDirUri = uriString; + } + } + + private static void onRefreshFinished() { + boolean runAgain; + synchronized (LOCK) { + refreshInProgress = false; + runAgain = refreshQueued; + refreshQueued = false; + } + + refreshEvents.postValue(SystemClock.elapsedRealtime()); + + if (runAgain) { + requestRefresh(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioWriter.java b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioWriter.java new file mode 100644 index 0000000..efd9735 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioWriter.java @@ -0,0 +1,393 @@ +package com.cappielloantonio.tempo.util; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.Settings; +import android.webkit.MimeTypeMap; + +import androidx.core.app.NotificationCompat; +import androidx.documentfile.provider.DocumentFile; +import androidx.media3.common.MediaItem; + +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.repository.DownloadRepository; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.ui.activity.MainActivity; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.Normalizer; +import java.util.Locale; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ExternalAudioWriter { + + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); + private static final int BUFFER_SIZE = 8192; + private static final int CONNECT_TIMEOUT_MS = 15_000; + private static final int READ_TIMEOUT_MS = 60_000; + + private ExternalAudioWriter() { + } + + private static String sanitizeFileName(String name) { + String sanitized = name.replaceAll("[\\/:*?\\\"<>|]", "_"); + sanitized = sanitized.replaceAll("\\s+", " ").trim(); + return sanitized; + } + + private static String normalizeForComparison(String name) { + String s = sanitizeFileName(name); + s = Normalizer.normalize(s, Normalizer.Form.NFKD); + s = s.replaceAll("\\p{InCombiningDiacriticalMarks}+", ""); + return s.toLowerCase(Locale.ROOT); + } + + private static DocumentFile findFile(DocumentFile dir, String fileName) { + String normalized = normalizeForComparison(fileName); + for (DocumentFile file : dir.listFiles()) { + if (file.isDirectory()) continue; + String existing = file.getName(); + if (existing != null && normalizeForComparison(existing).equals(normalized)) { + return file; + } + } + return null; + } + + public static void downloadToUserDirectory(Context context, Child child) { + if (context == null || child == null) { + return; + } + Context appContext = context.getApplicationContext(); + MediaItem mediaItem = MappingUtil.mapDownload(child); + String fallbackName = child.getTitle() != null ? child.getTitle() : child.getId(); + EXECUTOR.execute(() -> performDownload(appContext, mediaItem, fallbackName, child)); + } + + private static void performDownload(Context context, MediaItem mediaItem, String fallbackName, Child child) { + String uriString = Preferences.getDownloadDirectoryUri(); + if (uriString == null) { + notifyUnavailable(context); + return; + } + + DocumentFile directory = DocumentFile.fromTreeUri(context, Uri.parse(uriString)); + if (directory == null || !directory.canWrite()) { + notifyFailure(context, "Cannot write to folder."); + return; + } + + String artist = child.getArtist() != null ? child.getArtist() : ""; + String title = child.getTitle() != null ? child.getTitle() : fallbackName; + String album = child.getAlbum() != null ? child.getAlbum() : ""; + String baseName = artist.isEmpty() ? title : artist + " - " + title; + if (!album.isEmpty()) baseName += " (" + album + ")"; + if (baseName.isEmpty()) { + baseName = fallbackName != null ? fallbackName : "download"; + } + String metadataKey = normalizeForComparison(baseName); + + Uri mediaUri = mediaItem != null && mediaItem.requestMetadata != null + ? mediaItem.requestMetadata.mediaUri + : null; + if (mediaUri == null) { + notifyFailure(context, "Invalid media URI."); + ExternalDownloadMetadataStore.remove(metadataKey); + return; + } + + String scheme = mediaUri.getScheme() != null ? mediaUri.getScheme().toLowerCase(Locale.ROOT) : ""; + + HttpURLConnection connection = null; + DocumentFile sourceDocument = null; + File sourceFile = null; + long remoteLength = -1; + String mimeType = null; + DocumentFile targetFile = null; + + try { + if (scheme.equals("http") || scheme.equals("https")) { + connection = (HttpURLConnection) new URL(mediaUri.toString()).openConnection(); + connection.setConnectTimeout(CONNECT_TIMEOUT_MS); + connection.setReadTimeout(READ_TIMEOUT_MS); + connection.setRequestProperty("Accept-Encoding", "identity"); + connection.connect(); + + int responseCode = connection.getResponseCode(); + if (responseCode >= HttpURLConnection.HTTP_BAD_REQUEST) { + notifyFailure(context, "Server returned " + responseCode); + ExternalDownloadMetadataStore.remove(metadataKey); + return; + } + + mimeType = connection.getContentType(); + remoteLength = connection.getContentLengthLong(); + } else if (scheme.equals("content")) { + sourceDocument = DocumentFile.fromSingleUri(context, mediaUri); + mimeType = context.getContentResolver().getType(mediaUri); + if (sourceDocument != null) { + remoteLength = sourceDocument.length(); + } + } else if (scheme.equals("file")) { + String path = mediaUri.getPath(); + if (path != null) { + sourceFile = new File(path); + if (sourceFile.exists()) { + remoteLength = sourceFile.length(); + } + } + String ext = MimeTypeMap.getFileExtensionFromUrl(mediaUri.toString()); + if (ext != null && !ext.isEmpty()) { + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext); + } + } else { + notifyFailure(context, "Unsupported media URI."); + ExternalDownloadMetadataStore.remove(metadataKey); + return; + } + + if (mimeType == null || mimeType.isEmpty()) { + mimeType = "application/octet-stream"; + } + + String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + if ((extension == null || extension.isEmpty()) && sourceDocument != null && sourceDocument.getName() != null) { + String name = sourceDocument.getName(); + int dot = name.lastIndexOf('.'); + if (dot >= 0 && dot < name.length() - 1) { + extension = name.substring(dot + 1); + } + } + if ((extension == null || extension.isEmpty()) && sourceFile != null) { + String name = sourceFile.getName(); + int dot = name.lastIndexOf('.'); + if (dot >= 0 && dot < name.length() - 1) { + extension = name.substring(dot + 1); + } + } + if (extension == null || extension.isEmpty()) { + String suffix = child.getSuffix(); + if (suffix != null && !suffix.isEmpty()) { + extension = suffix; + } else { + extension = "bin"; + } + } + + String sanitized = sanitizeFileName(baseName); + if (sanitized.isEmpty()) sanitized = sanitizeFileName(fallbackName); + if (sanitized.isEmpty()) sanitized = "download"; + String fileName = sanitized + "." + extension; + + DocumentFile existingFile = findFile(directory, fileName); + Long recordedSize = ExternalDownloadMetadataStore.getSize(metadataKey); + if (existingFile != null && existingFile.exists()) { + long localLength = existingFile.length(); + boolean matches = false; + if (remoteLength > 0 && localLength == remoteLength) { + matches = true; + } else if (remoteLength <= 0 && recordedSize != null && localLength == recordedSize) { + matches = true; + } + if (matches) { + ExternalDownloadMetadataStore.recordSize(metadataKey, localLength); + recordDownload(child, existingFile.getUri()); + ExternalAudioReader.refreshCacheAsync(); + notifyExists(context, fileName); + return; + } else { + existingFile.delete(); + ExternalDownloadMetadataStore.remove(metadataKey); + } + } + + targetFile = directory.createFile(mimeType, fileName); + if (targetFile == null) { + notifyFailure(context, "Failed to create file."); + return; + } + + Uri targetUri = targetFile.getUri(); + try (InputStream in = openInputStream(context, mediaUri, scheme, connection, sourceFile); + OutputStream out = context.getContentResolver().openOutputStream(targetUri)) { + if (out == null) { + notifyFailure(context, "Cannot open output stream."); + targetFile.delete(); + return; + } + + byte[] buffer = new byte[BUFFER_SIZE]; + int len; + long total = 0; + while ((len = in.read(buffer)) != -1) { + out.write(buffer, 0, len); + total += len; + } + out.flush(); + + if (total <= 0) { + targetFile.delete(); + ExternalDownloadMetadataStore.remove(metadataKey); + notifyFailure(context, "Empty download."); + return; + } + + if (remoteLength > 0 && total != remoteLength) { + targetFile.delete(); + ExternalDownloadMetadataStore.remove(metadataKey); + notifyFailure(context, "Incomplete download."); + return; + } + + ExternalDownloadMetadataStore.recordSize(metadataKey, total); + recordDownload(child, targetUri); + notifySuccess(context, fileName, child, targetUri); + ExternalAudioReader.refreshCacheAsync(); + } + } catch (Exception e) { + if (targetFile != null) { + targetFile.delete(); + } + ExternalDownloadMetadataStore.remove(metadataKey); + notifyFailure(context, e.getMessage() != null ? e.getMessage() : "Download failed"); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + private static void notifyUnavailable(Context context) { + NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + Intent settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.getPackageName(), null)); + PendingIntent openSettings = PendingIntent.getActivity(context, 0, settingsIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID) + .setContentTitle("No download folder set") + .setContentText("Tap to set one in settings") + .setSmallIcon(android.R.drawable.stat_notify_error) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setSilent(true) + .setContentIntent(openSettings) + .setAutoCancel(true); + + manager.notify(1011, builder.build()); + } + + private static void notifyFailure(Context context, String message) { + NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID) + .setContentTitle("Download failed") + .setContentText(message) + .setSmallIcon(android.R.drawable.stat_notify_error) + .setAutoCancel(true); + manager.notify((int) System.currentTimeMillis(), builder.build()); + } + + private static void notifySuccess(Context context, String name, Child child, Uri fileUri) { + NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID) + .setContentTitle("Download complete") + .setContentText(name) + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setAutoCancel(true); + + PendingIntent playIntent = buildPlayIntent(context, child, fileUri); + if (playIntent != null) { + builder.setContentIntent(playIntent); + } + + manager.notify((int) System.currentTimeMillis(), builder.build()); + } + + private static void recordDownload(Child child, Uri fileUri) { + if (child == null) { + return; + } + + Download download = new Download(child); + download.setDownloadState(1); + if (fileUri != null) { + download.setDownloadUri(fileUri.toString()); + } + + new DownloadRepository().insert(download); + } + + private static void notifyExists(Context context, String name) { + NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID) + .setContentTitle("Already downloaded") + .setContentText(name) + .setSmallIcon(android.R.drawable.stat_sys_warning) + .setAutoCancel(true); + manager.notify((int) System.currentTimeMillis(), builder.build()); + } + + private static PendingIntent buildPlayIntent(Context context, Child child, Uri fileUri) { + if (fileUri == null) return null; + Intent intent = new Intent(context, MainActivity.class) + .setAction(Constants.ACTION_PLAY_EXTERNAL_DOWNLOAD) + .putExtra(Constants.EXTRA_DOWNLOAD_URI, fileUri.toString()) + .putExtra(Constants.EXTRA_DOWNLOAD_MEDIA_ID, child.getId()) + .putExtra(Constants.EXTRA_DOWNLOAD_TITLE, child.getTitle()) + .putExtra(Constants.EXTRA_DOWNLOAD_ARTIST, child.getArtist()) + .putExtra(Constants.EXTRA_DOWNLOAD_ALBUM, child.getAlbum()) + .putExtra(Constants.EXTRA_DOWNLOAD_DURATION, child.getDuration() != null ? child.getDuration() : 0) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + int requestCode; + if (child.getId() != null) { + requestCode = Math.abs(child.getId().hashCode()); + } else { + requestCode = Math.abs(fileUri.toString().hashCode()); + } + + return PendingIntent.getActivity( + context, + requestCode, + intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + } + + private static InputStream openInputStream(Context context, + Uri mediaUri, + String scheme, + HttpURLConnection connection, + File sourceFile) throws IOException { + switch (scheme) { + case "http": + case "https": + if (connection == null) { + throw new IOException("Connection not initialized"); + } + return connection.getInputStream(); + case "content": + InputStream contentStream = context.getContentResolver().openInputStream(mediaUri); + if (contentStream == null) { + throw new IOException("Cannot open content stream"); + } + return contentStream; + case "file": + if (sourceFile == null || !sourceFile.exists()) { + throw new IOException("Missing source file"); + } + return new FileInputStream(sourceFile); + default: + throw new IOException("Unsupported scheme " + scheme); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/ExternalDownloadMetadataStore.java b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalDownloadMetadataStore.java new file mode 100644 index 0000000..4bd4089 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalDownloadMetadataStore.java @@ -0,0 +1,123 @@ +package com.cappielloantonio.tempo.util; + +import android.content.SharedPreferences; + +import androidx.annotation.Nullable; + +import com.cappielloantonio.tempo.App; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +public final class ExternalDownloadMetadataStore { + + private static final String PREF_KEY = "external_download_metadata"; + + private ExternalDownloadMetadataStore() { + } + + private static SharedPreferences preferences() { + return App.getInstance().getPreferences(); + } + + private static JSONObject readAll() { + String raw = preferences().getString(PREF_KEY, "{}"); + try { + return new JSONObject(raw); + } catch (JSONException e) { + return new JSONObject(); + } + } + + private static void writeAll(JSONObject object) { + preferences().edit().putString(PREF_KEY, object.toString()).apply(); + } + + public static synchronized void clear() { + writeAll(new JSONObject()); + } + + public static synchronized void recordSize(String key, long size) { + if (key == null || size <= 0) { + return; + } + JSONObject object = readAll(); + try { + object.put(key, size); + } catch (JSONException ignored) { + } + writeAll(object); + } + + public static synchronized void remove(String key) { + if (key == null) { + return; + } + JSONObject object = readAll(); + object.remove(key); + writeAll(object); + } + + @Nullable + public static synchronized Long getSize(String key) { + if (key == null) { + return null; + } + JSONObject object = readAll(); + if (!object.has(key)) { + return null; + } + long size = object.optLong(key, -1L); + return size > 0 ? size : null; + } + + public static synchronized Map snapshot() { + JSONObject object = readAll(); + if (object.length() == 0) { + return Collections.emptyMap(); + } + Map sizes = new HashMap<>(); + Iterator keys = object.keys(); + while (keys.hasNext()) { + String key = keys.next(); + long size = object.optLong(key, -1L); + if (size > 0) { + sizes.put(key, size); + } + } + return sizes; + } + + public static synchronized void retainOnly(Set keysToKeep) { + if (keysToKeep == null || keysToKeep.isEmpty()) { + clear(); + return; + } + JSONObject object = readAll(); + if (object.length() == 0) { + return; + } + Set keys = new HashSet<>(); + Iterator iterator = object.keys(); + while (iterator.hasNext()) { + keys.add(iterator.next()); + } + boolean changed = false; + for (String key : keys) { + if (!keysToKeep.contains(key)) { + object.remove(key); + changed = true; + } + } + if (changed) { + writeAll(object); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/IndexUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/IndexUtil.java new file mode 100644 index 0000000..87b56eb --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/IndexUtil.java @@ -0,0 +1,29 @@ +package com.cappielloantonio.tempo.util; + +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.subsonic.models.Artist; +import com.cappielloantonio.tempo.subsonic.models.Index; +import com.cappielloantonio.tempo.subsonic.models.Indexes; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@OptIn(markerClass = UnstableApi.class) +public class IndexUtil { + public static List getArtist(Indexes indexes) { + if (indexes.getIndices() == null) return Collections.emptyList(); + + ArrayList toReturn = new ArrayList<>(); + + for (Index index : indexes.getIndices()) { + if (index.getArtists() != null) { + toReturn.addAll(index.getArtists()); + } + } + + return toReturn; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java new file mode 100644 index 0000000..0a6f33e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java @@ -0,0 +1,296 @@ +package com.cappielloantonio.tempo.util; + +import android.net.Uri; +import android.os.Bundle; + +import androidx.annotation.OptIn; +import androidx.lifecycle.LifecycleOwner; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.HeartRating; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.repository.DownloadRepository; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.List; + +@OptIn(markerClass = UnstableApi.class) +public class MappingUtil { + public static List mapMediaItems(List items) { + ArrayList mediaItems = new ArrayList<>(); + + for (int i = 0; i < items.size(); i++) { + mediaItems.add(mapMediaItem(items.get(i))); + } + + return mediaItems; + } + + public static MediaItem mapMediaItem(Child media) { + Uri uri = getUri(media); + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(media.getCoverArtId(), Preferences.getImageSize())); + + Bundle bundle = new Bundle(); + bundle.putString("id", media.getId()); + bundle.putString("parentId", media.getParentId()); + bundle.putBoolean("isDir", media.isDir()); + bundle.putString("title", media.getTitle()); + bundle.putString("album", media.getAlbum()); + bundle.putString("artist", media.getArtist()); + bundle.putInt("track", media.getTrack() != null ? media.getTrack() : 0); + bundle.putInt("year", media.getYear() != null ? media.getYear() : 0); + bundle.putString("genre", media.getGenre()); + bundle.putString("coverArtId", media.getCoverArtId()); + bundle.putLong("size", media.getSize() != null ? media.getSize() : 0); + bundle.putString("contentType", media.getContentType()); + bundle.putString("suffix", media.getSuffix()); + bundle.putString("transcodedContentType", media.getTranscodedContentType()); + bundle.putString("transcodedSuffix", media.getTranscodedSuffix()); + bundle.putInt("duration", media.getDuration() != null ? media.getDuration() : 0); + bundle.putInt("bitrate", media.getBitrate() != null ? media.getBitrate() : 0); + bundle.putInt("samplingRate", media.getSamplingRate() != null ? media.getSamplingRate() : 0); + bundle.putInt("bitDepth", media.getBitDepth() != null ? media.getBitDepth() : 0); + bundle.putString("path", media.getPath()); + bundle.putBoolean("isVideo", media.isVideo()); + bundle.putInt("userRating", media.getUserRating() != null ? media.getUserRating() : 0); + bundle.putDouble("averageRating", media.getAverageRating() != null ? media.getAverageRating() : 0); + bundle.putLong("playCount", media.getPlayCount() != null ? media.getPlayCount() : 0); + bundle.putInt("discNumber", media.getDiscNumber() != null ? media.getDiscNumber() : 0); + bundle.putLong("created", media.getCreated() != null ? media.getCreated().getTime() : 0); + bundle.putLong("starred", media.getStarred() != null ? media.getStarred().getTime() : 0); + bundle.putString("albumId", media.getAlbumId()); + bundle.putString("artistId", media.getArtistId()); + bundle.putString("type", Constants.MEDIA_TYPE_MUSIC); + bundle.putLong("bookmarkPosition", media.getBookmarkPosition() != null ? media.getBookmarkPosition() : 0); + bundle.putInt("originalWidth", media.getOriginalWidth() != null ? media.getOriginalWidth() : 0); + bundle.putInt("originalHeight", media.getOriginalHeight() != null ? media.getOriginalHeight() : 0); + bundle.putString("uri", uri.toString()); + bundle.putString("assetLinkSong", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, media.getId())); + bundle.putString("assetLinkAlbum", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, media.getAlbumId())); + bundle.putString("assetLinkArtist", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, media.getArtistId())); + bundle.putString("assetLinkGenre", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_GENRE, media.getGenre())); + Integer year = media.getYear(); + bundle.putString("assetLinkYear", year != null && year != 0 ? AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year)) : null); + + return new MediaItem.Builder() + .setMediaId(media.getId()) + .setMediaMetadata( + new MediaMetadata.Builder() + .setTitle(media.getTitle()) + .setTrackNumber(media.getTrack() != null ? media.getTrack() : 0) + .setDiscNumber(media.getDiscNumber() != null ? media.getDiscNumber() : 0) + .setReleaseYear(media.getYear() != null ? media.getYear() : 0) + .setAlbumTitle(media.getAlbum()) + .setArtist(media.getArtist()) + .setArtworkUri(artworkUri) + .setUserRating(new HeartRating(media.getStarred() != null)) + .setSupportedCommands( + ImmutableList.of( + Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON, + Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF + ) + ) + .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) + .build() + ) + .setRequestMetadata( + new MediaItem.RequestMetadata.Builder() + .setMediaUri(uri) + .setExtras(bundle) + .build() + ) + .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .setUri(uri) + .build(); + } + + public static MediaItem mapMediaItem(MediaItem old) { + String mediaId = null; + if (old.requestMetadata.extras != null) + mediaId = old.requestMetadata.extras.getString("id"); + + if (mediaId != null && DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(mediaId)) { + return old; + } + Uri uri = old.requestMetadata.mediaUri == null ? null : MusicUtil.updateStreamUri(old.requestMetadata.mediaUri); + return new MediaItem.Builder() + .setMediaId(old.mediaId) + .setMediaMetadata(old.mediaMetadata) + .setRequestMetadata( + new MediaItem.RequestMetadata.Builder() + .setMediaUri(uri) + .setExtras(old.requestMetadata.extras) + .build() + ) + .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .setUri(uri) + .build(); + } + + public static List mapDownloads(List items) { + ArrayList downloads = new ArrayList<>(); + + for (int i = 0; i < items.size(); i++) { + downloads.add(mapDownload(items.get(i))); + } + + return downloads; + } + + public static MediaItem mapDownload(Child media) { + + Bundle bundle = new Bundle(); + bundle.putInt("samplingRate", media.getSamplingRate() != null ? media.getSamplingRate() : 0); + bundle.putInt("bitDepth", media.getBitDepth() != null ? media.getBitDepth() : 0); + + return new MediaItem.Builder() + .setMediaId(media.getId()) + .setMediaMetadata( + new MediaMetadata.Builder() + .setTitle(media.getTitle()) + .setTrackNumber(media.getTrack() != null ? media.getTrack() : 0) + .setDiscNumber(media.getDiscNumber() != null ? media.getDiscNumber() : 0) + .setReleaseYear(media.getYear() != null ? media.getYear() : 0) + .setAlbumTitle(media.getAlbum()) + .setArtist(media.getArtist()) + .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) + .build() + ) + .setRequestMetadata( + new MediaItem.RequestMetadata.Builder() + .setExtras(bundle) + .setMediaUri(Preferences.preferTranscodedDownload() ? MusicUtil.getTranscodedDownloadUri(media.getId()) : MusicUtil.getDownloadUri(media.getId())) + .build() + ) + .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .setUri(Preferences.preferTranscodedDownload() ? MusicUtil.getTranscodedDownloadUri(media.getId()) : MusicUtil.getDownloadUri(media.getId())) + .build(); + } + + public static MediaItem mapInternetRadioStation(InternetRadioStation internetRadioStation) { + Uri uri = Uri.parse(internetRadioStation.getStreamUrl()); + + Bundle bundle = new Bundle(); + bundle.putString("id", internetRadioStation.getId()); + bundle.putString("title", internetRadioStation.getName()); + bundle.putString("uri", uri.toString()); + bundle.putString("type", Constants.MEDIA_TYPE_RADIO); + + return new MediaItem.Builder() + .setMediaId(internetRadioStation.getId()) + .setMediaMetadata( + new MediaMetadata.Builder() + .setTitle(internetRadioStation.getName()) + .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) + .build() + ) + .setRequestMetadata( + new MediaItem.RequestMetadata.Builder() + .setMediaUri(uri) + .setExtras(bundle) + .build() + ) + // .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .setUri(uri) + .build(); + } + + public static MediaItem mapMediaItem(PodcastEpisode podcastEpisode) { + Uri uri = getUri(podcastEpisode); + Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(podcastEpisode.getCoverArtId(), Preferences.getImageSize())); + + Bundle bundle = new Bundle(); + bundle.putString("id", podcastEpisode.getId()); + bundle.putString("parentId", podcastEpisode.getParentId()); + bundle.putBoolean("isDir", podcastEpisode.isDir()); + bundle.putString("title", podcastEpisode.getTitle()); + bundle.putString("album", podcastEpisode.getAlbum()); + bundle.putString("artist", podcastEpisode.getArtist()); + bundle.putInt("year", podcastEpisode.getYear() != null ? podcastEpisode.getYear() : 0); + bundle.putString("coverArtId", podcastEpisode.getCoverArtId()); + bundle.putLong("size", podcastEpisode.getSize() != null ? podcastEpisode.getSize() : 0); + bundle.putString("contentType", podcastEpisode.getContentType()); + bundle.putString("suffix", podcastEpisode.getSuffix()); + bundle.putInt("duration", podcastEpisode.getDuration() != null ? podcastEpisode.getDuration() : 0); + bundle.putInt("bitrate", podcastEpisode.getBitrate() != null ? podcastEpisode.getBitrate() : 0); + bundle.putBoolean("isVideo", podcastEpisode.isVideo()); + bundle.putLong("created", podcastEpisode.getCreated() != null ? podcastEpisode.getCreated().getTime() : 0); + bundle.putString("artistId", podcastEpisode.getArtistId()); + bundle.putString("description", podcastEpisode.getDescription()); + bundle.putString("type", Constants.MEDIA_TYPE_PODCAST); + bundle.putString("uri", uri.toString()); + + MediaItem item = new MediaItem.Builder() + .setMediaId(podcastEpisode.getId()) + .setMediaMetadata( + new MediaMetadata.Builder() + .setTitle(podcastEpisode.getTitle()) + .setReleaseYear(podcastEpisode.getYear() != null ? podcastEpisode.getYear() : 0) + .setAlbumTitle(podcastEpisode.getAlbum()) + .setArtist(podcastEpisode.getArtist()) + .setArtworkUri(artworkUri) + .setExtras(bundle) + .setIsBrowsable(false) + .setIsPlayable(true) + .build() + ) + .setRequestMetadata( + new MediaItem.RequestMetadata.Builder() + .setMediaUri(uri) + .setExtras(bundle) + .build() + ) + .setMimeType(MimeTypes.BASE_TYPE_AUDIO) + .setUri(uri) + .build(); + + return item; + } + + private static Uri getUri(Child media) { + if (Preferences.getDownloadDirectoryUri() != null) { + Uri local = ExternalAudioReader.getUri(media); + return local != null ? local : MusicUtil.getStreamUri(media.getId()); + } + return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId()) + ? getDownloadUri(media.getId()) + : MusicUtil.getStreamUri(media.getId()); + } + + private static Uri getUri(PodcastEpisode podcastEpisode) { + if (Preferences.getDownloadDirectoryUri() != null) { + Uri local = ExternalAudioReader.getUri(podcastEpisode); + return local != null ? local : MusicUtil.getStreamUri(podcastEpisode.getStreamId()); + } + return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(podcastEpisode.getStreamId()) + ? getDownloadUri(podcastEpisode.getStreamId()) + : MusicUtil.getStreamUri(podcastEpisode.getStreamId()); + } + + private static Uri getDownloadUri(String id) { + Download download = new DownloadRepository().getDownload(id); + return download != null && !download.getDownloadUri().isEmpty() ? Uri.parse(download.getDownloadUri()) : MusicUtil.getDownloadUri(id); + } + + public static void observeExternalAudioRefresh(LifecycleOwner owner, Runnable onRefresh) { + if (owner == null || onRefresh == null) { + return; + } + ExternalAudioReader.getRefreshEvents().observe(owner, event -> onRefresh.run()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java new file mode 100644 index 0000000..cd2c7a3 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java @@ -0,0 +1,357 @@ +package com.cappielloantonio.tempo.util; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.Uri; +import android.text.Html; +import android.util.Log; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.repository.DownloadRepository; +import com.cappielloantonio.tempo.subsonic.models.Child; + +import java.text.CharacterIterator; +import java.text.DecimalFormat; +import java.text.StringCharacterIterator; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class MusicUtil { + private static final String TAG = "MusicUtil"; + + private static final Pattern BITRATE_PATTERN = Pattern.compile("&maxBitRate=\\d+"); + private static final Pattern FORMAT_PATTERN = Pattern.compile("&format=\\w+"); + + public static Uri getStreamUri(String id) { + Map params = App.getSubsonicClientInstance(false).getParams(); + + StringBuilder uri = new StringBuilder(); + + uri.append(App.getSubsonicClientInstance(false).getUrl()); + uri.append("stream"); + + if (params.containsKey("u") && params.get("u") != null) + uri.append("?u=").append(Util.encode(params.get("u"))); + if (params.containsKey("p") && params.get("p") != null) + uri.append("&p=").append(params.get("p")); + if (params.containsKey("s") && params.get("s") != null) + uri.append("&s=").append(params.get("s")); + if (params.containsKey("t") && params.get("t") != null) + uri.append("&t=").append(params.get("t")); + if (params.containsKey("v") && params.get("v") != null) + uri.append("&v=").append(params.get("v")); + if (params.containsKey("c") && params.get("c") != null) + uri.append("&c=").append(params.get("c")); + + if (!Preferences.isServerPrioritized()) + uri.append("&maxBitRate=").append(getBitratePreference()); + if (!Preferences.isServerPrioritized()) + uri.append("&format=").append(getTranscodingFormatPreference()); + if (Preferences.askForEstimateContentLength()) + uri.append("&estimateContentLength=true"); + + uri.append("&id=").append(id); + + Log.d(TAG, "getStreamUri: " + uri); + + return Uri.parse(uri.toString()); + } + + public static Uri updateStreamUri(Uri uri) { + String s = uri.toString(); + Matcher m1 = BITRATE_PATTERN.matcher(s); + s = m1.replaceAll(""); + Matcher m2 = FORMAT_PATTERN.matcher(s); + s = m2.replaceAll(""); + s = s.replace("&estimateContentLength=true", ""); + + if (!Preferences.isServerPrioritized()) + s += "&maxBitRate=" + getBitratePreference(); + if (!Preferences.isServerPrioritized()) + s += "&format=" + getTranscodingFormatPreference(); + if (Preferences.askForEstimateContentLength()) + s += "&estimateContentLength=true"; + + return Uri.parse(s); + } + + public static Uri getDownloadUri(String id) { + StringBuilder uri = new StringBuilder(); + + Download download = new DownloadRepository().getDownload(id); + + if (download == null || download.getDownloadUri().isEmpty()) { + Map params = App.getSubsonicClientInstance(false).getParams(); + + uri.append(App.getSubsonicClientInstance(false).getUrl()); + uri.append("download"); + + if (params.containsKey("u") && params.get("u") != null) + uri.append("?u=").append(Util.encode(params.get("u"))); + if (params.containsKey("p") && params.get("p") != null) + uri.append("&p=").append(params.get("p")); + if (params.containsKey("s") && params.get("s") != null) + uri.append("&s=").append(params.get("s")); + if (params.containsKey("t") && params.get("t") != null) + uri.append("&t=").append(params.get("t")); + if (params.containsKey("v") && params.get("v") != null) + uri.append("&v=").append(params.get("v")); + if (params.containsKey("c") && params.get("c") != null) + uri.append("&c=").append(params.get("c")); + + uri.append("&id=").append(id); + } else { + uri.append(download.getDownloadUri()); + } + + Log.d(TAG, "getDownloadUri: " + uri); + + return Uri.parse(uri.toString()); + } + + public static Uri getTranscodedDownloadUri(String id) { + Map params = App.getSubsonicClientInstance(false).getParams(); + + StringBuilder uri = new StringBuilder(); + + uri.append(App.getSubsonicClientInstance(false).getUrl()); + uri.append("stream"); + + if (params.containsKey("u") && params.get("u") != null) + uri.append("?u=").append(Util.encode(params.get("u"))); + if (params.containsKey("p") && params.get("p") != null) + uri.append("&p=").append(params.get("p")); + if (params.containsKey("s") && params.get("s") != null) + uri.append("&s=").append(params.get("s")); + if (params.containsKey("t") && params.get("t") != null) + uri.append("&t=").append(params.get("t")); + if (params.containsKey("v") && params.get("v") != null) + uri.append("&v=").append(params.get("v")); + if (params.containsKey("c") && params.get("c") != null) + uri.append("&c=").append(params.get("c")); + + if (!Preferences.isServerPrioritizedInTranscodedDownload()) + uri.append("&maxBitRate=").append(getBitratePreferenceForDownload()); + if (!Preferences.isServerPrioritizedInTranscodedDownload()) + uri.append("&format=").append(getTranscodingFormatPreferenceForDownload()); + + uri.append("&id=").append(id); + + Log.d(TAG, "getTranscodedDownloadUri: " + uri); + + return Uri.parse(uri.toString()); + } + + + public static String getReadableDurationString(Long duration, boolean millis) { + long lenght = duration != null ? duration : 0; + + long minutes; + long seconds; + + if (millis) { + minutes = (lenght / 1000) / 60; + seconds = (lenght / 1000) % 60; + } else { + minutes = lenght / 60; + seconds = lenght % 60; + } + + if (minutes < 60) { + return String.format(Locale.getDefault(), "%01d:%02d", minutes, seconds); + } else { + long hours = minutes / 60; + minutes = minutes % 60; + return String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds); + } + } + + public static String getReadableDurationString(Integer duration, boolean millis) { + long lenght = duration != null ? duration : 0; + return getReadableDurationString(lenght, millis); + } + + public static String getReadableAudioQualityString(Child child) { + if (!Preferences.showAudioQuality() || child.getBitrate() == null) return ""; + + return "•" + + " " + + child.getBitrate() + + "kbps" + + " • " + + (child.getBitDepth() != null && child.getBitDepth() != 0 + ? child.getBitDepth() + "/" + (child.getSamplingRate() != null ? child.getSamplingRate() / 1000 : "") + : (child.getSamplingRate() != null + ? new DecimalFormat("0.#").format(child.getSamplingRate() / 1000.0) + "kHz" + : "")) + + " " + + child.getSuffix(); + } + + public static String getReadablePodcastDurationString(long duration) { + long minutes = duration / 60; + + if (minutes < 60) { + return String.format(Locale.getDefault(), "%01d min", minutes); + } else { + long hours = minutes / 60; + minutes = minutes % 60; + return String.format(Locale.getDefault(), "%d h %02d min", hours, minutes); + } + } + + public static String getReadableTrackNumber(Context context, Integer trackNumber) { + if (trackNumber != null) { + return String.valueOf(trackNumber); + } + + return context.getString(R.string.label_placeholder); + } + + public static String getReadableString(String string) { + if (string != null) { + return Html.fromHtml(string, Html.FROM_HTML_MODE_COMPACT).toString(); + } + + return ""; + } + + public static String forceReadableString(String string) { + if (string != null) { + return getReadableString(string) + .replaceAll(""", "\"") + .replaceAll("'", "'") + .replaceAll("&", "'") + .replaceAll("]+)>((?:.(?!))*.)", ""); + } + + return ""; + } + + public static String getReadableLyrics(String string) { + if (string != null) { + return string + .replaceAll(""", "\"") + .replaceAll("'", "'") + .replaceAll("&", "'") + .replaceAll(" ", "\n"); + } + + return ""; + } + + public static String getReadableByteCount(long bytes) { + long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes); + + if (absB < 1024) { + return bytes + " B"; + } + + long value = absB; + + CharacterIterator ci = new StringCharacterIterator("KMGTPE"); + + for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) { + value >>= 10; + ci.next(); + } + + value *= Long.signum(bytes); + + return String.format("%.1f %ciB", value / 1024.0, ci.current()); + } + + public static String passwordHexEncoding(String plainPassword) { + return "enc:" + plainPassword.chars().mapToObj(Integer::toHexString).collect(Collectors.joining()); + } + + public static String getBitratePreference() { + Network network = getConnectivityManager().getActiveNetwork(); + NetworkCapabilities networkCapabilities = getConnectivityManager().getNetworkCapabilities(network); + String audioTranscodeFormat = getTranscodingFormatPreference(); + + if (audioTranscodeFormat.equals("raw") || network == null || networkCapabilities == null) + return "0"; + + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + return Preferences.getMaxBitrateWifi(); + } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + return Preferences.getMaxBitrateMobile(); + } else { + return Preferences.getMaxBitrateWifi(); + } + } + + public static String getTranscodingFormatPreference() { + Network network = getConnectivityManager().getActiveNetwork(); + NetworkCapabilities networkCapabilities = getConnectivityManager().getNetworkCapabilities(network); + + if (network == null || networkCapabilities == null) return "raw"; + + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + return Preferences.getAudioTranscodeFormatWifi(); + } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + return Preferences.getAudioTranscodeFormatMobile(); + } else { + return Preferences.getAudioTranscodeFormatWifi(); + } + } + + public static String getBitratePreferenceForDownload() { + String audioTranscodeFormat = getTranscodingFormatPreferenceForDownload(); + + if (audioTranscodeFormat.equals("raw")) + return "0"; + + return Preferences.getBitrateTranscodedDownload(); + } + + public static String getTranscodingFormatPreferenceForDownload() { + return Preferences.getAudioTranscodeFormatTranscodedDownload(); + } + + public static List limitPlayableMedia(List toLimit, int position) { + if (!toLimit.isEmpty() && toLimit.size() > Constants.PLAYABLE_MEDIA_LIMIT) { + int from = position < Constants.PRE_PLAYABLE_MEDIA ? 0 : position - Constants.PRE_PLAYABLE_MEDIA; + int to = Math.min(from + Constants.PLAYABLE_MEDIA_LIMIT, toLimit.size()); + + return toLimit.subList(from, to); + } + + return toLimit; + } + + public static int getPlayableMediaPosition(List toLimit, int position) { + if (!toLimit.isEmpty() && toLimit.size() > Constants.PLAYABLE_MEDIA_LIMIT) { + return Math.min(position, Constants.PRE_PLAYABLE_MEDIA); + } + + return position; + } + + private static ConnectivityManager getConnectivityManager() { + return (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE); + } + + public static void ratingFilter(List toFilter) { + if (toFilter == null || toFilter.isEmpty()) return; + + List filtered = toFilter + .stream() + .filter(child -> (child.getUserRating() != null && child.getUserRating() >= Preferences.getMinStarRatingAccepted()) || (child.getUserRating() == null)) + .collect(Collectors.toList()); + + toFilter.clear(); + + toFilter.addAll(filtered); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/NetworkUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/NetworkUtil.java new file mode 100644 index 0000000..f3eca98 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/NetworkUtil.java @@ -0,0 +1,29 @@ +package com.cappielloantonio.tempo.util; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; + +import com.cappielloantonio.tempo.App; + +public class NetworkUtil { + public static boolean isOffline() { + ConnectivityManager connectivityManager = (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE); + + if (connectivityManager != null) { + Network network = connectivityManager.getActiveNetwork(); + + if (network != null) { + NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network); + + if (capabilities != null) { + return !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) || !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + } + } + } + + return true; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/OpenSubsonicExtensionsUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/OpenSubsonicExtensionsUtil.java new file mode 100644 index 0000000..78c59fb --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/OpenSubsonicExtensionsUtil.java @@ -0,0 +1,41 @@ +package com.cappielloantonio.tempo.util; + +import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; + +import java.util.List; + +public class OpenSubsonicExtensionsUtil { + private static List getOpenSubsonicExtensions() { + List extensions = null; + + if (Preferences.isOpenSubsonic() && Preferences.getOpenSubsonicExtensions() != null) { + extensions = new Gson().fromJson( + Preferences.getOpenSubsonicExtensions(), + new TypeToken>() { + }.getType() + ); + } + + return extensions; + } + + private static OpenSubsonicExtension getOpenSubsonicExtension(String extensionName) { + if (getOpenSubsonicExtensions() == null) return null; + + return getOpenSubsonicExtensions().stream().filter(openSubsonicExtension -> openSubsonicExtension.getName().equals(extensionName)).findAny().orElse(null); + } + + public static boolean isTranscodeOffsetExtensionAvailable() { + return getOpenSubsonicExtension("transcodeOffset") != null; + } + + public static boolean isFormPostExtensionAvailable() { + return getOpenSubsonicExtension("formPost") != null; + } + + public static boolean isSongLyricsExtensionAvailable() { + return getOpenSubsonicExtension("songLyrics") != null; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt new file mode 100644 index 0000000..e404c29 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -0,0 +1,677 @@ +package com.cappielloantonio.tempo.util + +import android.util.Log +import androidx.media3.common.Player +import com.cappielloantonio.tempo.App +import com.cappielloantonio.tempo.model.HomeSector +import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension +import com.google.gson.Gson + + +object Preferences { + const val THEME = "theme" + private const val SERVER = "server" + private const val USER = "user" + private const val PASSWORD = "password" + private const val TOKEN = "token" + private const val SALT = "salt" + private const val LOW_SECURITY = "low_security" + private const val BATTERY_OPTIMIZATION = "battery_optimization" + private const val SERVER_ID = "server_id" + private const val OPEN_SUBSONIC = "open_subsonic" + private const val OPEN_SUBSONIC_EXTENSIONS = "open_subsonic_extensions" + private const val LOCAL_ADDRESS = "local_address" + private const val IN_USE_SERVER_ADDRESS = "in_use_server_address" + private const val NEXT_SERVER_SWITCH = "next_server_switch" + private const val PLAYBACK_SPEED = "playback_speed" + private const val SKIP_SILENCE = "skip_silence" + private const val SHUFFLE_MODE = "shuffle_mode" + private const val REPEAT_MODE = "repeat_mode" + private const val IMAGE_CACHE_SIZE = "image_cache_size" + private const val STREAMING_CACHE_SIZE = "streaming_cache_size" + private const val IMAGE_SIZE = "image_size" + private const val MAX_BITRATE_WIFI = "max_bitrate_wifi" + private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile" + private const val AUDIO_TRANSCODE_FORMAT_WIFI = "audio_transcode_format_wifi" + private const val AUDIO_TRANSCODE_FORMAT_MOBILE = "audio_transcode_format_mobile" + private const val WIFI_ONLY = "wifi_only" + private const val DATA_SAVING_MODE = "data_saving_mode" + private const val SERVER_UNREACHABLE = "server_unreachable" + private const val SYNC_STARRED_ARTISTS_FOR_OFFLINE_USE = "sync_starred_artists_for_offline_use" + private const val SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE = "sync_starred_albums_for_offline_use" + private const val SYNC_STARRED_TRACKS_FOR_OFFLINE_USE = "sync_starred_tracks_for_offline_use" + private const val QUEUE_SYNCING = "queue_syncing" + private const val QUEUE_SYNCING_COUNTDOWN = "queue_syncing_countdown" + private const val ROUNDED_CORNER = "rounded_corner" + private const val ROUNDED_CORNER_SIZE = "rounded_corner_size" + private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility" + private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility" + private const val AUTO_DOWNLOAD_LYRICS = "auto_download_lyrics" + private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility" + private const val REPLAY_GAIN_MODE = "replay_gain_mode" + private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority" + private const val STREAMING_CACHE_STORAGE = "streaming_cache_storage" + private const val DOWNLOAD_STORAGE = "download_storage" + private const val DOWNLOAD_DIRECTORY_URI = "download_directory_uri" + private const val DEFAULT_DOWNLOAD_VIEW_TYPE = "default_download_view_type" + private const val AUDIO_TRANSCODE_DOWNLOAD = "audio_transcode_download" + private const val AUDIO_TRANSCODE_DOWNLOAD_PRIORITY = "audio_transcode_download_priority" + private const val MAX_BITRATE_DOWNLOAD = "max_bitrate_download" + private const val AUDIO_TRANSCODE_FORMAT_DOWNLOAD = "audio_transcode_format_download" + private const val SHARE = "share" + private const val SCROBBLING = "scrobbling" + private const val ESTIMATE_CONTENT_LENGTH = "estimate_content_length" + private const val BUFFERING_STRATEGY = "buffering_strategy" + private const val SKIP_MIN_STAR_RATING = "skip_min_star_rating" + private const val MIN_STAR_RATING = "min_star_rating" + private const val ALWAYS_ON_DISPLAY = "always_on_display" + private const val AUDIO_QUALITY_PER_ITEM = "audio_quality_per_item" + private const val HOME_SECTOR_LIST = "home_sector_list" + private const val SONG_RATING_PER_ITEM = "song_rating_per_item" + private const val RATING_PER_ITEM = "rating_per_item" + private const val NEXT_UPDATE_CHECK = "next_update_check" + private const val GITHUB_UPDATE_CHECK = "github_update_check" + private const val CONTINUOUS_PLAY = "continuous_play" + private const val LAST_INSTANT_MIX = "last_instant_mix" + private const val ALLOW_PLAYLIST_DUPLICATES = "allow_playlist_duplicates" + private const val EQUALIZER_ENABLED = "equalizer_enabled" + private const val EQUALIZER_BAND_LEVELS = "equalizer_band_levels" + private const val MINI_SHUFFLE_BUTTON_VISIBILITY = "mini_shuffle_button_visibility" + private const val ALBUM_DETAIL = "album_detail" + private const val ALBUM_SORT_ORDER = "album_sort_order" + private const val DEFAULT_ALBUM_SORT_ORDER = Constants.ALBUM_ORDER_BY_NAME + private const val ARTIST_SORT_BY_ALBUM_COUNT= "artist_sort_by_album_count" + + @JvmStatic + fun getServer(): String? { + return App.getInstance().preferences.getString(SERVER, null) + } + + @JvmStatic + fun setServer(server: String?) { + App.getInstance().preferences.edit().putString(SERVER, server).apply() + } + + @JvmStatic + fun getUser(): String? { + return App.getInstance().preferences.getString(USER, null) + } + + @JvmStatic + fun setUser(user: String?) { + App.getInstance().preferences.edit().putString(USER, user).apply() + } + + @JvmStatic + fun getPassword(): String? { + return App.getInstance().preferences.getString(PASSWORD, null) + } + + @JvmStatic + fun setPassword(password: String?) { + App.getInstance().preferences.edit().putString(PASSWORD, password).apply() + } + + @JvmStatic + fun getToken(): String? { + return App.getInstance().preferences.getString(TOKEN, null) + } + + @JvmStatic + fun setToken(token: String?) { + App.getInstance().preferences.edit().putString(TOKEN, token).apply() + } + + @JvmStatic + fun getSalt(): String? { + return App.getInstance().preferences.getString(SALT, null) + } + + @JvmStatic + fun setSalt(salt: String?) { + App.getInstance().preferences.edit().putString(SALT, salt).apply() + } + + @JvmStatic + fun isLowScurity(): Boolean { + return App.getInstance().preferences.getBoolean(LOW_SECURITY, false) + } + + @JvmStatic + fun setLowSecurity(isLowSecurity: Boolean) { + App.getInstance().preferences.edit().putBoolean(LOW_SECURITY, isLowSecurity).apply() + } + + @JvmStatic + fun getServerId(): String? { + return App.getInstance().preferences.getString(SERVER_ID, null) + } + + @JvmStatic + fun setServerId(serverId: String?) { + App.getInstance().preferences.edit().putString(SERVER_ID, serverId).apply() + } + + @JvmStatic + fun isOpenSubsonic(): Boolean { + return App.getInstance().preferences.getBoolean(OPEN_SUBSONIC, false) + } + + @JvmStatic + fun setOpenSubsonic(isOpenSubsonic: Boolean) { + App.getInstance().preferences.edit().putBoolean(OPEN_SUBSONIC, isOpenSubsonic).apply() + } + + @JvmStatic + fun getOpenSubsonicExtensions(): String? { + return App.getInstance().preferences.getString(OPEN_SUBSONIC_EXTENSIONS, null) + } + + @JvmStatic + fun setOpenSubsonicExtensions(extension: List) { + App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply() + } + + @JvmStatic + fun isAutoDownloadLyricsEnabled(): Boolean { + val preferences = App.getInstance().preferences + + if (preferences.contains(AUTO_DOWNLOAD_LYRICS)) { + return preferences.getBoolean(AUTO_DOWNLOAD_LYRICS, false) + } + + return false + } + + @JvmStatic + fun setAutoDownloadLyricsEnabled(isEnabled: Boolean) { + App.getInstance().preferences.edit() + .putBoolean(AUTO_DOWNLOAD_LYRICS, isEnabled) + .apply() + } + + @JvmStatic + fun getLocalAddress(): String? { + return App.getInstance().preferences.getString(LOCAL_ADDRESS, null) + } + + @JvmStatic + fun setLocalAddress(address: String?) { + App.getInstance().preferences.edit().putString(LOCAL_ADDRESS, address).apply() + } + + @JvmStatic + fun getInUseServerAddress(): String? { + return App.getInstance().preferences.getString(IN_USE_SERVER_ADDRESS, null) + ?.takeIf { it.isNotBlank() } + ?: getServer() + } + + @JvmStatic + fun isInUseServerAddressLocal(): Boolean { + return getInUseServerAddress() == getLocalAddress() + } + + @JvmStatic + fun switchInUseServerAddress() { + val inUseAddress = if (getInUseServerAddress() == getServer()) getLocalAddress() else getServer() + App.getInstance().preferences.edit().putString(IN_USE_SERVER_ADDRESS, inUseAddress).apply() + } + + @JvmStatic + fun isServerSwitchable(): Boolean { + return App.getInstance().preferences.getLong( + NEXT_SERVER_SWITCH, 0 + ) + 15000 < System.currentTimeMillis() && !getServer().isNullOrEmpty() && !getLocalAddress().isNullOrEmpty() + } + + @JvmStatic + fun setServerSwitchableTimer() { + App.getInstance().preferences.edit().putLong(NEXT_SERVER_SWITCH, System.currentTimeMillis()).apply() + } + + @JvmStatic + fun askForOptimization(): Boolean { + return App.getInstance().preferences.getBoolean(BATTERY_OPTIMIZATION, true) + } + + @JvmStatic + fun dontAskForOptimization() { + App.getInstance().preferences.edit().putBoolean(BATTERY_OPTIMIZATION, false).apply() + } + + @JvmStatic + fun getPlaybackSpeed(): Float { + return App.getInstance().preferences.getFloat(PLAYBACK_SPEED, 1f) + } + + @JvmStatic + fun setPlaybackSpeed(playbackSpeed: Float) { + App.getInstance().preferences.edit().putFloat(PLAYBACK_SPEED, playbackSpeed).apply() + } + + @JvmStatic + fun isSkipSilenceMode(): Boolean { + return App.getInstance().preferences.getBoolean(SKIP_SILENCE, false) + } + + @JvmStatic + fun setSkipSilenceMode(isSkipSilenceMode: Boolean) { + App.getInstance().preferences.edit().putBoolean(SKIP_SILENCE, isSkipSilenceMode).apply() + } + + @JvmStatic + fun isShuffleModeEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(SHUFFLE_MODE, false) + } + + @JvmStatic + fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) { + App.getInstance().preferences.edit().putBoolean(SHUFFLE_MODE, shuffleModeEnabled).apply() + } + + @JvmStatic + fun getRepeatMode(): Int { + return App.getInstance().preferences.getInt(REPEAT_MODE, Player.REPEAT_MODE_OFF) + } + + @JvmStatic + fun setRepeatMode(repeatMode: Int) { + App.getInstance().preferences.edit().putInt(REPEAT_MODE, repeatMode).apply() + } + + @JvmStatic + fun getImageCacheSize(): Int { + return App.getInstance().preferences.getString(IMAGE_CACHE_SIZE, "500")!!.toInt() + } + + @JvmStatic + fun getImageSize(): Int { + return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt() + } + + @JvmStatic + fun getStreamingCacheSize(): Long { + return App.getInstance().preferences.getString(STREAMING_CACHE_SIZE, "256")!!.toLong() + } + + @JvmStatic + fun getMaxBitrateWifi(): String { + return App.getInstance().preferences.getString(MAX_BITRATE_WIFI, "0")!! + } + + @JvmStatic + fun getMaxBitrateMobile(): String { + return App.getInstance().preferences.getString(MAX_BITRATE_MOBILE, "0")!! + } + + @JvmStatic + fun getAudioTranscodeFormatWifi(): String { + return App.getInstance().preferences.getString(AUDIO_TRANSCODE_FORMAT_WIFI, "raw")!! + } + + @JvmStatic + fun getAudioTranscodeFormatMobile(): String { + return App.getInstance().preferences.getString(AUDIO_TRANSCODE_FORMAT_MOBILE, "raw")!! + } + + @JvmStatic + fun isWifiOnly(): Boolean { + return App.getInstance().preferences.getBoolean(WIFI_ONLY, false) + } + + @JvmStatic + fun isDataSavingMode(): Boolean { + return App.getInstance().preferences.getBoolean(DATA_SAVING_MODE, false) + } + + @JvmStatic + fun setDataSavingMode(isDataSavingModeEnabled: Boolean) { + App.getInstance().preferences.edit().putBoolean(DATA_SAVING_MODE, isDataSavingModeEnabled) + .apply() + } + + @JvmStatic + fun isStarredArtistsSyncEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(SYNC_STARRED_ARTISTS_FOR_OFFLINE_USE, false) + } + + @JvmStatic + fun setStarredArtistsSyncEnabled(isStarredSyncEnabled: Boolean) { + App.getInstance().preferences.edit().putBoolean( + SYNC_STARRED_ARTISTS_FOR_OFFLINE_USE, isStarredSyncEnabled + ).apply() + } + + @JvmStatic + fun isStarredAlbumsSyncEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, false) + } + + @JvmStatic + fun setStarredAlbumsSyncEnabled(isStarredSyncEnabled: Boolean) { + App.getInstance().preferences.edit().putBoolean( + SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, isStarredSyncEnabled + ).apply() + } + + @JvmStatic + fun isStarredSyncEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(SYNC_STARRED_TRACKS_FOR_OFFLINE_USE, false) + } + + @JvmStatic + fun setStarredSyncEnabled(isStarredSyncEnabled: Boolean) { + App.getInstance().preferences.edit().putBoolean( + SYNC_STARRED_TRACKS_FOR_OFFLINE_USE, isStarredSyncEnabled + ).apply() + } + + @JvmStatic + fun showShuffleInsteadOfHeart(): Boolean { + return App.getInstance().preferences.getBoolean(MINI_SHUFFLE_BUTTON_VISIBILITY, false) + } + + @JvmStatic + fun setShuffleInsteadOfHeart(enabled: Boolean) { + App.getInstance().preferences.edit().putBoolean(MINI_SHUFFLE_BUTTON_VISIBILITY, enabled).apply() + } + + @JvmStatic + fun showServerUnreachableDialog(): Boolean { + return App.getInstance().preferences.getLong( + SERVER_UNREACHABLE, 0 + ) + 86400000 < System.currentTimeMillis() + } + + @JvmStatic + fun setServerUnreachableDatetime() { + App.getInstance().preferences.edit().putLong(SERVER_UNREACHABLE, System.currentTimeMillis()).apply() + } + + @JvmStatic + fun isSyncronizationEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(QUEUE_SYNCING, false) + } + + @JvmStatic + fun getSyncCountdownTimer(): Int { + return App.getInstance().preferences.getString(QUEUE_SYNCING_COUNTDOWN, "5")!!.toInt() + } + + @JvmStatic + fun isCornerRoundingEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(ROUNDED_CORNER, false) + } + + @JvmStatic + fun getRoundedCornerSize(): Int { + return App.getInstance().preferences.getString(ROUNDED_CORNER_SIZE, "12")!!.toInt() + } + + @JvmStatic + fun isPodcastSectionVisible(): Boolean { + return App.getInstance().preferences.getBoolean(PODCAST_SECTION_VISIBILITY, true) + } + + @JvmStatic + fun setPodcastSectionHidden() { + App.getInstance().preferences.edit().putBoolean(PODCAST_SECTION_VISIBILITY, false).apply() + } + + @JvmStatic + fun isRadioSectionVisible(): Boolean { + return App.getInstance().preferences.getBoolean(RADIO_SECTION_VISIBILITY, true) + } + + @JvmStatic + fun setRadioSectionHidden() { + App.getInstance().preferences.edit().putBoolean(RADIO_SECTION_VISIBILITY, false).apply() + } + + @JvmStatic + fun isMusicDirectorySectionVisible(): Boolean { + return App.getInstance().preferences.getBoolean(MUSIC_DIRECTORY_SECTION_VISIBILITY, true) + } + + @JvmStatic + fun getReplayGainMode(): String? { + return App.getInstance().preferences.getString(REPLAY_GAIN_MODE, "disabled") + } + + @JvmStatic + fun isServerPrioritized(): Boolean { + return App.getInstance().preferences.getBoolean(AUDIO_TRANSCODE_PRIORITY, false) + } + + @JvmStatic + fun getStreamingCacheStoragePreference(): Int { + return App.getInstance().preferences.getString(STREAMING_CACHE_STORAGE, "0")!!.toInt() + } + + @JvmStatic + fun setStreamingCacheStoragePreference(streamingCachePreference: Int) { + return App.getInstance().preferences.edit().putString( + STREAMING_CACHE_STORAGE, + streamingCachePreference.toString() + ).apply() + } + + @JvmStatic + fun getDownloadStoragePreference(): Int { + return App.getInstance().preferences.getString(DOWNLOAD_STORAGE, "0")!!.toInt() + } + + @JvmStatic + fun setDownloadStoragePreference(storagePreference: Int) { + return App.getInstance().preferences.edit().putString( + DOWNLOAD_STORAGE, + storagePreference.toString() + ).apply() + } + + @JvmStatic + fun getDownloadDirectoryUri(): String? { + return App.getInstance().preferences.getString(DOWNLOAD_DIRECTORY_URI, null) + } + + @JvmStatic + fun setDownloadDirectoryUri(uri: String?) { + val current = App.getInstance().preferences.getString(DOWNLOAD_DIRECTORY_URI, null) + if (current != uri) { + ExternalDownloadMetadataStore.clear() + } + App.getInstance().preferences.edit().putString(DOWNLOAD_DIRECTORY_URI, uri).apply() + } + + @JvmStatic + fun getDefaultDownloadViewType(): String { + return App.getInstance().preferences.getString( + DEFAULT_DOWNLOAD_VIEW_TYPE, + Constants.DOWNLOAD_TYPE_TRACK + )!! + } + + @JvmStatic + fun setDefaultDownloadViewType(viewType: String) { + return App.getInstance().preferences.edit().putString( + DEFAULT_DOWNLOAD_VIEW_TYPE, + viewType + ).apply() + } + + @JvmStatic + fun preferTranscodedDownload(): Boolean { + return App.getInstance().preferences.getBoolean(AUDIO_TRANSCODE_DOWNLOAD, false) + } + + @JvmStatic + fun isServerPrioritizedInTranscodedDownload(): Boolean { + return App.getInstance().preferences.getBoolean(AUDIO_TRANSCODE_DOWNLOAD_PRIORITY, false) + } + + @JvmStatic + fun getBitrateTranscodedDownload(): String { + return App.getInstance().preferences.getString(MAX_BITRATE_DOWNLOAD, "0")!! + } + + @JvmStatic + fun getAudioTranscodeFormatTranscodedDownload(): String { + return App.getInstance().preferences.getString(AUDIO_TRANSCODE_FORMAT_DOWNLOAD, "raw")!! + } + + @JvmStatic + fun isSharingEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(SHARE, false) + } + + @JvmStatic + fun isScrobblingEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(SCROBBLING, true) + } + + @JvmStatic + fun askForEstimateContentLength(): Boolean { + return App.getInstance().preferences.getBoolean(ESTIMATE_CONTENT_LENGTH, false) + } + + @JvmStatic + fun getBufferingStrategy(): Double { + return App.getInstance().preferences.getString(BUFFERING_STRATEGY, "1")!!.toDouble() + } + + @JvmStatic + fun getMinStarRatingAccepted(): Int { + return App.getInstance().preferences.getInt(MIN_STAR_RATING, 0) + } + + @JvmStatic + fun isDisplayAlwaysOn(): Boolean { + return App.getInstance().preferences.getBoolean(ALWAYS_ON_DISPLAY, false) + } + + @JvmStatic + fun showAudioQuality(): Boolean { + return App.getInstance().preferences.getBoolean(AUDIO_QUALITY_PER_ITEM, false) + } + + @JvmStatic + fun getHomeSectorList(): String? { + return App.getInstance().preferences.getString(HOME_SECTOR_LIST, null) + } + + @JvmStatic + fun setHomeSectorList(extension: List?) { + App.getInstance().preferences.edit().putString(HOME_SECTOR_LIST, Gson().toJson(extension)).apply() + } + + @JvmStatic + fun showItemStarRating(): Boolean { + return App.getInstance().preferences.getBoolean(SONG_RATING_PER_ITEM, false) + } + + @JvmStatic + fun showItemRating(): Boolean { + return App.getInstance().preferences.getBoolean(RATING_PER_ITEM, false) + } + + + @JvmStatic + fun isGithubUpdateEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(GITHUB_UPDATE_CHECK, true) + } + + @JvmStatic + fun showTempusUpdateDialog(): Boolean { + return App.getInstance().preferences.getLong( + NEXT_UPDATE_CHECK, 0 + ) + 86400000 < System.currentTimeMillis() + } + + @JvmStatic + fun setTempusUpdateReminder() { + App.getInstance().preferences.edit().putLong(NEXT_UPDATE_CHECK, System.currentTimeMillis()).apply() + } + + @JvmStatic + fun isContinuousPlayEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(CONTINUOUS_PLAY, true) + } + + @JvmStatic + fun setLastInstantMix() { + App.getInstance().preferences.edit().putLong(LAST_INSTANT_MIX, System.currentTimeMillis()).apply() + } + + @JvmStatic + fun isInstantMixUsable(): Boolean { + return App.getInstance().preferences.getLong( + LAST_INSTANT_MIX, 0 + ) + 5000 < System.currentTimeMillis() + } + + @JvmStatic + fun setAllowPlaylistDuplicates(allowDuplicates: Boolean) { + return App.getInstance().preferences.edit().putString( + ALLOW_PLAYLIST_DUPLICATES, + allowDuplicates.toString() + ).apply() + } + + @JvmStatic + fun allowPlaylistDuplicates(): Boolean { + return App.getInstance().preferences.getBoolean(ALLOW_PLAYLIST_DUPLICATES, false) + } + + @JvmStatic + fun setEqualizerEnabled(enabled: Boolean) { + App.getInstance().preferences.edit().putBoolean(EQUALIZER_ENABLED, enabled).apply() + } + + @JvmStatic + fun isEqualizerEnabled(): Boolean { + return App.getInstance().preferences.getBoolean(EQUALIZER_ENABLED, false) + } + + @JvmStatic + fun setEqualizerBandLevels(bandLevels: ShortArray) { + val asString = bandLevels.joinToString(",") + App.getInstance().preferences.edit().putString(EQUALIZER_BAND_LEVELS, asString).apply() + } + + @JvmStatic + fun getEqualizerBandLevels(bandCount: Short): ShortArray { + val str = App.getInstance().preferences.getString(EQUALIZER_BAND_LEVELS, null) + if (str.isNullOrBlank()) { + return ShortArray(bandCount.toInt()) + } + val parts = str.split(",") + if (parts.size < bandCount) return ShortArray(bandCount.toInt()) + return ShortArray(bandCount.toInt()) { i -> parts[i].toShortOrNull() ?: 0 } + } + + @JvmStatic + fun showAlbumDetail(): Boolean { + return App.getInstance().preferences.getBoolean(ALBUM_DETAIL, false) + } + + @JvmStatic + fun getAlbumSortOrder(): String { + return App.getInstance().preferences.getString(ALBUM_SORT_ORDER, DEFAULT_ALBUM_SORT_ORDER) ?: DEFAULT_ALBUM_SORT_ORDER + } + + @JvmStatic + fun setAlbumSortOrder(sortOrder: String) { + App.getInstance().preferences.edit().putString(ALBUM_SORT_ORDER, sortOrder).apply() + } + + @JvmStatic + fun getArtistSortOrder(): String { + val sort_by_album_count = App.getInstance().preferences.getBoolean(ARTIST_SORT_BY_ALBUM_COUNT, false) + Log.d("Preferences", "getSortOrder") + if (sort_by_album_count) + return Constants.ARTIST_ORDER_BY_ALBUM_COUNT + else + return Constants.ARTIST_ORDER_BY_NAME + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/ReplayGainUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/ReplayGainUtil.java new file mode 100644 index 0000000..510673c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/ReplayGainUtil.java @@ -0,0 +1,178 @@ +package com.cappielloantonio.tempo.util; + +import androidx.annotation.OptIn; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Metadata; +import androidx.media3.common.Tracks; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.ExoPlayer; + +import com.cappielloantonio.tempo.model.ReplayGain; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@OptIn(markerClass = UnstableApi.class) +public class ReplayGainUtil { + private static final String[] tags = {"REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN", "R128_TRACK_GAIN", "R128_ALBUM_GAIN"}; + + public static void setReplayGain(ExoPlayer player, Tracks tracks) { + List metadata = getMetadata(tracks); + List gains = getReplayGains(metadata); + + applyReplayGain(player, gains); + } + + private static List getMetadata(Tracks tracks) { + List metadata = new ArrayList<>(); + + if (tracks != null && !tracks.getGroups().isEmpty()) { + for (int i = 0; i < tracks.getGroups().size(); i++) { + Tracks.Group group = tracks.getGroups().get(i); + + if (group != null && group.getMediaTrackGroup() != null) { + for (int j = 0; j < group.getMediaTrackGroup().length; j++) { + metadata.add(group.getTrackFormat(j).metadata); + } + } + } + } + + return metadata; + } + + private static List getReplayGains(List metadata) { + List gains = new ArrayList<>(); + + if (metadata != null) { + for (int i = 0; i < metadata.size(); i++) { + Metadata singleMetadata = metadata.get(i); + + if (singleMetadata != null) { + for (int j = 0; j < singleMetadata.length(); j++) { + Metadata.Entry entry = singleMetadata.get(j); + + if (checkReplayGain(entry)) { + ReplayGain replayGain = setReplayGains(entry); + gains.add(replayGain); + } + } + } + } + } + + if (gains.size() == 0) gains.add(0, new ReplayGain()); + if (gains.size() == 1) gains.add(1, new ReplayGain()); + + return gains; + } + + private static boolean checkReplayGain(Metadata.Entry entry) { + for (String tag : tags) { + if (entry.toString().contains(tag)) { + return true; + } + } + + return false; + } + + private static ReplayGain setReplayGains(Metadata.Entry entry) { + ReplayGain replayGain = new ReplayGain(); + + if (entry.toString().contains(tags[0])) { + replayGain.setTrackGain(parseReplayGainTag(entry)); + } + + if (entry.toString().contains(tags[1])) { + replayGain.setAlbumGain(parseReplayGainTag(entry)); + } + + if (entry.toString().contains(tags[2])) { + replayGain.setTrackGain(parseReplayGainTag(entry) / 256f); + } + + if (entry.toString().contains(tags[3])) { + replayGain.setAlbumGain(parseReplayGainTag(entry) / 256f); + } + + return replayGain; + } + + private static Float parseReplayGainTag(Metadata.Entry entry) { + try { + return Float.parseFloat(entry.toString().replaceAll("[^\\d.-]", "")); + } catch (NumberFormatException exception) { + return 0f; + } + } + + private static void applyReplayGain(ExoPlayer player, List gains) { + if (Objects.equals(Preferences.getReplayGainMode(), "disabled") || gains == null || gains.isEmpty()) { + setNoReplayGain(player); + return; + } + + if (Objects.equals(Preferences.getReplayGainMode(), "auto")) { + if (areTracksConsecutive(player)) { + setAutoReplayGain(player, gains); + } else { + setTrackReplayGain(player, gains); + } + + return; + } + + if (Objects.equals(Preferences.getReplayGainMode(), "track")) { + setTrackReplayGain(player, gains); + return; + } + + if (Objects.equals(Preferences.getReplayGainMode(), "album")) { + setAlbumReplayGain(player, gains); + return; + } + + setNoReplayGain(player); + } + + private static void setNoReplayGain(ExoPlayer player) { + setReplayGain(player, 0f); + } + + private static void setTrackReplayGain(ExoPlayer player, List gains) { + float trackGain = gains.get(0).getTrackGain() != 0f ? gains.get(0).getTrackGain() : gains.get(1).getTrackGain(); + + setReplayGain(player, trackGain != 0f ? trackGain : 0f); + } + + private static void setAlbumReplayGain(ExoPlayer player, List gains) { + float albumGain = gains.get(0).getAlbumGain() != 0f ? gains.get(0).getAlbumGain() : gains.get(1).getAlbumGain(); + + setReplayGain(player, albumGain != 0f ? albumGain : 0f); + } + + private static void setAutoReplayGain(ExoPlayer player, List gains) { + float albumGain = gains.get(0).getAlbumGain() != 0f ? gains.get(0).getAlbumGain() : gains.get(1).getAlbumGain(); + float trackGain = gains.get(0).getTrackGain() != 0f ? gains.get(0).getTrackGain() : gains.get(1).getTrackGain(); + + setReplayGain(player, albumGain != 0f ? albumGain : trackGain); + } + + private static boolean areTracksConsecutive(ExoPlayer player) { + MediaItem currentMediaItem = player.getCurrentMediaItem(); + int currentMediaItemIndex = player.getCurrentMediaItemIndex(); + MediaItem pastMediaItem = currentMediaItemIndex > 0 ? player.getMediaItemAt(currentMediaItemIndex - 1) : null; + + return currentMediaItem != null && + pastMediaItem != null && + pastMediaItem.mediaMetadata.albumTitle != null && + currentMediaItem.mediaMetadata.albumTitle != null && + pastMediaItem.mediaMetadata.albumTitle.toString().equals(currentMediaItem.mediaMetadata.albumTitle.toString()); + } + + private static void setReplayGain(ExoPlayer player, float gain) { + player.setVolume((float) Math.pow(10f, gain / 20f)); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/StreamingCacheDataSource.kt b/app/src/main/java/com/cappielloantonio/tempo/util/StreamingCacheDataSource.kt new file mode 100644 index 0000000..68b52cb --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/StreamingCacheDataSource.kt @@ -0,0 +1,62 @@ +package com.cappielloantonio.tempo.util + +import android.net.Uri +import android.util.Log +import androidx.media3.common.C +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DataSpec +import androidx.media3.datasource.TransferListener +import androidx.media3.datasource.cache.CacheDataSource +import androidx.media3.datasource.cache.ContentMetadata + +@UnstableApi +class StreamingCacheDataSource private constructor( + private val cacheDataSource: CacheDataSource, +): DataSource { + private val TAG = "StreamingCacheDataSource" + + private var currentDataSpec: DataSpec? = null + + class Factory(private val cacheDatasourceFactory: CacheDataSource.Factory): DataSource.Factory { + override fun createDataSource(): DataSource { + return StreamingCacheDataSource(cacheDatasourceFactory.createDataSource()) + } + } + + override fun read(buffer: ByteArray, offset: Int, length: Int): Int { + return cacheDataSource.read(buffer, offset, length) + } + + override fun addTransferListener(transferListener: TransferListener) { + return cacheDataSource.addTransferListener(transferListener) + } + + override fun open(dataSpec: DataSpec): Long { + val ret = cacheDataSource.open(dataSpec) + currentDataSpec = dataSpec + return ret + } + + override fun getUri(): Uri? { + return cacheDataSource.uri + } + + override fun close() { + cacheDataSource.close() + + val dataSpec = currentDataSpec + + if (dataSpec != null) { + val cacheKey = cacheDataSource.cacheKeyFactory.buildCacheKey(dataSpec) + val contentLength = ContentMetadata.getContentLength(cacheDataSource.cache.getContentMetadata(cacheKey)); + + if (contentLength == C.LENGTH_UNSET.toLong()) { + Log.d(TAG, "Removing partial cache for $cacheKey") + cacheDataSource.cache.removeResource(cacheKey) + } else { + Log.d(TAG, "Key $cacheKey has been fully cached") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/UIUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/UIUtil.java new file mode 100644 index 0000000..2184c10 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/UIUtil.java @@ -0,0 +1,115 @@ +package com.cappielloantonio.tempo.util; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; + +import androidx.core.os.LocaleListCompat; +import androidx.recyclerview.widget.DividerItemDecoration; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.R; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class UIUtil { + public static int getSpanCount(int itemCount, int maxSpan) { + int itemSize = itemCount == 0 ? 1 : itemCount; + + if (itemSize / maxSpan > 0) { + return maxSpan; + } else { + return itemSize % maxSpan; + } + } + + public static DividerItemDecoration getDividerItemDecoration(Context context) { + int[] ATTRS = new int[]{android.R.attr.listDivider}; + + TypedArray a = context.obtainStyledAttributes(ATTRS); + Drawable divider = a.getDrawable(0); + InsetDrawable insetDivider = new InsetDrawable(divider, 42, 0, 42, 42); + a.recycle(); + + DividerItemDecoration itemDecoration = new DividerItemDecoration(context, DividerItemDecoration.VERTICAL); + itemDecoration.setDrawable(insetDivider); + + return itemDecoration; + } + + private static LocaleListCompat getLocalesFromResources(Context context) { + final List tagsList = new ArrayList<>(); + + XmlPullParser xpp = context.getResources().getXml(R.xml.locale_config); + + try { + while (xpp.getEventType() != XmlPullParser.END_DOCUMENT) { + String tagName = xpp.getName(); + + if (xpp.getEventType() == XmlPullParser.START_TAG) { + if ("locale".equals(tagName) && xpp.getAttributeCount() > 0 && xpp.getAttributeName(0).equals("name")) { + tagsList.add(xpp.getAttributeValue(0)); + } + } + + xpp.next(); + } + } catch (XmlPullParserException | IOException e) { + e.printStackTrace(); + } + + return LocaleListCompat.forLanguageTags(String.join(",", tagsList)); + } + + public static Map getLangPreferenceDropdownEntries(Context context) { + LocaleListCompat localeList = getLocalesFromResources(context); + + List> localeArrayList = new ArrayList<>(); + + String systemDefaultLabel = App.getContext().getString(R.string.settings_system_language); + String systemDefaultValue = "default"; + + for (int i = 0; i < localeList.size(); i++) { + Locale locale = localeList.get(i); + if (locale != null) { + localeArrayList.add( + new AbstractMap.SimpleEntry<>( + Util.toPascalCase(locale.getDisplayName()), + locale.toLanguageTag() + ) + ); + } + } + + localeArrayList.sort(Map.Entry.comparingByKey(String.CASE_INSENSITIVE_ORDER)); + + LinkedHashMap orderedMap = new LinkedHashMap<>(); + orderedMap.put(systemDefaultLabel, systemDefaultValue); + for (Map.Entry entry : localeArrayList) { + orderedMap.put(entry.getKey(), entry.getValue()); + } + + return orderedMap; + } + + public static String getReadableDate(Date date) { + if (date == null) { + return App.getContext().getString(R.string.share_no_expiration); + } + SimpleDateFormat formatter = new SimpleDateFormat("dd MMM, yyyy", Locale.getDefault()); + return formatter.format(date); + } + +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Util.java b/app/src/main/java/com/cappielloantonio/tempo/util/Util.java new file mode 100644 index 0000000..822238b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Util.java @@ -0,0 +1,64 @@ +package com.cappielloantonio.tempo.util; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; + +public class Util { + public static Predicate distinctByKey(Function keyExtractor) { + try { + Map uniqueMap = new ConcurrentHashMap<>(); + return t -> uniqueMap.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; + } catch (NullPointerException exception) { + return null; + } + } + + public static String toPascalCase(String name) { + if (name == null || name.isEmpty()) { + return name; + } + + StringBuilder pascalCase = new StringBuilder(); + + char newChar; + boolean toUpper = false; + char[] charArray = name.toCharArray(); + + for (int ctr = 0; ctr <= charArray.length - 1; ctr++) { + if (ctr == 0) { + newChar = Character.toUpperCase(charArray[ctr]); + pascalCase = new StringBuilder(Character.toString(newChar)); + continue; + } + + if (charArray[ctr] == '_') { + toUpper = true; + continue; + } + + if (toUpper) { + newChar = Character.toUpperCase(charArray[ctr]); + pascalCase.append(newChar); + toUpper = false; + continue; + } + + pascalCase.append(charArray[ctr]); + } + + return pascalCase.toString(); + } + + public static String encode(String value) { + try { + return URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException ex) { + return value; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java new file mode 100644 index 0000000..da1ec83 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumBottomSheetViewModel.java @@ -0,0 +1,132 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; + +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.interfaces.StarCallback; +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.FavoriteRepository; +import com.cappielloantonio.tempo.repository.SharingRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Share; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.NetworkUtil; +import com.cappielloantonio.tempo.util.Preferences; + +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +public class AlbumBottomSheetViewModel extends AndroidViewModel { + private final AlbumRepository albumRepository; + private final ArtistRepository artistRepository; + private final FavoriteRepository favoriteRepository; + private final SharingRepository sharingRepository; + + private AlbumID3 album; + + public AlbumBottomSheetViewModel(@NonNull Application application) { + super(application); + + albumRepository = new AlbumRepository(); + artistRepository = new ArtistRepository(); + favoriteRepository = new FavoriteRepository(); + sharingRepository = new SharingRepository(); + } + + public AlbumID3 getAlbum() { + return album; + } + + public void setAlbum(AlbumID3 album) { + this.album = album; + } + + public LiveData getArtist() { + return artistRepository.getArtist(album.getArtistId()); + } + + public MutableLiveData> getAlbumTracks() { + return albumRepository.getAlbumTracks(album.getId()); + } + + public void setFavorite(Context context) { + if (album.getStarred() != null) { + if (NetworkUtil.isOffline()) { + removeFavoriteOffline(); + } else { + removeFavoriteOnline(); + } + } else { + if (NetworkUtil.isOffline()) { + setFavoriteOffline(); + } else { + setFavoriteOnline(context); + } + } + } + + public MutableLiveData shareAlbum() { + return sharingRepository.createShare(album.getId(), album.getName(), null); + } + + private void removeFavoriteOffline() { + favoriteRepository.starLater(null, album.getId(), null, false); + album.setStarred(null); + } + + private void removeFavoriteOnline() { + favoriteRepository.unstar(null, album.getId(), null, new StarCallback() { + @Override + public void onError() { + favoriteRepository.starLater(null, album.getId(), null, false); + } + }); + + album.setStarred(null); + } + + private void setFavoriteOffline() { + favoriteRepository.starLater(null, album.getId(), null, true); + album.setStarred(new Date()); + } + + private void setFavoriteOnline(Context context) { + favoriteRepository.star(null, album.getId(), null, new StarCallback() { + @Override + public void onError() { + favoriteRepository.starLater(null, album.getId(), null, true); + } + }); + + album.setStarred(new Date()); + if (Preferences.isStarredAlbumsSyncEnabled()) { + AlbumRepository albumRepository = new AlbumRepository(); + MutableLiveData> tracksLiveData = albumRepository.getAlbumTracks(album.getId()); + + tracksLiveData.observeForever(new Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null && !songs.isEmpty()) { + DownloadUtil.getDownloadTracker(context).download( + MappingUtil.mapDownloads(songs), + songs.stream().map(Download::new).collect(Collectors.toList()) + ); + } + tracksLiveData.removeObserver(this); + } + }); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumCatalogueViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumCatalogueViewModel.java new file mode 100644 index 0000000..a18e645 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumCatalogueViewModel.java @@ -0,0 +1,105 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.interfaces.MediaCallback; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; + +public class AlbumCatalogueViewModel extends AndroidViewModel { + private final MutableLiveData> albumList = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData loading = new MutableLiveData<>(true); + + private int page = 0; + private Status status = Status.STOPPED; + + public AlbumCatalogueViewModel(@NonNull Application application) { + super(application); + } + + public LiveData> getAlbumList() { + return albumList; + } + + public LiveData getLoadingStatus() { + return loading; + } + + public void loadAlbums() { + page = 0; + status = Status.RUNNING; + albumList.setValue(new ArrayList<>()); + loadAlbums(500); + } + + public void stopLoading() { + status = Status.STOPPED; + } + + private void loadAlbums(int size) { + retrieveAlbums(new MediaCallback() { + @Override + public void onError(Exception exception) { + } + + @Override + public void onLoadMedia(List media) { + if (status == Status.STOPPED) { + loading.setValue(false); + return; + } + + List liveAlbum = albumList.getValue(); + + liveAlbum.addAll((List) media); + albumList.setValue(liveAlbum); + + if (media.size() == size) { + loadAlbums(size); + loading.setValue(true); + } else { + status = Status.STOPPED; + loading.setValue(false); + } + } + }, size, size * page++); + } + + + private void retrieveAlbums(MediaCallback callback, int size, int offset) { + App.getSubsonicClientInstance(false) + .getAlbumSongListClient() + .getAlbumList2("alphabeticalByName", size, offset, null, null) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) { + List albumList = new ArrayList<>(response.body().getSubsonicResponse().getAlbumList2().getAlbums()); + callback.onLoadMedia(albumList); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + callback.onError(new Exception(t.getMessage())); + } + }); + } + + private enum Status { + RUNNING, + STOPPED + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumListPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumListPageViewModel.java new file mode 100644 index 0000000..956ba6f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumListPageViewModel.java @@ -0,0 +1,67 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.repository.DownloadRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Comparator; +import java.util.List; + +public class AlbumListPageViewModel extends AndroidViewModel { + private final AlbumRepository albumRepository; + private final DownloadRepository downloadRepository; + + public String title; + public ArtistID3 artist; + + private MutableLiveData> albumList; + + public int maxNumber = 500; + + public AlbumListPageViewModel(@NonNull Application application) { + super(application); + + albumRepository = new AlbumRepository(); + downloadRepository = new DownloadRepository(); + } + + public LiveData> getAlbumList(LifecycleOwner owner) { + albumList = new MutableLiveData<>(new ArrayList<>()); + + switch (title) { + case Constants.ALBUM_RECENTLY_PLAYED: + albumRepository.getAlbums("recent", maxNumber, null, null).observe(owner, albums -> albumList.setValue(albums)); + break; + case Constants.ALBUM_MOST_PLAYED: + albumRepository.getAlbums("frequent", maxNumber, null, null).observe(owner, albums -> albumList.setValue(albums)); + break; + case Constants.ALBUM_RECENTLY_ADDED: + albumRepository.getAlbums("newest", maxNumber, null, null).observe(owner, albums -> albumList.setValue(albums)); + break; + case Constants.ALBUM_STARRED: + albumList = albumRepository.getStarredAlbums(false, -1); + break; + case Constants.ALBUM_NEW_RELEASES: + int currentYear = Calendar.getInstance().get(Calendar.YEAR); + albumRepository.getAlbums("byYear", maxNumber, currentYear, currentYear).observe(owner, albums -> { + albums.sort(Comparator.comparing(AlbumID3::getCreated).reversed()); + albumList.postValue(albums.subList(0, Math.min(20, albums.size()))); + }); + break; + } + + return albumList; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java new file mode 100644 index 0000000..0979f40 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumPageViewModel.java @@ -0,0 +1,59 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.AlbumInfo; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; + +import java.util.List; + +public class AlbumPageViewModel extends AndroidViewModel { + private final AlbumRepository albumRepository; + private final ArtistRepository artistRepository; + private String albumId; + private String artistId; + private final MutableLiveData album = new MutableLiveData<>(null); + + public AlbumPageViewModel(@NonNull Application application) { + super(application); + + albumRepository = new AlbumRepository(); + artistRepository = new ArtistRepository(); + } + + public LiveData> getAlbumSongLiveList() { + return albumRepository.getAlbumTracks(albumId); + } + + public MutableLiveData getAlbum() { + return album; + } + + public void setAlbum(LifecycleOwner owner, AlbumID3 album) { + this.albumId = album.getId(); + this.album.postValue(album); + this.artistId = album.getArtistId(); + + albumRepository.getAlbum(album.getId()).observe(owner, albums -> { + if (albums != null) this.album.setValue(albums); + }); + } + + public LiveData getArtist() { + return artistRepository.getArtistInfo(artistId); + } + + public LiveData getAlbumInfo() { + return albumRepository.getAlbumInfo(albumId); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistBottomSheetViewModel.java new file mode 100644 index 0000000..2c008d8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistBottomSheetViewModel.java @@ -0,0 +1,118 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.interfaces.StarCallback; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.FavoriteRepository; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.NetworkUtil; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.Preferences; + +import java.util.Date; +import java.util.stream.Collectors; +import java.util.List; + +public class ArtistBottomSheetViewModel extends AndroidViewModel { + private final ArtistRepository artistRepository; + private final FavoriteRepository favoriteRepository; + + private ArtistID3 artist; + + public ArtistBottomSheetViewModel(@NonNull Application application) { + super(application); + + artistRepository = new ArtistRepository(); + favoriteRepository = new FavoriteRepository(); + } + + public ArtistID3 getArtist() { + return artist; + } + + public void setArtist(ArtistID3 artist) { + this.artist = artist; + } + + public void setFavorite(Context context) { + if (artist.getStarred() != null) { + if (NetworkUtil.isOffline()) { + removeFavoriteOffline(); + } else { + removeFavoriteOnline(); + } + } else { + if (NetworkUtil.isOffline()) { + setFavoriteOffline(context); + } else { + setFavoriteOnline(context); + } + } + } + + private void removeFavoriteOffline() { + favoriteRepository.starLater(null, null, artist.getId(), false); + artist.setStarred(null); + } + + private void removeFavoriteOnline() { + favoriteRepository.unstar(null, null, artist.getId(), new StarCallback() { + @Override + public void onError() { + favoriteRepository.starLater(null, null, artist.getId(), false); + } + }); + + artist.setStarred(null); + } + + private void setFavoriteOffline(Context context) { + favoriteRepository.starLater(null, null, artist.getId(), true); + artist.setStarred(new Date()); + } + + private void setFavoriteOnline(Context context) { + favoriteRepository.star(null, null, artist.getId(), new StarCallback() { + @Override + public void onError() { + favoriteRepository.starLater(null, null, artist.getId(), true); + } + }); + + artist.setStarred(new Date()); + + Log.d("ArtistSync", "Checking preference: " + Preferences.isStarredArtistsSyncEnabled()); + + if (Preferences.isStarredArtistsSyncEnabled()) { + Log.d("ArtistSync", "Starting artist sync for: " + artist.getName()); + + artistRepository.getArtistAllSongs(artist.getId(), new ArtistRepository.ArtistSongsCallback() { + @Override + public void onSongsCollected(List songs) { + Log.d("ArtistSync", "Callback triggered with songs: " + (songs != null ? songs.size() : 0)); + if (songs != null && !songs.isEmpty()) { + Log.d("ArtistSync", "Starting download of " + songs.size() + " songs"); + DownloadUtil.getDownloadTracker(context).download( + MappingUtil.mapDownloads(songs), + songs.stream().map(Download::new).collect(Collectors.toList()) + ); + Log.d("ArtistSync", "Download started successfully"); + } else { + Log.d("ArtistSync", "No songs to download"); + } + } + }); + } else { + Log.d("ArtistSync", "Artist sync preference is disabled"); + } + } + /// +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistCatalogueViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistCatalogueViewModel.java new file mode 100644 index 0000000..76b5397 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistCatalogueViewModel.java @@ -0,0 +1,56 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.IndexID3; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; + +public class ArtistCatalogueViewModel extends AndroidViewModel { + private final MutableLiveData> artistList = new MutableLiveData<>(new ArrayList<>()); + + public ArtistCatalogueViewModel(@NonNull Application application) { + super(application); + } + + public LiveData> getArtistList() { + return artistList; + } + + public void loadArtists() { + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getArtists() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtists() != null) { + List artists = new ArrayList<>(); + + for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) { + artists.addAll(index.getArtists()); + } + + artistList.setValue(artists); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistListPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistListPageViewModel.java new file mode 100644 index 0000000..7d4d09a --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistListPageViewModel.java @@ -0,0 +1,61 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.DownloadRepository; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.TreeSet; +import java.util.stream.Collectors; + +public class ArtistListPageViewModel extends AndroidViewModel { + private final ArtistRepository artistRepository; + private final DownloadRepository downloadRepository; + + public String title; + + private MutableLiveData> artistList; + + public ArtistListPageViewModel(@NonNull Application application) { + super(application); + + artistRepository = new ArtistRepository(); + downloadRepository = new DownloadRepository(); + } + + public LiveData> getArtistList(LifecycleOwner owner) { + artistList = new MutableLiveData<>(new ArrayList<>()); + + switch (title) { + case Constants.ARTIST_STARRED: + artistList = artistRepository.getStarredArtists(false, -1); + break; + case Constants.ARTIST_DOWNLOADED: + downloadRepository.getLiveDownload().observe(owner, downloads -> { + List unique = downloads + .stream() + .collect(Collectors.collectingAndThen( + Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Download::getArtist))), ArrayList::new) + ); + + // TODO + // artistList.setValue(MappingUtil.mapDownloadToArtist(unique)); + }); + break; + } + + return artistList; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java new file mode 100644 index 0000000..a389cf7 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java @@ -0,0 +1,58 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2; +import com.cappielloantonio.tempo.subsonic.models.Child; + +import java.util.List; + +public class ArtistPageViewModel extends AndroidViewModel { + private final AlbumRepository albumRepository; + private final ArtistRepository artistRepository; + + private ArtistID3 artist; + + public ArtistPageViewModel(@NonNull Application application) { + super(application); + + albumRepository = new AlbumRepository(); + artistRepository = new ArtistRepository(); + } + + public LiveData> getAlbumList() { + return albumRepository.getArtistAlbums(artist.getId()); + } + + public LiveData getArtistInfo(String id) { + return artistRepository.getArtistFullInfo(id); + } + + public LiveData> getArtistTopSongList() { + return artistRepository.getTopSongs(artist.getName(), 20); + } + + public LiveData> getArtistShuffleList() { + return artistRepository.getRandomSong(artist, 50); + } + + public LiveData> getArtistInstantMix() { + return artistRepository.getInstantMix(artist, 20); + } + + public ArtistID3 getArtist() { + return artist; + } + + public void setArtist(ArtistID3 artist) { + this.artist = artist; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/DirectoryViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/DirectoryViewModel.java new file mode 100644 index 0000000..4fd7480 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/DirectoryViewModel.java @@ -0,0 +1,24 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.repository.DirectoryRepository; +import com.cappielloantonio.tempo.subsonic.models.Directory; + +public class DirectoryViewModel extends AndroidViewModel { + private final DirectoryRepository directoryRepository; + + public DirectoryViewModel(@NonNull Application application) { + super(application); + + directoryRepository = new DirectoryRepository(); + } + + public LiveData loadMusicDirectory(String id) { + return directoryRepository.getMusicDirectory(id); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/DownloadViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/DownloadViewModel.java new file mode 100644 index 0000000..6f69cee --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/DownloadViewModel.java @@ -0,0 +1,127 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.documentfile.provider.DocumentFile; + +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.model.DownloadStack; +import com.cappielloantonio.tempo.repository.DownloadRepository; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.util.Preferences; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class DownloadViewModel extends AndroidViewModel { + private static final String TAG = "DownloadViewModel"; + + private final DownloadRepository downloadRepository; + + private final MutableLiveData> downloadedTrackSample = new MutableLiveData<>(null); + private final MutableLiveData> viewStack = new MutableLiveData<>(null); + private final MutableLiveData refreshResult = new MutableLiveData<>(); + + public DownloadViewModel(@NonNull Application application) { + super(application); + + downloadRepository = new DownloadRepository(); + + initViewStack(new DownloadStack(Preferences.getDefaultDownloadViewType(), null)); + } + + public LiveData> getDownloadedTracks(LifecycleOwner owner) { + downloadRepository.getLiveDownload().observe(owner, downloads -> downloadedTrackSample.postValue(downloads.stream().map(download -> (Child) download).collect(Collectors.toList()))); + return downloadedTrackSample; + } + + public LiveData> getViewStack() { + return viewStack; + } + + public LiveData getRefreshResult() { + return refreshResult; + } + + public void initViewStack(DownloadStack level) { + ArrayList stack = new ArrayList<>(); + stack.add(level); + viewStack.setValue(stack); + } + + public void pushViewStack(DownloadStack level) { + ArrayList stack = viewStack.getValue(); + stack.add(level); + viewStack.setValue(stack); + } + + public void popViewStack() { + ArrayList stack = viewStack.getValue(); + stack.remove(stack.size() - 1); + viewStack.setValue(stack); + } + + public void refreshExternalDownloads() { + new Thread(() -> { + String directoryUri = Preferences.getDownloadDirectoryUri(); + if (directoryUri == null) { + refreshResult.postValue(-1); + return; + } + + List downloads = downloadRepository.getAllDownloads(); + if (downloads == null || downloads.isEmpty()) { + refreshResult.postValue(0); + return; + } + + ArrayList toRemove = new ArrayList<>(); + + for (Download download : downloads) { + String uriString = download.getDownloadUri(); + if (uriString == null || uriString.isEmpty()) { + continue; + } + + Uri uri = Uri.parse(uriString); + if (uri.getScheme() == null || !uri.getScheme().equalsIgnoreCase("content")) { + continue; + } + + DocumentFile file; + try { + file = DocumentFile.fromSingleUri(getApplication(), uri); + } catch (SecurityException exception) { + file = null; + } + + if (file == null || !file.exists()) { + toRemove.add(download); + } + } + + if (!toRemove.isEmpty()) { + ArrayList ids = new ArrayList<>(); + for (Download download : toRemove) { + ids.add(download.getId()); + ExternalAudioReader.removeMetadata(download); + } + + downloadRepository.delete(ids); + ExternalAudioReader.refreshCache(); + refreshResult.postValue(ids.size()); + } else { + refreshResult.postValue(0); + } + }).start(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/FilterViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/FilterViewModel.java new file mode 100644 index 0000000..92d5b66 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/FilterViewModel.java @@ -0,0 +1,48 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.repository.GenreRepository; +import com.cappielloantonio.tempo.subsonic.models.Genre; + +import java.util.ArrayList; +import java.util.List; + +public class FilterViewModel extends AndroidViewModel { + private final GenreRepository genreRepository; + + private final ArrayList selectedFiltersID = new ArrayList<>(); + private final ArrayList selectedFilters = new ArrayList<>(); + + public FilterViewModel(@NonNull Application application) { + super(application); + + genreRepository = new GenreRepository(); + } + + public LiveData> getGenreList() { + return genreRepository.getGenres(false, -1); + } + + public void addFilter(String filterID, String filterName) { + selectedFiltersID.add(filterID); + selectedFilters.add(filterName); + } + + public void removeFilter(String filterID, String filterName) { + selectedFiltersID.remove(filterID); + selectedFilters.remove(filterName); + } + + public ArrayList getFilters() { + return selectedFiltersID; + } + + public ArrayList getFilterNames() { + return selectedFilters; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/GenreCatalogueViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/GenreCatalogueViewModel.java new file mode 100644 index 0000000..3166d8e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/GenreCatalogueViewModel.java @@ -0,0 +1,26 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.repository.GenreRepository; +import com.cappielloantonio.tempo.subsonic.models.Genre; + +import java.util.List; + +public class GenreCatalogueViewModel extends AndroidViewModel { + private final GenreRepository genreRepository; + + public GenreCatalogueViewModel(@NonNull Application application) { + super(application); + + genreRepository = new GenreRepository(); + } + + public LiveData> getGenreList() { + return genreRepository.getGenres(false, -1); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeRearrangementViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeRearrangementViewModel.java new file mode 100644 index 0000000..9c34b29 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeRearrangementViewModel.java @@ -0,0 +1,78 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.model.HomeSector; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.List; + +public class HomeRearrangementViewModel extends AndroidViewModel { + private List sectors = new ArrayList<>(); + + public HomeRearrangementViewModel(@NonNull Application application) { + super(application); + } + + public List getHomeSectorList() { + if (sectors != null && !sectors.isEmpty()) return sectors; + + if (Preferences.getHomeSectorList() != null && !Preferences.getHomeSectorList().equals("null")) { + sectors = new Gson().fromJson( + Preferences.getHomeSectorList(), + new TypeToken>() { + }.getType() + ); + } else { + sectors = fillStandardHomeSectorList(); + } + + return sectors; + } + + public void orderSectorLiveListAfterSwap(List sectors) { + this.sectors = sectors; + } + + public void saveHomeSectorList(List sectors) { + Preferences.setHomeSectorList(sectors); + } + + public void resetHomeSectorList() { + Preferences.setHomeSectorList(null); + } + + public void closeDialog() { + sectors = null; + } + + private List fillStandardHomeSectorList() { + List sectors = new ArrayList<>(); + + sectors.add(new HomeSector(Constants.HOME_SECTOR_DISCOVERY, getApplication().getString(R.string.home_title_discovery), true, 1)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_MADE_FOR_YOU, getApplication().getString(R.string.home_title_made_for_you), true, 2)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_BEST_OF, getApplication().getString(R.string.home_title_best_of), true, 3)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_RADIO_STATION, getApplication().getString(R.string.home_title_radio_station), true, 4)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_TOP_SONGS, getApplication().getString(R.string.home_title_top_songs), true, 5)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_STARRED_TRACKS, getApplication().getString(R.string.home_title_starred_tracks), true, 6)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_STARRED_ALBUMS, getApplication().getString(R.string.home_title_starred_albums), true, 7)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_STARRED_ARTISTS, getApplication().getString(R.string.home_title_starred_artists), true, 8)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_NEW_RELEASES, getApplication().getString(R.string.home_title_new_releases), true, 9)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_FLASHBACK, getApplication().getString(R.string.home_title_flashback), true, 10)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_MOST_PLAYED, getApplication().getString(R.string.home_title_most_played), true, 11)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_LAST_PLAYED, getApplication().getString(R.string.home_title_last_played), true, 12)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_RECENTLY_ADDED, getApplication().getString(R.string.home_title_recently_added), true, 13)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_PINNED_PLAYLISTS, getApplication().getString(R.string.home_title_pinned_playlists), true, 14)); + sectors.add(new HomeSector(Constants.HOME_SECTOR_SHARED, getApplication().getString(R.string.home_title_shares), true, 15)); + + return sectors; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java new file mode 100644 index 0000000..2089ce2 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/HomeViewModel.java @@ -0,0 +1,470 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.interfaces.StarCallback; +import com.cappielloantonio.tempo.model.Chronology; +import com.cappielloantonio.tempo.model.Favorite; +import com.cappielloantonio.tempo.model.HomeSector; +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.ChronologyRepository; +import com.cappielloantonio.tempo.repository.FavoriteRepository; +import com.cappielloantonio.tempo.repository.PlaylistRepository; +import com.cappielloantonio.tempo.repository.SharingRepository; +import com.cappielloantonio.tempo.repository.SongRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Playlist; +import com.cappielloantonio.tempo.subsonic.models.Share; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +public class HomeViewModel extends AndroidViewModel { + private static final String TAG = "HomeViewModel"; + + private final SongRepository songRepository; + private final AlbumRepository albumRepository; + private final ArtistRepository artistRepository; + private final ChronologyRepository chronologyRepository; + private final FavoriteRepository favoriteRepository; + private final PlaylistRepository playlistRepository; + private final SharingRepository sharingRepository; + + private final StarredAlbumsSyncViewModel albumsSyncViewModel; + private final StarredArtistsSyncViewModel artistSyncViewModel; + + private final MutableLiveData> dicoverSongSample = new MutableLiveData<>(null); + private final MutableLiveData> newReleasedAlbum = new MutableLiveData<>(null); + private final MutableLiveData> starredTracksSample = new MutableLiveData<>(null); + private final MutableLiveData> starredArtistsSample = new MutableLiveData<>(null); + private final MutableLiveData> bestOfArtists = new MutableLiveData<>(null); + private final MutableLiveData> starredTracks = new MutableLiveData<>(null); + private final MutableLiveData> starredAlbums = new MutableLiveData<>(null); + private final MutableLiveData> starredArtists = new MutableLiveData<>(null); + private final MutableLiveData> mostPlayedAlbumSample = new MutableLiveData<>(null); + private final MutableLiveData> recentlyPlayedAlbumSample = new MutableLiveData<>(null); + private final MutableLiveData> years = new MutableLiveData<>(null); + private final MutableLiveData> recentlyAddedAlbumSample = new MutableLiveData<>(null); + + private final MutableLiveData> thisGridTopSong = new MutableLiveData<>(null); + private final MutableLiveData> mediaInstantMix = new MutableLiveData<>(null); + private final MutableLiveData> artistInstantMix = new MutableLiveData<>(null); + private final MutableLiveData> artistBestOf = new MutableLiveData<>(null); + private final MutableLiveData> pinnedPlaylists = new MutableLiveData<>(null); + private final MutableLiveData> shares = new MutableLiveData<>(null); + + private List sectors; + + public HomeViewModel(@NonNull Application application) { + super(application); + + setHomeSectorList(); + + songRepository = new SongRepository(); + albumRepository = new AlbumRepository(); + artistRepository = new ArtistRepository(); + chronologyRepository = new ChronologyRepository(); + favoriteRepository = new FavoriteRepository(); + playlistRepository = new PlaylistRepository(); + sharingRepository = new SharingRepository(); + + albumsSyncViewModel = new StarredAlbumsSyncViewModel(application); + artistSyncViewModel = new StarredArtistsSyncViewModel(application); + + setOfflineFavorite(); + } + + public LiveData> getDiscoverSongSample(LifecycleOwner owner) { + if (dicoverSongSample.getValue() == null) { + songRepository.getRandomSample(10, null, null).observe(owner, dicoverSongSample::postValue); + } + + return dicoverSongSample; + } + + public LiveData> getRandomShuffleSample() { + return songRepository.getRandomSample(1000, null, null); + } + + public LiveData> getChronologySample(LifecycleOwner owner) { + Calendar cal = Calendar.getInstance(); + String server = Preferences.getServerId(); + + int currentWeek = cal.get(Calendar.WEEK_OF_YEAR); + long start = cal.getTimeInMillis(); + + cal.set(Calendar.WEEK_OF_YEAR, currentWeek - 1); + long end = cal.getTimeInMillis(); + + chronologyRepository.getChronology(server, start, end).observe(owner, thisGridTopSong::postValue); + return thisGridTopSong; + } + + public LiveData> getRecentlyReleasedAlbums(LifecycleOwner owner) { + if (newReleasedAlbum.getValue() == null) { + int currentYear = Calendar.getInstance().get(Calendar.YEAR); + + albumRepository.getAlbums("byYear", 500, currentYear, currentYear).observe(owner, albums -> { + if (albums != null) { + albums.sort(Comparator.comparing(AlbumID3::getCreated).reversed()); + newReleasedAlbum.postValue(albums.subList(0, Math.min(20, albums.size()))); + } + }); + } + + return newReleasedAlbum; + } + + public LiveData> getStarredTracksSample(LifecycleOwner owner) { + if (starredTracksSample.getValue() == null) { + songRepository.getStarredSongs(true, 10).observe(owner, starredTracksSample::postValue); + } + + return starredTracksSample; + } + + public LiveData> getStarredArtistsSample(LifecycleOwner owner) { + if (starredArtistsSample.getValue() == null) { + artistRepository.getStarredArtists(true, 10).observe(owner, starredArtistsSample::postValue); + } + + return starredArtistsSample; + } + + public LiveData> getBestOfArtists(LifecycleOwner owner) { + if (bestOfArtists.getValue() == null) { + artistRepository.getStarredArtists(true, 20).observe(owner, bestOfArtists::postValue); + } + + return bestOfArtists; + } + + public LiveData> getStarredTracks(LifecycleOwner owner) { + if (starredTracks.getValue() == null) { + songRepository.getStarredSongs(true, 20).observe(owner, starredTracks::postValue); + } + + return starredTracks; + } + + public LiveData> getStarredAlbums(LifecycleOwner owner) { + if (starredAlbums.getValue() == null) { + albumRepository.getStarredAlbums(true, 20).observe(owner, starredAlbums::postValue); + } + + return starredAlbums; + } + + public LiveData> getAllStarredAlbumSongs() { + return albumsSyncViewModel.getAllStarredAlbumSongs(); + } + + public LiveData> getAllStarredArtistSongs() { + return artistSyncViewModel.getAllStarredArtistSongs(); + } + + public LiveData> getStarredArtists(LifecycleOwner owner) { + if (starredArtists.getValue() == null) { + artistRepository.getStarredArtists(true, 20).observe(owner, starredArtists::postValue); + } + + return starredArtists; + } + + public LiveData> getYearList(LifecycleOwner owner) { + if (years.getValue() == null) { + albumRepository.getDecades().observe(owner, years::postValue); + } + + return years; + } + + public LiveData> getMostPlayedAlbums(LifecycleOwner owner) { + if (mostPlayedAlbumSample.getValue() == null) { + albumRepository.getAlbums("frequent", 20, null, null).observe(owner, mostPlayedAlbumSample::postValue); + } + + return mostPlayedAlbumSample; + } + + public LiveData> getMostRecentlyAddedAlbums(LifecycleOwner owner) { + if (recentlyAddedAlbumSample.getValue() == null) { + albumRepository.getAlbums("newest", 20, null, null).observe(owner, recentlyAddedAlbumSample::postValue); + } + + return recentlyAddedAlbumSample; + } + + public LiveData> getRecentlyPlayedAlbumList(LifecycleOwner owner) { + if (recentlyPlayedAlbumSample.getValue() == null) { + albumRepository.getAlbums("recent", 20, null, null).observe(owner, recentlyPlayedAlbumSample::postValue); + } + + return recentlyPlayedAlbumSample; + } + + public LiveData> getMediaInstantMix(LifecycleOwner owner, Child media) { + mediaInstantMix.setValue(Collections.emptyList()); + + songRepository.getInstantMix(media.getId(), 20).observe(owner, mediaInstantMix::postValue); + + return mediaInstantMix; + } + + public LiveData> getArtistInstantMix(LifecycleOwner owner, ArtistID3 artist) { + artistInstantMix.setValue(Collections.emptyList()); + + artistRepository.getTopSongs(artist.getName(), 10).observe(owner, artistInstantMix::postValue); + + return artistInstantMix; + } + + public LiveData> getArtistBestOf(LifecycleOwner owner, ArtistID3 artist) { + artistBestOf.setValue(Collections.emptyList()); + + artistRepository.getTopSongs(artist.getName(), 10).observe(owner, artistBestOf::postValue); + + return artistBestOf; + } + + public LiveData> getPinnedPlaylists(LifecycleOwner owner) { + pinnedPlaylists.setValue(Collections.emptyList()); + + playlistRepository.getPlaylists(false, -1).observe(owner, remotes -> { + playlistRepository.getPinnedPlaylists().observe(owner, locals -> { + if (remotes != null && locals != null) { + List toReturn = remotes.stream() + .filter(remote -> locals.stream().anyMatch(local -> local.getId().equals(remote.getId()))) + .collect(Collectors.toList()); + + pinnedPlaylists.setValue(toReturn); + } + }); + }); + + return pinnedPlaylists; + } + + public LiveData> getShares(LifecycleOwner owner) { + if (shares.getValue() == null) { + sharingRepository.getShares().observe(owner, shares::postValue); + } + + return shares; + } + + public LiveData> getAllStarredTracks() { + return songRepository.getStarredSongs(false, -1); + } + + public void changeChronologyPeriod(LifecycleOwner owner, int period) { + Calendar cal = Calendar.getInstance(); + String server = Preferences.getServerId(); + int currentWeek = cal.get(Calendar.WEEK_OF_YEAR); + + long start = 0; + long end = 0; + + if (period == 0) { + start = cal.getTimeInMillis(); + cal.set(Calendar.WEEK_OF_YEAR, currentWeek - 1); + end = cal.getTimeInMillis(); + } else if (period == 1) { + start = cal.getTimeInMillis(); + cal.set(Calendar.WEEK_OF_YEAR, currentWeek - 4); + end = cal.getTimeInMillis(); + } else if (period == 2) { + start = cal.getTimeInMillis(); + cal.set(Calendar.WEEK_OF_YEAR, currentWeek - 52); + end = cal.getTimeInMillis(); + } + + chronologyRepository.getChronology(server, start, end).observe(owner, thisGridTopSong::postValue); + } + + public void refreshDiscoverySongSample(LifecycleOwner owner) { + songRepository.getRandomSample(10, null, null).observe(owner, dicoverSongSample::postValue); + } + + public void refreshSimilarSongSample(LifecycleOwner owner) { + songRepository.getStarredSongs(true, 10).observe(owner, starredTracksSample::postValue); + } + + public void refreshRadioArtistSample(LifecycleOwner owner) { + artistRepository.getStarredArtists(true, 10).observe(owner, starredArtistsSample::postValue); + } + + public void refreshBestOfArtist(LifecycleOwner owner) { + artistRepository.getStarredArtists(true, 20).observe(owner, bestOfArtists::postValue); + } + + public void refreshStarredTracks(LifecycleOwner owner) { + songRepository.getStarredSongs(true, 20).observe(owner, starredTracks::postValue); + } + + public void refreshStarredAlbums(LifecycleOwner owner) { + albumRepository.getStarredAlbums(true, 20).observe(owner, starredAlbums::postValue); + } + + public void refreshStarredArtists(LifecycleOwner owner) { + artistRepository.getStarredArtists(true, 20).observe(owner, starredArtists::postValue); + } + + public void refreshMostPlayedAlbums(LifecycleOwner owner) { + albumRepository.getAlbums("frequent", 20, null, null).observe(owner, mostPlayedAlbumSample::postValue); + } + + public void refreshMostRecentlyAddedAlbums(LifecycleOwner owner) { + albumRepository.getAlbums("newest", 20, null, null).observe(owner, recentlyAddedAlbumSample::postValue); + } + + public void refreshRecentlyPlayedAlbumList(LifecycleOwner owner) { + albumRepository.getAlbums("recent", 20, null, null).observe(owner, recentlyPlayedAlbumSample::postValue); + } + + public void refreshShares(LifecycleOwner owner) { + sharingRepository.getShares().observe(owner, this.shares::postValue); + } + + private void setHomeSectorList() { + if (Preferences.getHomeSectorList() != null && !Preferences.getHomeSectorList().equals("null")) { + sectors = new Gson().fromJson( + Preferences.getHomeSectorList(), + new TypeToken>() { + }.getType() + ); + } + } + + public List getHomeSectorList() { + return sectors; + } + + public boolean checkHomeSectorVisibility(String sectorId) { + return sectors != null && sectors.stream().filter(sector -> sector.getId().equals(sectorId)) + .findAny() + .orElse(null) == null; + } + + public void setOfflineFavorite() { + ArrayList favorites = getFavorites(); + ArrayList favoritesToSave = getFavoritesToSave(favorites); + ArrayList favoritesToDelete = getFavoritesToDelete(favorites, favoritesToSave); + + manageFavoriteToSave(favoritesToSave); + manageFavoriteToDelete(favoritesToDelete); + } + + private ArrayList getFavorites() { + return new ArrayList<>(favoriteRepository.getFavorites()); + } + + private ArrayList getFavoritesToSave(ArrayList favorites) { + HashMap filteredMap = new HashMap<>(); + + for (Favorite favorite : favorites) { + String key = favorite.toString(); + + if (!filteredMap.containsKey(key) || favorite.getTimestamp() > filteredMap.get(key).getTimestamp()) { + filteredMap.put(key, favorite); + } + } + + return new ArrayList<>(filteredMap.values()); + } + + private ArrayList getFavoritesToDelete(ArrayList favorites, ArrayList favoritesToSave) { + ArrayList favoritesToDelete = new ArrayList<>(); + + for (Favorite favorite : favorites) { + if (!favoritesToSave.contains(favorite)) { + favoritesToDelete.add(favorite); + } + } + + return favoritesToDelete; + } + + private void manageFavoriteToSave(ArrayList favoritesToSave) { + for (Favorite favorite : favoritesToSave) { + if (favorite.getToStar()) { + favoriteToStar(favorite); + } else { + favoriteToUnstar(favorite); + } + } + } + + private void manageFavoriteToDelete(ArrayList favoritesToDelete) { + for (Favorite favorite : favoritesToDelete) { + favoriteRepository.delete(favorite); + } + } + + private void favoriteToStar(Favorite favorite) { + if (favorite.getSongId() != null) { + favoriteRepository.star(favorite.getSongId(), null, null, new StarCallback() { + @Override + public void onSuccess() { + favoriteRepository.delete(favorite); + } + }); + } else if (favorite.getAlbumId() != null) { + favoriteRepository.star(null, favorite.getAlbumId(), null, new StarCallback() { + @Override + public void onSuccess() { + favoriteRepository.delete(favorite); + } + }); + } else if (favorite.getArtistId() != null) { + favoriteRepository.star(null, null, favorite.getArtistId(), new StarCallback() { + @Override + public void onSuccess() { + favoriteRepository.delete(favorite); + } + }); + } + } + + private void favoriteToUnstar(Favorite favorite) { + if (favorite.getSongId() != null) { + favoriteRepository.unstar(favorite.getSongId(), null, null, new StarCallback() { + @Override + public void onSuccess() { + favoriteRepository.delete(favorite); + } + }); + } else if (favorite.getAlbumId() != null) { + favoriteRepository.unstar(null, favorite.getAlbumId(), null, new StarCallback() { + @Override + public void onSuccess() { + favoriteRepository.delete(favorite); + } + }); + } else if (favorite.getArtistId() != null) { + favoriteRepository.unstar(null, null, favorite.getArtistId(), new StarCallback() { + @Override + public void onSuccess() { + favoriteRepository.delete(favorite); + } + }); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/IndexViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/IndexViewModel.java new file mode 100644 index 0000000..8801519 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/IndexViewModel.java @@ -0,0 +1,35 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.DirectoryRepository; +import com.cappielloantonio.tempo.subsonic.models.Indexes; +import com.cappielloantonio.tempo.subsonic.models.MusicFolder; + +public class IndexViewModel extends AndroidViewModel { + private final DirectoryRepository directoryRepository; + + private MusicFolder musicFolder; + + public IndexViewModel(@NonNull Application application) { + super(application); + + directoryRepository = new DirectoryRepository(); + } + + public MutableLiveData getIndexes(String musicFolderId) { + return directoryRepository.getIndexes(musicFolderId, null); + } + + public String getMusicFolderName() { + return musicFolder != null ? musicFolder.getName() : ""; + } + + public void setMusicFolder(MusicFolder musicFolder) { + this.musicFolder = musicFolder; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/LibraryViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/LibraryViewModel.java new file mode 100644 index 0000000..61efbd9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/LibraryViewModel.java @@ -0,0 +1,114 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.DirectoryRepository; +import com.cappielloantonio.tempo.repository.GenreRepository; +import com.cappielloantonio.tempo.repository.PlaylistRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Genre; +import com.cappielloantonio.tempo.subsonic.models.Indexes; +import com.cappielloantonio.tempo.subsonic.models.MusicFolder; +import com.cappielloantonio.tempo.subsonic.models.Playlist; + +import java.util.List; + +public class LibraryViewModel extends AndroidViewModel { + private static final String TAG = "LibraryViewModel"; + + private final DirectoryRepository directoryRepository; + private final AlbumRepository albumRepository; + private final ArtistRepository artistRepository; + private final GenreRepository genreRepository; + private final PlaylistRepository playlistRepository; + + private final MutableLiveData> musicFolders = new MutableLiveData<>(null); + private final MutableLiveData indexes = new MutableLiveData<>(null); + private final MutableLiveData> playlistSample = new MutableLiveData<>(null); + private final MutableLiveData> sampleAlbum = new MutableLiveData<>(null); + private final MutableLiveData> sampleArtist = new MutableLiveData<>(null); + private final MutableLiveData> sampleGenres = new MutableLiveData<>(null); + + public LibraryViewModel(@NonNull Application application) { + super(application); + + directoryRepository = new DirectoryRepository(); + albumRepository = new AlbumRepository(); + artistRepository = new ArtistRepository(); + genreRepository = new GenreRepository(); + playlistRepository = new PlaylistRepository(); + } + + public LiveData> getMusicFolders(LifecycleOwner owner) { + if (musicFolders.getValue() == null) { + directoryRepository.getMusicFolders().observe(owner, musicFolders::postValue); + } + + return musicFolders; + } + + public LiveData getIndexes(LifecycleOwner owner) { + if (indexes.getValue() == null) { + directoryRepository.getIndexes("0", null).observe(owner, indexes::postValue); + } + + return indexes; + } + + public LiveData> getAlbumSample(LifecycleOwner owner) { + if (sampleAlbum.getValue() == null) { + albumRepository.getAlbums("random", 10, null, null).observe(owner, sampleAlbum::postValue); + } + + return sampleAlbum; + } + + public LiveData> getArtistSample(LifecycleOwner owner) { + if (sampleArtist.getValue() == null) { + artistRepository.getArtists(true, 10).observe(owner, sampleArtist::postValue); + } + + return sampleArtist; + } + + public LiveData> getGenreSample(LifecycleOwner owner) { + if (sampleGenres.getValue() == null) { + genreRepository.getGenres(true, 15).observe(owner, sampleGenres::postValue); + } + + return sampleGenres; + } + + public LiveData> getPlaylistSample(LifecycleOwner owner) { + if (playlistSample.getValue() == null) { + playlistRepository.getPlaylists(true, 10).observe(owner, playlistSample::postValue); + } + + return playlistSample; + } + + public void refreshAlbumSample(LifecycleOwner owner) { + albumRepository.getAlbums("random", 10, null, null).observe(owner, sampleAlbum::postValue); + } + + public void refreshArtistSample(LifecycleOwner owner) { + artistRepository.getArtists(true, 10).observe(owner, sampleArtist::postValue); + } + + public void refreshGenreSample(LifecycleOwner owner) { + genreRepository.getGenres(true, 15).observe(owner, sampleGenres::postValue); + } + + public void refreshPlaylistSample(LifecycleOwner owner) { + playlistRepository.getPlaylists(true, 10).observe(owner, playlistSample::postValue); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/LoginViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/LoginViewModel.java new file mode 100644 index 0000000..1ff311b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/LoginViewModel.java @@ -0,0 +1,48 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.model.Server; +import com.cappielloantonio.tempo.repository.ServerRepository; + +import java.util.List; + +public class LoginViewModel extends AndroidViewModel { + private final ServerRepository serverRepository; + + private Server toEdit = null; + + public LoginViewModel(@NonNull Application application) { + super(application); + + serverRepository = new ServerRepository(); + } + + public LiveData> getServerList() { + return serverRepository.getLiveServer(); + } + + public void addServer(Server server) { + serverRepository.insert(server); + } + + public void deleteServer(Server server) { + if (server != null) { + serverRepository.delete(server); + } else if (toEdit != null) { + serverRepository.delete(toEdit); + } + } + + public void setServerToEdit(Server server) { + toEdit = server; + } + + public Server getServerToEdit() { + return toEdit; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/MainViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/MainViewModel.java new file mode 100644 index 0000000..529abaf --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/MainViewModel.java @@ -0,0 +1,44 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.github.models.LatestRelease; +import com.cappielloantonio.tempo.repository.QueueRepository; +import com.cappielloantonio.tempo.repository.SystemRepository; +import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension; +import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse; + +import java.util.List; + +public class MainViewModel extends AndroidViewModel { + private static final String TAG = "SearchViewModel"; + + private final SystemRepository systemRepository; + + public MainViewModel(@NonNull Application application) { + super(application); + + systemRepository = new SystemRepository(); + } + + public boolean isQueueLoaded() { + QueueRepository queueRepository = new QueueRepository(); + return queueRepository.count() != 0; + } + + public LiveData ping() { + return systemRepository.ping(); + } + + public LiveData> getOpenSubsonicExtensions() { + return systemRepository.getOpenSubsonicExtensions(); + } + + public LiveData checkTempoUpdate() { + return systemRepository.checkTempoUpdate(); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaybackViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaybackViewModel.java new file mode 100644 index 0000000..b1808d9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaybackViewModel.java @@ -0,0 +1,35 @@ +package com.cappielloantonio.tempo.viewmodel; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import java.util.Objects; + +public class PlaybackViewModel extends ViewModel { + + private final MutableLiveData currentSongId = new MutableLiveData<>(null); + private final MutableLiveData isPlaying = new MutableLiveData<>(false); + + public LiveData getCurrentSongId() { + return currentSongId; + } + + public LiveData getIsPlaying() { + return isPlaying; + } + + public void update(String songId, boolean playing) { + if (!Objects.equals(currentSongId.getValue(), songId)) { + currentSongId.postValue(songId); + } + if (!Objects.equals(isPlaying.getValue(), playing)) { + isPlaying.postValue(playing); + } + } + + public void clear() { + currentSongId.postValue(null); + isPlaying.postValue(false); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java new file mode 100644 index 0000000..2a100fb --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlayerBottomSheetViewModel.java @@ -0,0 +1,407 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; +import android.content.Context; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.interfaces.StarCallback; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.model.LyricsCache; +import com.cappielloantonio.tempo.model.Queue; +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.FavoriteRepository; +import com.cappielloantonio.tempo.repository.LyricsRepository; +import com.cappielloantonio.tempo.repository.OpenRepository; +import com.cappielloantonio.tempo.repository.QueueRepository; +import com.cappielloantonio.tempo.repository.SongRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.LyricsList; +import com.cappielloantonio.tempo.subsonic.models.PlayQueue; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.NetworkUtil; +import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.gson.Gson; + +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@OptIn(markerClass = UnstableApi.class) +public class PlayerBottomSheetViewModel extends AndroidViewModel { + private static final String TAG = "PlayerBottomSheetViewModel"; + + private final SongRepository songRepository; + private final AlbumRepository albumRepository; + private final ArtistRepository artistRepository; + private final QueueRepository queueRepository; + private final FavoriteRepository favoriteRepository; + private final OpenRepository openRepository; + private final LyricsRepository lyricsRepository; + private final MutableLiveData lyricsLiveData = new MutableLiveData<>(null); + private final MutableLiveData lyricsListLiveData = new MutableLiveData<>(null); + private final MutableLiveData lyricsCachedLiveData = new MutableLiveData<>(false); + private final MutableLiveData descriptionLiveData = new MutableLiveData<>(null); + private final MutableLiveData liveMedia = new MutableLiveData<>(null); + private final MutableLiveData liveAlbum = new MutableLiveData<>(null); + private final MutableLiveData liveArtist = new MutableLiveData<>(null); + private final MutableLiveData> instantMix = new MutableLiveData<>(null); + private final Gson gson = new Gson(); + private boolean lyricsSyncState = true; + private LiveData cachedLyricsSource; + private String currentSongId; + private final Observer cachedLyricsObserver = this::onCachedLyricsChanged; + + + public PlayerBottomSheetViewModel(@NonNull Application application) { + super(application); + + songRepository = new SongRepository(); + albumRepository = new AlbumRepository(); + artistRepository = new ArtistRepository(); + queueRepository = new QueueRepository(); + favoriteRepository = new FavoriteRepository(); + openRepository = new OpenRepository(); + lyricsRepository = new LyricsRepository(); + } + + public LiveData> getQueueSong() { + return queueRepository.getLiveQueue(); + } + + public void setFavorite(Context context, Child media) { + if (media != null) { + if (media.getStarred() != null) { + if (NetworkUtil.isOffline()) { + removeFavoriteOffline(media); + } else { + removeFavoriteOnline(media); + } + } else { + if (NetworkUtil.isOffline()) { + setFavoriteOffline(media); + } else { + setFavoriteOnline(context, media); + } + } + } + } + + private void removeFavoriteOffline(Child media) { + favoriteRepository.starLater(media.getId(), null, null, false); + media.setStarred(null); + } + + private void removeFavoriteOnline(Child media) { + favoriteRepository.unstar(media.getId(), null, null, new StarCallback() { + @Override + public void onError() { + // media.setStarred(new Date()); + favoriteRepository.starLater(media.getId(), null, null, false); + } + }); + media.setStarred(null); + } + + private void setFavoriteOffline(Child media) { + favoriteRepository.starLater(media.getId(), null, null, true); + media.setStarred(new Date()); + } + + private void setFavoriteOnline(Context context, Child media) { + favoriteRepository.star(media.getId(), null, null, new StarCallback() { + @Override + public void onError() { + // media.setStarred(null); + favoriteRepository.starLater(media.getId(), null, null, true); + } + }); + + media.setStarred(new Date()); + + if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) { + DownloadUtil.getDownloadTracker(context).download( + MappingUtil.mapDownload(media), + new Download(media) + ); + } + } + + public LiveData getLiveLyrics() { + return lyricsLiveData; + } + + public LiveData getLiveLyricsList() { + return lyricsListLiveData; + } + + public void refreshMediaInfo(LifecycleOwner owner, Child media) { + lyricsLiveData.postValue(null); + lyricsListLiveData.postValue(null); + lyricsCachedLiveData.postValue(false); + + clearCachedLyricsObserver(); + + String songId = media != null ? media.getId() : currentSongId; + + if (TextUtils.isEmpty(songId) || owner == null) { + return; + } + + currentSongId = songId; + + observeCachedLyrics(owner, songId); + + LyricsCache cachedLyrics = lyricsRepository.getLyrics(songId); + if (cachedLyrics != null) { + onCachedLyricsChanged(cachedLyrics); + } + + if (NetworkUtil.isOffline() || media == null) { + return; + } + + if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) { + openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsList -> { + lyricsListLiveData.postValue(lyricsList); + lyricsLiveData.postValue(null); + + if (shouldAutoDownloadLyrics() && hasStructuredLyrics(lyricsList)) { + saveLyricsToCache(media, null, lyricsList); + } + }); + } else { + songRepository.getSongLyrics(media).observe(owner, lyrics -> { + lyricsLiveData.postValue(lyrics); + lyricsListLiveData.postValue(null); + + if (shouldAutoDownloadLyrics() && !TextUtils.isEmpty(lyrics)) { + saveLyricsToCache(media, lyrics, null); + } + }); + } + } + + public LiveData getLiveMedia() { + return liveMedia; + } + + public void setLiveMedia(LifecycleOwner owner, String mediaType, String mediaId) { + currentSongId = mediaId; + + if (!TextUtils.isEmpty(mediaId)) { + refreshMediaInfo(owner, null); + } else { + clearCachedLyricsObserver(); + lyricsLiveData.postValue(null); + lyricsListLiveData.postValue(null); + lyricsCachedLiveData.postValue(false); + } + + if (mediaType != null) { + switch (mediaType) { + case Constants.MEDIA_TYPE_MUSIC: + songRepository.getSong(mediaId).observe(owner, liveMedia::postValue); + descriptionLiveData.postValue(null); + break; + case Constants.MEDIA_TYPE_PODCAST: + liveMedia.postValue(null); + break; + default: + liveMedia.postValue(null); + break; + } + } else { + liveMedia.postValue(null); + } + } + + public LiveData getLiveAlbum() { + return liveAlbum; + } + + public void setLiveAlbum(LifecycleOwner owner, String mediaType, String AlbumId) { + if (mediaType != null) { + switch (mediaType) { + case Constants.MEDIA_TYPE_MUSIC: + albumRepository.getAlbum(AlbumId).observe(owner, liveAlbum::postValue); + break; + case Constants.MEDIA_TYPE_PODCAST: + liveAlbum.postValue(null); + break; + } + } + } + + public LiveData getLiveArtist() { + return liveArtist; + } + + public void setLiveArtist(LifecycleOwner owner, String mediaType, String ArtistId) { + if (mediaType != null) { + switch (mediaType) { + case Constants.MEDIA_TYPE_MUSIC: + artistRepository.getArtist(ArtistId).observe(owner, liveArtist::postValue); + break; + case Constants.MEDIA_TYPE_PODCAST: + liveArtist.postValue(null); + break; + } + } + } + + public void setLiveDescription(String description) { + descriptionLiveData.postValue(description); + } + + public LiveData getLiveDescription() { + return descriptionLiveData; + } + + public LiveData> getMediaInstantMix(LifecycleOwner owner, Child media) { + instantMix.setValue(Collections.emptyList()); + + songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue); + + return instantMix; + } + + public LiveData getPlayQueue() { + return queueRepository.getPlayQueue(); + } + + public boolean savePlayQueue() { + Child media = getLiveMedia().getValue(); + List queue = queueRepository.getMedia(); + List ids = queue.stream().map(Child::getId).collect(Collectors.toList()); + + if (media != null) { + queueRepository.savePlayQueue(ids, media.getId(), 0); + return true; + } + + return false; + } + + private void observeCachedLyrics(LifecycleOwner owner, String songId) { + if (TextUtils.isEmpty(songId)) { + return; + } + + cachedLyricsSource = lyricsRepository.observeLyrics(songId); + cachedLyricsSource.observe(owner, cachedLyricsObserver); + } + + private void clearCachedLyricsObserver() { + if (cachedLyricsSource != null) { + cachedLyricsSource.removeObserver(cachedLyricsObserver); + cachedLyricsSource = null; + } + } + + private void onCachedLyricsChanged(LyricsCache lyricsCache) { + if (lyricsCache == null) { + lyricsCachedLiveData.postValue(false); + return; + } + + lyricsCachedLiveData.postValue(true); + + if (!TextUtils.isEmpty(lyricsCache.getStructuredLyrics())) { + try { + LyricsList cachedList = gson.fromJson(lyricsCache.getStructuredLyrics(), LyricsList.class); + lyricsListLiveData.postValue(cachedList); + lyricsLiveData.postValue(null); + } catch (Exception exception) { + lyricsListLiveData.postValue(null); + lyricsLiveData.postValue(lyricsCache.getLyrics()); + } + } else { + lyricsListLiveData.postValue(null); + lyricsLiveData.postValue(lyricsCache.getLyrics()); + } + } + + private void saveLyricsToCache(Child media, String lyrics, LyricsList lyricsList) { + if (media == null) { + return; + } + + if ((lyricsList == null || !hasStructuredLyrics(lyricsList)) && TextUtils.isEmpty(lyrics)) { + return; + } + + LyricsCache lyricsCache = new LyricsCache(media.getId()); + lyricsCache.setArtist(media.getArtist()); + lyricsCache.setTitle(media.getTitle()); + lyricsCache.setUpdatedAt(System.currentTimeMillis()); + + if (lyricsList != null && hasStructuredLyrics(lyricsList)) { + lyricsCache.setStructuredLyrics(gson.toJson(lyricsList)); + lyricsCache.setLyrics(null); + } else { + lyricsCache.setLyrics(lyrics); + lyricsCache.setStructuredLyrics(null); + } + + lyricsRepository.insert(lyricsCache); + lyricsCachedLiveData.postValue(true); + } + + private boolean hasStructuredLyrics(LyricsList lyricsList) { + return lyricsList != null + && lyricsList.getStructuredLyrics() != null + && !lyricsList.getStructuredLyrics().isEmpty() + && lyricsList.getStructuredLyrics().get(0) != null + && lyricsList.getStructuredLyrics().get(0).getLine() != null + && !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty(); + } + + private boolean shouldAutoDownloadLyrics() { + return Preferences.isAutoDownloadLyricsEnabled(); + } + + public boolean downloadCurrentLyrics() { + Child media = getLiveMedia().getValue(); + if (media == null) { + return false; + } + + LyricsList lyricsList = lyricsListLiveData.getValue(); + String lyrics = lyricsLiveData.getValue(); + + if ((lyricsList == null || !hasStructuredLyrics(lyricsList)) && TextUtils.isEmpty(lyrics)) { + return false; + } + + saveLyricsToCache(media, lyrics, lyricsList); + return true; + } + + public LiveData getLyricsCachedState() { + return lyricsCachedLiveData; + } + + public void changeSyncLyricsState() { + lyricsSyncState = !lyricsSyncState; + } + + public boolean getSyncLyricsState() { + return lyricsSyncState; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistCatalogueViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistCatalogueViewModel.java new file mode 100644 index 0000000..e183265 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistCatalogueViewModel.java @@ -0,0 +1,44 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.PlaylistRepository; +import com.cappielloantonio.tempo.subsonic.models.Playlist; + +import java.util.List; + +public class PlaylistCatalogueViewModel extends AndroidViewModel { + private final PlaylistRepository playlistRepository; + + private String type; + + private final MutableLiveData> playlistList = new MutableLiveData<>(null); + + public PlaylistCatalogueViewModel(@NonNull Application application) { + super(application); + + playlistRepository = new PlaylistRepository(); + } + + public LiveData> getPlaylistList(LifecycleOwner owner) { + if (playlistList.getValue() == null) { + playlistRepository.getPlaylists(false, -1).observe(owner, playlistList::postValue); + } + + return playlistList; + } + + public void setType(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistChooserViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistChooserViewModel.java new file mode 100644 index 0000000..ca7af15 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistChooserViewModel.java @@ -0,0 +1,63 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; +import android.app.Dialog; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.PlaylistRepository; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Playlist; +import com.cappielloantonio.tempo.util.Preferences; +import com.google.common.collect.Lists; + +import java.util.ArrayList; +import java.util.List; + +public class PlaylistChooserViewModel extends AndroidViewModel { + private final PlaylistRepository playlistRepository; + + private final MutableLiveData> playlists = new MutableLiveData<>(null); + private ArrayList toAdd = new ArrayList<>(); + + public PlaylistChooserViewModel(@NonNull Application application) { + super(application); + + playlistRepository = new PlaylistRepository(); + } + + public LiveData> getPlaylistList(LifecycleOwner owner) { + playlistRepository.getPlaylists(false, -1).observe(owner, playlists::postValue); + return playlists; + } + + public void addSongsToPlaylist(LifecycleOwner owner, Dialog dialog, String playlistId) { + List songIds = Lists.transform(toAdd, Child::getId); + if (Preferences.allowPlaylistDuplicates()) { + playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds)); + dialog.dismiss(); + } else { + playlistRepository.getPlaylistSongs(playlistId).observe(owner, playlistSongs -> { + if (playlistSongs != null) { + List playlistSongIds = Lists.transform(playlistSongs, Child::getId); + songIds.removeAll(playlistSongIds); + } + playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds)); + dialog.dismiss(); + }); + } + } + + public void setSongsToAdd(ArrayList songs) { + toAdd = songs; + } + + public ArrayList getSongsToAdd() { + return toAdd; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistEditorViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistEditorViewModel.java new file mode 100644 index 0000000..aceedb9 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistEditorViewModel.java @@ -0,0 +1,104 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.PlaylistRepository; +import com.cappielloantonio.tempo.repository.SharingRepository; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Playlist; +import com.cappielloantonio.tempo.subsonic.models.Share; +import com.google.common.collect.Lists; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class PlaylistEditorViewModel extends AndroidViewModel { + private static final String TAG = "PlaylistEditorViewModel"; + + private final PlaylistRepository playlistRepository; + private final SharingRepository sharingRepository; + + private ArrayList toAdd; + private Playlist toEdit; + + private MutableLiveData> songLiveList = new MutableLiveData<>(); + + public PlaylistEditorViewModel(@NonNull Application application) { + super(application); + + playlistRepository = new PlaylistRepository(); + sharingRepository = new SharingRepository(); + } + + public void createPlaylist(String name) { + playlistRepository.createPlaylist(null, name, new ArrayList(Lists.transform(toAdd, Child::getId))); + } + + public void updatePlaylist(String name) { + playlistRepository.updatePlaylist(toEdit.getId(), name, getPlaylistSongIds()); + } + + public void deletePlaylist() { + if (toEdit != null) playlistRepository.deletePlaylist(toEdit.getId()); + } + + public void setSongsToAdd(ArrayList songs) { + toAdd = songs; + } + + public ArrayList getSongsToAdd() { + return toAdd; + } + + public Playlist getPlaylistToEdit() { + return toEdit; + } + + public void setPlaylistToEdit(Playlist playlist) { + this.toEdit = playlist; + + if (playlist != null) { + this.songLiveList = playlistRepository.getPlaylistSongs(toEdit.getId()); + } else { + this.songLiveList = new MutableLiveData<>(); + } + } + + public LiveData> getPlaylistSongLiveList() { + return songLiveList; + } + + public void removeFromPlaylistSongLiveList(int position) { + List songs = songLiveList.getValue(); + Objects.requireNonNull(songs).remove(position); + songLiveList.postValue(songs); + } + + public void orderPlaylistSongLiveListAfterSwap(List songs) { + songLiveList.postValue(songs); + } + + private ArrayList getPlaylistSongIds() { + List songs = songLiveList.getValue(); + ArrayList ids = new ArrayList<>(); + + if (songs != null && !songs.isEmpty()) { + for (Child song : songs) { + ids.add(song.getId()); + } + } + + return ids; + } + + public MutableLiveData sharePlaylist() { + return sharingRepository.createShare(toEdit.getId(), toEdit.getName(), null); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistPageViewModel.java new file mode 100644 index 0000000..d59f5ac --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PlaylistPageViewModel.java @@ -0,0 +1,58 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.PlaylistRepository; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Playlist; + +import java.util.List; + +public class PlaylistPageViewModel extends AndroidViewModel { + private final PlaylistRepository playlistRepository; + + private Playlist playlist; + private boolean isOffline; + + public PlaylistPageViewModel(@NonNull Application application) { + super(application); + + playlistRepository = new PlaylistRepository(); + } + + public LiveData> getPlaylistSongLiveList() { + return playlistRepository.getPlaylistSongs(playlist.getId()); + } + + public Playlist getPlaylist() { + return playlist; + } + + public void setPlaylist(Playlist playlist) { + this.playlist = playlist; + } + + public LiveData isPinned(LifecycleOwner owner) { + MutableLiveData isPinnedLive = new MutableLiveData<>(); + + playlistRepository.getPinnedPlaylists().observe(owner, playlists -> { + isPinnedLive.postValue(playlists.stream().anyMatch(obj -> obj.getId().equals(playlist.getId()))); + }); + + return isPinnedLive; + } + + public void setPinned(boolean isNowPinned) { + if (isNowPinned) { + playlistRepository.insert(playlist); + } else { + playlistRepository.delete(playlist); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelBottomSheetViewModel.java new file mode 100644 index 0000000..fe2b6ac --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelBottomSheetViewModel.java @@ -0,0 +1,33 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import com.cappielloantonio.tempo.repository.PodcastRepository; +import com.cappielloantonio.tempo.subsonic.models.PodcastChannel; + +public class PodcastChannelBottomSheetViewModel extends AndroidViewModel { + private final PodcastRepository podcastRepository; + + private PodcastChannel podcastChannel; + + public PodcastChannelBottomSheetViewModel(@NonNull Application application) { + super(application); + + podcastRepository = new PodcastRepository(); + } + + public PodcastChannel getPodcastChannel() { + return podcastChannel; + } + + public void setPodcastChannel(PodcastChannel podcastChannel) { + this.podcastChannel = podcastChannel; + } + + public void deletePodcastChannel() { + if (podcastChannel != null) podcastRepository.deletePodcastChannel(podcastChannel.getId()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelCatalogueViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelCatalogueViewModel.java new file mode 100644 index 0000000..b54f7d6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelCatalogueViewModel.java @@ -0,0 +1,35 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.PodcastRepository; +import com.cappielloantonio.tempo.subsonic.models.PodcastChannel; + +import java.util.List; + +public class PodcastChannelCatalogueViewModel extends AndroidViewModel { + private final PodcastRepository podcastRepository; + + private final MutableLiveData> podcastChannels = new MutableLiveData<>(null); + + + public PodcastChannelCatalogueViewModel(@NonNull Application application) { + super(application); + + podcastRepository = new PodcastRepository(); + } + + public LiveData> getPodcastChannels(LifecycleOwner owner) { + if (podcastChannels.getValue() == null) { + podcastRepository.getPodcastChannels(false, null).observe(owner, podcastChannels::postValue); + } + + return podcastChannels; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelEditorViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelEditorViewModel.java new file mode 100644 index 0000000..759da75 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelEditorViewModel.java @@ -0,0 +1,27 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import com.cappielloantonio.tempo.repository.PodcastRepository; +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; + +public class PodcastChannelEditorViewModel extends AndroidViewModel { + private static final String TAG = "RadioEditorViewModel"; + + private final PodcastRepository podcastRepository; + + private InternetRadioStation toEdit; + + public PodcastChannelEditorViewModel(@NonNull Application application) { + super(application); + + podcastRepository = new PodcastRepository(); + } + + public void createChannel(String url) { + podcastRepository.createPodcastChannel(url); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelPageViewModel.java new file mode 100644 index 0000000..7cb7827 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelPageViewModel.java @@ -0,0 +1,41 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.repository.PodcastRepository; +import com.cappielloantonio.tempo.subsonic.models.PodcastChannel; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; + +import java.util.List; + +public class PodcastChannelPageViewModel extends AndroidViewModel { + private final PodcastRepository podcastRepository; + + private PodcastChannel podcastChannel; + + public PodcastChannelPageViewModel(@NonNull Application application) { + super(application); + + podcastRepository = new PodcastRepository(); + } + + public LiveData> getPodcastChannelEpisodes() { + return podcastRepository.getPodcastChannels(true, podcastChannel.getId()); + } + + public PodcastChannel getPodcastChannel() { + return podcastChannel; + } + + public void setPodcastChannel(PodcastChannel podcastChannel) { + this.podcastChannel = podcastChannel; + } + + public void requestPodcastEpisodeDownload(PodcastEpisode podcastEpisode) { + podcastRepository.downloadPodcastEpisode(podcastEpisode.getId()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastEpisodeBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastEpisodeBottomSheetViewModel.java new file mode 100644 index 0000000..baba42b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastEpisodeBottomSheetViewModel.java @@ -0,0 +1,33 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import com.cappielloantonio.tempo.repository.PodcastRepository; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; + +public class PodcastEpisodeBottomSheetViewModel extends AndroidViewModel { + private final PodcastRepository podcastRepository; + + private PodcastEpisode podcastEpisode; + + public PodcastEpisodeBottomSheetViewModel(@NonNull Application application) { + super(application); + + podcastRepository = new PodcastRepository(); + } + + public PodcastEpisode getPodcastEpisode() { + return podcastEpisode; + } + + public void setPodcastEpisode(PodcastEpisode podcast) { + this.podcastEpisode = podcast; + } + + public void deletePodcastEpisode() { + if (podcastEpisode != null) podcastRepository.deletePodcastEpisode(podcastEpisode.getId()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastViewModel.java new file mode 100644 index 0000000..4625e83 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastViewModel.java @@ -0,0 +1,52 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.PodcastRepository; +import com.cappielloantonio.tempo.subsonic.models.PodcastChannel; +import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; + +import java.util.List; + +public class PodcastViewModel extends AndroidViewModel { + private final PodcastRepository podcastRepository; + + private final MutableLiveData> newestPodcastEpisodes = new MutableLiveData<>(null); + private final MutableLiveData> podcastChannels = new MutableLiveData<>(null); + + public PodcastViewModel(@NonNull Application application) { + super(application); + + podcastRepository = new PodcastRepository(); + } + + public LiveData> getNewestPodcastEpisodes(LifecycleOwner owner) { + if (newestPodcastEpisodes.getValue() == null) { + podcastRepository.getNewestPodcastEpisodes(20).observe(owner, newestPodcastEpisodes::postValue); + } + + return newestPodcastEpisodes; + } + + public LiveData> getPodcastChannels(LifecycleOwner owner) { + if (podcastChannels.getValue() == null) { + podcastRepository.getPodcastChannels(false, null).observe(owner, podcastChannels::postValue); + } + + return podcastChannels; + } + + public void refreshNewestPodcastEpisodes(LifecycleOwner owner) { + podcastRepository.getNewestPodcastEpisodes(20).observe(owner, newestPodcastEpisodes::postValue); + } + + public void refreshPodcastChannels(LifecycleOwner owner) { + podcastRepository.getPodcastChannels(false, null).observe(owner, podcastChannels::postValue); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioEditorViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioEditorViewModel.java new file mode 100644 index 0000000..c15ea93 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioEditorViewModel.java @@ -0,0 +1,43 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import com.cappielloantonio.tempo.repository.RadioRepository; +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; + +public class RadioEditorViewModel extends AndroidViewModel { + private static final String TAG = "RadioEditorViewModel"; + + private final RadioRepository radioRepository; + + private InternetRadioStation toEdit; + + public RadioEditorViewModel(@NonNull Application application) { + super(application); + + radioRepository = new RadioRepository(); + } + + public InternetRadioStation getRadioToEdit() { + return toEdit; + } + + public void setRadioToEdit(InternetRadioStation internetRadioStation) { + this.toEdit = internetRadioStation; + } + + public void createRadio(String name, String streamURL, String homepageURL) { + radioRepository.createInternetRadioStation(name, streamURL, homepageURL); + } + + public void updateRadio(String name, String streamURL, String homepageURL) { + if (toEdit != null) radioRepository.updateInternetRadioStation(toEdit.getId(), name, streamURL, homepageURL); + } + + public void deleteRadio() { + if (toEdit != null) radioRepository.deleteInternetRadioStation(toEdit.getId()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioViewModel.java new file mode 100644 index 0000000..53dfff4 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioViewModel.java @@ -0,0 +1,35 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.RadioRepository; +import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; + +import java.util.List; + +public class RadioViewModel extends AndroidViewModel { + private final RadioRepository radioRepository; + + private final MutableLiveData> internetRadioStations = new MutableLiveData<>(null); + + public RadioViewModel(@NonNull Application application) { + super(application); + + radioRepository = new RadioRepository(); + } + + public LiveData> getInternetRadioStations(LifecycleOwner owner) { + radioRepository.getInternetRadioStations().observe(owner, internetRadioStations::postValue); + return internetRadioStations; + } + + public void refreshInternetRadioStations(LifecycleOwner owner) { + radioRepository.getInternetRadioStations().observe(owner, internetRadioStations::postValue); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RatingViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RatingViewModel.java new file mode 100644 index 0000000..83cdc0f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RatingViewModel.java @@ -0,0 +1,84 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.SongRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; + +public class RatingViewModel extends AndroidViewModel { + private final SongRepository songRepository; + private final AlbumRepository albumRepository; + private final ArtistRepository artistRepository; + + private Child song; + private AlbumID3 album; + private ArtistID3 artist; + + public RatingViewModel(@NonNull Application application) { + super(application); + + songRepository = new SongRepository(); + albumRepository = new AlbumRepository(); + artistRepository = new ArtistRepository(); + } + + public Child getSong() { + return song; + } + + public LiveData getLiveSong() { + return songRepository.getSong(song.getId()); + } + + public void setSong(Child song) { + this.song = song; + this.album = null; + this.artist = null; + } + + public AlbumID3 getAlbum() { + return album; + } + + public LiveData getLiveAlbum() { + return albumRepository.getAlbum(album.getId()); + } + + public void setAlbum(AlbumID3 album) { + this.song = null; + this.album = album; + this.artist = null; + } + + public ArtistID3 getArtist() { + return artist; + } + + public LiveData getLiveArtist() { + return artistRepository.getArtist(artist.getId()); + } + + public void setArtist(ArtistID3 artist) { + this.song = null; + this.album = null; + this.artist = artist; + } + + public void rate(int star) { + if (song != null) { + songRepository.setRating(song.getId(), star); + } else if (album != null) { + albumRepository.setRating(album.getId(), star); + } else if (artist != null) { + artistRepository.setRating(artist.getId(), star); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SearchViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SearchViewModel.java new file mode 100644 index 0000000..42521a1 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SearchViewModel.java @@ -0,0 +1,68 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import com.cappielloantonio.tempo.model.RecentSearch; +import com.cappielloantonio.tempo.repository.SearchingRepository; +import com.cappielloantonio.tempo.subsonic.models.SearchResult2; +import com.cappielloantonio.tempo.subsonic.models.SearchResult3; + +import java.util.ArrayList; +import java.util.List; + +public class SearchViewModel extends AndroidViewModel { + private static final String TAG = "SearchViewModel"; + + private String query = ""; + + private final SearchingRepository searchingRepository; + + public SearchViewModel(@NonNull Application application) { + super(application); + + searchingRepository = new SearchingRepository(); + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + + if (!query.isEmpty()) { + insertNewSearch(query); + } + } + + public LiveData search2(String title) { + return searchingRepository.search2(title); + } + + public LiveData search3(String title) { + return searchingRepository.search3(title); + } + + public void insertNewSearch(String search) { + searchingRepository.insert(new RecentSearch(search)); + } + + public void deleteRecentSearch(String search) { + searchingRepository.delete(new RecentSearch(search)); + } + + public LiveData> getSearchSuggestion(String query) { + return searchingRepository.getSuggestions(query); + } + + public List getRecentSearchSuggestion() { + ArrayList suggestions = new ArrayList<>(); + suggestions.addAll(searchingRepository.getRecentSearchSuggestion()); + + return suggestions; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SettingViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SettingViewModel.java new file mode 100644 index 0000000..fcf7619 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SettingViewModel.java @@ -0,0 +1,49 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import com.cappielloantonio.tempo.interfaces.ScanCallback; +import com.cappielloantonio.tempo.repository.ScanRepository; + +public class SettingViewModel extends AndroidViewModel { + private static final String TAG = "SettingViewModel"; + + private final ScanRepository scanRepository; + + public SettingViewModel(@NonNull Application application) { + super(application); + + scanRepository = new ScanRepository(); + } + + public void launchScan(ScanCallback callback) { + scanRepository.startScan(new ScanCallback() { + @Override + public void onError(Exception exception) { + callback.onError(exception); + } + + @Override + public void onSuccess(boolean isScanning, long count) { + callback.onSuccess(isScanning, count); + } + }); + } + + public void getScanStatus(ScanCallback callback) { + scanRepository.getScanStatus(new ScanCallback() { + @Override + public void onError(Exception exception) { + callback.onError(exception); + } + + @Override + public void onSuccess(boolean isScanning, long count) { + callback.onSuccess(isScanning, count); + } + }); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ShareBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ShareBottomSheetViewModel.java new file mode 100644 index 0000000..7fc03b1 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ShareBottomSheetViewModel.java @@ -0,0 +1,37 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; + +import com.cappielloantonio.tempo.repository.SharingRepository; +import com.cappielloantonio.tempo.subsonic.models.Share; + +public class ShareBottomSheetViewModel extends AndroidViewModel { + private final SharingRepository sharingRepository; + + private Share share; + + public ShareBottomSheetViewModel(@NonNull Application application) { + super(application); + + sharingRepository = new SharingRepository(); + } + + public Share getShare() { + return share; + } + + public void setShare(Share share) { + this.share = share; + } + + public void updateShare(String description, long expires) { + sharingRepository.updateShare(share.getId(), description, expires); + } + + public void deleteShare() { + sharingRepository.deleteShare(share.getId()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java new file mode 100644 index 0000000..de379dc --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongBottomSheetViewModel.java @@ -0,0 +1,139 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.interfaces.StarCallback; +import com.cappielloantonio.tempo.model.Download; +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.FavoriteRepository; +import com.cappielloantonio.tempo.repository.SharingRepository; +import com.cappielloantonio.tempo.repository.SongRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Share; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.MappingUtil; +import com.cappielloantonio.tempo.util.NetworkUtil; +import com.cappielloantonio.tempo.util.Preferences; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +@UnstableApi +public class SongBottomSheetViewModel extends AndroidViewModel { + private final SongRepository songRepository; + private final AlbumRepository albumRepository; + private final ArtistRepository artistRepository; + private final FavoriteRepository favoriteRepository; + private final SharingRepository sharingRepository; + + private Child song; + + private final MutableLiveData> instantMix = new MutableLiveData<>(null); + + public SongBottomSheetViewModel(@NonNull Application application) { + super(application); + + songRepository = new SongRepository(); + albumRepository = new AlbumRepository(); + artistRepository = new ArtistRepository(); + favoriteRepository = new FavoriteRepository(); + sharingRepository = new SharingRepository(); + } + + public Child getSong() { + return song; + } + + public void setSong(Child song) { + this.song = song; + } + + public void setFavorite(Context context) { + if (song.getStarred() != null) { + if (NetworkUtil.isOffline()) { + removeFavoriteOffline(song); + } else { + removeFavoriteOnline(song); + } + } else { + if (NetworkUtil.isOffline()) { + setFavoriteOffline(song); + } else { + setFavoriteOnline(context, song); + } + } + } + + private void removeFavoriteOffline(Child media) { + favoriteRepository.starLater(media.getId(), null, null, false); + media.setStarred(null); + } + + private void removeFavoriteOnline(Child media) { + favoriteRepository.unstar(media.getId(), null, null, new StarCallback() { + @Override + public void onError() { + // media.setStarred(new Date()); + favoriteRepository.starLater(media.getId(), null, null, false); + } + }); + + media.setStarred(null); + } + + private void setFavoriteOffline(Child media) { + favoriteRepository.starLater(media.getId(), null, null, true); + media.setStarred(new Date()); + } + + private void setFavoriteOnline(Context context, Child media) { + favoriteRepository.star(media.getId(), null, null, new StarCallback() { + @Override + public void onError() { + // media.setStarred(null); + favoriteRepository.starLater(media.getId(), null, null, true); + } + }); + + media.setStarred(new Date()); + + if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) { + DownloadUtil.getDownloadTracker(context).download( + MappingUtil.mapDownload(media), + new Download(media) + ); + } + } + + public LiveData getAlbum() { + return albumRepository.getAlbum(song.getAlbumId()); + } + + public LiveData getArtist() { + return artistRepository.getArtist(song.getArtistId()); + } + + public LiveData> getInstantMix(LifecycleOwner owner, Child media) { + instantMix.setValue(Collections.emptyList()); + + songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue); + + return instantMix; + } + + public MutableLiveData shareTrack() { + return sharingRepository.createShare(song.getId(), song.getTitle(), null); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongListPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongListPageViewModel.java new file mode 100644 index 0000000..acd95b1 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongListPageViewModel.java @@ -0,0 +1,100 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.repository.SongRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Genre; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.ArrayList; +import java.util.List; + +public class SongListPageViewModel extends AndroidViewModel { + private final SongRepository songRepository; + private final ArtistRepository artistRepository; + + public String title; + public String toolbarTitle; + public Genre genre; + public ArtistID3 artist; + public AlbumID3 album; + + private MutableLiveData> songList; + + public ArrayList filters = new ArrayList<>(); + public ArrayList filterNames = new ArrayList<>(); + + public int year = 0; + public int maxNumberByYear = 500; + public int maxNumberByGenre = 500; + + public SongListPageViewModel(@NonNull Application application) { + super(application); + + songRepository = new SongRepository(); + artistRepository = new ArtistRepository(); + } + + public LiveData> getSongList() { + songList = new MutableLiveData<>(new ArrayList<>()); + + switch (title) { + case Constants.MEDIA_BY_GENRE: + songList = songRepository.getRandomSampleWithGenre(maxNumberByGenre, 0, 3000, genre.getGenre()); + break; + case Constants.MEDIA_BY_ARTIST: + songList = artistRepository.getTopSongs(artist.getName(), 50); + break; + case Constants.MEDIA_BY_GENRES: + songList = songRepository.getSongsByGenres(filters); + break; + case Constants.MEDIA_BY_YEAR: + songList = songRepository.getRandomSample(maxNumberByYear, year, year + 10); + break; + case Constants.MEDIA_STARRED: + songList = songRepository.getStarredSongs(false, -1); + break; + } + + return songList; + } + + public void getSongsByPage(LifecycleOwner owner) { + switch (title) { + case Constants.MEDIA_BY_GENRE: + int songCount = songList.getValue() != null ? songList.getValue().size() : 0; + + if (songCount > 0 && songCount % maxNumberByGenre != 0) return; + + int page = songCount / maxNumberByGenre; + songRepository.getSongsByGenre(genre.getGenre(), page).observe(owner, children -> { + if (children != null && !children.isEmpty()) { + List currentMedia = songList.getValue(); + currentMedia.addAll(children); + songList.setValue(currentMedia); + } + }); + break; + case Constants.MEDIA_BY_ARTIST: + case Constants.MEDIA_BY_GENRES: + case Constants.MEDIA_BY_YEAR: + case Constants.MEDIA_STARRED: + break; + } + } + + public String getFiltersTitle() { + return TextUtils.join(", ", filterNames); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredAlbumsSyncViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredAlbumsSyncViewModel.java new file mode 100644 index 0000000..5967caf --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredAlbumsSyncViewModel.java @@ -0,0 +1,90 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; +import android.app.Activity; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.AlbumRepository; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.subsonic.models.Child; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +public class StarredAlbumsSyncViewModel extends AndroidViewModel { + private final AlbumRepository albumRepository; + + private final MutableLiveData> starredAlbums = new MutableLiveData<>(null); + private final MutableLiveData> starredAlbumSongs = new MutableLiveData<>(null); + + public StarredAlbumsSyncViewModel(@NonNull Application application) { + super(application); + albumRepository = new AlbumRepository(); + } + + public LiveData> getStarredAlbums(LifecycleOwner owner) { + albumRepository.getStarredAlbums(false, -1).observe(owner, starredAlbums::postValue); + return starredAlbums; + } + + public LiveData> getAllStarredAlbumSongs() { + albumRepository.getStarredAlbums(false, -1).observeForever(new Observer>() { + @Override + public void onChanged(List albums) { + if (albums != null && !albums.isEmpty()) { + collectAllAlbumSongs(albums, starredAlbumSongs::postValue); + } else { + starredAlbumSongs.postValue(new ArrayList<>()); + } + albumRepository.getStarredAlbums(false, -1).removeObserver(this); + } + }); + + return starredAlbumSongs; + } + + public LiveData> getStarredAlbumSongs(Activity activity) { + albumRepository.getStarredAlbums(false, -1).observe((LifecycleOwner) activity, albums -> { + if (albums != null && !albums.isEmpty()) { + collectAllAlbumSongs(albums, starredAlbumSongs::postValue); + } else { + starredAlbumSongs.postValue(new ArrayList<>()); + } + }); + return starredAlbumSongs; + } + + private void collectAllAlbumSongs(List albums, AlbumSongsCallback callback) { + List allSongs = new ArrayList<>(); + CountDownLatch latch = new CountDownLatch(albums.size()); + + for (AlbumID3 album : albums) { + LiveData> albumTracks = albumRepository.getAlbumTracks(album.getId()); + albumTracks.observeForever(new Observer>() { + @Override + public void onChanged(List songs) { + if (songs != null) { + allSongs.addAll(songs); + } + latch.countDown(); + + if (latch.getCount() == 0) { + callback.onSongsCollected(allSongs); + albumTracks.removeObserver(this); + } + } + }); + } + } + + private interface AlbumSongsCallback { + void onSongsCollected(List songs); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredArtistsSyncViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredArtistsSyncViewModel.java new file mode 100644 index 0000000..474cbe8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredArtistsSyncViewModel.java @@ -0,0 +1,94 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; +import android.app.Activity; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.ArtistRepository; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.subsonic.models.Child; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +public class StarredArtistsSyncViewModel extends AndroidViewModel { + private final ArtistRepository artistRepository; + + private final MutableLiveData> starredArtists = new MutableLiveData<>(null); + private final MutableLiveData> starredArtistSongs = new MutableLiveData<>(null); + + public StarredArtistsSyncViewModel(@NonNull Application application) { + super(application); + artistRepository = new ArtistRepository(); + } + + public LiveData> getStarredArtists(LifecycleOwner owner) { + artistRepository.getStarredArtists(false, -1).observe(owner, starredArtists::postValue); + return starredArtists; + } + + public LiveData> getAllStarredArtistSongs() { + artistRepository.getStarredArtists(false, -1).observeForever(new Observer>() { + @Override + public void onChanged(List artists) { + if (artists != null && !artists.isEmpty()) { + collectAllArtistSongs(artists, starredArtistSongs::postValue); + } else { + starredArtistSongs.postValue(new ArrayList<>()); + } + artistRepository.getStarredArtists(false, -1).removeObserver(this); + } + }); + + return starredArtistSongs; + } + + public LiveData> getStarredArtistSongs(Activity activity) { + artistRepository.getStarredArtists(false, -1).observe((LifecycleOwner) activity, artists -> { + if (artists != null && !artists.isEmpty()) { + collectAllArtistSongs(artists, starredArtistSongs::postValue); + } else { + starredArtistSongs.postValue(new ArrayList<>()); + } + }); + return starredArtistSongs; + } + + private void collectAllArtistSongs(List artists, ArtistSongsCallback callback) { + if (artists == null || artists.isEmpty()) { + callback.onSongsCollected(new ArrayList<>()); + return; + } + + List allSongs = new ArrayList<>(); + AtomicInteger remainingArtists = new AtomicInteger(artists.size()); + + for (ArtistID3 artist : artists) { + artistRepository.getArtistAllSongs(artist.getId(), new ArtistRepository.ArtistSongsCallback() { + @Override + public void onSongsCollected(List songs) { + if (songs != null) { + allSongs.addAll(songs); + } + + int remaining = remainingArtists.decrementAndGet(); + if (remaining == 0) { + callback.onSongsCollected(allSongs); + } + } + }); + } + } + + private interface ArtistSongsCallback { + void onSongsCollected(List songs); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredSyncViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredSyncViewModel.java new file mode 100644 index 0000000..082a3d1 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/StarredSyncViewModel.java @@ -0,0 +1,31 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.cappielloantonio.tempo.repository.SongRepository; +import com.cappielloantonio.tempo.subsonic.models.Child; + +import java.util.List; + +public class StarredSyncViewModel extends AndroidViewModel { + private final SongRepository songRepository; + + private final MutableLiveData> starredTracks = new MutableLiveData<>(null); + + public StarredSyncViewModel(@NonNull Application application) { + super(application); + + songRepository = new SongRepository(); + } + + public LiveData> getStarredTracks(LifecycleOwner owner) { + songRepository.getStarredSongs(false, -1).observe(owner, starredTracks::postValue); + return starredTracks; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetActions.java b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetActions.java new file mode 100644 index 0000000..c6bd8e6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetActions.java @@ -0,0 +1,62 @@ +package com.cappielloantonio.tempo.widget; + +import android.content.ComponentName; +import android.content.Context; +import android.util.Log; + +import androidx.media3.common.Player; +import androidx.media3.session.MediaController; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.service.MediaService; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.concurrent.ExecutionException; + +public final class WidgetActions { + public static void dispatchToMediaSession(Context ctx, String action) { + Log.d("TempoWidget", "dispatch action=" + action); + Context appCtx = ctx.getApplicationContext(); + SessionToken token = new SessionToken(appCtx, new ComponentName(appCtx, MediaService.class)); + ListenableFuture future = new MediaController.Builder(appCtx, token).buildAsync(); + future.addListener(() -> { + try { + if (!future.isDone()) return; + MediaController c = future.get(); + Log.d("TempoWidget", "controller connected, isPlaying=" + c.isPlaying()); + switch (action) { + case WidgetProvider.ACT_PLAY_PAUSE: + if (c.isPlaying()) c.pause(); + else c.play(); + break; + case WidgetProvider.ACT_NEXT: + c.seekToNext(); + break; + case WidgetProvider.ACT_PREV: + c.seekToPrevious(); + break; + case WidgetProvider.ACT_TOGGLE_SHUFFLE: + c.setShuffleModeEnabled(!c.getShuffleModeEnabled()); + break; + case WidgetProvider.ACT_CYCLE_REPEAT: + int repeatMode = c.getRepeatMode(); + int nextMode; + if (repeatMode == Player.REPEAT_MODE_OFF) { + nextMode = Player.REPEAT_MODE_ALL; + } else if (repeatMode == Player.REPEAT_MODE_ALL) { + nextMode = Player.REPEAT_MODE_ONE; + } else { + nextMode = Player.REPEAT_MODE_OFF; + } + c.setRepeatMode(nextMode); + break; + } + WidgetUpdateManager.refreshFromController(ctx); + c.release(); + } catch (ExecutionException | InterruptedException e) { + Log.e("TempoWidget", "dispatch failed", e); + } + }, MoreExecutors.directExecutor()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider.java b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider.java new file mode 100644 index 0000000..93a1a7e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider.java @@ -0,0 +1,137 @@ +package com.cappielloantonio.tempo.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.TextUtils; +import android.widget.RemoteViews; + +import com.cappielloantonio.tempo.R; + +import android.app.TaskStackBuilder; + +import com.cappielloantonio.tempo.ui.activity.MainActivity; + +import android.util.Log; + +import androidx.annotation.Nullable; + +public class WidgetProvider extends AppWidgetProvider { + private static final String TAG = "TempoWidget"; + public static final String ACT_PLAY_PAUSE = "tempo.widget.PLAY_PAUSE"; + public static final String ACT_NEXT = "tempo.widget.NEXT"; + public static final String ACT_PREV = "tempo.widget.PREV"; + public static final String ACT_TOGGLE_SHUFFLE = "tempo.widget.SHUFFLE"; + public static final String ACT_CYCLE_REPEAT = "tempo.widget.REPEAT"; + + @Override + public void onUpdate(Context ctx, AppWidgetManager mgr, int[] ids) { + for (int id : ids) { + RemoteViews rv = WidgetUpdateManager.chooseBuild(ctx, id); + attachIntents(ctx, rv, id, null, null, null); + mgr.updateAppWidget(id, rv); + } + } + + @Override + public void onReceive(Context ctx, Intent intent) { + super.onReceive(ctx, intent); + String a = intent.getAction(); + Log.d(TAG, "onReceive action=" + a); + if (ACT_PLAY_PAUSE.equals(a) || ACT_NEXT.equals(a) || ACT_PREV.equals(a) + || ACT_TOGGLE_SHUFFLE.equals(a) || ACT_CYCLE_REPEAT.equals(a)) { + WidgetActions.dispatchToMediaSession(ctx, a); + } else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(a)) { + WidgetUpdateManager.refreshFromController(ctx); + } + } + + @Override + public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, android.os.Bundle newOptions) { + super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); + RemoteViews rv = WidgetUpdateManager.chooseBuild(context, appWidgetId); + attachIntents(context, rv, appWidgetId, null, null, null); + appWidgetManager.updateAppWidget(appWidgetId, rv); + WidgetUpdateManager.refreshFromController(context); + } + + public static void attachIntents(Context ctx, RemoteViews rv) { + attachIntents(ctx, rv, 0, null, null, null); + } + + public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase) { + attachIntents(ctx, rv, requestCodeBase, null, null, null); + } + + public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase, + String songLink, + String albumLink, + String artistLink) { + PendingIntent playPause = PendingIntent.getBroadcast( + ctx, + requestCodeBase + 0, + new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_PLAY_PAUSE), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + PendingIntent next = PendingIntent.getBroadcast( + ctx, + requestCodeBase + 1, + new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_NEXT), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + PendingIntent prev = PendingIntent.getBroadcast( + ctx, + requestCodeBase + 2, + new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_PREV), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + PendingIntent shuffle = PendingIntent.getBroadcast( + ctx, + requestCodeBase + 3, + new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_TOGGLE_SHUFFLE), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + PendingIntent repeat = PendingIntent.getBroadcast( + ctx, + requestCodeBase + 4, + new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_CYCLE_REPEAT), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + + rv.setOnClickPendingIntent(R.id.btn_play_pause, playPause); + rv.setOnClickPendingIntent(R.id.btn_next, next); + rv.setOnClickPendingIntent(R.id.btn_prev, prev); + rv.setOnClickPendingIntent(R.id.btn_shuffle, shuffle); + rv.setOnClickPendingIntent(R.id.btn_repeat, repeat); + + PendingIntent launch = buildMainActivityPendingIntent(ctx, requestCodeBase + 10, null); + rv.setOnClickPendingIntent(R.id.root, launch); + + PendingIntent songPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 20, songLink); + PendingIntent artistPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 21, artistLink); + PendingIntent albumPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 22, albumLink); + + PendingIntent fallback = launch; + rv.setOnClickPendingIntent(R.id.album_art, songPending != null ? songPending : fallback); + rv.setOnClickPendingIntent(R.id.title, songPending != null ? songPending : fallback); + rv.setOnClickPendingIntent(R.id.subtitle, + artistPending != null ? artistPending : (songPending != null ? songPending : fallback)); + rv.setOnClickPendingIntent(R.id.album, albumPending != null ? albumPending : fallback); + } + + private static PendingIntent buildMainActivityPendingIntent(Context ctx, int requestCode, @Nullable String link) { + Intent intent; + if (!TextUtils.isEmpty(link)) { + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link), ctx, MainActivity.class); + } else { + intent = new Intent(ctx, MainActivity.class); + } + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + TaskStackBuilder stackBuilder = TaskStackBuilder.create(ctx); + stackBuilder.addNextIntentWithParentStack(intent); + return stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider4x1.java b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider4x1.java new file mode 100644 index 0000000..b4e5923 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider4x1.java @@ -0,0 +1,9 @@ +package com.cappielloantonio.tempo.widget; + +/** + * AppWidget provider entry for the 4x1 widget card. Inherits all behavior + * from {@link WidgetProvider}. + */ +public class WidgetProvider4x1 extends WidgetProvider { +} + diff --git a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java new file mode 100644 index 0000000..f159c52 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java @@ -0,0 +1,309 @@ +package com.cappielloantonio.tempo.widget; + +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; + +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.R; + +import androidx.media3.common.C; +import androidx.media3.session.MediaController; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.util.AssetLinkUtil; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.concurrent.ExecutionException; + +public final class WidgetUpdateManager { + + private static final int WIDGET_SAFE_ART_SIZE = 512; + + public static void updateFromState(Context ctx, + String title, + String artist, + String album, + Bitmap art, + boolean playing, + boolean shuffleEnabled, + int repeatMode, + long positionMs, + long durationMs, + String songLink, + String albumLink, + String artistLink) { + if (TextUtils.isEmpty(title)) title = ctx.getString(R.string.widget_not_playing); + if (TextUtils.isEmpty(artist)) artist = ctx.getString(R.string.widget_placeholder_subtitle); + if (TextUtils.isEmpty(album)) album = ""; + + final TimingInfo timing = createTimingInfo(positionMs, durationMs); + + AppWidgetManager mgr = AppWidgetManager.getInstance(ctx); + int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class)); + for (int id : ids) { + android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, album, art, playing, + timing.elapsedText, timing.totalText, timing.progress, shuffleEnabled, repeatMode, id); + WidgetProvider.attachIntents(ctx, rv, id, songLink, albumLink, artistLink); + mgr.updateAppWidget(id, rv); + } + } + + public static void pushNow(Context ctx) { + AppWidgetManager mgr = AppWidgetManager.getInstance(ctx); + int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class)); + for (int id : ids) { + android.widget.RemoteViews rv = chooseBuild(ctx, id); + WidgetProvider.attachIntents(ctx, rv, id, null, null, null); + mgr.updateAppWidget(id, rv); + } + } + + public static void updateFromState(Context ctx, + String title, + String artist, + String album, + String coverArtId, + boolean playing, + boolean shuffleEnabled, + int repeatMode, + long positionMs, + long durationMs, + String songLink, + String albumLink, + String artistLink) { + final Context appCtx = ctx.getApplicationContext(); + final String t = TextUtils.isEmpty(title) ? appCtx.getString(R.string.widget_not_playing) : title; + final String a = TextUtils.isEmpty(artist) ? appCtx.getString(R.string.widget_placeholder_subtitle) : artist; + final String alb = !TextUtils.isEmpty(album) ? album : ""; + final boolean p = playing; + final boolean sh = shuffleEnabled; + final int rep = repeatMode; + final TimingInfo timing = createTimingInfo(positionMs, durationMs); + final String songLinkFinal = songLink; + final String albumLinkFinal = albumLink; + final String artistLinkFinal = artistLink; + + if (!TextUtils.isEmpty(coverArtId)) { + CustomGlideRequest.loadAlbumArtBitmap( + appCtx, + coverArtId, + WIDGET_SAFE_ART_SIZE, + new CustomTarget() { + @Override + public void onResourceReady(Bitmap resource, Transition transition) { + AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx); + int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class)); + for (int id : ids) { + android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, resource, p, + timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); + WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal); + mgr.updateAppWidget(id, rv); + } + } + + @Override + public void onLoadCleared(Drawable placeholder) { + AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx); + int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class)); + for (int id : ids) { + android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p, + timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); + WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal); + mgr.updateAppWidget(id, rv); + } + } + } + ); + } else { + AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx); + int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class)); + for (int id : ids) { + android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p, + timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); + WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal); + mgr.updateAppWidget(id, rv); + } + } + } + + public static void refreshFromController(Context ctx) { + final Context appCtx = ctx.getApplicationContext(); + SessionToken token = new SessionToken(appCtx, new ComponentName(appCtx, MediaService.class)); + ListenableFuture future = new MediaController.Builder(appCtx, token).buildAsync(); + future.addListener(() -> { + try { + if (!future.isDone()) return; + MediaController c = future.get(); + androidx.media3.common.MediaItem mi = c.getCurrentMediaItem(); + String title = null, artist = null, album = null, coverId = null; + String songLink = null, albumLink = null, artistLink = null; + if (mi != null && mi.mediaMetadata != null) { + if (mi.mediaMetadata.title != null) title = mi.mediaMetadata.title.toString(); + if (mi.mediaMetadata.artist != null) + artist = mi.mediaMetadata.artist.toString(); + if (mi.mediaMetadata.albumTitle != null) + album = mi.mediaMetadata.albumTitle.toString(); + if (mi.mediaMetadata.extras != null) { + Bundle extras = mi.mediaMetadata.extras; + if (title == null) title = mi.mediaMetadata.extras.getString("title"); + if (artist == null) artist = mi.mediaMetadata.extras.getString("artist"); + if (album == null) album = mi.mediaMetadata.extras.getString("album"); + coverId = extras.getString("coverArtId"); + + songLink = extras.getString("assetLinkSong"); + if (songLink == null) { + songLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras.getString("id")); + } + + albumLink = extras.getString("assetLinkAlbum"); + if (albumLink == null) { + albumLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras.getString("albumId")); + } + + artistLink = extras.getString("assetLinkArtist"); + if (artistLink == null) { + artistLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras.getString("artistId")); + } + } + } + long position = c.getCurrentPosition(); + long duration = c.getDuration(); + if (position == C.TIME_UNSET) position = 0; + if (duration == C.TIME_UNSET) duration = 0; + updateFromState(appCtx, + title != null ? title : appCtx.getString(R.string.widget_not_playing), + artist != null ? artist : appCtx.getString(R.string.widget_placeholder_subtitle), + album, + coverId, + c.isPlaying(), + c.getShuffleModeEnabled(), + c.getRepeatMode(), + position, + duration, + songLink, + albumLink, + artistLink); + c.release(); + } catch (ExecutionException | InterruptedException ignored) { + } + }, MoreExecutors.directExecutor()); + } + + private static TimingInfo createTimingInfo(long positionMs, long durationMs) { + long safePosition = Math.max(0L, positionMs); + long safeDuration = durationMs > 0 ? durationMs : 0L; + if (safeDuration > 0 && safePosition > safeDuration) { + safePosition = safeDuration; + } + + String elapsed = (safeDuration > 0 || safePosition > 0) + ? MusicUtil.getReadableDurationString(safePosition, true) + : null; + String total = safeDuration > 0 + ? MusicUtil.getReadableDurationString(safeDuration, true) + : null; + + int progress = 0; + if (safeDuration > 0) { + long scaled = safePosition * WidgetViewsFactory.PROGRESS_MAX; + long progressLong = scaled / safeDuration; + if (progressLong < 0) { + progress = 0; + } else if (progressLong > WidgetViewsFactory.PROGRESS_MAX) { + progress = WidgetViewsFactory.PROGRESS_MAX; + } else { + progress = (int) progressLong; + } + } + + return new TimingInfo(elapsed, total, progress); + } + + public static android.widget.RemoteViews chooseBuild(Context ctx, int appWidgetId) { + LayoutSize size = resolveLayoutSize(ctx, appWidgetId); + switch (size) { + case MEDIUM: + return WidgetViewsFactory.buildMedium(ctx); + case LARGE: + return WidgetViewsFactory.buildLarge(ctx); + case EXPANDED: + return WidgetViewsFactory.buildExpanded(ctx); + case COMPACT: + default: + return WidgetViewsFactory.buildCompact(ctx); + } + } + + private static android.widget.RemoteViews choosePopulate(Context ctx, + String title, + String artist, + String album, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + boolean shuffleEnabled, + int repeatMode, + int appWidgetId) { + LayoutSize size = resolveLayoutSize(ctx, appWidgetId); + switch (size) { + case MEDIUM: + return WidgetViewsFactory.populateMedium(ctx, title, artist, album, art, playing, + elapsedText, totalText, progress, shuffleEnabled, repeatMode); + case LARGE: + return WidgetViewsFactory.populateLarge(ctx, title, artist, album, art, playing, + elapsedText, totalText, progress, shuffleEnabled, repeatMode); + case EXPANDED: + return WidgetViewsFactory.populateExpanded(ctx, title, artist, album, art, playing, + elapsedText, totalText, progress, shuffleEnabled, repeatMode); + case COMPACT: + default: + return WidgetViewsFactory.populateCompact(ctx, title, artist, album, art, playing, + elapsedText, totalText, progress, shuffleEnabled, repeatMode); + } + } + + private static LayoutSize resolveLayoutSize(Context ctx, int appWidgetId) { + AppWidgetManager mgr = AppWidgetManager.getInstance(ctx); + android.os.Bundle opts = mgr.getAppWidgetOptions(appWidgetId); + int minH = opts != null ? opts.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) : 0; + int expandedThreshold = ctx.getResources().getInteger(R.integer.widget_expanded_min_height_dp); + int largeThreshold = ctx.getResources().getInteger(R.integer.widget_large_min_height_dp); + int mediumThreshold = ctx.getResources().getInteger(R.integer.widget_medium_min_height_dp); + if (minH >= expandedThreshold) return LayoutSize.EXPANDED; + if (minH >= largeThreshold) return LayoutSize.LARGE; + if (minH >= mediumThreshold) return LayoutSize.MEDIUM; + return LayoutSize.COMPACT; + } + + private enum LayoutSize { + COMPACT, + MEDIUM, + LARGE, + EXPANDED + } + + private static final class TimingInfo { + final String elapsedText; + final String totalText; + final int progress; + + TimingInfo(String elapsedText, String totalText, int progress) { + this.elapsedText = elapsedText; + this.totalText = totalText; + this.progress = progress; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java new file mode 100644 index 0000000..c66fd1c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java @@ -0,0 +1,252 @@ +package com.cappielloantonio.tempo.widget; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Shader; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.View; +import android.widget.RemoteViews; + +import androidx.core.content.ContextCompat; +import androidx.media3.common.Player; + +import com.cappielloantonio.tempo.R; + +public final class WidgetViewsFactory { + + static final int PROGRESS_MAX = 1000; + private static final float ALBUM_ART_CORNER_RADIUS_DP = 6f; + + private WidgetViewsFactory() { + } + + public static RemoteViews buildCompact(Context ctx) { + return build(ctx, R.layout.widget_layout_compact, false, false); + } + + public static RemoteViews buildMedium(Context ctx) { + return build(ctx, R.layout.widget_layout_medium, false, false); + } + + public static RemoteViews buildLarge(Context ctx) { + return build(ctx, R.layout.widget_layout_large_short, true, true); + } + + public static RemoteViews buildExpanded(Context ctx) { + return build(ctx, R.layout.widget_layout_large, true, true); + } + + private static RemoteViews build(Context ctx, + int layoutRes, + boolean showAlbum, + boolean showSecondaryControls) { + RemoteViews rv = new RemoteViews(ctx.getPackageName(), layoutRes); + rv.setTextViewText(R.id.title, ctx.getString(R.string.widget_not_playing)); + rv.setTextViewText(R.id.subtitle, ctx.getString(R.string.widget_placeholder_subtitle)); + rv.setTextViewText(R.id.album, ""); + rv.setViewVisibility(R.id.album, showAlbum ? View.INVISIBLE : View.GONE); + rv.setTextViewText(R.id.time_elapsed, ctx.getString(R.string.widget_time_elapsed_placeholder)); + rv.setTextViewText(R.id.time_total, ctx.getString(R.string.widget_time_duration_placeholder)); + rv.setProgressBar(R.id.progress, PROGRESS_MAX, 0, false); + rv.setImageViewResource(R.id.btn_play_pause, R.drawable.ic_play); + rv.setImageViewResource(R.id.album_art, R.drawable.ic_splash_logo); + applySecondaryControlsDefaults(ctx, rv, showSecondaryControls); + return rv; + } + + private static void applySecondaryControlsDefaults(Context ctx, + RemoteViews rv, + boolean show) { + int visibility = show ? View.VISIBLE : View.GONE; + rv.setViewVisibility(R.id.controls_secondary, visibility); + rv.setViewVisibility(R.id.btn_shuffle, visibility); + rv.setViewVisibility(R.id.btn_repeat, visibility); + if (show) { + int defaultColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint); + rv.setImageViewResource(R.id.btn_shuffle, R.drawable.ic_shuffle); + rv.setImageViewResource(R.id.btn_repeat, R.drawable.ic_repeat); + rv.setInt(R.id.btn_shuffle, "setColorFilter", defaultColor); + rv.setInt(R.id.btn_repeat, "setColorFilter", defaultColor); + } + } + + public static RemoteViews populateCompact(Context ctx, + String title, + String subtitle, + String album, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + boolean shuffleEnabled, + int repeatMode) { + return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText, + progress, R.layout.widget_layout_compact, false, false, shuffleEnabled, repeatMode); + } + + public static RemoteViews populateMedium(Context ctx, + String title, + String subtitle, + String album, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + boolean shuffleEnabled, + int repeatMode) { + return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText, + progress, R.layout.widget_layout_medium, true, true, shuffleEnabled, repeatMode); + } + + public static RemoteViews populateLarge(Context ctx, + String title, + String subtitle, + String album, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + boolean shuffleEnabled, + int repeatMode) { + return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText, + progress, R.layout.widget_layout_large_short, true, true, shuffleEnabled, repeatMode); + } + + public static RemoteViews populateExpanded(Context ctx, + String title, + String subtitle, + String album, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + boolean shuffleEnabled, + int repeatMode) { + return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText, + progress, R.layout.widget_layout_large, true, true, shuffleEnabled, repeatMode); + } + + private static RemoteViews populateWithLayout(Context ctx, + String title, + String subtitle, + String album, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + int layoutRes, + boolean showAlbum, + boolean showSecondaryControls, + boolean shuffleEnabled, + int repeatMode) { + RemoteViews rv = new RemoteViews(ctx.getPackageName(), layoutRes); + rv.setTextViewText(R.id.title, title); + rv.setTextViewText(R.id.subtitle, subtitle); + + if (showAlbum && !TextUtils.isEmpty(album)) { + rv.setTextViewText(R.id.album, album); + rv.setViewVisibility(R.id.album, View.VISIBLE); + } else { + rv.setTextViewText(R.id.album, ""); + rv.setViewVisibility(R.id.album, View.GONE); + } + + if (art != null) { + Bitmap rounded = maybeRoundBitmap(ctx, art); + rv.setImageViewBitmap(R.id.album_art, rounded != null ? rounded : art); + } else { + rv.setImageViewResource(R.id.album_art, R.drawable.ic_splash_logo); + } + + rv.setImageViewResource(R.id.btn_play_pause, + playing ? R.drawable.ic_pause : R.drawable.ic_play); + + String elapsed = !TextUtils.isEmpty(elapsedText) + ? elapsedText + : ctx.getString(R.string.widget_time_elapsed_placeholder); + String total = !TextUtils.isEmpty(totalText) + ? totalText + : ctx.getString(R.string.widget_time_duration_placeholder); + + int safeProgress = progress; + if (safeProgress < 0) safeProgress = 0; + if (safeProgress > PROGRESS_MAX) safeProgress = PROGRESS_MAX; + + rv.setTextViewText(R.id.time_elapsed, elapsed); + rv.setTextViewText(R.id.time_total, total); + rv.setProgressBar(R.id.progress, PROGRESS_MAX, safeProgress, false); + + applySecondaryControls(ctx, rv, showSecondaryControls, shuffleEnabled, repeatMode); + + return rv; + } + + private static Bitmap maybeRoundBitmap(Context ctx, Bitmap source) { + if (source == null || source.isRecycled()) { + return null; + } + + try { + int width = source.getWidth(); + int height = source.getHeight(); + if (width <= 0 || height <= 0) { + return null; + } + + Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); + + float radiusPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + ALBUM_ART_CORNER_RADIUS_DP, + ctx.getResources().getDisplayMetrics()); + float maxRadius = Math.min(width, height) / 2f; + float safeRadius = Math.min(radiusPx, maxRadius); + + canvas.drawRoundRect(new RectF(0f, 0f, width, height), safeRadius, safeRadius, paint); + return output; + } catch (RuntimeException | OutOfMemoryError e) { + android.util.Log.w("TempoWidget", "Failed to round album art", e); + return null; + } + } + + private static void applySecondaryControls(Context ctx, + RemoteViews rv, + boolean show, + boolean shuffleEnabled, + int repeatMode) { + if (!show) { + rv.setViewVisibility(R.id.controls_secondary, View.GONE); + rv.setViewVisibility(R.id.btn_shuffle, View.GONE); + rv.setViewVisibility(R.id.btn_repeat, View.GONE); + return; + } + + int inactiveColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint); + int activeColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint_active); + + rv.setViewVisibility(R.id.controls_secondary, View.VISIBLE); + rv.setViewVisibility(R.id.btn_shuffle, View.VISIBLE); + rv.setViewVisibility(R.id.btn_repeat, View.VISIBLE); + rv.setImageViewResource(R.id.btn_shuffle, R.drawable.ic_shuffle); + rv.setImageViewResource(R.id.btn_repeat, + repeatMode == Player.REPEAT_MODE_ONE ? R.drawable.ic_repeat_one : R.drawable.ic_repeat); + rv.setInt(R.id.btn_shuffle, "setColorFilter", shuffleEnabled ? activeColor : inactiveColor); + rv.setInt(R.id.btn_repeat, "setColorFilter", + repeatMode == Player.REPEAT_MODE_OFF ? inactiveColor : activeColor); + } +} diff --git a/app/src/main/res/drawable/button_favorite_selector.xml b/app/src/main/res/drawable/button_favorite_selector.xml new file mode 100644 index 0000000..6742158 --- /dev/null +++ b/app/src/main/res/drawable/button_favorite_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_play_pause_selector.xml b/app/src/main/res/drawable/button_play_pause_selector.xml new file mode 100644 index 0000000..cb5c785 --- /dev/null +++ b/app/src/main/res/drawable/button_play_pause_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_skip_silence_selector.xml b/app/src/main/res/drawable/button_skip_silence_selector.xml new file mode 100644 index 0000000..cc8a011 --- /dev/null +++ b/app/src/main/res/drawable/button_skip_silence_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fast_scrollbar_bubble.xml b/app/src/main/res/drawable/fast_scrollbar_bubble.xml new file mode 100644 index 0000000..1b2bfd4 --- /dev/null +++ b/app/src/main/res/drawable/fast_scrollbar_bubble.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fast_scrollbar_handle.xml b/app/src/main/res/drawable/fast_scrollbar_handle.xml new file mode 100644 index 0000000..ae1ce48 --- /dev/null +++ b/app/src/main/res/drawable/fast_scrollbar_handle.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gradient_backdrop_background_image.xml b/app/src/main/res/drawable/gradient_backdrop_background_image.xml new file mode 100644 index 0000000..a98cd90 --- /dev/null +++ b/app/src/main/res/drawable/gradient_backdrop_background_image.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gradient_discover_background_image.xml b/app/src/main/res/drawable/gradient_discover_background_image.xml new file mode 100644 index 0000000..1356bdd --- /dev/null +++ b/app/src/main/res/drawable/gradient_discover_background_image.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/gradient_player_cover_background_image.xml b/app/src/main/res/drawable/gradient_player_cover_background_image.xml new file mode 100644 index 0000000..7e7a22e --- /dev/null +++ b/app/src/main/res/drawable/gradient_player_cover_background_image.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..57198eb --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..77bb91d --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 0000000..59ec814 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bookmark.xml b/app/src/main/res/drawable/ic_bookmark.xml new file mode 100644 index 0000000..c2042b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bookmark_sync.xml b/app/src/main/res/drawable/ic_bookmark_sync.xml new file mode 100644 index 0000000..b27f3a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_sync.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 0000000..7435e42 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..c1aa0f1 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_done.xml b/app/src/main/res/drawable/ic_done.xml new file mode 100644 index 0000000..fee2fd7 --- /dev/null +++ b/app/src/main/res/drawable/ic_done.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100644 index 0000000..afbabb4 --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_drag_handle.xml b/app/src/main/res/drawable/ic_drag_handle.xml new file mode 100644 index 0000000..d283f15 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_handle.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_eq.xml b/app/src/main/res/drawable/ic_eq.xml new file mode 100644 index 0000000..5f3a8b4 --- /dev/null +++ b/app/src/main/res/drawable/ic_eq.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml new file mode 100644 index 0000000..8cffcf1 --- /dev/null +++ b/app/src/main/res/drawable/ic_error.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite.xml b/app/src/main/res/drawable/ic_favorite.xml new file mode 100644 index 0000000..753a422 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorites_outlined.xml b/app/src/main/res/drawable/ic_favorites_outlined.xml new file mode 100644 index 0000000..03706ff --- /dev/null +++ b/app/src/main/res/drawable/ic_favorites_outlined.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_feed.xml b/app/src/main/res/drawable/ic_feed.xml new file mode 100644 index 0000000..2baa451 --- /dev/null +++ b/app/src/main/res/drawable/ic_feed.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_file_download.xml b/app/src/main/res/drawable/ic_file_download.xml new file mode 100644 index 0000000..3f16729 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_download.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_filter_list.xml b/app/src/main/res/drawable/ic_filter_list.xml new file mode 100644 index 0000000..9c116d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_filter_list.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_folder.xml b/app/src/main/res/drawable/ic_folder.xml new file mode 100644 index 0000000..2c6f69f --- /dev/null +++ b/app/src/main/res/drawable/ic_folder.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_forward.xml b/app/src/main/res/drawable/ic_forward.xml new file mode 100644 index 0000000..220a440 --- /dev/null +++ b/app/src/main/res/drawable/ic_forward.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_graphic_eq.xml b/app/src/main/res/drawable/ic_graphic_eq.xml new file mode 100644 index 0000000..c9f4b0f --- /dev/null +++ b/app/src/main/res/drawable/ic_graphic_eq.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 0000000..e6b3b45 --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml new file mode 100644 index 0000000..083e7c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_home.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_stream.xml b/app/src/main/res/drawable/ic_info_stream.xml new file mode 100644 index 0000000..5165a16 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_stream.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..89ac22c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml new file mode 100644 index 0000000..5592db2 --- /dev/null +++ b/app/src/main/res/drawable/ic_link.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_lyrics.xml b/app/src/main/res/drawable/ic_lyrics.xml new file mode 100644 index 0000000..cac983a --- /dev/null +++ b/app/src/main/res/drawable/ic_lyrics.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_lyrics_sync_lock.xml b/app/src/main/res/drawable/ic_lyrics_sync_lock.xml new file mode 100644 index 0000000..5576242 --- /dev/null +++ b/app/src/main/res/drawable/ic_lyrics_sync_lock.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_mix_from_here.xml b/app/src/main/res/drawable/ic_mix_from_here.xml new file mode 100644 index 0000000..8c351ab --- /dev/null +++ b/app/src/main/res/drawable/ic_mix_from_here.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_vert.xml b/app/src/main/res/drawable/ic_more_vert.xml new file mode 100644 index 0000000..69fb9ec --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_navigate_next.xml b/app/src/main/res/drawable/ic_navigate_next.xml new file mode 100644 index 0000000..56c0ca8 --- /dev/null +++ b/app/src/main/res/drawable/ic_navigate_next.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml new file mode 100644 index 0000000..d9b8d16 --- /dev/null +++ b/app/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_placeholder_album.xml b/app/src/main/res/drawable/ic_placeholder_album.xml new file mode 100644 index 0000000..1e5ecfd --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_album.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_placeholder_artist.xml b/app/src/main/res/drawable/ic_placeholder_artist.xml new file mode 100644 index 0000000..2f71051 --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_artist.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/ic_placeholder_directory.xml b/app/src/main/res/drawable/ic_placeholder_directory.xml new file mode 100644 index 0000000..956cc9d --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_directory.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_placeholder_folder.xml b/app/src/main/res/drawable/ic_placeholder_folder.xml new file mode 100644 index 0000000..956cc9d --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_folder.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_placeholder_playlist.xml b/app/src/main/res/drawable/ic_placeholder_playlist.xml new file mode 100644 index 0000000..e8d5fba --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_playlist.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_placeholder_podcast.xml b/app/src/main/res/drawable/ic_placeholder_podcast.xml new file mode 100644 index 0000000..7562864 --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_podcast.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_placeholder_radio.xml b/app/src/main/res/drawable/ic_placeholder_radio.xml new file mode 100644 index 0000000..6874184 --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_radio.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_placeholder_song.xml b/app/src/main/res/drawable/ic_placeholder_song.xml new file mode 100644 index 0000000..1dbadbd --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_song.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml new file mode 100644 index 0000000..f8f81bd --- /dev/null +++ b/app/src/main/res/drawable/ic_play.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_for_work.xml b/app/src/main/res/drawable/ic_play_for_work.xml new file mode 100644 index 0000000..c7514fe --- /dev/null +++ b/app/src/main/res/drawable/ic_play_for_work.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist_add.xml b/app/src/main/res/drawable/ic_playlist_add.xml new file mode 100644 index 0000000..25c22a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist_add.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_podcast_download.xml b/app/src/main/res/drawable/ic_podcast_download.xml new file mode 100644 index 0000000..24a34c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_podcast_download.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_queue.xml b/app/src/main/res/drawable/ic_queue.xml new file mode 100644 index 0000000..6a04876 --- /dev/null +++ b/app/src/main/res/drawable/ic_queue.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..f3dcb53 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_repeat.xml b/app/src/main/res/drawable/ic_repeat.xml new file mode 100644 index 0000000..244a36e --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_repeat_one.xml b/app/src/main/res/drawable/ic_repeat_one.xml new file mode 100644 index 0000000..f422f79 --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat_one.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_replay.xml b/app/src/main/res/drawable/ic_replay.xml new file mode 100644 index 0000000..2f803b7 --- /dev/null +++ b/app/src/main/res/drawable/ic_replay.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..8ee9978 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_server_transcode_priority.xml b/app/src/main/res/drawable/ic_server_transcode_priority.xml new file mode 100644 index 0000000..5af6fea --- /dev/null +++ b/app/src/main/res/drawable/ic_server_transcode_priority.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..24d3261 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000..900774a --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_shuffle.xml b/app/src/main/res/drawable/ic_shuffle.xml new file mode 100644 index 0000000..73de35f --- /dev/null +++ b/app/src/main/res/drawable/ic_shuffle.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_skip_next.xml b/app/src/main/res/drawable/ic_skip_next.xml new file mode 100644 index 0000000..1529802 --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_next.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_skip_previous.xml b/app/src/main/res/drawable/ic_skip_previous.xml new file mode 100644 index 0000000..783c31d --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_previous.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_skip_silence.xml b/app/src/main/res/drawable/ic_skip_silence.xml new file mode 100644 index 0000000..1fef7bf --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_silence.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_skip_silence_outlined.xml b/app/src/main/res/drawable/ic_skip_silence_outlined.xml new file mode 100644 index 0000000..1938a5b --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_silence_outlined.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sort_list.xml b/app/src/main/res/drawable/ic_sort_list.xml new file mode 100644 index 0000000..33efc4a --- /dev/null +++ b/app/src/main/res/drawable/ic_sort_list.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_splash_logo.xml b/app/src/main/res/drawable/ic_splash_logo.xml new file mode 100644 index 0000000..ed526ec --- /dev/null +++ b/app/src/main/res/drawable/ic_splash_logo.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_star.xml b/app/src/main/res/drawable/ic_star.xml new file mode 100644 index 0000000..2d74f8a --- /dev/null +++ b/app/src/main/res/drawable/ic_star.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_star_outlined.xml b/app/src/main/res/drawable/ic_star_outlined.xml new file mode 100644 index 0000000..7637008 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_outlined.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_tap.xml b/app/src/main/res/drawable/ic_tap.xml new file mode 100644 index 0000000..7d29510 --- /dev/null +++ b/app/src/main/res/drawable/ic_tap.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toolbar_motion_on.xml b/app/src/main/res/drawable/ic_toolbar_motion_on.xml new file mode 100644 index 0000000..8119556 --- /dev/null +++ b/app/src/main/res/drawable/ic_toolbar_motion_on.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_toolbar_tempo.xml b/app/src/main/res/drawable/ic_toolbar_tempo.xml new file mode 100644 index 0000000..63e72c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_toolbar_tempo.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_transcode.xml b/app/src/main/res/drawable/ic_transcode.xml new file mode 100644 index 0000000..805d809 --- /dev/null +++ b/app/src/main/res/drawable/ic_transcode.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/logo.xml b/app/src/main/res/drawable/logo.xml new file mode 100644 index 0000000..8d9ad6d --- /dev/null +++ b/app/src/main/res/drawable/logo.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ui_empty_description.xml b/app/src/main/res/drawable/ui_empty_description.xml new file mode 100644 index 0000000..f96d063 --- /dev/null +++ b/app/src/main/res/drawable/ui_empty_description.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ui_empty_list.xml b/app/src/main/res/drawable/ui_empty_list.xml new file mode 100644 index 0000000..3630ed6 --- /dev/null +++ b/app/src/main/res/drawable/ui_empty_list.xml @@ -0,0 +1,597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ui_empty_podcast.xml b/app/src/main/res/drawable/ui_empty_podcast.xml new file mode 100644 index 0000000..d85dce1 --- /dev/null +++ b/app/src/main/res/drawable/ui_empty_podcast.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ui_empty_radio_station.xml b/app/src/main/res/drawable/ui_empty_radio_station.xml new file mode 100644 index 0000000..d85dce1 --- /dev/null +++ b/app/src/main/res/drawable/ui_empty_radio_station.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ui_eq_not_supported.xml b/app/src/main/res/drawable/ui_eq_not_supported.xml new file mode 100644 index 0000000..fc8a364 --- /dev/null +++ b/app/src/main/res/drawable/ui_eq_not_supported.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ui_splash_screen.xml b/app/src/main/res/drawable/ui_splash_screen.xml new file mode 100644 index 0000000..f789f88 --- /dev/null +++ b/app/src/main/res/drawable/ui_splash_screen.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/widget_bg.xml b/app/src/main/res/drawable/widget_bg.xml new file mode 100644 index 0000000..c569bbe --- /dev/null +++ b/app/src/main/res/drawable/widget_bg.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/font/inter.xml b/app/src/main/res/font/inter.xml new file mode 100644 index 0000000..059f7b5 --- /dev/null +++ b/app/src/main/res/font/inter.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/inter_black.ttf b/app/src/main/res/font/inter_black.ttf new file mode 100644 index 0000000..5653757 Binary files /dev/null and b/app/src/main/res/font/inter_black.ttf differ diff --git a/app/src/main/res/font/inter_bold.ttf b/app/src/main/res/font/inter_bold.ttf new file mode 100644 index 0000000..e98b84c Binary files /dev/null and b/app/src/main/res/font/inter_bold.ttf differ diff --git a/app/src/main/res/font/inter_extra_bold.ttf b/app/src/main/res/font/inter_extra_bold.ttf new file mode 100644 index 0000000..7f16a0f Binary files /dev/null and b/app/src/main/res/font/inter_extra_bold.ttf differ diff --git a/app/src/main/res/font/inter_extra_light.ttf b/app/src/main/res/font/inter_extra_light.ttf new file mode 100644 index 0000000..69426a3 Binary files /dev/null and b/app/src/main/res/font/inter_extra_light.ttf differ diff --git a/app/src/main/res/font/inter_light.ttf b/app/src/main/res/font/inter_light.ttf new file mode 100644 index 0000000..a5f0736 Binary files /dev/null and b/app/src/main/res/font/inter_light.ttf differ diff --git a/app/src/main/res/font/inter_medium.ttf b/app/src/main/res/font/inter_medium.ttf new file mode 100644 index 0000000..721147d Binary files /dev/null and b/app/src/main/res/font/inter_medium.ttf differ diff --git a/app/src/main/res/font/inter_regular.ttf b/app/src/main/res/font/inter_regular.ttf new file mode 100644 index 0000000..96fd6a1 Binary files /dev/null and b/app/src/main/res/font/inter_regular.ttf differ diff --git a/app/src/main/res/font/inter_semi_bold.ttf b/app/src/main/res/font/inter_semi_bold.ttf new file mode 100644 index 0000000..ddb2792 Binary files /dev/null and b/app/src/main/res/font/inter_semi_bold.ttf differ diff --git a/app/src/main/res/font/inter_thin.ttf b/app/src/main/res/font/inter_thin.ttf new file mode 100644 index 0000000..76be625 Binary files /dev/null and b/app/src/main/res/font/inter_thin.ttf differ diff --git a/app/src/main/res/layout-land/inner_fragment_player_controller_layout.xml b/app/src/main/res/layout-land/inner_fragment_player_controller_layout.xml new file mode 100644 index 0000000..cb3ae9c --- /dev/null +++ b/app/src/main/res/layout-land/inner_fragment_player_controller_layout.xml @@ -0,0 +1,404 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +