Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82c22ed247 | ||
|
|
48ce3a2a4f | ||
|
|
b93acc6563 | ||
|
|
9c088a7e88 | ||
|
|
de2f1067a7 | ||
|
|
1c21546461 | ||
|
|
a4121e8d49 | ||
|
|
4cc4cc7363 | ||
|
|
c5ef274916 | ||
|
|
2c53f36a18 | ||
|
|
6c637dcbcb | ||
|
|
89fa38f5a0 | ||
|
|
87f8bdc618 | ||
|
|
903fde4bdc | ||
|
|
3824dd882c | ||
|
|
602bab6414 | ||
|
|
d891e429b6 | ||
|
|
ebefd77027 | ||
|
|
57f34affd9 | ||
|
|
50b5ab38bc | ||
|
|
9f61d70fca | ||
|
|
a97a2d5b50 | ||
|
|
8d73a2cd36 | ||
|
|
ca5a0698bb | ||
|
|
04f34e03d1 | ||
|
|
233bc9987e | ||
|
|
f0e418687e | ||
|
|
e87b658447 | ||
|
|
19c985c9e4 | ||
|
|
4ab122a9d7 | ||
|
|
ff0c42d14c | ||
|
|
d1e247f9e2 | ||
|
|
f1d19142fa | ||
|
|
45793c343a | ||
|
|
aa4249842d | ||
|
|
126663f1e5 | ||
|
|
ec19e8c401 | ||
|
|
717f95a04a | ||
|
|
ccce01a61b | ||
|
|
a7682d7656 | ||
|
|
84de93a4f1 | ||
|
|
78c4c89eca | ||
|
|
328beaff90 | ||
|
|
9d5d89d648 | ||
|
|
cd8b06f544 | ||
|
|
47a0def06c | ||
|
|
1c2f1aa061 | ||
|
|
30281e8f2d | ||
|
|
3c58e6fbb2 | ||
|
|
99a399b4d7 | ||
|
|
1da0a0b810 | ||
|
|
539920965e | ||
|
|
9a64eeabe6 | ||
|
|
791190f681 | ||
|
|
c03fca8039 | ||
|
|
620fba0a14 | ||
|
|
1357c5c062 | ||
|
|
682f63ef38 | ||
|
|
24864637f9 | ||
|
|
3ba19be4d9 | ||
|
|
cce6456951 | ||
|
|
fda586c4d8 | ||
|
|
57be72d5d4 | ||
|
|
c2354d4d42 | ||
|
|
a940af934c | ||
|
|
5891ec800c | ||
|
|
7259a82b67 | ||
|
|
c2b6d7eed5 | ||
|
|
2335bf2095 | ||
|
|
8bb6c02e46 | ||
|
|
47380a79a5 | ||
|
|
a187ba1e75 | ||
|
|
3eb9b2fb5c | ||
|
|
a547e19361 | ||
|
|
f4722fa0a8 | ||
|
|
ee738bc4c7 | ||
|
|
a22883fdde | ||
|
|
2acf11023a | ||
|
|
9736890e3c | ||
|
|
e790bf3eb6 | ||
|
|
e1d63a9eef | ||
|
|
134a1605ad | ||
|
|
1b45036963 | ||
|
|
0ba12c3d84 | ||
|
|
8cc3356b14 | ||
|
|
9d439b726b | ||
|
|
d4c0e30fd1 | ||
|
|
bb23d7e866 | ||
|
|
7321ef46f2 | ||
|
|
2fe2c2b28b | ||
|
|
a9318ec5d0 | ||
|
|
eb29dc2fb2 | ||
|
|
287e4a2b10 | ||
|
|
b7d56c2d70 | ||
|
|
5a6d101bdf | ||
|
|
969f0b5b21 | ||
|
|
14939d20fd | ||
|
|
35af1f9038 | ||
|
|
b79cfa4af0 | ||
|
|
e81e1a5356 | ||
|
|
cc0e264a17 | ||
|
|
a83495f353 | ||
|
|
be4346b3d1 | ||
|
|
2e29e9537a | ||
|
|
bc0adfe8e0 | ||
|
|
5261ca317b | ||
|
|
bf4ff3f1f9 | ||
|
|
cd195dbba0 | ||
|
|
7ec78991a5 | ||
|
|
e1c5a60805 | ||
|
|
f74813ef69 | ||
|
|
040558198e | ||
|
|
5ab68e4a98 | ||
|
|
aa8fac43a6 | ||
|
|
905bb3e3c5 | ||
|
|
d810010090 | ||
|
|
52ba783a90 | ||
|
|
d72855e160 | ||
|
|
82a9f00173 | ||
|
|
3f5749f7e1 | ||
|
|
cd2ab36351 | ||
|
|
a6688f897a | ||
|
|
64658dda1f | ||
|
|
d9f701d9d3 | ||
|
|
60fee3c77c | ||
|
|
b89086c5be | ||
|
|
d3dd236054 | ||
|
|
2e3330b63f | ||
|
|
e604c9ba86 | ||
|
|
2bf39d846e | ||
|
|
06066f1f66 | ||
|
|
7c0d44680f | ||
|
|
d4cb6c5c9a | ||
|
|
fab18c130e | ||
|
|
bd753f4489 | ||
|
|
e43a2b6fe5 | ||
|
|
c62d2ace4d | ||
|
|
6d403f808c | ||
|
|
1223062388 | ||
|
|
b0ddd5388b | ||
|
|
92f79a8e3d | ||
|
|
473d7e4e9c | ||
|
|
7ca0415274 | ||
|
|
cf7feacdc0 | ||
|
|
4740028a44 |
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: eddyizm
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
62
.github/workflows/github_release.yml
vendored
62
.github/workflows/github_release.yml
vendored
@@ -35,12 +35,18 @@ jobs:
|
|||||||
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
||||||
echo Last build tool version is: $BUILD_TOOL_VERSION
|
echo Last build tool version is: $BUILD_TOOL_VERSION
|
||||||
|
|
||||||
- name: Build APK
|
- name: Build All APKs
|
||||||
id: build
|
id: build
|
||||||
run: bash ./gradlew assembleTempoRelease
|
run: |
|
||||||
|
# Build release variants
|
||||||
|
bash ./gradlew assembleTempoRelease
|
||||||
|
bash ./gradlew assembleNotquitemyRelease
|
||||||
|
# Build debug variants
|
||||||
|
bash ./gradlew assembleTempoDebug
|
||||||
|
bash ./gradlew assembleNotquitemyDebug
|
||||||
|
|
||||||
- name: Sign APK
|
- name: Sign Tempo Release APKs
|
||||||
id: sign_apk
|
id: sign_tempo_release
|
||||||
uses: r0adkll/sign-android-release@v1
|
uses: r0adkll/sign-android-release@v1
|
||||||
with:
|
with:
|
||||||
releaseDirectory: app/build/outputs/apk/tempo/release
|
releaseDirectory: app/build/outputs/apk/tempo/release
|
||||||
@@ -51,11 +57,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
||||||
|
|
||||||
- name: Make artifact
|
- name: Sign NotQuiteMy Release APKs
|
||||||
uses: actions/upload-artifact@v4
|
id: sign_notquitemy_release
|
||||||
|
uses: r0adkll/sign-android-release@v1
|
||||||
with:
|
with:
|
||||||
name: app-release-signed
|
releaseDirectory: app/build/outputs/apk/notquitemy/release
|
||||||
path: ${{steps.sign_apk.outputs.signedReleaseFile}}
|
signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
|
||||||
|
alias: ${{ secrets.KEY_ALIAS_GITHUB }}
|
||||||
|
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD_GITHUB }}
|
||||||
|
env:
|
||||||
|
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
@@ -67,12 +79,40 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload Release APKs
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: ${{steps.sign_apk.outputs.signedReleaseFile}}
|
asset_path: ${{steps.sign_tempo_release.outputs.signedReleaseFile}}
|
||||||
asset_name: app-tempo-release.apk
|
asset_name: app-tempo-release.apk
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/vnd.android.package-archive
|
||||||
|
|
||||||
|
- name: Upload NotQuiteMy Release APK
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ${{steps.sign_notquitemy_release.outputs.signedReleaseFile}}
|
||||||
|
asset_name: app-notquitemy-release.apk
|
||||||
|
asset_content_type: application/vnd.android.package-archive
|
||||||
|
|
||||||
|
- name: Upload Debug APKs as artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: debug-apks
|
||||||
|
path: |
|
||||||
|
app/build/outputs/apk/tempo/debug/
|
||||||
|
app/build/outputs/apk/notquitemy/debug/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Upload Release APKs as artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-apks
|
||||||
|
path: |
|
||||||
|
${{steps.sign_tempo_release.outputs.signedReleaseFile}}
|
||||||
|
${{steps.sign_notquitemy_release.outputs.signedReleaseFile}}
|
||||||
|
retention-days: 30
|
||||||
67
CHANGELOG.md
67
CHANGELOG.md
@@ -2,6 +2,73 @@
|
|||||||
|
|
||||||
***This log is for this fork to detail updates since 3.9.0 from the main repo.***
|
***This log is for this fork to detail updates since 3.9.0 from the main repo.***
|
||||||
|
|
||||||
|
## [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)
|
## [3.14.1](https://github.com/eddyizm/tempo/releases/tag/v3.14.1) (2025-08-30)
|
||||||
## What's Changed
|
## What's Changed
|
||||||
* feat: rating dialog added to album page by @eddyizm in https://github.com/eddyizm/tempo/pull/52
|
* feat: rating dialog added to album page by @eddyizm in https://github.com/eddyizm/tempo/pull/52
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -24,8 +24,21 @@ Tempo does not rely on magic algorithms to decide what you should listen to. Ins
|
|||||||
|
|
||||||
## Fork
|
## Fork
|
||||||
|
|
||||||
|
sha256 signing key fingerprint
|
||||||
|
`SHA256: 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`
|
||||||
|
|
||||||
This fork is my attempt to keep development moving forward and merge in PR's that have been sitting for a while in the main repo. Thankful to @CappielloAntonio for the amazing app and hopefully we can continue to build on top of it. I will only be releasing on github and if I am not able to merge back to the main repo, I plan to rename the app to be able to publish it to fdroid and possibly google play? We will see.
|
This fork is my attempt to keep development moving forward and merge in PR's that have been sitting for a while in the main repo. Thankful to @CappielloAntonio for the amazing app and hopefully we can continue to build on top of it. I will only be releasing on github and if I am not able to merge back to the main repo, I plan to rename the app to be able to publish it to fdroid and possibly google play? We will see.
|
||||||
|
|
||||||
|
### Releases
|
||||||
|
|
||||||
|
Please note the two variants in the release assets include release/debug and 32/64 bit flavors.
|
||||||
|
|
||||||
|
`app-tempo` <- The github release with all the android auto/chromecast features
|
||||||
|
|
||||||
|
`app-notquitemy*` <- The f-droid release that goes without any of the google stuff. It was last released at 3.8.1 from the original repo. Since I don't have access to that original repo, I am releasing the apk's here on github.
|
||||||
|
|
||||||
|
As mentioned above, I am working towards a rebrand to get into app stores with a new name an icon.
|
||||||
|
|
||||||
Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md)
|
Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md)
|
||||||
|
|
||||||
Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
|
Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
|
||||||
@@ -47,12 +60,9 @@ Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
|
|||||||
- **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.
|
- **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.
|
- **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.
|
||||||
|
|
||||||
## Sponsors
|
## Credits
|
||||||
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (3.9.0)
|
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (3.9.0)
|
||||||
|
|
||||||
Tempo is an open-source project developed and maintained solely by me. I would like to express my heartfelt thanks to all the users who have shown their love and support for Tempo. Your contributions and encouragement mean a lot to me, and they help drive the development and improvement of the app.
|
|
||||||
|
|
||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -87,6 +97,16 @@ Tempo is an open-source project developed and maintained solely by me. I would l
|
|||||||
<img src="mockup/dark/8_screenshot.png" width=200>
|
<img src="mockup/dark/8_screenshot.png" width=200>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Please fork and open PR's against the development branch. Make sure your PR builds successfully.
|
||||||
|
|
||||||
|
If there is a UI change, please provide a before/after screenshot and a short video/gif if that helps elaborating the fix/feature in the PR.
|
||||||
|
|
||||||
|
Currently there are not 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.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Tempo 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.
|
Tempo 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.
|
||||||
|
|||||||
17
USAGE.md
17
USAGE.md
@@ -1,4 +1,4 @@
|
|||||||
# App Name Usage Guide
|
# Tempo Usage Guide
|
||||||
[<- back home](README.md)
|
[<- back home](README.md)
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
@@ -60,7 +60,20 @@ This app works with any service that implements the Subsonic API, including:
|
|||||||
**TODO**
|
**TODO**
|
||||||
|
|
||||||
### Now Playing Screen
|
### Now Playing Screen
|
||||||
**TODO**
|
|
||||||
|
On the main player control screen, tapping on the artwork will reveal a small collection of 4 buttons/icons.
|
||||||
|
<p align="left">
|
||||||
|
<img src="mockup/usage/player_icons.png" width=159>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
*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
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ android {
|
|||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
|
|
||||||
versionCode 30
|
versionCode 35
|
||||||
versionName '3.14.1'
|
versionName '3.17.0'
|
||||||
|
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
@@ -23,8 +22,21 @@ android {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
enable true
|
||||||
|
reset()
|
||||||
|
//noinspection ChromeOsAbiSupport
|
||||||
|
include 'armeabi-v7a', 'arm64-v8a'
|
||||||
|
universalApk false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
flavorDimensions += "default"
|
flavorDimensions += "default"
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
@@ -38,10 +50,6 @@ android {
|
|||||||
applicationId "com.cappielloantonio.notquitemy.tempo"
|
applicationId "com.cappielloantonio.notquitemy.tempo"
|
||||||
}
|
}
|
||||||
|
|
||||||
play {
|
|
||||||
dimension = "default"
|
|
||||||
applicationId "com.cappielloantonio.play.tempo"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -51,6 +59,11 @@ android {
|
|||||||
debuggable false
|
debuggable false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
debuggable true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -98,7 +111,7 @@ dependencies {
|
|||||||
implementation 'androidx.media3:media3-ui:1.5.1'
|
implementation 'androidx.media3:media3-ui:1.5.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-hls:1.5.1'
|
implementation 'androidx.media3:media3-exoplayer-hls:1.5.1'
|
||||||
tempoImplementation 'androidx.media3:media3-cast:1.5.1'
|
tempoImplementation 'androidx.media3:media3-cast:1.5.1'
|
||||||
playImplementation 'androidx.media3:media3-cast:1.5.1'
|
|
||||||
|
|
||||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||||
annotationProcessor 'androidx.room:room-compiler:2.6.1'
|
annotationProcessor 'androidx.room:room-compiler:2.6.1'
|
||||||
@@ -112,4 +125,4 @@ java {
|
|||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(17)
|
languageVersion = JavaLanguageVersion.of(17)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1151
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json
Normal file
1151
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,16 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="asset"
|
||||||
|
android:scheme="tempo" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
@@ -73,5 +83,20 @@
|
|||||||
android:name="autoStoreLocales"
|
android:name="autoStoreLocales"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widget.WidgetProvider4x1"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/widget_label">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_info"/>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.cappielloantonio.tempo.database.converter.DateConverters;
|
|||||||
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
|
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.DownloadDao;
|
import com.cappielloantonio.tempo.database.dao.DownloadDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
|
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.PlaylistDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.QueueDao;
|
import com.cappielloantonio.tempo.database.dao.QueueDao;
|
||||||
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
||||||
@@ -20,6 +21,7 @@ import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao;
|
|||||||
import com.cappielloantonio.tempo.model.Chronology;
|
import com.cappielloantonio.tempo.model.Chronology;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.model.Favorite;
|
import com.cappielloantonio.tempo.model.Favorite;
|
||||||
|
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||||
import com.cappielloantonio.tempo.model.Queue;
|
import com.cappielloantonio.tempo.model.Queue;
|
||||||
import com.cappielloantonio.tempo.model.RecentSearch;
|
import com.cappielloantonio.tempo.model.RecentSearch;
|
||||||
import com.cappielloantonio.tempo.model.Server;
|
import com.cappielloantonio.tempo.model.Server;
|
||||||
@@ -28,9 +30,9 @@ import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
|||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@Database(
|
@Database(
|
||||||
version = 11,
|
version = 12,
|
||||||
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class},
|
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)}
|
autoMigrations = {@AutoMigration(from = 10, to = 11), @AutoMigration(from = 11, to = 12)}
|
||||||
)
|
)
|
||||||
@TypeConverters({DateConverters.class})
|
@TypeConverters({DateConverters.class})
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
@@ -62,4 +64,6 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||||||
public abstract SessionMediaItemDao sessionMediaItemDao();
|
public abstract SessionMediaItemDao sessionMediaItemDao();
|
||||||
|
|
||||||
public abstract PlaylistDao playlistDao();
|
public abstract PlaylistDao playlistDao();
|
||||||
|
|
||||||
|
public abstract LyricsDao lyricsDao();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ public interface DownloadDao {
|
|||||||
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
|
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
|
||||||
LiveData<List<Download>> getAll();
|
LiveData<List<Download>> getAll();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
|
||||||
|
List<Download> getAllSync();
|
||||||
|
|
||||||
@Query("SELECT * FROM download WHERE id = :id")
|
@Query("SELECT * FROM download WHERE id = :id")
|
||||||
Download getOne(String id);
|
Download getOne(String id);
|
||||||
|
|
||||||
@@ -30,6 +33,9 @@ public interface DownloadDao {
|
|||||||
@Query("DELETE FROM download WHERE id = :id")
|
@Query("DELETE FROM download WHERE id = :id")
|
||||||
void delete(String id);
|
void delete(String id);
|
||||||
|
|
||||||
|
@Query("DELETE FROM download WHERE id IN (:ids)")
|
||||||
|
void deleteByIds(List<String> ids);
|
||||||
|
|
||||||
@Query("DELETE FROM download")
|
@Query("DELETE FROM download")
|
||||||
void deleteAll();
|
void deleteAll();
|
||||||
}
|
}
|
||||||
@@ -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<LyricsCache> observeOne(String songId);
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
void insert(LyricsCache lyricsCache);
|
||||||
|
|
||||||
|
@Query("DELETE FROM lyrics_cache WHERE song_id = :songId")
|
||||||
|
void delete(String songId);
|
||||||
|
}
|
||||||
@@ -4,14 +4,18 @@ import android.content.Context;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.GlideBuilder;
|
import com.bumptech.glide.GlideBuilder;
|
||||||
import com.bumptech.glide.annotation.GlideModule;
|
import com.bumptech.glide.annotation.GlideModule;
|
||||||
import com.bumptech.glide.load.DecodeFormat;
|
import com.bumptech.glide.load.DecodeFormat;
|
||||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
|
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
|
||||||
|
import com.bumptech.glide.Registry;
|
||||||
import com.bumptech.glide.module.AppGlideModule;
|
import com.bumptech.glide.module.AppGlideModule;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
public class CustomGlideModule extends AppGlideModule {
|
public class CustomGlideModule extends AppGlideModule {
|
||||||
@Override
|
@Override
|
||||||
@@ -20,4 +24,9 @@ public class CustomGlideModule extends AppGlideModule {
|
|||||||
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "cache", diskCacheSize));
|
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "cache", diskCacheSize));
|
||||||
builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.cappielloantonio.tempo.glide;
|
package com.cappielloantonio.tempo.glide;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -16,6 +17,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
|||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||||
import com.bumptech.glide.request.RequestOptions;
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
import com.bumptech.glide.request.target.CustomTarget;
|
||||||
import com.bumptech.glide.signature.ObjectKey;
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
@@ -109,9 +111,21 @@ public class CustomGlideRequest {
|
|||||||
return uri.toString();
|
return uri.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void loadAlbumArtBitmap(Context context,
|
||||||
|
String coverId,
|
||||||
|
int size,
|
||||||
|
CustomTarget<Bitmap> target) {
|
||||||
|
String url = createUrl(coverId, size);
|
||||||
|
Glide.with(context)
|
||||||
|
.asBitmap()
|
||||||
|
.load(url)
|
||||||
|
.apply(createRequestOptions(context, coverId, ResourceType.Album))
|
||||||
|
.into(target);
|
||||||
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
private final RequestManager requestManager;
|
private final RequestManager requestManager;
|
||||||
private Object item;
|
private String item;
|
||||||
|
|
||||||
private Builder(Context context, String item, ResourceType type) {
|
private Builder(Context context, String item, ResourceType type) {
|
||||||
this.requestManager = Glide.with(context);
|
this.requestManager = Glide.with(context);
|
||||||
|
|||||||
@@ -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<String, InputStream> {
|
||||||
|
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<InputStream> 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<InputStream> {
|
||||||
|
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<? super InputStream> 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<InputStream> getDataClass() {
|
||||||
|
return InputStream.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public DataSource getDataSource() {
|
||||||
|
return DataSource.REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Factory implements ModelLoaderFactory<String, InputStream> {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||||
|
return new IPv6StringLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void teardown() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,8 @@ class Download(@PrimaryKey override val id: String) : Child(id) {
|
|||||||
transcodedSuffix = child.transcodedSuffix
|
transcodedSuffix = child.transcodedSuffix
|
||||||
duration = child.duration
|
duration = child.duration
|
||||||
bitrate = child.bitrate
|
bitrate = child.bitrate
|
||||||
|
samplingRate = child.samplingRate
|
||||||
|
bitDepth = child.bitDepth
|
||||||
path = child.path
|
path = child.path
|
||||||
isVideo = child.isVideo
|
isVideo = child.isVideo
|
||||||
userRating = child.userRating
|
userRating = child.userRating
|
||||||
|
|||||||
@@ -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()
|
||||||
|
)
|
||||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.model
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
|
import androidx.media3.common.HeartRating
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaItem.RequestMetadata
|
import androidx.media3.common.MediaItem.RequestMetadata
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
@@ -243,6 +244,13 @@ class SessionMediaItem() {
|
|||||||
.setAlbumTitle(album)
|
.setAlbumTitle(album)
|
||||||
.setArtist(artist)
|
.setArtist(artist)
|
||||||
.setArtworkUri(artworkUri)
|
.setArtworkUri(artworkUri)
|
||||||
|
.setUserRating(HeartRating(starred != null))
|
||||||
|
.setSupportedCommands(
|
||||||
|
listOf(
|
||||||
|
Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON,
|
||||||
|
Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF
|
||||||
|
)
|
||||||
|
)
|
||||||
.setExtras(bundle)
|
.setExtras(bundle)
|
||||||
.setIsBrowsable(false)
|
.setIsBrowsable(false)
|
||||||
.setIsPlayable(true)
|
.setIsPlayable(true)
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package com.cappielloantonio.tempo.repository;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
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.ArtistInfo2;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
||||||
@@ -13,12 +15,92 @@ import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class ArtistRepository {
|
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<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null &&
|
||||||
|
response.body().getSubsonicResponse().getArtist() != null &&
|
||||||
|
response.body().getSubsonicResponse().getArtist().getAlbums() != null) {
|
||||||
|
|
||||||
|
List<AlbumID3> 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<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
Log.d("ArtistSync", "Error getting artist info: " + t.getMessage());
|
||||||
|
callback.onSongsCollected(new ArrayList<>());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchAllAlbumSongsWithCallback(List<AlbumID3> albums, ArtistSongsCallback callback) {
|
||||||
|
if (albums == null || albums.isEmpty()) {
|
||||||
|
Log.d("ArtistSync", "No albums to process");
|
||||||
|
callback.onSongsCollected(new ArrayList<>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Child> 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<List<Child>> 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<Child> songs);
|
||||||
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<ArtistID3>> getStarredArtists(boolean random, int size) {
|
public MutableLiveData<List<ArtistID3>> getStarredArtists(boolean random, int size) {
|
||||||
MutableLiveData<List<ArtistID3>> starredArtists = new MutableLiveData<>(new ArrayList<>());
|
MutableLiveData<List<ArtistID3>> starredArtists = new MutableLiveData<>(new ArrayList<>());
|
||||||
|
|
||||||
@@ -89,7 +171,7 @@ public class ArtistRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Metodo che mi restituisce le informazioni essenzionali dell'artista (cover, numero di album...)
|
* Method that returns essential artist information (cover, album number, etc.)
|
||||||
*/
|
*/
|
||||||
public void getArtistInfo(List<ArtistID3> artists, MutableLiveData<List<ArtistID3>> list) {
|
public void getArtistInfo(List<ArtistID3> artists, MutableLiveData<List<ArtistID3>> list) {
|
||||||
List<ArtistID3> liveArtists = list.getValue();
|
List<ArtistID3> liveArtists = list.getValue();
|
||||||
|
|||||||
@@ -18,6 +18,20 @@ public class DownloadRepository {
|
|||||||
return downloadDao.getAll();
|
return downloadDao.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Download> 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) {
|
public Download getDownload(String id) {
|
||||||
Download download = null;
|
Download download = null;
|
||||||
|
|
||||||
@@ -35,6 +49,24 @@ public class DownloadRepository {
|
|||||||
return download;
|
return download;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class GetAllDownloadsThreadSafe implements Runnable {
|
||||||
|
private final DownloadDao downloadDao;
|
||||||
|
private List<Download> downloads;
|
||||||
|
|
||||||
|
public GetAllDownloadsThreadSafe(DownloadDao downloadDao) {
|
||||||
|
this.downloadDao = downloadDao;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
downloads = downloadDao.getAllSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Download> getDownloads() {
|
||||||
|
return downloads;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class GetDownloadThreadSafe implements Runnable {
|
private static class GetDownloadThreadSafe implements Runnable {
|
||||||
private final DownloadDao downloadDao;
|
private final DownloadDao downloadDao;
|
||||||
private final String id;
|
private final String id;
|
||||||
@@ -143,6 +175,12 @@ public class DownloadRepository {
|
|||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void delete(List<String> ids) {
|
||||||
|
DeleteMultipleThreadSafe delete = new DeleteMultipleThreadSafe(downloadDao, ids);
|
||||||
|
Thread thread = new Thread(delete);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
private static class DeleteThreadSafe implements Runnable {
|
private static class DeleteThreadSafe implements Runnable {
|
||||||
private final DownloadDao downloadDao;
|
private final DownloadDao downloadDao;
|
||||||
private final String id;
|
private final String id;
|
||||||
@@ -157,4 +195,19 @@ public class DownloadRepository {
|
|||||||
downloadDao.delete(id);
|
downloadDao.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class DeleteMultipleThreadSafe implements Runnable {
|
||||||
|
private final DownloadDao downloadDao;
|
||||||
|
private final List<String> ids;
|
||||||
|
|
||||||
|
public DeleteMultipleThreadSafe(DownloadDao downloadDao, List<String> ids) {
|
||||||
|
this.downloadDao = downloadDao;
|
||||||
|
this.ids = ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
downloadDao.deleteByIds(ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<LyricsCache> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,21 +80,52 @@ public class PlaylistRepository {
|
|||||||
return listLivePlaylistSongs;
|
return listLivePlaylistSongs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
public MutableLiveData<Playlist> getPlaylist(String id) {
|
||||||
|
MutableLiveData<Playlist> playlistLiveData = new MutableLiveData<>();
|
||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
.updatePlaylist(playlistId, null, true, songsId, null)
|
.getPlaylist(id)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_success), Toast.LENGTH_SHORT).show();
|
if (response.isSuccessful()
|
||||||
|
&& response.body() != null
|
||||||
|
&& response.body().getSubsonicResponse().getPlaylist() != null) {
|
||||||
|
playlistLiveData.setValue(response.body().getSubsonicResponse().getPlaylist());
|
||||||
|
} else {
|
||||||
|
playlistLiveData.setValue(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_failure), Toast.LENGTH_SHORT).show();
|
playlistLiveData.setValue(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return playlistLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSongToPlaylist(String playlistId, ArrayList<String> 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<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> 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<ApiResponse> 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<String> songsId) {
|
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||||
@@ -131,23 +162,6 @@ public class PlaylistRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updatePlaylist(String playlistId, String name, boolean isPublic, ArrayList<String> songIdToAdd, ArrayList<Integer> songIndexToRemove) {
|
|
||||||
App.getSubsonicClientInstance(false)
|
|
||||||
.getPlaylistClient()
|
|
||||||
.updatePlaylist(playlistId, name, isPublic, songIdToAdd, songIndexToRemove)
|
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deletePlaylist(String playlistId) {
|
public void deletePlaylist(String playlistId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
package com.cappielloantonio.tempo.service;
|
package com.cappielloantonio.tempo.service;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.common.Timeline;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.session.MediaBrowser;
|
import androidx.media3.session.MediaBrowser;
|
||||||
import androidx.media3.session.SessionToken;
|
import androidx.media3.session.SessionToken;
|
||||||
@@ -21,14 +27,79 @@ import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
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.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
public class MediaManager {
|
public class MediaManager {
|
||||||
private static final String TAG = "MediaManager";
|
private static final String TAG = "MediaManager";
|
||||||
|
private static WeakReference<MediaBrowser> attachedBrowserRef = new WeakReference<>(null);
|
||||||
|
|
||||||
|
public static void registerPlaybackObserver(
|
||||||
|
ListenableFuture<MediaBrowser> browserFuture,
|
||||||
|
PlaybackViewModel playbackViewModel
|
||||||
|
) {
|
||||||
|
if (browserFuture == null) return;
|
||||||
|
|
||||||
|
Futures.addCallback(browserFuture, new FutureCallback<MediaBrowser>() {
|
||||||
|
@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<MediaBrowser> mediaBrowserListenableFuture) {
|
public static void reset(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture) {
|
||||||
if (mediaBrowserListenableFuture != null) {
|
if (mediaBrowserListenableFuture != null) {
|
||||||
@@ -107,11 +178,24 @@ public class MediaManager {
|
|||||||
mediaBrowserListenableFuture.addListener(() -> {
|
mediaBrowserListenableFuture.addListener(() -> {
|
||||||
try {
|
try {
|
||||||
if (mediaBrowserListenableFuture.isDone()) {
|
if (mediaBrowserListenableFuture.isDone()) {
|
||||||
mediaBrowserListenableFuture.get().clearMediaItems();
|
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||||
mediaBrowserListenableFuture.get().setMediaItems(MappingUtil.mapMediaItems(media));
|
browser.clearMediaItems();
|
||||||
mediaBrowserListenableFuture.get().prepare();
|
browser.setMediaItems(MappingUtil.mapMediaItems(media));
|
||||||
mediaBrowserListenableFuture.get().seekTo(startIndex, 0);
|
browser.prepare();
|
||||||
mediaBrowserListenableFuture.get().play();
|
|
||||||
|
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);
|
enqueueDatabase(media, true, 0);
|
||||||
}
|
}
|
||||||
} catch (ExecutionException | InterruptedException e) {
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
@@ -139,6 +223,25 @@ public class MediaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void playDownloadedMediaItem(ListenableFuture<MediaBrowser> 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<MediaBrowser> mediaBrowserListenableFuture, InternetRadioStation internetRadioStation) {
|
public static void startRadio(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, InternetRadioStation internetRadioStation) {
|
||||||
if (mediaBrowserListenableFuture != null) {
|
if (mediaBrowserListenableFuture != null) {
|
||||||
mediaBrowserListenableFuture.addListener(() -> {
|
mediaBrowserListenableFuture.addListener(() -> {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import android.util.Log;
|
|||||||
import com.cappielloantonio.tempo.subsonic.RetrofitClient;
|
import com.cappielloantonio.tempo.subsonic.RetrofitClient;
|
||||||
import com.cappielloantonio.tempo.subsonic.Subsonic;
|
import com.cappielloantonio.tempo.subsonic.Subsonic;
|
||||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
|
|
||||||
@@ -21,7 +24,15 @@ public class SystemClient {
|
|||||||
|
|
||||||
public Call<ApiResponse> ping() {
|
public Call<ApiResponse> ping() {
|
||||||
Log.d(TAG, "ping()");
|
Log.d(TAG, "ping()");
|
||||||
return systemService.ping(subsonic.getParams());
|
Call<ApiResponse> 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<ApiResponse> getLicense() {
|
public Call<ApiResponse> getLicense() {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package com.cappielloantonio.tempo.ui.activity;
|
package com.cappielloantonio.tempo.ui.activity;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -13,7 +16,10 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.core.splashscreen.SplashScreen;
|
import androidx.core.splashscreen.SplashScreen;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.MediaMetadata;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
@@ -31,6 +37,8 @@ import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog;
|
|||||||
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
|
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
|
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
|
||||||
import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment;
|
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.Constants;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
||||||
@@ -54,8 +62,11 @@ public class MainActivity extends BaseActivity {
|
|||||||
private BottomNavigationView bottomNavigationView;
|
private BottomNavigationView bottomNavigationView;
|
||||||
public NavController navController;
|
public NavController navController;
|
||||||
private BottomSheetBehavior bottomSheetBehavior;
|
private BottomSheetBehavior bottomSheetBehavior;
|
||||||
|
private AssetLinkNavigator assetLinkNavigator;
|
||||||
|
private AssetLinkUtil.AssetLink pendingAssetLink;
|
||||||
|
|
||||||
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
||||||
|
private Intent pendingDownloadPlaybackIntent;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -69,6 +80,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
setContentView(view);
|
setContentView(view);
|
||||||
|
|
||||||
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
|
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
|
||||||
|
assetLinkNavigator = new AssetLinkNavigator(this);
|
||||||
|
|
||||||
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
|
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
|
||||||
connectivityStatusReceiverManager(true);
|
connectivityStatusReceiverManager(true);
|
||||||
@@ -77,12 +89,16 @@ public class MainActivity extends BaseActivity {
|
|||||||
checkConnectionType();
|
checkConnectionType();
|
||||||
getOpenSubsonicExtensions();
|
getOpenSubsonicExtensions();
|
||||||
checkTempoUpdate();
|
checkTempoUpdate();
|
||||||
|
|
||||||
|
maybeSchedulePlaybackIntent(getIntent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
pingServer();
|
||||||
initService();
|
initService();
|
||||||
|
consumePendingPlaybackIntent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -98,6 +114,14 @@ public class MainActivity extends BaseActivity {
|
|||||||
bind = null;
|
bind = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
setIntent(intent);
|
||||||
|
maybeSchedulePlaybackIntent(intent);
|
||||||
|
consumePendingPlaybackIntent();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
|
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
|
||||||
@@ -292,6 +316,24 @@ public class MainActivity extends BaseActivity {
|
|||||||
public void goFromLogin() {
|
public void goFromLogin() {
|
||||||
setBottomSheetInPeek(mainViewModel.isQueueLoaded());
|
setBottomSheetInPeek(mainViewModel.isQueueLoaded());
|
||||||
goToHome();
|
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() {
|
public void quit() {
|
||||||
@@ -351,6 +393,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
Preferences.switchInUseServerAddress();
|
Preferences.switchInUseServerAddress();
|
||||||
App.refreshSubsonicClient();
|
App.refreshSubsonicClient();
|
||||||
pingServer();
|
pingServer();
|
||||||
|
resetView();
|
||||||
} else {
|
} else {
|
||||||
Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic());
|
Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic());
|
||||||
}
|
}
|
||||||
@@ -361,6 +404,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
Preferences.switchInUseServerAddress();
|
Preferences.switchInUseServerAddress();
|
||||||
App.refreshSubsonicClient();
|
App.refreshSubsonicClient();
|
||||||
pingServer();
|
pingServer();
|
||||||
|
resetView();
|
||||||
} else {
|
} else {
|
||||||
mainViewModel.ping().observe(this, subsonicResponse -> {
|
mainViewModel.ping().observe(this, subsonicResponse -> {
|
||||||
if (subsonicResponse == null) {
|
if (subsonicResponse == null) {
|
||||||
@@ -376,6 +420,13 @@ public class MainActivity extends BaseActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void resetView() {
|
||||||
|
resetViewModel();
|
||||||
|
int id = Objects.requireNonNull(navController.getCurrentDestination()).getId();
|
||||||
|
navController.popBackStack(id, true);
|
||||||
|
navController.navigate(id);
|
||||||
|
}
|
||||||
|
|
||||||
private void getOpenSubsonicExtensions() {
|
private void getOpenSubsonicExtensions() {
|
||||||
if (Preferences.getToken() != null) {
|
if (Preferences.getToken() != null) {
|
||||||
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
|
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
|
||||||
@@ -408,4 +459,98 @@ public class MainActivity extends BaseActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
|
|||||||
R.string.song_subtitle_formatter,
|
R.string.song_subtitle_formatter,
|
||||||
song.getArtist(),
|
song.getArtist(),
|
||||||
MusicUtil.getReadableDurationString(song.getDuration(), false),
|
MusicUtil.getReadableDurationString(song.getDuration(), false),
|
||||||
""
|
MusicUtil.getReadableAudioQualityString(song)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter;
|
|||||||
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -23,17 +24,24 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueueAdapter.ViewHolder> {
|
public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueueAdapter.ViewHolder> {
|
||||||
|
private static final String TAG = "PlayerSongQueueAdapter";
|
||||||
private final ClickCallback click;
|
private final ClickCallback click;
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
private List<Child> songs;
|
private List<Child> songs;
|
||||||
|
|
||||||
|
private String currentPlayingId;
|
||||||
|
private boolean isPlaying;
|
||||||
|
private List<Integer> currentPlayingPositions = Collections.emptyList();
|
||||||
|
|
||||||
public PlayerSongQueueAdapter(ClickCallback click) {
|
public PlayerSongQueueAdapter(ClickCallback click) {
|
||||||
this.click = click;
|
this.click = click;
|
||||||
this.songs = Collections.emptyList();
|
this.songs = Collections.emptyList();
|
||||||
@@ -104,6 +112,46 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
|
|||||||
} else {
|
} else {
|
||||||
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
|
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<Child> getItems() {
|
public List<Child> getItems() {
|
||||||
@@ -132,6 +180,46 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
|
|||||||
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
|
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPlaybackState(String mediaId, boolean playing) {
|
||||||
|
String oldId = this.currentPlayingId;
|
||||||
|
boolean oldPlaying = this.isPlaying;
|
||||||
|
List<Integer> oldPositions = currentPlayingPositions;
|
||||||
|
|
||||||
|
this.currentPlayingId = mediaId;
|
||||||
|
this.isPlaying = playing;
|
||||||
|
|
||||||
|
if (Objects.equals(oldId, mediaId) && oldPlaying == playing) {
|
||||||
|
List<Integer> 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<Integer> findPositionsById(String id) {
|
||||||
|
if (id == null) return Collections.emptyList();
|
||||||
|
List<Integer> 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) {
|
public Child getItem(int id) {
|
||||||
return songs.get(id);
|
return songs.get(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.cappielloantonio.tempo.ui.adapter;
|
package com.cappielloantonio.tempo.ui.adapter;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -9,7 +11,9 @@ import android.widget.Filterable;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.session.MediaBrowser;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
@@ -21,8 +25,11 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.DiscTitle;
|
import com.cappielloantonio.tempo.subsonic.models.DiscTitle;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
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.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -30,6 +37,7 @@ import java.util.Comparator;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAdapter.ViewHolder> implements Filterable {
|
public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAdapter.ViewHolder> implements Filterable {
|
||||||
@@ -42,6 +50,11 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||||||
private List<Child> songs;
|
private List<Child> songs;
|
||||||
private String currentFilter;
|
private String currentFilter;
|
||||||
|
|
||||||
|
private String currentPlayingId;
|
||||||
|
private boolean isPlaying;
|
||||||
|
private List<Integer> currentPlayingPositions = Collections.emptyList();
|
||||||
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
private final Filter filtering = new Filter() {
|
private final Filter filtering = new Filter() {
|
||||||
@Override
|
@Override
|
||||||
protected FilterResults performFiltering(CharSequence constraint) {
|
protected FilterResults performFiltering(CharSequence constraint) {
|
||||||
@@ -70,10 +83,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||||
songs = (List<Child>) results.values;
|
songs = (List<Child>) results.values;
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
|
|
||||||
|
for (int pos : currentPlayingPositions) {
|
||||||
|
if (pos >= 0 && pos < songs.size()) {
|
||||||
|
notifyItemChanged(pos, "payload_playback");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public SongHorizontalAdapter(ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) {
|
public SongHorizontalAdapter(LifecycleOwner lifecycleOwner, ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) {
|
||||||
this.click = click;
|
this.click = click;
|
||||||
this.showCoverArt = showCoverArt;
|
this.showCoverArt = showCoverArt;
|
||||||
this.showAlbum = showAlbum;
|
this.showAlbum = showAlbum;
|
||||||
@@ -81,6 +100,11 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||||||
this.songsFull = Collections.emptyList();
|
this.songsFull = Collections.emptyList();
|
||||||
this.currentFilter = "";
|
this.currentFilter = "";
|
||||||
this.album = album;
|
this.album = album;
|
||||||
|
setHasStableIds(false);
|
||||||
|
|
||||||
|
if (lifecycleOwner != null) {
|
||||||
|
MappingUtil.observeExternalAudioRefresh(lifecycleOwner, this::handleExternalAudioRefresh);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@@ -91,7 +115,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> 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);
|
Child song = songs.get(position);
|
||||||
|
|
||||||
holder.item.searchResultSongTitleTextView.setText(song.getTitle());
|
holder.item.searchResultSongTitleTextView.setText(song.getTitle());
|
||||||
@@ -109,10 +142,18 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||||||
|
|
||||||
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
|
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
|
||||||
|
|
||||||
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
|
||||||
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
if (ExternalAudioReader.getUri(song) != null) {
|
||||||
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showCoverArt) CustomGlideRequest.Builder
|
if (showCoverArt) CustomGlideRequest.Builder
|
||||||
@@ -165,6 +206,39 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||||||
} else {
|
} else {
|
||||||
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
|
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
|
@Override
|
||||||
@@ -188,6 +262,46 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPlaybackState(String mediaId, boolean playing) {
|
||||||
|
String oldId = this.currentPlayingId;
|
||||||
|
boolean oldPlaying = this.isPlaying;
|
||||||
|
List<Integer> oldPositions = currentPlayingPositions;
|
||||||
|
|
||||||
|
this.currentPlayingId = mediaId;
|
||||||
|
this.isPlaying = playing;
|
||||||
|
|
||||||
|
if (Objects.equals(oldId, mediaId) && oldPlaying == playing) {
|
||||||
|
List<Integer> 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<Integer> findPositionsById(String id) {
|
||||||
|
if (id == null) return Collections.emptyList();
|
||||||
|
List<Integer> positions = new ArrayList<>();
|
||||||
|
for (int i = 0; i < songs.size(); i++) {
|
||||||
|
if (id.equals(songs.get(i).getId())) {
|
||||||
|
positions.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Filter getFilter() {
|
public Filter getFilter() {
|
||||||
return filtering;
|
return filtering;
|
||||||
@@ -215,11 +329,29 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onClick() {
|
public void onClick() {
|
||||||
|
int pos = getBindingAdapterPosition();
|
||||||
|
Child tappedSong = songs.get(pos);
|
||||||
|
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(MusicUtil.limitPlayableMedia(songs, getBindingAdapterPosition())));
|
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(MusicUtil.limitPlayableMedia(songs, getBindingAdapterPosition())));
|
||||||
bundle.putInt(Constants.ITEM_POSITION, MusicUtil.getPlayableMediaPosition(songs, getBindingAdapterPosition()));
|
bundle.putInt(Constants.ITEM_POSITION, MusicUtil.getPlayableMediaPosition(songs, getBindingAdapterPosition()));
|
||||||
|
|
||||||
click.onMediaClick(bundle);
|
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() {
|
private boolean onLongClick() {
|
||||||
@@ -247,4 +379,8 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||||||
|
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMediaBrowserListenableFuture(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture) {
|
||||||
|
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package com.cappielloantonio.tempo.ui.dialog;
|
|||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
@@ -12,6 +15,9 @@ import androidx.media3.common.util.UnstableApi;
|
|||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
|
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
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;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
@@ -42,7 +48,21 @@ public class DeleteDownloadStorageDialog extends DialogFragment {
|
|||||||
if (dialog != null) {
|
if (dialog != null) {
|
||||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||||
positiveButton.setOnClickListener(v -> {
|
positiveButton.setOnClickListener(v -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
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();
|
dialog.dismiss();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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<Intent> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ public class DownloadStorageDialog extends DialogFragment {
|
|||||||
.setTitle(R.string.download_storage_dialog_title)
|
.setTitle(R.string.download_storage_dialog_title)
|
||||||
.setPositiveButton(R.string.download_storage_external_dialog_positive_button, null)
|
.setPositiveButton(R.string.download_storage_external_dialog_positive_button, null)
|
||||||
.setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null)
|
.setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null)
|
||||||
|
.setNeutralButton(R.string.download_storage_directory_dialog_neutral_button, null)
|
||||||
.create();
|
.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +75,20 @@ public class DownloadStorageDialog extends DialogFragment {
|
|||||||
|
|
||||||
dialog.dismiss();
|
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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
|||||||
|
|
||||||
private PlaylistDialogHorizontalAdapter playlistDialogHorizontalAdapter;
|
private PlaylistDialogHorizontalAdapter playlistDialogHorizontalAdapter;
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
@@ -100,8 +101,7 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
|||||||
public void onPlaylistClick(Bundle bundle) {
|
public void onPlaylistClick(Bundle bundle) {
|
||||||
if (playlistChooserViewModel.getSongsToAdd() != null && !playlistChooserViewModel.getSongsToAdd().isEmpty()) {
|
if (playlistChooserViewModel.getSongsToAdd() != null && !playlistChooserViewModel.getSongsToAdd().isEmpty()) {
|
||||||
Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT);
|
Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT);
|
||||||
playlistChooserViewModel.addSongsToPlaylist(playlist.getId());
|
playlistChooserViewModel.addSongsToPlaylist(this, getDialog(), playlist.getId());
|
||||||
dismiss();
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_add_failure, Toast.LENGTH_SHORT).show();
|
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_add_failure, Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ public class StarredSyncDialog extends DialogFragment {
|
|||||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||||
positiveButton.setOnClickListener(v -> {
|
positiveButton.setOnClickListener(v -> {
|
||||||
starredSyncViewModel.getStarredTracks(requireActivity()).observe(requireActivity(), songs -> {
|
starredSyncViewModel.getStarredTracks(requireActivity()).observe(requireActivity(), songs -> {
|
||||||
if (songs != null) {
|
if (songs != null && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(context).download(
|
DownloadUtil.getDownloadTracker(context).download(
|
||||||
MappingUtil.mapDownloads(songs),
|
MappingUtil.mapDownloads(songs),
|
||||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.dialog;
|
|||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
@@ -10,6 +11,7 @@ import androidx.media3.common.MediaMetadata;
|
|||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.databinding.DialogTrackInfoBinding;
|
import com.cappielloantonio.tempo.databinding.DialogTrackInfoBinding;
|
||||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
|
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
@@ -21,6 +23,11 @@ public class TrackInfoDialog extends DialogFragment {
|
|||||||
private DialogTrackInfoBinding bind;
|
private DialogTrackInfoBinding bind;
|
||||||
|
|
||||||
private final MediaMetadata mediaMetadata;
|
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) {
|
public TrackInfoDialog(MediaMetadata mediaMetadata) {
|
||||||
this.mediaMetadata = mediaMetadata;
|
this.mediaMetadata = mediaMetadata;
|
||||||
@@ -52,6 +59,8 @@ public class TrackInfoDialog extends DialogFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setTrackInfo() {
|
private void setTrackInfo() {
|
||||||
|
genreLink = null;
|
||||||
|
yearLink = null;
|
||||||
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
||||||
bind.trakArtistInfoTextView.setText(
|
bind.trakArtistInfoTextView.setText(
|
||||||
mediaMetadata.artist != null
|
mediaMetadata.artist != null
|
||||||
@@ -61,17 +70,41 @@ public class TrackInfoDialog extends DialogFragment {
|
|||||||
: "");
|
: "");
|
||||||
|
|
||||||
if (mediaMetadata.extras != null) {
|
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
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), mediaMetadata.extras.getString("coverArtId", ""), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), mediaMetadata.extras.getString("coverArtId", ""), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.into(bind.trackCoverInfoImageView);
|
.into(bind.trackCoverInfoImageView);
|
||||||
|
|
||||||
bind.titleValueSector.setText(mediaMetadata.extras.getString("title", getString(R.string.label_placeholder)));
|
bindAssetLink(bind.trackCoverInfoImageView, albumLink != null ? albumLink : songLink);
|
||||||
bind.albumValueSector.setText(mediaMetadata.extras.getString("album", getString(R.string.label_placeholder)));
|
bindAssetLink(bind.trakTitleInfoTextView, songLink);
|
||||||
bind.artistValueSector.setText(mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder)));
|
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.trackNumberValueSector.setText(mediaMetadata.extras.getInt("track", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("track", 0)) : getString(R.string.label_placeholder));
|
||||||
bind.yearValueSector.setText(mediaMetadata.extras.getInt("year", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("year", 0)) : getString(R.string.label_placeholder));
|
bind.yearValueSector.setText(yearValue != 0 ? String.valueOf(yearValue) : getString(R.string.label_placeholder));
|
||||||
bind.genreValueSector.setText(mediaMetadata.extras.getString("genre", 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.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.contentTypeValueSector.setText(mediaMetadata.extras.getString("contentType", getString(R.string.label_placeholder)));
|
||||||
bind.suffixValueSector.setText(mediaMetadata.extras.getString("suffix", getString(R.string.label_placeholder)));
|
bind.suffixValueSector.setText(mediaMetadata.extras.getString("suffix", getString(R.string.label_placeholder)));
|
||||||
@@ -83,6 +116,12 @@ public class TrackInfoDialog extends DialogFragment {
|
|||||||
bind.bitDepthValueSector.setText(mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + " bits" : 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.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));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,4 +174,31 @@ public class TrackInfoDialog extends DialogFragment {
|
|||||||
bind.trakTranscodingInfoTextView.setText(info);
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,11 +35,15 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
|||||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
||||||
|
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
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.AlbumPageViewModel;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -52,6 +56,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||||||
private FragmentAlbumPageBinding bind;
|
private FragmentAlbumPageBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private AlbumPageViewModel albumPageViewModel;
|
private AlbumPageViewModel albumPageViewModel;
|
||||||
|
private PlaybackViewModel playbackViewModel;
|
||||||
private SongHorizontalAdapter songHorizontalAdapter;
|
private SongHorizontalAdapter songHorizontalAdapter;
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
@@ -74,6 +79,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentAlbumPageBinding.inflate(inflater, container, false);
|
bind = FragmentAlbumPageBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
albumPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumPageViewModel.class);
|
albumPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumPageViewModel.class);
|
||||||
|
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||||
|
|
||||||
init();
|
init();
|
||||||
initAppBar();
|
initAppBar();
|
||||||
@@ -91,6 +97,14 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
initializeMediaBrowser();
|
initializeMediaBrowser();
|
||||||
|
|
||||||
|
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||||
|
observePlayback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -119,7 +133,14 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
if (item.getItemId() == R.id.action_download_album) {
|
if (item.getItemId() == R.id.action_download_album) {
|
||||||
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(MappingUtil.mapDownloads(songs), songs.stream().map(Download::new).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));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -157,8 +178,35 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
bind.albumNameLabel.setText(album.getName());
|
bind.albumNameLabel.setText(album.getName());
|
||||||
bind.albumArtistLabel.setText(album.getArtist());
|
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()) : "");
|
bind.albumReleaseYearLabel.setText(album.getYear() != 0 ? String.valueOf(album.getYear()) : "");
|
||||||
bind.albumReleaseYearLabel.setVisibility(album.getYear() != 0 ? View.VISIBLE : View.GONE);
|
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));
|
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()) {
|
if (album.getGenre() != null && !album.getGenre().isEmpty()) {
|
||||||
bind.albumGenresTextview.setText(album.getGenre());
|
bind.albumGenresTextview.setText(album.getGenre());
|
||||||
@@ -269,10 +317,15 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||||||
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
bind.songRecyclerView.setHasFixedSize(true);
|
bind.songRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
songHorizontalAdapter = new SongHorizontalAdapter(this, false, false, album);
|
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, false, false, album);
|
||||||
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
||||||
|
setMediaBrowserListenableFuture();
|
||||||
|
reapplyPlayback();
|
||||||
|
|
||||||
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));
|
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
|
songHorizontalAdapter.setItems(songs);
|
||||||
|
reapplyPlayback();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -295,4 +348,50 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||||||
public void onMediaLongClick(Bundle bundle) {
|
public void onMediaLongClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,19 +29,16 @@ import com.cappielloantonio.tempo.service.MediaManager;
|
|||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumArtistPageOrSimilarAdapter;
|
|
||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.ArtistSimilarAdapter;
|
|
||||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
|
||||||
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
|
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@@ -49,6 +46,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
private FragmentArtistPageBinding bind;
|
private FragmentArtistPageBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private ArtistPageViewModel artistPageViewModel;
|
private ArtistPageViewModel artistPageViewModel;
|
||||||
|
private PlaybackViewModel playbackViewModel;
|
||||||
|
|
||||||
private SongHorizontalAdapter songHorizontalAdapter;
|
private SongHorizontalAdapter songHorizontalAdapter;
|
||||||
private AlbumCatalogueAdapter albumCatalogueAdapter;
|
private AlbumCatalogueAdapter albumCatalogueAdapter;
|
||||||
@@ -63,6 +61,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentArtistPageBinding.inflate(inflater, container, false);
|
bind = FragmentArtistPageBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
|
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
|
||||||
|
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||||
|
|
||||||
init();
|
init();
|
||||||
initAppBar();
|
initAppBar();
|
||||||
@@ -80,6 +79,13 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
initializeMediaBrowser();
|
initializeMediaBrowser();
|
||||||
|
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||||
|
observePlayback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -159,7 +165,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
bind.artistPageRadioButton.setOnClickListener(v -> {
|
bind.artistPageRadioButton.setOnClickListener(v -> {
|
||||||
artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> {
|
artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> {
|
||||||
if (!songs.isEmpty()) {
|
if (songs != null && !songs.isEmpty()) {
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||||
activity.setBottomSheetInPeek(true);
|
activity.setBottomSheetInPeek(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -172,8 +178,10 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
private void initTopSongsView() {
|
private void initTopSongsView() {
|
||||||
bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
|
|
||||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, true, null);
|
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, true, null);
|
||||||
bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter);
|
bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter);
|
||||||
|
setMediaBrowserListenableFuture();
|
||||||
|
reapplyPlayback();
|
||||||
artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> {
|
artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
if (songs == null) {
|
if (songs == null) {
|
||||||
if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE);
|
if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE);
|
||||||
@@ -183,6 +191,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
if (bind != null)
|
if (bind != null)
|
||||||
bind.artistPageShuffleButton.setEnabled(!songs.isEmpty());
|
bind.artistPageShuffleButton.setEnabled(!songs.isEmpty());
|
||||||
songHorizontalAdapter.setItems(songs);
|
songHorizontalAdapter.setItems(songs);
|
||||||
|
reapplyPlayback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -273,4 +282,31 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
public void onArtistLongClick(Bundle bundle) {
|
public void onArtistLongClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,9 @@ import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
|
|||||||
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
|
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
@@ -109,10 +111,14 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
|||||||
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
|
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
|
||||||
if (isVisible() && getActivity() != null) {
|
if (isVisible() && getActivity() != null) {
|
||||||
List<Child> songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList());
|
List<Child> songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList());
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
MappingUtil.mapDownloads(songs),
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
MappingUtil.mapDownloads(songs),
|
||||||
);
|
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,17 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
|||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
|
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
|
||||||
import com.google.android.material.appbar.MaterialToolbar;
|
import com.google.android.material.appbar.MaterialToolbar;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
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.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -40,6 +46,7 @@ import java.util.Objects;
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class DownloadFragment extends Fragment implements ClickCallback {
|
public class DownloadFragment extends Fragment implements ClickCallback {
|
||||||
private static final String TAG = "DownloadFragment";
|
private static final String TAG = "DownloadFragment";
|
||||||
|
private static final int REQUEST_CODE_PICK_DIRECTORY = 1002;
|
||||||
|
|
||||||
private FragmentDownloadBinding bind;
|
private FragmentDownloadBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
@@ -129,8 +136,27 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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.downloadedGroupByImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.download_popup_menu));
|
||||||
bind.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack());
|
bind.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack());
|
||||||
|
bind.downloadedRefreshImageView.setOnClickListener(view -> downloadViewModel.refreshExternalDownloads());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void finishDownloadView(List<Child> songs) {
|
private void finishDownloadView(List<Child> songs) {
|
||||||
@@ -216,6 +242,10 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
|||||||
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null));
|
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null));
|
||||||
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR);
|
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR);
|
||||||
return true;
|
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;
|
return false;
|
||||||
@@ -267,4 +297,21 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
|||||||
public void onDownloadGroupLongClick(Bundle bundle) {
|
public void onDownloadGroupLongClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.downloadBottomSheetDialog, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
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.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.*
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
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<SeekBar>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
requireActivity().unbindService(connection)
|
||||||
|
equalizerManager = null
|
||||||
|
}
|
||||||
|
|
||||||
|
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<LinearLayout>(R.id.equalizer_not_supported_container)
|
||||||
|
val switchRow = view?.findViewById<View>(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()
|
||||||
@@ -9,6 +9,7 @@ import android.view.LayoutInflater;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.PopupMenu;
|
import android.widget.PopupMenu;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -40,6 +41,7 @@ import com.cappielloantonio.tempo.service.MediaService;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Share;
|
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
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.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter;
|
||||||
@@ -60,9 +62,12 @@ import com.cappielloantonio.tempo.util.MusicUtil;
|
|||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.util.UIUtil;
|
import com.cappielloantonio.tempo.util.UIUtil;
|
||||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -74,6 +79,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
private FragmentHomeTabMusicBinding bind;
|
private FragmentHomeTabMusicBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private HomeViewModel homeViewModel;
|
private HomeViewModel homeViewModel;
|
||||||
|
private PlaybackViewModel playbackViewModel;
|
||||||
|
|
||||||
private DiscoverSongAdapter discoverSongAdapter;
|
private DiscoverSongAdapter discoverSongAdapter;
|
||||||
private SimilarTrackAdapter similarMusicAdapter;
|
private SimilarTrackAdapter similarMusicAdapter;
|
||||||
@@ -101,6 +107,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentHomeTabMusicBinding.inflate(inflater, container, false);
|
bind = FragmentHomeTabMusicBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class);
|
homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class);
|
||||||
|
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
@@ -113,6 +120,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
initSyncStarredView();
|
initSyncStarredView();
|
||||||
initSyncStarredAlbumsView();
|
initSyncStarredAlbumsView();
|
||||||
|
initSyncStarredArtistsView();
|
||||||
initDiscoverSongSlideView();
|
initDiscoverSongSlideView();
|
||||||
initSimilarSongView();
|
initSimilarSongView();
|
||||||
initArtistRadio();
|
initArtistRadio();
|
||||||
@@ -138,12 +146,18 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
initializeMediaBrowser();
|
initializeMediaBrowser();
|
||||||
|
|
||||||
|
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||||
|
observeStarredSongsPlayback();
|
||||||
|
observeTopSongsPlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
refreshSharesView();
|
refreshSharesView();
|
||||||
|
if (topSongAdapter != null) setTopSongsMediaBrowserListenableFuture();
|
||||||
|
if (starredSongAdapter != null) setStarredSongsMediaBrowserListenableFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -265,7 +279,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initSyncStarredView() {
|
private void initSyncStarredView() {
|
||||||
if (Preferences.isStarredSyncEnabled()) {
|
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
|
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(List<Child> songs) {
|
public void onChanged(List<Child> songs) {
|
||||||
@@ -318,32 +332,12 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
private void initSyncStarredAlbumsView() {
|
private void initSyncStarredAlbumsView() {
|
||||||
if (Preferences.isStarredAlbumsSyncEnabled()) {
|
if (Preferences.isStarredAlbumsSyncEnabled()) {
|
||||||
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observeForever(new Observer<List<AlbumID3>>() {
|
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer<List<AlbumID3>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(List<AlbumID3> albums) {
|
public void onChanged(List<AlbumID3> albums) {
|
||||||
if (albums != null) {
|
if (albums != null && !albums.isEmpty()) {
|
||||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
checkIfAlbumsNeedSync(albums);
|
||||||
List<String> albumsToSync = new ArrayList<>();
|
|
||||||
int albumCount = 0;
|
|
||||||
|
|
||||||
for (AlbumID3 album : albums) {
|
|
||||||
boolean needsSync = false;
|
|
||||||
albumCount++;
|
|
||||||
albumsToSync.add(album.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (albumCount > 0) {
|
|
||||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE);
|
|
||||||
String message = getResources().getQuantityString(
|
|
||||||
R.plurals.home_sync_starred_albums_count,
|
|
||||||
albumCount,
|
|
||||||
albumCount
|
|
||||||
);
|
|
||||||
bind.homeSyncStarredAlbumsToSync.setText(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).removeObserver(this);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -353,26 +347,157 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
});
|
});
|
||||||
|
|
||||||
bind.homeSyncStarredAlbumsDownload.setOnClickListener(v -> {
|
bind.homeSyncStarredAlbumsDownload.setOnClickListener(v -> {
|
||||||
homeViewModel.getAllStarredAlbumSongs().observeForever(new Observer<List<Child>>() {
|
homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(List<Child> allSongs) {
|
public void onChanged(List<Child> allSongs) {
|
||||||
if (allSongs != null) {
|
if (allSongs != null && !allSongs.isEmpty()) {
|
||||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||||
|
int songsToDownload = 0;
|
||||||
|
|
||||||
for (Child song : allSongs) {
|
for (Child song : allSongs) {
|
||||||
if (!manager.isDownloaded(song.getId())) {
|
if (!manager.isDownloaded(song.getId())) {
|
||||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||||
|
songsToDownload++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
homeViewModel.getAllStarredAlbumSongs().removeObserver(this);
|
if (songsToDownload > 0) {
|
||||||
|
Toast.makeText(requireContext(),
|
||||||
|
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
|
||||||
|
Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkIfAlbumsNeedSync(List<AlbumID3> albums) {
|
||||||
|
homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<Child> allSongs) {
|
||||||
|
if (allSongs != null) {
|
||||||
|
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||||
|
int songsToDownload = 0;
|
||||||
|
List<String> 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<List<ArtistID3>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<ArtistID3> 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<List<Child>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<Child> 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<ArtistID3> artists) {
|
||||||
|
homeViewModel.getAllStarredArtistSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<Child> allSongs) {
|
||||||
|
if (allSongs != null) {
|
||||||
|
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||||
|
int songsToDownload = 0;
|
||||||
|
List<String> 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() {
|
private void initDiscoverSongSlideView() {
|
||||||
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
|
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
|
||||||
|
|
||||||
@@ -475,8 +600,10 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
bind.topSongsRecyclerView.setHasFixedSize(true);
|
bind.topSongsRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
topSongAdapter = new SongHorizontalAdapter(this, true, false, null);
|
topSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||||
bind.topSongsRecyclerView.setAdapter(topSongAdapter);
|
bind.topSongsRecyclerView.setAdapter(topSongAdapter);
|
||||||
|
setTopSongsMediaBrowserListenableFuture();
|
||||||
|
reapplyTopSongsPlayback();
|
||||||
homeViewModel.getChronologySample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> {
|
homeViewModel.getChronologySample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> {
|
||||||
if (chronologies == null || chronologies.isEmpty()) {
|
if (chronologies == null || chronologies.isEmpty()) {
|
||||||
if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE);
|
if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE);
|
||||||
@@ -492,6 +619,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
topSongAdapter.setItems(topSongs);
|
topSongAdapter.setItems(topSongs);
|
||||||
|
reapplyTopSongsPlayback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -513,8 +641,10 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
bind.starredTracksRecyclerView.setHasFixedSize(true);
|
bind.starredTracksRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
starredSongAdapter = new SongHorizontalAdapter(this, true, false, null);
|
starredSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||||
bind.starredTracksRecyclerView.setAdapter(starredSongAdapter);
|
bind.starredTracksRecyclerView.setAdapter(starredSongAdapter);
|
||||||
|
setStarredSongsMediaBrowserListenableFuture();
|
||||||
|
reapplyStarredSongsPlayback();
|
||||||
homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
|
homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
|
||||||
if (songs == null) {
|
if (songs == null) {
|
||||||
if (bind != null) bind.starredTracksSector.setVisibility(View.GONE);
|
if (bind != null) bind.starredTracksSector.setVisibility(View.GONE);
|
||||||
@@ -525,6 +655,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
bind.starredTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(songs.size(), 5), GridLayoutManager.HORIZONTAL, false));
|
bind.starredTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(songs.size(), 5), GridLayoutManager.HORIZONTAL, false));
|
||||||
|
|
||||||
starredSongAdapter.setItems(songs);
|
starredSongAdapter.setItems(songs);
|
||||||
|
reapplyStarredSongsPlayback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -954,6 +1085,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
|
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
|
||||||
activity.setBottomSheetInPeek(true);
|
activity.setBottomSheetInPeek(true);
|
||||||
}
|
}
|
||||||
|
topSongAdapter.notifyDataSetChanged();
|
||||||
|
starredSongAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1043,4 +1176,58 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
public void onShareLongClick(Bundle bundle) {
|
public void onShareLongClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.shareBottomSheetDialog, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ public class PlayerBottomSheetFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void setMediaControllerUI(MediaBrowser mediaBrowser) {
|
private void setMediaControllerUI(MediaBrowser mediaBrowser) {
|
||||||
if (mediaBrowser.getMediaMetadata().extras != null) {
|
if (mediaBrowser.getMediaMetadata().extras != null) {
|
||||||
switch (mediaBrowser.getMediaMetadata().extras.getString("type", Constants.MEDIA_TYPE_MUSIC)) {
|
switch (mediaBrowser.getMediaMetadata().extras.getString("type", Constants.MEDIA_TYPE_MUSIC)) {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package com.cappielloantonio.tempo.ui.fragment;
|
package com.cappielloantonio.tempo.ui.fragment;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -9,9 +13,10 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.RatingBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
import android.widget.RatingBar;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
@@ -24,22 +29,27 @@ import androidx.media3.common.util.RepeatModeUtil;
|
|||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.session.MediaBrowser;
|
import androidx.media3.session.MediaBrowser;
|
||||||
import androidx.media3.session.SessionToken;
|
import androidx.media3.session.SessionToken;
|
||||||
|
import androidx.navigation.NavController;
|
||||||
|
import androidx.navigation.NavOptions;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.viewpager2.widget.ViewPager2;
|
import androidx.viewpager2.widget.ViewPager2;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBinding;
|
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBinding;
|
||||||
|
import com.cappielloantonio.tempo.service.EqualizerManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
|
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
|
||||||
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager;
|
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.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.RatingViewModel;
|
import com.cappielloantonio.tempo.viewmodel.RatingViewModel;
|
||||||
import com.google.android.material.chip.Chip;
|
import com.google.android.material.chip.Chip;
|
||||||
|
import com.google.android.material.chip.ChipGroup;
|
||||||
import com.google.android.material.elevation.SurfaceColors;
|
import com.google.android.material.elevation.SurfaceColors;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
@@ -68,11 +78,19 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
private ImageButton playerOpenQueueButton;
|
private ImageButton playerOpenQueueButton;
|
||||||
private ImageButton playerTrackInfo;
|
private ImageButton playerTrackInfo;
|
||||||
private LinearLayout ratingContainer;
|
private LinearLayout ratingContainer;
|
||||||
|
private ImageButton equalizerButton;
|
||||||
|
private ChipGroup assetLinkChipGroup;
|
||||||
|
private Chip playerSongLinkChip;
|
||||||
|
private Chip playerAlbumLinkChip;
|
||||||
|
private Chip playerArtistLinkChip;
|
||||||
|
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
|
private MediaService.LocalBinder mediaServiceBinder;
|
||||||
|
private boolean isServiceBound = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
activity = (MainActivity) getActivity();
|
activity = (MainActivity) getActivity();
|
||||||
@@ -89,6 +107,7 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
initMediaListenable();
|
initMediaListenable();
|
||||||
initMediaLabelButton();
|
initMediaLabelButton();
|
||||||
initArtistLabelButton();
|
initArtistLabelButton();
|
||||||
|
initEqualizerButton();
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
@@ -126,6 +145,11 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
playerTrackInfo = bind.getRoot().findViewById(R.id.player_info_track);
|
playerTrackInfo = bind.getRoot().findViewById(R.id.player_info_track);
|
||||||
songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar);
|
songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar);
|
||||||
ratingContainer = bind.getRoot().findViewById(R.id.rating_container);
|
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();
|
checkAndSetRatingContainerVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +230,8 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
|| mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null
|
|| mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null
|
||||||
? View.VISIBLE
|
? View.VISIBLE
|
||||||
: View.GONE);
|
: View.GONE);
|
||||||
|
|
||||||
|
updateAssetLinkChips(mediaMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setMediaInfo(MediaMetadata mediaMetadata) {
|
private void setMediaInfo(MediaMetadata mediaMetadata) {
|
||||||
@@ -246,6 +272,110 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private void setMediaControllerUI(MediaBrowser mediaBrowser) {
|
||||||
initPlaybackSpeedButton(mediaBrowser);
|
initPlaybackSpeedButton(mediaBrowser);
|
||||||
|
|
||||||
@@ -426,6 +556,18 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
public void goToControllerPage() {
|
||||||
playerMediaCoverViewPager.setCurrentItem(0, false);
|
playerMediaCoverViewPager.setCurrentItem(0, false);
|
||||||
}
|
}
|
||||||
@@ -461,4 +603,66 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100));
|
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100));
|
||||||
// TODO Resettare lo skip del silenzio
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import java.util.ArrayList;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MediaMetadata;
|
import androidx.media3.common.MediaMetadata;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
@@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
@@ -115,10 +117,14 @@ public class PlayerCoverFragment extends Fragment {
|
|||||||
playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), song -> {
|
playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), song -> {
|
||||||
if (song != null && bind != null) {
|
if (song != null && bind != null) {
|
||||||
bind.innerButtonTopLeft.setOnClickListener(view -> {
|
bind.innerButtonTopLeft.setOnClickListener(view -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
MappingUtil.mapDownload(song),
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
new Download(song)
|
MappingUtil.mapDownload(song),
|
||||||
);
|
new Download(song)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bind.innerButtonTopRight.setOnClickListener(view -> {
|
bind.innerButtonTopRight.setOnClickListener(view -> {
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import android.annotation.SuppressLint;
|
|||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.text.Layout;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.Layout;
|
import android.text.TextUtils;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -29,10 +30,10 @@ import com.cappielloantonio.tempo.service.MediaService;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.Line;
|
import com.cappielloantonio.tempo.subsonic.models.Line;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
|
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.android.material.button.MaterialButton;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -48,6 +49,9 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
private MediaBrowser mediaBrowser;
|
private MediaBrowser mediaBrowser;
|
||||||
private Handler syncLyricsHandler;
|
private Handler syncLyricsHandler;
|
||||||
private Runnable syncLyricsRunnable;
|
private Runnable syncLyricsRunnable;
|
||||||
|
private String currentLyrics;
|
||||||
|
private LyricsList currentLyricsList;
|
||||||
|
private String currentDescription;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
@@ -66,6 +70,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
initPanelContent();
|
initPanelContent();
|
||||||
|
observeDownloadState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -101,12 +106,26 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
bind = null;
|
bind = null;
|
||||||
|
currentLyrics = null;
|
||||||
|
currentLyricsList = null;
|
||||||
|
currentDescription = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initOverlay() {
|
private void initOverlay() {
|
||||||
bind.syncLyricsTapButton.setOnClickListener(view -> {
|
bind.syncLyricsTapButton.setOnClickListener(view -> {
|
||||||
playerBottomSheetViewModel.changeSyncLyricsState();
|
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() {
|
private void initializeBrowser() {
|
||||||
@@ -136,50 +155,91 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initPanelContent() {
|
private void initPanelContent() {
|
||||||
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
|
||||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
currentLyrics = lyrics;
|
||||||
setPanelContent(null, lyricsList);
|
updatePanelContent();
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
|
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||||
setPanelContent(lyrics, null);
|
currentLyricsList = lyricsList;
|
||||||
});
|
updatePanelContent();
|
||||||
}
|
});
|
||||||
|
|
||||||
|
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
||||||
|
currentDescription = description;
|
||||||
|
updatePanelContent();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPanelContent(String lyrics, LyricsList lyricsList) {
|
private void observeDownloadState() {
|
||||||
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
playerBottomSheetViewModel.getLyricsCachedState().observe(getViewLifecycleOwner(), cached -> {
|
||||||
if (bind != null) {
|
if (bind != null) {
|
||||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
MaterialButton downloadButton = (MaterialButton) bind.downloadLyricsButton;
|
||||||
|
if (cached != null && cached) {
|
||||||
if (lyrics != null && !lyrics.trim().equals("")) {
|
downloadButton.setIconResource(R.drawable.ic_done);
|
||||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics));
|
downloadButton.setContentDescription(getString(R.string.player_lyrics_downloaded_content_description));
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
|
||||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
|
||||||
} else if (lyricsList != null && lyricsList.getStructuredLyrics() != null) {
|
|
||||||
setSyncLirics(lyricsList);
|
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
|
||||||
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
|
|
||||||
} else if (description != null && !description.trim().equals("")) {
|
|
||||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(description));
|
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
|
||||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
|
||||||
} else {
|
} else {
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
|
downloadButton.setIconResource(R.drawable.ic_download);
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
|
downloadButton.setContentDescription(getString(R.string.player_lyrics_download_content_description));
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
|
|
||||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
@SuppressLint("DefaultLocale")
|
||||||
private void setSyncLirics(LyricsList lyricsList) {
|
private void setSyncLirics(LyricsList lyricsList) {
|
||||||
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
||||||
@@ -198,28 +258,28 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
|
|
||||||
private void defineProgressHandler() {
|
private void defineProgressHandler() {
|
||||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||||
if (lyricsList != null) {
|
if (!hasStructuredLyrics(lyricsList)) {
|
||||||
|
|
||||||
if (lyricsList.getStructuredLyrics() != null && lyricsList.getStructuredLyrics().get(0) != null && !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);
|
|
||||||
} else {
|
|
||||||
releaseHandler();
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +287,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue();
|
LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue();
|
||||||
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
||||||
|
|
||||||
if (lyricsList != null && lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
if (hasStructuredLyrics(lyricsList)) {
|
||||||
StringBuilder lyricsBuilder = new StringBuilder();
|
StringBuilder lyricsBuilder = new StringBuilder();
|
||||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.cappielloantonio.tempo.service.MediaService;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
@@ -38,6 +39,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
private InnerFragmentPlayerQueueBinding bind;
|
private InnerFragmentPlayerQueueBinding bind;
|
||||||
|
|
||||||
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
||||||
|
private PlaybackViewModel playbackViewModel;
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
private PlayerSongQueueAdapter playerSongQueueAdapter;
|
private PlayerSongQueueAdapter playerSongQueueAdapter;
|
||||||
@@ -48,6 +50,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
|
|
||||||
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
|
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
|
||||||
|
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||||
|
|
||||||
initQueueRecyclerView();
|
initQueueRecyclerView();
|
||||||
|
|
||||||
@@ -59,6 +62,9 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
super.onStart();
|
super.onStart();
|
||||||
initializeBrowser();
|
initializeBrowser();
|
||||||
bindMediaController();
|
bindMediaController();
|
||||||
|
|
||||||
|
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||||
|
observePlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -110,9 +116,12 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
playerSongQueueAdapter = new PlayerSongQueueAdapter(this);
|
playerSongQueueAdapter = new PlayerSongQueueAdapter(this);
|
||||||
bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter);
|
bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter);
|
||||||
|
reapplyPlayback();
|
||||||
|
|
||||||
playerBottomSheetViewModel.getQueueSong().observe(getViewLifecycleOwner(), queue -> {
|
playerBottomSheetViewModel.getQueueSong().observe(getViewLifecycleOwner(), queue -> {
|
||||||
if (queue != null) {
|
if (queue != null) {
|
||||||
playerSongQueueAdapter.setItems(queue.stream().map(item -> (Child) item).collect(Collectors.toList()));
|
playerSongQueueAdapter.setItems(queue.stream().map(item -> (Child) item).collect(Collectors.toList()));
|
||||||
|
reapplyPlayback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -216,4 +225,27 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||||||
public void onMediaClick(Bundle bundle) {
|
public void onMediaClick(Bundle bundle) {
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,9 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
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.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
@@ -49,6 +52,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
private FragmentPlaylistPageBinding bind;
|
private FragmentPlaylistPageBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private PlaylistPageViewModel playlistPageViewModel;
|
private PlaylistPageViewModel playlistPageViewModel;
|
||||||
|
private PlaybackViewModel playbackViewModel;
|
||||||
|
|
||||||
private SongHorizontalAdapter songHorizontalAdapter;
|
private SongHorizontalAdapter songHorizontalAdapter;
|
||||||
|
|
||||||
@@ -94,6 +98,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentPlaylistPageBinding.inflate(inflater, container, false);
|
bind = FragmentPlaylistPageBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
playlistPageViewModel = new ViewModelProvider(requireActivity()).get(PlaylistPageViewModel.class);
|
playlistPageViewModel = new ViewModelProvider(requireActivity()).get(PlaylistPageViewModel.class);
|
||||||
|
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||||
|
|
||||||
init();
|
init();
|
||||||
initAppBar();
|
initAppBar();
|
||||||
@@ -109,6 +114,15 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
initializeMediaBrowser();
|
initializeMediaBrowser();
|
||||||
|
|
||||||
|
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||||
|
observePlayback();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -128,7 +142,8 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
if (item.getItemId() == R.id.action_download_playlist) {
|
if (item.getItemId() == R.id.action_download_playlist) {
|
||||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
if (isVisible() && getActivity() != null) {
|
if (isVisible() && getActivity() != null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
MappingUtil.mapDownloads(songs),
|
MappingUtil.mapDownloads(songs),
|
||||||
songs.stream().map(child -> {
|
songs.stream().map(child -> {
|
||||||
Download toDownload = new Download(child);
|
Download toDownload = new Download(child);
|
||||||
@@ -136,7 +151,10 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName());
|
toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName());
|
||||||
return toDownload;
|
return toDownload;
|
||||||
}).collect(Collectors.toList())
|
}).collect(Collectors.toList())
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
@@ -246,10 +264,15 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
bind.songRecyclerView.setHasFixedSize(true);
|
bind.songRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||||
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
||||||
|
setMediaBrowserListenableFuture();
|
||||||
|
reapplyPlayback();
|
||||||
|
|
||||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));
|
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
|
songHorizontalAdapter.setItems(songs);
|
||||||
|
reapplyPlayback();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeMediaBrowser() {
|
private void initializeMediaBrowser() {
|
||||||
@@ -270,4 +293,31 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
public void onMediaLongClick(Bundle bundle) {
|
public void onMediaLongClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,11 @@ import android.content.ComponentName;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.inputmethod.EditorInfo;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -34,6 +31,7 @@ import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
|||||||
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
|
import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
@@ -46,6 +44,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
private FragmentSearchBinding bind;
|
private FragmentSearchBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private SearchViewModel searchViewModel;
|
private SearchViewModel searchViewModel;
|
||||||
|
private PlaybackViewModel playbackViewModel;
|
||||||
|
|
||||||
private ArtistAdapter artistAdapter;
|
private ArtistAdapter artistAdapter;
|
||||||
private AlbumAdapter albumAdapter;
|
private AlbumAdapter albumAdapter;
|
||||||
@@ -61,6 +60,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentSearchBinding.inflate(inflater, container, false);
|
bind = FragmentSearchBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
searchViewModel = new ViewModelProvider(requireActivity()).get(SearchViewModel.class);
|
searchViewModel = new ViewModelProvider(requireActivity()).get(SearchViewModel.class);
|
||||||
|
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||||
|
|
||||||
initSearchResultView();
|
initSearchResultView();
|
||||||
initSearchView();
|
initSearchView();
|
||||||
@@ -73,6 +73,15 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
initializeMediaBrowser();
|
initializeMediaBrowser();
|
||||||
|
|
||||||
|
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||||
|
observePlayback();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -112,7 +121,10 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
bind.searchResultTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
bind.searchResultTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
bind.searchResultTracksRecyclerView.setHasFixedSize(true);
|
bind.searchResultTracksRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||||
|
setMediaBrowserListenableFuture();
|
||||||
|
reapplyPlayback();
|
||||||
|
|
||||||
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
|
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +254,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isQueryValid(String query) {
|
private boolean isQueryValid(String query) {
|
||||||
return !query.equals("") && query.trim().length() > 2;
|
return !query.equals("") && query.trim().length() > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void inputFocus() {
|
private void inputFocus() {
|
||||||
@@ -260,6 +272,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
@Override
|
@Override
|
||||||
public void onMediaClick(Bundle bundle) {
|
public void onMediaClick(Bundle bundle) {
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
|
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
|
||||||
|
songHorizontalAdapter.notifyDataSetChanged();
|
||||||
activity.setBottomSheetInPeek(true);
|
activity.setBottomSheetInPeek(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,4 +300,31 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
public void onArtistLongClick(Bundle bundle) {
|
public void onArtistLongClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
package com.cappielloantonio.tempo.ui.fragment;
|
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.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
import android.media.audiofx.AudioEffect;
|
import android.media.audiofx.AudioEffect;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.IBinder;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
@@ -18,6 +24,9 @@ import androidx.appcompat.app.AppCompatDelegate;
|
|||||||
import androidx.core.os.LocaleListCompat;
|
import androidx.core.os.LocaleListCompat;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
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.ListPreference;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
@@ -28,15 +37,19 @@ import com.cappielloantonio.tempo.R;
|
|||||||
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
||||||
import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
|
import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
|
||||||
import com.cappielloantonio.tempo.interfaces.ScanCallback;
|
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.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog;
|
import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog;
|
import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog;
|
import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog;
|
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.ui.dialog.StreamingCacheStorageDialog;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.util.UIUtil;
|
import com.cappielloantonio.tempo.util.UIUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -49,15 +62,41 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private SettingViewModel settingViewModel;
|
private SettingViewModel settingViewModel;
|
||||||
|
|
||||||
private ActivityResultLauncher<Intent> someActivityResultLauncher;
|
private ActivityResultLauncher<Intent> equalizerResultLauncher;
|
||||||
|
private ActivityResultLauncher<Intent> directoryPickerLauncher;
|
||||||
|
|
||||||
|
private MediaService.LocalBinder mediaServiceBinder;
|
||||||
|
private boolean isServiceBound = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
someActivityResultLauncher = registerForActivityResult(
|
equalizerResultLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {}
|
||||||
|
);
|
||||||
|
|
||||||
|
directoryPickerLauncher = registerForActivityResult(
|
||||||
new ActivityResultContracts.StartActivityForResult(),
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
result -> {
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +125,10 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
checkEqualizer();
|
checkSystemEqualizer();
|
||||||
checkCacheStorage();
|
checkCacheStorage();
|
||||||
checkStorage();
|
checkStorage();
|
||||||
|
checkDownloadDirectory();
|
||||||
|
|
||||||
setStreamingCacheSize();
|
setStreamingCacheSize();
|
||||||
setAppLanguage();
|
setAppLanguage();
|
||||||
@@ -98,10 +138,17 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
actionScan();
|
actionScan();
|
||||||
actionSyncStarredAlbums();
|
actionSyncStarredAlbums();
|
||||||
actionSyncStarredTracks();
|
actionSyncStarredTracks();
|
||||||
|
actionSyncStarredArtists();
|
||||||
actionChangeStreamingCacheStorage();
|
actionChangeStreamingCacheStorage();
|
||||||
actionChangeDownloadStorage();
|
actionChangeDownloadStorage();
|
||||||
|
actionSetDownloadDirectory();
|
||||||
actionDeleteDownloadStorage();
|
actionDeleteDownloadStorage();
|
||||||
actionKeepScreenOn();
|
actionKeepScreenOn();
|
||||||
|
actionAutoDownloadLyrics();
|
||||||
|
actionMiniPlayerHeart();
|
||||||
|
|
||||||
|
bindMediaService();
|
||||||
|
actionAppEqualizer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -124,8 +171,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkEqualizer() {
|
private void checkSystemEqualizer() {
|
||||||
Preference equalizer = findPreference("equalizer");
|
Preference equalizer = findPreference("system_equalizer");
|
||||||
|
|
||||||
if (equalizer == null) return;
|
if (equalizer == null) return;
|
||||||
|
|
||||||
@@ -133,7 +180,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
|
|
||||||
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
||||||
equalizer.setOnPreferenceClickListener(preference -> {
|
equalizer.setOnPreferenceClickListener(preference -> {
|
||||||
someActivityResultLauncher.launch(intent);
|
equalizerResultLauncher.launch(intent);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -150,7 +197,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||||
storage.setVisible(false);
|
storage.setVisible(false);
|
||||||
} else {
|
} else {
|
||||||
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||||
}
|
}
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
storage.setVisible(false);
|
storage.setVisible(false);
|
||||||
@@ -166,13 +213,46 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||||
storage.setVisible(false);
|
storage.setVisible(false);
|
||||||
} else {
|
} else {
|
||||||
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
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) {
|
} catch (Exception exception) {
|
||||||
storage.setVisible(false);
|
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() {
|
private void setStreamingCacheSize() {
|
||||||
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
||||||
|
|
||||||
@@ -245,7 +325,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(boolean isScanning, long count) {
|
public void onSuccess(boolean isScanning, long count) {
|
||||||
findPreference("scan_library").setSummary("Scanning: counting " + count + " tracks");
|
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
||||||
if (isScanning) getScanStatus();
|
if (isScanning) getScanStatus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -281,7 +361,21 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
return true;
|
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() {
|
private void actionChangeStreamingCacheStorage() {
|
||||||
findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> {
|
findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> {
|
||||||
StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() {
|
StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() {
|
||||||
@@ -306,11 +400,19 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
@Override
|
@Override
|
||||||
public void onPositiveClick() {
|
public void onPositiveClick() {
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||||
|
checkDownloadDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNegativeClick() {
|
public void onNegativeClick() {
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
|
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);
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
@@ -318,6 +420,31 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
private void actionDeleteDownloadStorage() {
|
||||||
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
||||||
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
|
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
|
||||||
@@ -326,6 +453,36 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
private void getScanStatus() {
|
||||||
settingViewModel.getScanStatus(new ScanCallback() {
|
settingViewModel.getScanStatus(new ScanCallback() {
|
||||||
@Override
|
@Override
|
||||||
@@ -335,7 +492,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(boolean isScanning, long count) {
|
public void onSuccess(boolean isScanning, long count) {
|
||||||
findPreference("scan_library").setSummary("Scanning: counting " + count + " tracks");
|
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
||||||
if (isScanning) getScanStatus();
|
if (isScanning) getScanStatus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -353,4 +510,63 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
|||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.SongListPageViewModel;
|
import com.cappielloantonio.tempo.viewmodel.SongListPageViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||||||
private FragmentSongListPageBinding bind;
|
private FragmentSongListPageBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private SongListPageViewModel songListPageViewModel;
|
private SongListPageViewModel songListPageViewModel;
|
||||||
|
private PlaybackViewModel playbackViewModel;
|
||||||
|
|
||||||
private SongHorizontalAdapter songHorizontalAdapter;
|
private SongHorizontalAdapter songHorizontalAdapter;
|
||||||
|
|
||||||
@@ -69,6 +71,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentSongListPageBinding.inflate(inflater, container, false);
|
bind = FragmentSongListPageBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
songListPageViewModel = new ViewModelProvider(requireActivity()).get(SongListPageViewModel.class);
|
songListPageViewModel = new ViewModelProvider(requireActivity()).get(SongListPageViewModel.class);
|
||||||
|
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||||
|
|
||||||
init();
|
init();
|
||||||
initAppBar();
|
initAppBar();
|
||||||
@@ -82,6 +85,15 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
initializeMediaBrowser();
|
initializeMediaBrowser();
|
||||||
|
|
||||||
|
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||||
|
observePlayback();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
setMediaBrowserListenableFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -189,11 +201,14 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||||||
bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
bind.songListRecyclerView.setHasFixedSize(true);
|
bind.songListRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||||
bind.songListRecyclerView.setAdapter(songHorizontalAdapter);
|
bind.songListRecyclerView.setAdapter(songHorizontalAdapter);
|
||||||
|
setMediaBrowserListenableFuture();
|
||||||
|
reapplyPlayback();
|
||||||
songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> {
|
songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
songHorizontalAdapter.setItems(songs);
|
songHorizontalAdapter.setItems(songs);
|
||||||
|
reapplyPlayback();
|
||||||
setSongListPageSubtitle(songs);
|
setSongListPageSubtitle(songs);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -325,4 +340,31 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||||||
public void onMediaLongClick(Bundle bundle) {
|
public void onMediaLongClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ import android.widget.TextView;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
@@ -37,6 +38,8 @@ import com.cappielloantonio.tempo.util.DownloadUtil;
|
|||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
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.AlbumBottomSheetViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
@@ -54,6 +57,10 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
private AlbumBottomSheetViewModel albumBottomSheetViewModel;
|
private AlbumBottomSheetViewModel albumBottomSheetViewModel;
|
||||||
private AlbumID3 album;
|
private AlbumID3 album;
|
||||||
|
|
||||||
|
private TextView removeAllTextView;
|
||||||
|
private List<Child> currentAlbumTracks = Collections.emptyList();
|
||||||
|
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -72,6 +79,12 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateRemoveAllVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
@@ -163,7 +176,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||||
|
|
||||||
downloadAll.setOnClickListener(v -> {
|
downloadAll.setOnClickListener(v -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
|
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
|
||||||
|
} else {
|
||||||
|
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||||
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -182,19 +199,23 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
removeAllTextView = view.findViewById(R.id.remove_all_text_view);
|
||||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
currentAlbumTracks = songs != null ? songs : Collections.emptyList();
|
||||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
currentAlbumMediaItems = MappingUtil.mapDownloads(currentAlbumTracks);
|
||||||
|
|
||||||
removeAll.setOnClickListener(v -> {
|
removeAllTextView.setOnClickListener(v -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
|
List<Download> downloads = currentAlbumTracks.stream().map(Download::new).collect(Collectors.toList());
|
||||||
|
DownloadUtil.getDownloadTracker(requireContext()).remove(currentAlbumMediaItems, downloads);
|
||||||
|
} else {
|
||||||
|
currentAlbumTracks.forEach(ExternalAudioReader::delete);
|
||||||
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
updateRemoveAllVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
initDownloadUI(removeAll);
|
|
||||||
|
|
||||||
TextView goToArtist = view.findViewById(R.id.go_to_artist_text_view);
|
TextView goToArtist = view.findViewById(R.id.go_to_artist_text_view);
|
||||||
goToArtist.setOnClickListener(v -> albumBottomSheetViewModel.getArtist().observe(getViewLifecycleOwner(), artist -> {
|
goToArtist.setOnClickListener(v -> albumBottomSheetViewModel.getArtist().observe(getViewLifecycleOwner(), artist -> {
|
||||||
if (artist != null) {
|
if (artist != null) {
|
||||||
@@ -234,14 +255,29 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initDownloadUI(TextView removeAll) {
|
private void updateRemoveAllVisibility() {
|
||||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
if (removeAllTextView == null) {
|
||||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
if (currentAlbumTracks == null || currentAlbumTracks.isEmpty()) {
|
||||||
removeAll.setVisibility(View.VISIBLE);
|
removeAllTextView.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
|
List<MediaItem> 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() {
|
private void initializeMediaBrowser() {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
|||||||
super.onStop();
|
super.onStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Utilizzare il viewmodel come tramite ed evitare le chiamate dirette
|
// TODO Use the viewmodel as a conduit and avoid direct calls
|
||||||
private void init(View view) {
|
private void init(View view) {
|
||||||
ImageView coverArtist = view.findViewById(R.id.artist_cover_image_view);
|
ImageView coverArtist = view.findViewById(R.id.artist_cover_image_view);
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
@@ -81,7 +81,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
|||||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||||
favoriteToggle.setChecked(artistBottomSheetViewModel.getArtist().getStarred() != null);
|
favoriteToggle.setChecked(artistBottomSheetViewModel.getArtist().getStarred() != null);
|
||||||
favoriteToggle.setOnClickListener(v -> {
|
favoriteToggle.setOnClickListener(v -> {
|
||||||
artistBottomSheetViewModel.setFavorite();
|
artistBottomSheetViewModel.setFavorite(requireContext());
|
||||||
});
|
});
|
||||||
|
|
||||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
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.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
@@ -117,10 +119,13 @@ public class DownloadedBottomSheetDialog extends BottomSheetDialogFragment imple
|
|||||||
|
|
||||||
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
||||||
removeAll.setOnClickListener(v -> {
|
removeAll.setOnClickListener(v -> {
|
||||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||||
|
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
||||||
|
} else {
|
||||||
|
songs.forEach(ExternalAudioReader::delete);
|
||||||
|
}
|
||||||
|
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import android.widget.TextView;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
@@ -29,16 +30,24 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
|||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
||||||
|
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
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 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.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
@@ -48,6 +57,16 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
private SongBottomSheetViewModel songBottomSheetViewModel;
|
private SongBottomSheetViewModel songBottomSheetViewModel;
|
||||||
private Child song;
|
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<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -66,6 +85,12 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateDownloadButtons);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
@@ -94,6 +119,11 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
TextView artistSong = view.findViewById(R.id.song_artist_text_view);
|
TextView artistSong = view.findViewById(R.id.song_artist_text_view);
|
||||||
artistSong.setText(songBottomSheetViewModel.getSong().getArtist());
|
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);
|
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||||
favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null);
|
favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null);
|
||||||
favoriteToggle.setOnClickListener(v -> {
|
favoriteToggle.setOnClickListener(v -> {
|
||||||
@@ -157,25 +187,33 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
||||||
TextView download = view.findViewById(R.id.download_text_view);
|
downloadButton = view.findViewById(R.id.download_text_view);
|
||||||
download.setOnClickListener(v -> {
|
downloadButton.setOnClickListener(v -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
MappingUtil.mapDownload(song),
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
new Download(song)
|
MappingUtil.mapDownload(song),
|
||||||
);
|
new Download(song)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||||
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
||||||
TextView remove = view.findViewById(R.id.remove_text_view);
|
removeButton = view.findViewById(R.id.remove_text_view);
|
||||||
remove.setOnClickListener(v -> {
|
removeButton.setOnClickListener(v -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).remove(
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
MappingUtil.mapDownload(song),
|
DownloadUtil.getDownloadTracker(requireContext()).remove(
|
||||||
new Download(song)
|
MappingUtil.mapDownload(song),
|
||||||
);
|
new Download(song)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ExternalAudioReader.delete(song);
|
||||||
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
||||||
initDownloadUI(download, remove);
|
updateDownloadButtons();
|
||||||
|
|
||||||
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
||||||
addToPlaylist.setOnClickListener(v -> {
|
addToPlaylist.setOnClickListener(v -> {
|
||||||
@@ -243,13 +281,109 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initDownloadUI(TextView download, TextView remove) {
|
private void updateDownloadButtons() {
|
||||||
if (DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId())) {
|
if (downloadButton == null || removeButton == null) {
|
||||||
remove.setVisibility(View.VISIBLE);
|
return;
|
||||||
} else {
|
|
||||||
download.setVisibility(View.VISIBLE);
|
|
||||||
remove.setVisibility(View.GONE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
private void initializeMediaBrowser() {
|
||||||
@@ -263,4 +397,4 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
private void refreshShares() {
|
private void refreshShares() {
|
||||||
homeViewModel.refreshShares(requireActivity());
|
homeViewModel.refreshShares(requireActivity());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Child> liveData = songRepository.getSong(id);
|
||||||
|
Observer<Child> observer = new Observer<Child>() {
|
||||||
|
@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<AlbumID3> liveData = albumRepository.getAlbum(id);
|
||||||
|
Observer<AlbumID3> observer = new Observer<AlbumID3>() {
|
||||||
|
@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<ArtistID3> liveData = artistRepository.getArtist(id);
|
||||||
|
Observer<ArtistID3> observer = new Observer<ArtistID3>() {
|
||||||
|
@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<Playlist> liveData = playlistRepository.getPlaylist(id);
|
||||||
|
Observer<Playlist> observer = new Observer<Playlist>() {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,6 +85,13 @@ object Constants {
|
|||||||
const val MEDIA_LEAST_RECENTLY_STARRED = "MEDIA_LEAST_RECENTLY_STARRED"
|
const val MEDIA_LEAST_RECENTLY_STARRED = "MEDIA_LEAST_RECENTLY_STARRED"
|
||||||
|
|
||||||
const val DOWNLOAD_URI = "rest/download"
|
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_TRACK = "download_type_track"
|
||||||
const val DOWNLOAD_TYPE_ALBUM = "download_type_album"
|
const val DOWNLOAD_TYPE_ALBUM = "download_type_album"
|
||||||
@@ -116,4 +123,13 @@ object Constants {
|
|||||||
const val HOME_SECTOR_RECENTLY_ADDED = "HOME_SECTOR_RECENTLY_ADDED"
|
const val HOME_SECTOR_RECENTLY_ADDED = "HOME_SECTOR_RECENTLY_ADDED"
|
||||||
const val HOME_SECTOR_PINNED_PLAYLISTS = "HOME_SECTOR_PINNED_PLAYLISTS"
|
const val HOME_SECTOR_PINNED_PLAYLISTS = "HOME_SECTOR_PINNED_PLAYLISTS"
|
||||||
const val HOME_SECTOR_SHARED = "HOME_SECTOR_SHARED"
|
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"
|
||||||
}
|
}
|
||||||
@@ -78,32 +78,26 @@ public final class DownloadUtil {
|
|||||||
return httpDataSourceFactory;
|
return httpDataSourceFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
|
public static synchronized DataSource.Factory getUpstreamDataSourceFactory(Context context) {
|
||||||
if (dataSourceFactory == null) {
|
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory());
|
||||||
context = context.getApplicationContext();
|
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||||
|
return dataSourceFactory;
|
||||||
|
}
|
||||||
|
|
||||||
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory());
|
public static synchronized DataSource.Factory getCacheDataSourceFactory(Context context) {
|
||||||
|
CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory()
|
||||||
if (Preferences.getStreamingCacheSize() > 0) {
|
.setCache(getStreamingCache(context))
|
||||||
CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory()
|
.setUpstreamDataSourceFactory(getUpstreamDataSourceFactory(context));
|
||||||
.setCache(getStreamingCache(context))
|
|
||||||
.setUpstreamDataSourceFactory(upstreamFactory);
|
|
||||||
|
|
||||||
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));
|
|
||||||
} else {
|
|
||||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(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;
|
return dataSourceFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,19 +187,21 @@ public final class DownloadUtil {
|
|||||||
|
|
||||||
private static synchronized File getDownloadDirectory(Context context) {
|
private static synchronized File getDownloadDirectory(Context context) {
|
||||||
if (downloadDirectory == null) {
|
if (downloadDirectory == null) {
|
||||||
if (Preferences.getDownloadStoragePreference() == 0) {
|
int pref = Preferences.getDownloadStoragePreference();
|
||||||
|
if (pref == 0) {
|
||||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||||
if (downloadDirectory == null) {
|
if (downloadDirectory == null) {
|
||||||
downloadDirectory = context.getFilesDir();
|
downloadDirectory = context.getFilesDir();
|
||||||
}
|
}
|
||||||
} else {
|
} else if (pref == 1) {
|
||||||
try {
|
try {
|
||||||
downloadDirectory = context.getExternalFilesDirs(null)[1];
|
downloadDirectory = context.getExternalFilesDirs(null)[1];
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||||
Preferences.setDownloadStoragePreference(0);
|
Preferences.setDownloadStoragePreference(0);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, DocumentFile> cache = new ConcurrentHashMap<>();
|
||||||
|
private static final Object LOCK = new Object();
|
||||||
|
private static final ExecutorService REFRESH_EXECUTOR = Executors.newSingleThreadExecutor();
|
||||||
|
private static final MutableLiveData<Long> 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<Long> 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<String, Long> expectedSizes = ExternalDownloadMetadataStore.snapshot();
|
||||||
|
Set<String> verifiedKeys = new HashSet<>();
|
||||||
|
Map<String, DocumentFile> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Long> snapshot() {
|
||||||
|
JSONObject object = readAll();
|
||||||
|
if (object.length() == 0) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, Long> sizes = new HashMap<>();
|
||||||
|
Iterator<String> 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<String> keysToKeep) {
|
||||||
|
if (keysToKeep == null || keysToKeep.isEmpty()) {
|
||||||
|
clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONObject object = readAll();
|
||||||
|
if (object.length() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> keys = new HashSet<>();
|
||||||
|
Iterator<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,12 @@ import android.net.Uri;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MediaMetadata;
|
import androidx.media3.common.MediaMetadata;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.common.HeartRating;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
@@ -16,6 +18,7 @@ import com.cappielloantonio.tempo.repository.DownloadRepository;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -71,6 +74,12 @@ public class MappingUtil {
|
|||||||
bundle.putInt("originalWidth", media.getOriginalWidth() != null ? media.getOriginalWidth() : 0);
|
bundle.putInt("originalWidth", media.getOriginalWidth() != null ? media.getOriginalWidth() : 0);
|
||||||
bundle.putInt("originalHeight", media.getOriginalHeight() != null ? media.getOriginalHeight() : 0);
|
bundle.putInt("originalHeight", media.getOriginalHeight() != null ? media.getOriginalHeight() : 0);
|
||||||
bundle.putString("uri", uri.toString());
|
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()
|
return new MediaItem.Builder()
|
||||||
.setMediaId(media.getId())
|
.setMediaId(media.getId())
|
||||||
@@ -83,6 +92,13 @@ public class MappingUtil {
|
|||||||
.setAlbumTitle(media.getAlbum())
|
.setAlbumTitle(media.getAlbum())
|
||||||
.setArtist(media.getArtist())
|
.setArtist(media.getArtist())
|
||||||
.setArtworkUri(artworkUri)
|
.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)
|
.setExtras(bundle)
|
||||||
.setIsBrowsable(false)
|
.setIsBrowsable(false)
|
||||||
.setIsPlayable(true)
|
.setIsPlayable(true)
|
||||||
@@ -110,6 +126,11 @@ public class MappingUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static MediaItem mapDownload(Child media) {
|
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()
|
return new MediaItem.Builder()
|
||||||
.setMediaId(media.getId())
|
.setMediaId(media.getId())
|
||||||
.setMediaMetadata(
|
.setMediaMetadata(
|
||||||
@@ -120,12 +141,14 @@ public class MappingUtil {
|
|||||||
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
|
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
|
||||||
.setAlbumTitle(media.getAlbum())
|
.setAlbumTitle(media.getAlbum())
|
||||||
.setArtist(media.getArtist())
|
.setArtist(media.getArtist())
|
||||||
|
.setExtras(bundle)
|
||||||
.setIsBrowsable(false)
|
.setIsBrowsable(false)
|
||||||
.setIsPlayable(true)
|
.setIsPlayable(true)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.setRequestMetadata(
|
.setRequestMetadata(
|
||||||
new MediaItem.RequestMetadata.Builder()
|
new MediaItem.RequestMetadata.Builder()
|
||||||
|
.setExtras(bundle)
|
||||||
.setMediaUri(Preferences.preferTranscodedDownload() ? MusicUtil.getTranscodedDownloadUri(media.getId()) : MusicUtil.getDownloadUri(media.getId()))
|
.setMediaUri(Preferences.preferTranscodedDownload() ? MusicUtil.getTranscodedDownloadUri(media.getId()) : MusicUtil.getDownloadUri(media.getId()))
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@@ -217,12 +240,20 @@ public class MappingUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Uri getUri(Child media) {
|
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())
|
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId())
|
||||||
? getDownloadUri(media.getId())
|
? getDownloadUri(media.getId())
|
||||||
: MusicUtil.getStreamUri(media.getId());
|
: MusicUtil.getStreamUri(media.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri getUri(PodcastEpisode podcastEpisode) {
|
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())
|
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(podcastEpisode.getStreamId())
|
||||||
? getDownloadUri(podcastEpisode.getStreamId())
|
? getDownloadUri(podcastEpisode.getStreamId())
|
||||||
: MusicUtil.getStreamUri(podcastEpisode.getStreamId());
|
: MusicUtil.getStreamUri(podcastEpisode.getStreamId());
|
||||||
@@ -232,4 +263,11 @@ public class MappingUtil {
|
|||||||
Download download = new DownloadRepository().getDownload(id);
|
Download download = new DownloadRepository().getDownload(id);
|
||||||
return download != null && !download.getDownloadUri().isEmpty() ? Uri.parse(download.getDownloadUri()) : MusicUtil.getDownloadUri(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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ object Preferences {
|
|||||||
private const val WIFI_ONLY = "wifi_only"
|
private const val WIFI_ONLY = "wifi_only"
|
||||||
private const val DATA_SAVING_MODE = "data_saving_mode"
|
private const val DATA_SAVING_MODE = "data_saving_mode"
|
||||||
private const val SERVER_UNREACHABLE = "server_unreachable"
|
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_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 SYNC_STARRED_TRACKS_FOR_OFFLINE_USE = "sync_starred_tracks_for_offline_use"
|
||||||
private const val QUEUE_SYNCING = "queue_syncing"
|
private const val QUEUE_SYNCING = "queue_syncing"
|
||||||
@@ -45,11 +46,13 @@ object Preferences {
|
|||||||
private const val ROUNDED_CORNER_SIZE = "rounded_corner_size"
|
private const val ROUNDED_CORNER_SIZE = "rounded_corner_size"
|
||||||
private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility"
|
private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility"
|
||||||
private const val RADIO_SECTION_VISIBILITY = "radio_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 MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility"
|
||||||
private const val REPLAY_GAIN_MODE = "replay_gain_mode"
|
private const val REPLAY_GAIN_MODE = "replay_gain_mode"
|
||||||
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
|
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
|
||||||
private const val STREAMING_CACHE_STORAGE = "streaming_cache_storage"
|
private const val STREAMING_CACHE_STORAGE = "streaming_cache_storage"
|
||||||
private const val DOWNLOAD_STORAGE = "download_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 DEFAULT_DOWNLOAD_VIEW_TYPE = "default_download_view_type"
|
||||||
private const val AUDIO_TRANSCODE_DOWNLOAD = "audio_transcode_download"
|
private const val AUDIO_TRANSCODE_DOWNLOAD = "audio_transcode_download"
|
||||||
private const val AUDIO_TRANSCODE_DOWNLOAD_PRIORITY = "audio_transcode_download_priority"
|
private const val AUDIO_TRANSCODE_DOWNLOAD_PRIORITY = "audio_transcode_download_priority"
|
||||||
@@ -69,7 +72,10 @@ object Preferences {
|
|||||||
private const val NEXT_UPDATE_CHECK = "next_update_check"
|
private const val NEXT_UPDATE_CHECK = "next_update_check"
|
||||||
private const val CONTINUOUS_PLAY = "continuous_play"
|
private const val CONTINUOUS_PLAY = "continuous_play"
|
||||||
private const val LAST_INSTANT_MIX = "last_instant_mix"
|
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"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getServer(): String? {
|
fun getServer(): String? {
|
||||||
@@ -161,6 +167,24 @@ object Preferences {
|
|||||||
App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply()
|
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
|
@JvmStatic
|
||||||
fun getLocalAddress(): String? {
|
fun getLocalAddress(): String? {
|
||||||
return App.getInstance().preferences.getString(LOCAL_ADDRESS, null)
|
return App.getInstance().preferences.getString(LOCAL_ADDRESS, null)
|
||||||
@@ -302,6 +326,18 @@ object Preferences {
|
|||||||
.apply()
|
.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
|
@JvmStatic
|
||||||
fun isStarredAlbumsSyncEnabled(): Boolean {
|
fun isStarredAlbumsSyncEnabled(): Boolean {
|
||||||
return App.getInstance().preferences.getBoolean(SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, false)
|
return App.getInstance().preferences.getBoolean(SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, false)
|
||||||
@@ -326,6 +362,16 @@ object Preferences {
|
|||||||
).apply()
|
).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
|
@JvmStatic
|
||||||
fun showServerUnreachableDialog(): Boolean {
|
fun showServerUnreachableDialog(): Boolean {
|
||||||
return App.getInstance().preferences.getLong(
|
return App.getInstance().preferences.getLong(
|
||||||
@@ -419,6 +465,20 @@ object Preferences {
|
|||||||
).apply()
|
).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
|
@JvmStatic
|
||||||
fun getDefaultDownloadViewType(): String {
|
fun getDefaultDownloadViewType(): String {
|
||||||
return App.getInstance().preferences.getString(
|
return App.getInstance().preferences.getString(
|
||||||
@@ -538,4 +598,44 @@ object Preferences {
|
|||||||
LAST_INSTANT_MIX, 0
|
LAST_INSTANT_MIX, 0
|
||||||
) + 5000 < System.currentTimeMillis()
|
) + 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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
package com.cappielloantonio.tempo.viewmodel;
|
package com.cappielloantonio.tempo.viewmodel;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
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.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.Date;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||||
private final ArtistRepository artistRepository;
|
private final ArtistRepository artistRepository;
|
||||||
@@ -34,7 +42,7 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
|||||||
this.artist = artist;
|
this.artist = artist;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFavorite() {
|
public void setFavorite(Context context) {
|
||||||
if (artist.getStarred() != null) {
|
if (artist.getStarred() != null) {
|
||||||
if (NetworkUtil.isOffline()) {
|
if (NetworkUtil.isOffline()) {
|
||||||
removeFavoriteOffline();
|
removeFavoriteOffline();
|
||||||
@@ -43,9 +51,9 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (NetworkUtil.isOffline()) {
|
if (NetworkUtil.isOffline()) {
|
||||||
setFavoriteOffline();
|
setFavoriteOffline(context);
|
||||||
} else {
|
} else {
|
||||||
setFavoriteOnline();
|
setFavoriteOnline(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +67,6 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
|||||||
favoriteRepository.unstar(null, null, artist.getId(), new StarCallback() {
|
favoriteRepository.unstar(null, null, artist.getId(), new StarCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onError() {
|
public void onError() {
|
||||||
// artist.setStarred(new Date());
|
|
||||||
favoriteRepository.starLater(null, null, artist.getId(), false);
|
favoriteRepository.starLater(null, null, artist.getId(), false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -67,20 +74,45 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
|||||||
artist.setStarred(null);
|
artist.setStarred(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setFavoriteOffline() {
|
private void setFavoriteOffline(Context context) {
|
||||||
favoriteRepository.starLater(null, null, artist.getId(), true);
|
favoriteRepository.starLater(null, null, artist.getId(), true);
|
||||||
artist.setStarred(new Date());
|
artist.setStarred(new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setFavoriteOnline() {
|
private void setFavoriteOnline(Context context) {
|
||||||
favoriteRepository.star(null, null, artist.getId(), new StarCallback() {
|
favoriteRepository.star(null, null, artist.getId(), new StarCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onError() {
|
public void onError() {
|
||||||
// artist.setStarred(null);
|
|
||||||
favoriteRepository.starLater(null, null, artist.getId(), true);
|
favoriteRepository.starLater(null, null, artist.getId(), true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
artist.setStarred(new Date());
|
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<Child> 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
///
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.cappielloantonio.tempo.viewmodel;
|
package com.cappielloantonio.tempo.viewmodel;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.net.Uri;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -8,10 +9,13 @@ import androidx.lifecycle.AndroidViewModel;
|
|||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
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.model.DownloadStack;
|
||||||
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -25,6 +29,7 @@ public class DownloadViewModel extends AndroidViewModel {
|
|||||||
|
|
||||||
private final MutableLiveData<List<Child>> downloadedTrackSample = new MutableLiveData<>(null);
|
private final MutableLiveData<List<Child>> downloadedTrackSample = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<ArrayList<DownloadStack>> viewStack = new MutableLiveData<>(null);
|
private final MutableLiveData<ArrayList<DownloadStack>> viewStack = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Integer> refreshResult = new MutableLiveData<>();
|
||||||
|
|
||||||
public DownloadViewModel(@NonNull Application application) {
|
public DownloadViewModel(@NonNull Application application) {
|
||||||
super(application);
|
super(application);
|
||||||
@@ -43,6 +48,10 @@ public class DownloadViewModel extends AndroidViewModel {
|
|||||||
return viewStack;
|
return viewStack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<Integer> getRefreshResult() {
|
||||||
|
return refreshResult;
|
||||||
|
}
|
||||||
|
|
||||||
public void initViewStack(DownloadStack level) {
|
public void initViewStack(DownloadStack level) {
|
||||||
ArrayList<DownloadStack> stack = new ArrayList<>();
|
ArrayList<DownloadStack> stack = new ArrayList<>();
|
||||||
stack.add(level);
|
stack.add(level);
|
||||||
@@ -60,4 +69,59 @@ public class DownloadViewModel extends AndroidViewModel {
|
|||||||
stack.remove(stack.size() - 1);
|
stack.remove(stack.size() - 1);
|
||||||
viewStack.setValue(stack);
|
viewStack.setValue(stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void refreshExternalDownloads() {
|
||||||
|
new Thread(() -> {
|
||||||
|
String directoryUri = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (directoryUri == null) {
|
||||||
|
refreshResult.postValue(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Download> downloads = downloadRepository.getAllDownloads();
|
||||||
|
if (downloads == null || downloads.isEmpty()) {
|
||||||
|
refreshResult.postValue(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<Download> 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<String> 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public class HomeViewModel extends AndroidViewModel {
|
|||||||
private final SharingRepository sharingRepository;
|
private final SharingRepository sharingRepository;
|
||||||
|
|
||||||
private final StarredAlbumsSyncViewModel albumsSyncViewModel;
|
private final StarredAlbumsSyncViewModel albumsSyncViewModel;
|
||||||
|
private final StarredArtistsSyncViewModel artistSyncViewModel;
|
||||||
|
|
||||||
private final MutableLiveData<List<Child>> dicoverSongSample = new MutableLiveData<>(null);
|
private final MutableLiveData<List<Child>> dicoverSongSample = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<List<AlbumID3>> newReleasedAlbum = new MutableLiveData<>(null);
|
private final MutableLiveData<List<AlbumID3>> newReleasedAlbum = new MutableLiveData<>(null);
|
||||||
@@ -85,6 +86,7 @@ public class HomeViewModel extends AndroidViewModel {
|
|||||||
sharingRepository = new SharingRepository();
|
sharingRepository = new SharingRepository();
|
||||||
|
|
||||||
albumsSyncViewModel = new StarredAlbumsSyncViewModel(application);
|
albumsSyncViewModel = new StarredAlbumsSyncViewModel(application);
|
||||||
|
artistSyncViewModel = new StarredArtistsSyncViewModel(application);
|
||||||
|
|
||||||
setOfflineFavorite();
|
setOfflineFavorite();
|
||||||
}
|
}
|
||||||
@@ -174,6 +176,10 @@ public class HomeViewModel extends AndroidViewModel {
|
|||||||
return albumsSyncViewModel.getAllStarredAlbumSongs();
|
return albumsSyncViewModel.getAllStarredAlbumSongs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Child>> getAllStarredArtistSongs() {
|
||||||
|
return artistSyncViewModel.getAllStarredArtistSongs();
|
||||||
|
}
|
||||||
|
|
||||||
public LiveData<List<ArtistID3>> getStarredArtists(LifecycleOwner owner) {
|
public LiveData<List<ArtistID3>> getStarredArtists(LifecycleOwner owner) {
|
||||||
if (starredArtists.getValue() == null) {
|
if (starredArtists.getValue() == null) {
|
||||||
artistRepository.getStarredArtists(true, 20).observe(owner, starredArtists::postValue);
|
artistRepository.getStarredArtists(true, 20).observe(owner, starredArtists::postValue);
|
||||||
|
|||||||
@@ -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<String> currentSongId = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Boolean> isPlaying = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
public LiveData<String> getCurrentSongId() {
|
||||||
|
return currentSongId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.viewmodel;
|
|||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
@@ -9,14 +10,17 @@ import androidx.lifecycle.AndroidViewModel;
|
|||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
|
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||||
import com.cappielloantonio.tempo.model.Queue;
|
import com.cappielloantonio.tempo.model.Queue;
|
||||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||||
|
import com.cappielloantonio.tempo.repository.LyricsRepository;
|
||||||
import com.cappielloantonio.tempo.repository.OpenRepository;
|
import com.cappielloantonio.tempo.repository.OpenRepository;
|
||||||
import com.cappielloantonio.tempo.repository.QueueRepository;
|
import com.cappielloantonio.tempo.repository.QueueRepository;
|
||||||
import com.cappielloantonio.tempo.repository.SongRepository;
|
import com.cappielloantonio.tempo.repository.SongRepository;
|
||||||
@@ -31,6 +35,7 @@ import com.cappielloantonio.tempo.util.MappingUtil;
|
|||||||
import com.cappielloantonio.tempo.util.NetworkUtil;
|
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||||
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -47,14 +52,20 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||||||
private final QueueRepository queueRepository;
|
private final QueueRepository queueRepository;
|
||||||
private final FavoriteRepository favoriteRepository;
|
private final FavoriteRepository favoriteRepository;
|
||||||
private final OpenRepository openRepository;
|
private final OpenRepository openRepository;
|
||||||
|
private final LyricsRepository lyricsRepository;
|
||||||
private final MutableLiveData<String> lyricsLiveData = new MutableLiveData<>(null);
|
private final MutableLiveData<String> lyricsLiveData = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<LyricsList> lyricsListLiveData = new MutableLiveData<>(null);
|
private final MutableLiveData<LyricsList> lyricsListLiveData = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Boolean> lyricsCachedLiveData = new MutableLiveData<>(false);
|
||||||
private final MutableLiveData<String> descriptionLiveData = new MutableLiveData<>(null);
|
private final MutableLiveData<String> descriptionLiveData = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<Child> liveMedia = new MutableLiveData<>(null);
|
private final MutableLiveData<Child> liveMedia = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<AlbumID3> liveAlbum = new MutableLiveData<>(null);
|
private final MutableLiveData<AlbumID3> liveAlbum = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<ArtistID3> liveArtist = new MutableLiveData<>(null);
|
private final MutableLiveData<ArtistID3> liveArtist = new MutableLiveData<>(null);
|
||||||
private final MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(null);
|
private final MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(null);
|
||||||
|
private final Gson gson = new Gson();
|
||||||
private boolean lyricsSyncState = true;
|
private boolean lyricsSyncState = true;
|
||||||
|
private LiveData<LyricsCache> cachedLyricsSource;
|
||||||
|
private String currentSongId;
|
||||||
|
private final Observer<LyricsCache> cachedLyricsObserver = this::onCachedLyricsChanged;
|
||||||
|
|
||||||
|
|
||||||
public PlayerBottomSheetViewModel(@NonNull Application application) {
|
public PlayerBottomSheetViewModel(@NonNull Application application) {
|
||||||
@@ -66,6 +77,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||||||
queueRepository = new QueueRepository();
|
queueRepository = new QueueRepository();
|
||||||
favoriteRepository = new FavoriteRepository();
|
favoriteRepository = new FavoriteRepository();
|
||||||
openRepository = new OpenRepository();
|
openRepository = new OpenRepository();
|
||||||
|
lyricsRepository = new LyricsRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<List<Queue>> getQueueSong() {
|
public LiveData<List<Queue>> getQueueSong() {
|
||||||
@@ -122,7 +134,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||||||
|
|
||||||
media.setStarred(new Date());
|
media.setStarred(new Date());
|
||||||
|
|
||||||
if (Preferences.isStarredSyncEnabled()) {
|
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(context).download(
|
DownloadUtil.getDownloadTracker(context).download(
|
||||||
MappingUtil.mapDownload(media),
|
MappingUtil.mapDownload(media),
|
||||||
new Download(media)
|
new Download(media)
|
||||||
@@ -139,12 +151,49 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void refreshMediaInfo(LifecycleOwner owner, Child media) {
|
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()) {
|
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
||||||
openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsListLiveData::postValue);
|
openRepository.getLyricsBySongId(media.getId()).observe(owner, lyricsList -> {
|
||||||
lyricsLiveData.postValue(null);
|
lyricsListLiveData.postValue(lyricsList);
|
||||||
|
lyricsLiveData.postValue(null);
|
||||||
|
|
||||||
|
if (shouldAutoDownloadLyrics() && hasStructuredLyrics(lyricsList)) {
|
||||||
|
saveLyricsToCache(media, null, lyricsList);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
songRepository.getSongLyrics(media).observe(owner, lyricsLiveData::postValue);
|
songRepository.getSongLyrics(media).observe(owner, lyrics -> {
|
||||||
lyricsListLiveData.postValue(null);
|
lyricsLiveData.postValue(lyrics);
|
||||||
|
lyricsListLiveData.postValue(null);
|
||||||
|
|
||||||
|
if (shouldAutoDownloadLyrics() && !TextUtils.isEmpty(lyrics)) {
|
||||||
|
saveLyricsToCache(media, lyrics, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +202,17 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setLiveMedia(LifecycleOwner owner, String mediaType, String mediaId) {
|
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) {
|
if (mediaType != null) {
|
||||||
switch (mediaType) {
|
switch (mediaType) {
|
||||||
case Constants.MEDIA_TYPE_MUSIC:
|
case Constants.MEDIA_TYPE_MUSIC:
|
||||||
@@ -162,7 +222,12 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||||||
case Constants.MEDIA_TYPE_PODCAST:
|
case Constants.MEDIA_TYPE_PODCAST:
|
||||||
liveMedia.postValue(null);
|
liveMedia.postValue(null);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
liveMedia.postValue(null);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
liveMedia.postValue(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +298,105 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||||||
return false;
|
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<Boolean> getLyricsCachedState() {
|
||||||
|
return lyricsCachedLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
public void changeSyncLyricsState() {
|
public void changeSyncLyricsState() {
|
||||||
lyricsSyncState = !lyricsSyncState;
|
lyricsSyncState = !lyricsSyncState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.cappielloantonio.tempo.viewmodel;
|
package com.cappielloantonio.tempo.viewmodel;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
@@ -11,10 +13,10 @@ import androidx.lifecycle.MutableLiveData;
|
|||||||
import com.cappielloantonio.tempo.repository.PlaylistRepository;
|
import com.cappielloantonio.tempo.repository.PlaylistRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class PlaylistChooserViewModel extends AndroidViewModel {
|
public class PlaylistChooserViewModel extends AndroidViewModel {
|
||||||
@@ -34,8 +36,21 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
|
|||||||
return playlists;
|
return playlists;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSongsToPlaylist(String playlistId) {
|
public void addSongsToPlaylist(LifecycleOwner owner, Dialog dialog, String playlistId) {
|
||||||
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(Lists.transform(toAdd, Child::getId)));
|
List<String> 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<String> playlistSongIds = Lists.transform(playlistSongs, Child::getId);
|
||||||
|
songIds.removeAll(playlistSongIds);
|
||||||
|
}
|
||||||
|
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds));
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSongsToAdd(ArrayList<Child> songs) {
|
public void setSongsToAdd(ArrayList<Child> songs) {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
|||||||
|
|
||||||
media.setStarred(new Date());
|
media.setStarred(new Date());
|
||||||
|
|
||||||
if (Preferences.isStarredSyncEnabled()) {
|
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(context).download(
|
DownloadUtil.getDownloadTracker(context).download(
|
||||||
MappingUtil.mapDownload(media),
|
MappingUtil.mapDownload(media),
|
||||||
new Download(media)
|
new Download(media)
|
||||||
|
|||||||
@@ -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<List<ArtistID3>> starredArtists = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<List<Child>> starredArtistSongs = new MutableLiveData<>(null);
|
||||||
|
|
||||||
|
public StarredArtistsSyncViewModel(@NonNull Application application) {
|
||||||
|
super(application);
|
||||||
|
artistRepository = new ArtistRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<ArtistID3>> getStarredArtists(LifecycleOwner owner) {
|
||||||
|
artistRepository.getStarredArtists(false, -1).observe(owner, starredArtists::postValue);
|
||||||
|
return starredArtists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Child>> getAllStarredArtistSongs() {
|
||||||
|
artistRepository.getStarredArtists(false, -1).observeForever(new Observer<List<ArtistID3>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<ArtistID3> 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<List<Child>> 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<ArtistID3> artists, ArtistSongsCallback callback) {
|
||||||
|
if (artists == null || artists.isEmpty()) {
|
||||||
|
callback.onSongsCollected(new ArrayList<>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Child> 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<Child> songs) {
|
||||||
|
if (songs != null) {
|
||||||
|
allSongs.addAll(songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
int remaining = remainingArtists.decrementAndGet();
|
||||||
|
if (remaining == 0) {
|
||||||
|
callback.onSongsCollected(allSongs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ArtistSongsCallback {
|
||||||
|
void onSongsCollected(List<Child> songs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<MediaController> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
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 {
|
||||||
|
|
||||||
|
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,
|
||||||
|
com.cappielloantonio.tempo.util.Preferences.getImageSize(),
|
||||||
|
new CustomTarget<Bitmap>() {
|
||||||
|
@Override
|
||||||
|
public void onResourceReady(Bitmap resource, Transition<? super Bitmap> 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<MediaController> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/src/main/res/drawable/ic_eq.xml
Normal file
11
app/src/main/res/drawable/ic_eq.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:autoMirrored="true">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/titleTextColor"
|
||||||
|
android:pathData="M160,800L160,480L320,480L320,800L160,800ZM400,800L400,160L560,160L560,800L400,800ZM640,800L640,360L800,360L800,800L640,800Z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_folder.xml
Normal file
9
app/src/main/res/drawable/ic_folder.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/titleTextColor"
|
||||||
|
android:pathData="M10,4L12,6H20c1.1,0 2,0.9 2,2v10c0,1.1 -0.9,2 -2,2H4c-1.1,0 -2,-0.9 -2,-2V6c0,-1.1 0.9,-2 2,-2h6z"/>
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/ic_link.xml
Normal file
10
app/src/main/res/drawable/ic_link.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:pathData="M3.9,12c0,1.71 1.39,3.1 3.1,3.1h3v1.8h-3c-2.7,0 -4.9,-2.2 -4.9,-4.9s2.2,-4.9 4.9,-4.9h3v1.8h-3c-1.71,0 -3.1,1.39 -3.1,3.1zM7,13h10v-2H7v2zM17,6.9h-3v-1.8h3c2.7,0 4.9,2.2 4.9,4.9s-2.2,4.9 -4.9,4.9h-3v-1.8h3c1.71,0 3.1,-1.39 3.1,-3.1s-1.39,-3.1 -3.1,-3.1z" />
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_refresh.xml
Normal file
9
app/src/main/res/drawable/ic_refresh.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/titleTextColor"
|
||||||
|
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -8,3.58 -8,8h2c0,-3.31 2.69,-6 6,-6 1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35zM19,12c0,3.31 -2.69,6 -6,6 -1.66,0 -3.14,-0.69 -4.22,-1.78L11,13H4v7l2.35,-2.35C7.8,19.1 9.79,20 12,20c4.42,0 8,-3.58 8,-8h-2z" />
|
||||||
|
</vector>
|
||||||
12
app/src/main/res/drawable/ic_repeat_one.xml
Normal file
12
app/src/main/res/drawable/ic_repeat_one.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/titleTextColor"
|
||||||
|
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17H7v-3l-4,4 4,4v-3h12v-6h-2v4z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/titleTextColor"
|
||||||
|
android:pathData="M12,9h-2v2h1v6h2V9h-1z" />
|
||||||
|
</vector>
|
||||||
93
app/src/main/res/drawable/ui_eq_not_supported.xml
Normal file
93
app/src/main/res/drawable/ui_eq_not_supported.xml
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="757.96dp"
|
||||||
|
android:height="743.73dp"
|
||||||
|
android:viewportWidth="757.96"
|
||||||
|
android:viewportHeight="743.73">
|
||||||
|
<path
|
||||||
|
android:pathData="M91.45,0a32.04,32.04 0,0 0,-32 32L59.45,710.43a32.04,32.04 0,0 0,32 32h297a32.04,32.04 0,0 0,32 -32L420.45,32a32.04,32.04 0,0 0,-32 -32Z"
|
||||||
|
android:fillColor="#e6e6e6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M400.66,156.98v-54.44a125.25,125.25 0,0 1,-80.86 -60.19h0a23.79,23.79 0,0 1,-14.22 4.68L262.35,47.03A178.55,178.55 0,0 0,400.66 156.98Z"
|
||||||
|
android:fillColor="#fff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M400.66,99.42v-52.3a29.12,29.12 0,0 0,-29.13 -29.13h-41.97v5.05a23.92,23.92 0,0 1,-7.4 17.33,122.3 122.3,0 0,0 78.5,59.05Z"
|
||||||
|
android:fillColor="#fff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M198.77,47.03L171.74,47.03a23.99,23.99 0,0 1,-23.98 -23.99v-5.05L108.38,17.99a29.13,29.13 0,0 0,-29.13 29.13v648.2a29.08,29.08 0,0 0,29.13 29.11h263.15a28.36,28.36 0,0 0,3.59 -0.22,29.15 29.15,0 0,0 25.54,-28.89L400.66,218.15C304.95,207.07 225.2,138.77 198.77,47.03Z"
|
||||||
|
android:fillColor="#fff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M259.07,47.03h-57.14c26.3,90.04 104.68,157.03 198.73,168.07v-55.02A181.67,181.67 0,0 1,259.07 47.03Z"
|
||||||
|
android:fillColor="#fff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M380.61,532.78h-270a5.01,5.01 0,0 1,-5 -5L105.61,460.81a5.01,5.01 0,0 1,5 -5h270a5.01,5.01 0,0 1,5 5v66.98A5.01,5.01 0,0 1,380.61 532.78ZM110.61,457.8a3,3 0,0 0,-3 3v66.98a3,3 0,0 0,3 3h270a3,3 0,0 0,3 -3L383.61,460.81a3,3 0,0 0,-3 -3Z"
|
||||||
|
android:fillColor="#e6e6e6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M145.61,494.29m-21,0a21,21 0,1 1,42 0a21,21 0,1 1,-42 0"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M194.11,480.29a3.5,3.5 0,0 0,0 7h165a3.5,3.5 0,1 0,0 -7Z"
|
||||||
|
android:fillColor="#e6e6e6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M194.11,501.29a3.5,3.5 0,0 0,0 7h165a3.5,3.5 0,1 0,0 -7Z"
|
||||||
|
android:fillColor="#e6e6e6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M380.61,644.78h-270a5.01,5.01 0,0 1,-5 -5L105.61,572.81a5.01,5.01 0,0 1,5 -5h270a5.01,5.01 0,0 1,5 5v66.98A5.01,5.01 0,0 1,380.61 644.78ZM110.61,569.8a3,3 0,0 0,-3 3v66.98a3,3 0,0 0,3 3h270a3,3 0,0 0,3 -3L383.61,572.81a3,3 0,0 0,-3 -3Z"
|
||||||
|
android:fillColor="#e6e6e6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M145.61,606.29m-21,0a21,21 0,1 1,42 0a21,21 0,1 1,-42 0"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M194.11,592.29a3.5,3.5 0,0 0,0 7h165a3.5,3.5 0,1 0,0 -7Z"
|
||||||
|
android:fillColor="#e6e6e6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M194.11,613.29a3.5,3.5 0,0 0,0 7h165a3.5,3.5 0,1 0,0 -7Z"
|
||||||
|
android:fillColor="#e6e6e6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M239.93,394a94.96,94.96 0,0 1,-95 -95c0,-0.2 0,-0.41 0.01,-0.61 0.29,-52.03 42.9,-94.39 94.99,-94.39a95,95 0,1 1,0 190ZM239.93,206a93.2,93.2 0,0 0,-92.99 92.46c-0.01,0.21 -0.01,0.38 -0.01,0.54a93.01,93.01 0,1 0,93 -93Z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M282.95,296.81l-65.02,-37.54a2,2 0,0 0,-3 1.73L214.93,336.08a2,2 0,0 0,3 1.73l65.02,-37.54a2,2 0,0 0,0 -3.46l-65.02,-37.54a2,2 0,0 0,-3 1.73L214.93,336.08a2,2 0,0 0,3 1.73l65.02,-37.54a2,2 0,0 0,0 -3.46Z"
|
||||||
|
android:fillColor="#6c63ff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M757.57,743.73H0v-2.18H757.96Z"
|
||||||
|
android:fillColor="#3f3d56"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M590.68,338.14m-27.94,0a27.94,27.94 0,1 1,55.87 0a27.94,27.94 0,1 1,-55.87 0"
|
||||||
|
android:fillColor="#ffb8b8"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M588.87,494.75a12.51,12.51 0,0 1,9.47 -16.1,11.89 11.89,0 0,1 1.66,-0.2l29.43,-47.23L602.55,405.66A10.73,10.73 0,1 1,617.47 390.25l37.11,36.6 0.08,0.09a9.72,9.72 0,0 1,-0.68 11.58L612.75,487.28a11.73,11.73 0,0 1,0.31 1.19,12.51 12.51,0 0,1 -11.23,14.92q-0.53,0.05 -1.06,0.05A12.55,12.55 0,0 1,588.87 494.75Z"
|
||||||
|
android:fillColor="#ffb8b8"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M544.67,726.93L530.72,726.93l-6.63,-53.79 20.58,0Z"
|
||||||
|
android:fillColor="#ffb8b8"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M548.79,741.02l-46.1,0L502.69,739.88a18.07,18.07 0,0 1,18.07 -18.07h28.03Z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M683.27,707.66l-11.98,7.14 -33.22,-42.82 17.68,-10.53Z"
|
||||||
|
android:fillColor="#ffb8b8"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M654.41,741.23l-0.58,-0.98a18.07,18.07 0,0 1,6.28 -24.77l24.08,-14.34 9.83,16.5Z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M522.33,703.25c-9.34,-109.99 -14.9,-212.18 19.25,-253.86l0.26,-0.32 57.47,22.99 0.09,0.2c0.19,0.42 19.31,42.46 14.85,70.74l14.18,65.21 46.22,77.39a5.12,5.12 0,0 1,-2.33 7.31l-20.09,8.84a5.14,5.14 0,0 1,-6.42 -2.01L595.53,617.75l-28.4,-62.88a1.71,1.71 0,0 0,-3.25 0.52L548.14,703.36a5.11,5.11 0,0 1,-5.09 4.58L527.43,707.94A5.15,5.15 0,0 1,522.33 703.25Z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M541.77,450.26l-0.27,-0.13 -0.04,-0.3c-2.15,-15.02 0.39,-31.72 7.55,-49.62a39.4,39.4 0,0 1,45.73 -23.59h0a39.35,39.35 0,0 1,25.09 19.3,38.92 38.92,0 0,1 2.7,31.19c-9.02,26.39 -20.73,51.08 -20.85,51.32l-0.25,0.51Z"
|
||||||
|
android:fillColor="#6c63ff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M500.42,512.57a12.78,12.78 0,0 1,9.16 -13.94l53.74,-103.17a10.3,10.3 0,1 1,17.52 10.82L525.84,508.73a12.42,12.42 0,0 1,0.2 1.89,12.86 12.86,0 0,1 -13.03,13.21h0a12.87,12.87 0,0 1,-9.87 -4.83,12.71 12.71,0 0,1 -2.71,-6.43Z"
|
||||||
|
android:fillColor="#ffb8b8"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M556.81,322.35h44.36L601.16,303.02c-9.74,-3.87 -19.26,-7.16 -25.02,0a19.34,19.34 0,0 0,-19.34 19.34Z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M603.62,299.61c26.52,0 33.94,33.24 33.94,51.99 0,10.46 -4.73,14.2 -12.16,15.46l-2.63,-14 -6.15,14.6c-2.09,0.01 -4.28,-0.03 -6.55,-0.07l-2.08,-4.29 -4.65,4.22c-18.62,0.03 -33.66,2.74 -33.66,-15.92C569.68,332.85 576.19,299.61 603.62,299.61Z"
|
||||||
|
android:fillColor="#2f2e41"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M595.72,327L595.72,301.13a2.33,2.33 0,0 0,-2.33 -2.33h-4.67a2.33,2.33 0,0 0,-2.33 2.33L586.39,325.44a14.74,14.74 0,1 0,9.33 1.56Z"
|
||||||
|
android:fillColor="#6c63ff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M589.5,340.01m-7,0a7,7 0,1 1,14 0a7,7 0,1 1,-14 0"
|
||||||
|
android:fillColor="#fff"/>
|
||||||
|
</vector>
|
||||||
6
app/src/main/res/drawable/widget_bg.xml
Normal file
6
app/src/main/res/drawable/widget_bg.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
<solid android:color="@color/widget_bg" />
|
||||||
|
</shape>
|
||||||
@@ -382,11 +382,23 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toEndOf="@+id/player_open_equalizer_button"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:srcCompat="@drawable/ic_queue" />
|
app:srcCompat="@drawable/ic_queue" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/player_open_equalizer_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/player_open_queue_button"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_eq" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -68,6 +68,14 @@
|
|||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/song_asset_link_row"
|
||||||
|
layout="@layout/view_asset_link_row"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="12dp" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/option_linear_layout"
|
android:id="@+id/option_linear_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -209,4 +217,4 @@
|
|||||||
android:text="@string/song_bottom_sheet_share"
|
android:text="@string/song_bottom_sheet_share"
|
||||||
android:visibility="gone"/>
|
android:visibility="gone"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/playlist_dialog_recycler_view"
|
android:id="@+id/playlist_dialog_recycler_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:clipToPadding="false" />
|
android:clipToPadding="false" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
14
app/src/main/res/layout/dialog_starred_artist_sync.xml
Normal file
14
app/src/main/res/layout/dialog_starred_artist_sync.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:text="@string/starred_artist_sync_dialog_summary" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/download_title_section"
|
android:text="@string/download_title_section"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/downloaded_go_back_image_view"
|
app:layout_constraintEnd_toStartOf="@+id/downloaded_refresh_image_view"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
@@ -94,6 +94,19 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/downloaded_text_view_refreshable"/>
|
app:layout_constraintTop_toBottomOf="@+id/downloaded_text_view_refreshable"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/downloaded_refresh_image_view"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/ic_refresh"
|
||||||
|
android:contentDescription="@string/download_refresh_button_content_description"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/downloaded_go_back_image_view"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/downloaded_text_view_refreshable"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/downloaded_go_back_image_view"
|
android:id="@+id/downloaded_go_back_image_view"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
@@ -103,6 +116,7 @@
|
|||||||
android:background="@drawable/ic_arrow_back"
|
android:background="@drawable/ic_arrow_back"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
|
||||||
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
|
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/downloaded_refresh_image_view"
|
||||||
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
|||||||
105
app/src/main/res/layout/fragment_equalizer.xml
Normal file
105
app/src/main/res/layout/fragment_equalizer.xml
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/eq_frame_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/eq_scroll_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/eq_root_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/equalizer_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/equalizer_fragment_title"
|
||||||
|
style="@style/HeadlineSmall"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:paddingBottom="16dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/equalizer_switch_row"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/equalizer_switch_label"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
style="@style/LabelMedium"
|
||||||
|
android:text="@string/equalizer_enable" />
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/equalizer_switch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/eq_bands_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/equalizer_reset_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/equalizer_reset"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
|
android:layout_marginTop="24dp"/>
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/equalizer_bottom_space"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="128dp"
|
||||||
|
android:layout_marginTop="0dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/equalizer_not_supported_container"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/equalizer_not_supported_image"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:maxWidth="240dp"
|
||||||
|
android:maxHeight="240dp"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:src="@drawable/ui_eq_not_supported" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/equalizer_not_supported_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/equalizer_not_supported"
|
||||||
|
android:gravity="center"
|
||||||
|
style="@style/BodyMedium"
|
||||||
|
android:layout_marginTop="16dp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -198,6 +198,98 @@
|
|||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Download/Sync starred artists -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/home_sync_starred_artists_card"
|
||||||
|
style="?attr/materialCardViewOutlinedStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="20dp"
|
||||||
|
android:paddingVertical="12dp">
|
||||||
|
|
||||||
|
<!-- Title, secondary and supporting text -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/home_sync_starred_artists_title"
|
||||||
|
style="@style/TitleLarge"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/home_sync_starred_artists_title"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:textFontWeight="600"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/vertical_guideline_artists"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/home_sync_starred_artists_subtitle"
|
||||||
|
style="@style/TitleMedium"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/home_sync_starred_artists_subtitle"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/home_sync_starred_artists_title" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/home_sync_starred_artists_to_sync"
|
||||||
|
style="@style/TitleSmall"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:text="@string/home_sync_starred_artists_subtitle"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/home_sync_starred_artists_subtitle" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="end"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/home_sync_starred_artists_to_sync">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/home_sync_starred_artists_cancel"
|
||||||
|
style="?attr/materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@string/home_sync_starred_cancel" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/home_sync_starred_artists_download"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/home_sync_starred_download" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/vertical_guideline_artists"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintGuide_percent="0.90" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<!-- Discover music -->
|
<!-- Discover music -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/home_discover_sector"
|
android:id="@+id/home_discover_sector"
|
||||||
|
|||||||
@@ -57,6 +57,17 @@
|
|||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/player_asset_link_row"
|
||||||
|
layout="@layout/view_asset_link_row"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/player_media_quality_sector" />
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
android:id="@+id/player_media_cover_view_pager"
|
android:id="@+id/player_media_cover_view_pager"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@@ -66,7 +77,7 @@
|
|||||||
app:layout_constraintBottom_toTopOf="@id/guideline"
|
app:layout_constraintBottom_toTopOf="@id/guideline"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/player_media_quality_sector" />
|
app:layout_constraintTop_toBottomOf="@+id/player_asset_link_row" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Guideline
|
<androidx.constraintlayout.widget.Guideline
|
||||||
android:id="@+id/guideline"
|
android:id="@+id/guideline"
|
||||||
@@ -381,11 +392,23 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toEndOf="@+id/player_open_equalizer_button"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:srcCompat="@drawable/ic_queue" />
|
app:srcCompat="@drawable/ic_queue" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/player_open_equalizer_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/player_open_queue_button"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_eq" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -51,7 +51,25 @@
|
|||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
<Button
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/download_lyrics_button"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:alpha="0.7"
|
||||||
|
android:contentDescription="@string/player_lyrics_download_content_description"
|
||||||
|
android:insetLeft="0dp"
|
||||||
|
android:insetTop="0dp"
|
||||||
|
android:insetRight="0dp"
|
||||||
|
android:insetBottom="0dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:cornerRadius="64dp"
|
||||||
|
app:icon="@drawable/ic_download"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/sync_lyrics_tap_button"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/now_playing_song_lyrics_sroll_view" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/sync_lyrics_tap_button"
|
android:id="@+id/sync_lyrics_tap_button"
|
||||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
|
|||||||
@@ -55,6 +55,27 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/different_disk_divider_sector" />
|
app:layout_constraintTop_toBottomOf="@+id/different_disk_divider_sector" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/cover_art_overlay"
|
||||||
|
android:layout_width="52dp"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:background="#80000000"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/song_cover_image_view"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/different_disk_divider_sector" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/play_pause_icon"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="28dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/different_disk_divider_sector" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/track_number_text_view"
|
android:id="@+id/track_number_text_view"
|
||||||
style="@style/LabelLarge"
|
style="@style/LabelLarge"
|
||||||
|
|||||||
@@ -20,6 +20,27 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/cover_art_overlay"
|
||||||
|
android:layout_width="52dp"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:layout_marginStart="2dp"
|
||||||
|
android:background="#80000000"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/play_pause_icon"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_margin="14dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/queue_song_title_text_view"
|
android:id="@+id/queue_song_title_text_view"
|
||||||
style="@style/LabelMedium"
|
style="@style/LabelMedium"
|
||||||
|
|||||||
55
app/src/main/res/layout/view_asset_link_row.xml
Normal file
55
app/src/main/res/layout/view_asset_link_row.xml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.chip.ChipGroup xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/asset_link_chip_group"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
app:singleLine="true"
|
||||||
|
app:selectionRequired="false"
|
||||||
|
app:singleSelection="false">
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/asset_link_song_chip"
|
||||||
|
style="@style/Widget.Material3.Chip.Assist"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checkable="false"
|
||||||
|
android:clickable="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text=""
|
||||||
|
app:chipIcon="@drawable/ic_link"
|
||||||
|
app:chipIconTint="?attr/colorOnSurfaceVariant"
|
||||||
|
app:rippleColor="@color/ripple_material_light" />
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/asset_link_album_chip"
|
||||||
|
style="@style/Widget.Material3.Chip.Assist"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checkable="false"
|
||||||
|
android:clickable="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text=""
|
||||||
|
app:chipIcon="@drawable/ic_link"
|
||||||
|
app:chipIconTint="?attr/colorOnSurfaceVariant"
|
||||||
|
app:rippleColor="@color/ripple_material_light" />
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/asset_link_artist_chip"
|
||||||
|
style="@style/Widget.Material3.Chip.Assist"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checkable="false"
|
||||||
|
android:clickable="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text=""
|
||||||
|
app:chipIcon="@drawable/ic_link"
|
||||||
|
app:chipIconTint="?attr/colorOnSurfaceVariant"
|
||||||
|
app:rippleColor="@color/ripple_material_light" />
|
||||||
|
</com.google.android.material.chip.ChipGroup>
|
||||||
175
app/src/main/res/layout/widget_layout_compact.xml
Normal file
175
app/src/main/res/layout/widget_layout_compact.xml
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:background="@drawable/widget_bg">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/album_art"
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/widget_content_desc_album_art"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/texts"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_toRightOf="@id/album_art"
|
||||||
|
android:layout_toEndOf="@id/album_art"
|
||||||
|
android:layout_toLeftOf="@id/controls"
|
||||||
|
android:layout_toStartOf="@id/controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/widget_title"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/album"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="1000"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressBackgroundTint="@color/widget_subtitle"
|
||||||
|
android:progressTint="@color/widget_icon_tint"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/timing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_elapsed"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/widget_time_elapsed_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:includeFontPadding="false"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_total"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/widget_time_duration_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:includeFontPadding="false"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_shuffle"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||||
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:tint="@color/widget_icon_tint"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_repeat"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_repeat"
|
||||||
|
android:src="@drawable/ic_repeat"
|
||||||
|
android:tint="@color/widget_icon_tint"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_prev"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:src="@drawable/ic_skip_previous"
|
||||||
|
android:contentDescription="@string/widget_content_desc_prev"
|
||||||
|
android:tint="@color/widget_icon_tint"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_play_pause"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:src="@drawable/ic_play"
|
||||||
|
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||||
|
android:tint="@color/widget_icon_tint"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_next"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:src="@drawable/ic_skip_next"
|
||||||
|
android:contentDescription="@string/widget_content_desc_next"
|
||||||
|
android:tint="@color/widget_icon_tint"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</RelativeLayout>
|
||||||
189
app/src/main/res/layout/widget_layout_large.xml
Normal file
189
app/src/main/res/layout/widget_layout_large.xml
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minHeight="200dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@drawable/widget_bg">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/header"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:baselineAligned="false">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/album_art"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="150dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/widget_content_desc_album_art" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/text_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textColor="@color/widget_title"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/album"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:visibility="invisible" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="6dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="1000"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressBackgroundTint="@color/widget_subtitle"
|
||||||
|
android:progressTint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/timing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_elapsed"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/widget_time_elapsed_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_total"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/widget_time_duration_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_prev"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_prev"
|
||||||
|
android:src="@drawable/ic_skip_previous"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_play_pause"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||||
|
android:src="@drawable/ic_play"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_next"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_next"
|
||||||
|
android:src="@drawable/ic_skip_next"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_shuffle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||||
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_repeat"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_repeat"
|
||||||
|
android:src="@drawable/ic_repeat"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
198
app/src/main/res/layout/widget_layout_large_short.xml
Normal file
198
app/src/main/res/layout/widget_layout_large_short.xml
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minHeight="172dp"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@drawable/widget_bg">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/header"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/album_art_container"
|
||||||
|
android:layout_width="90dp"
|
||||||
|
android:layout_height="90dp"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/album_art"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/widget_content_desc_album_art" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/text_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textColor="@color/widget_title"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/album"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:visibility="invisible" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="6dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="1000"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressBackgroundTint="@color/widget_subtitle"
|
||||||
|
android:progressTint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/timing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_elapsed"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/widget_time_elapsed_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_total"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/widget_time_duration_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_shuffle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||||
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:layout_weight="3"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_prev"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_prev"
|
||||||
|
android:src="@drawable/ic_skip_previous"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_play_pause"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||||
|
android:src="@drawable/ic_play"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_next"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_next"
|
||||||
|
android:src="@drawable/ic_skip_next"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_repeat"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_repeat"
|
||||||
|
android:src="@drawable/ic_repeat"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
216
app/src/main/res/layout/widget_layout_medium.xml
Normal file
216
app/src/main/res/layout/widget_layout_medium.xml
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minHeight="120dp"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="12dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:background="@drawable/widget_bg">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/album_art_container"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/album_art"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/widget_content_desc_album_art" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:weightSum="1">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/text_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="@color/widget_title"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/subtitle_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="1dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:baselineAligned="false">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/album"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:gravity="end"
|
||||||
|
android:textAlignment="viewEnd"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:freezesText="true"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="3dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:indeterminate="false"
|
||||||
|
android:max="1000"
|
||||||
|
android:progress="0"
|
||||||
|
android:progressBackgroundTint="@color/widget_subtitle"
|
||||||
|
android:progressTint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/timing"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_elapsed"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/widget_time_elapsed_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="10sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time_total"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="end"
|
||||||
|
android:text="@string/widget_time_duration_placeholder"
|
||||||
|
android:textColor="@color/widget_subtitle"
|
||||||
|
android:textSize="10sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls_secondary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_shuffle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="1dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_shuffle"
|
||||||
|
android:src="@drawable/ic_shuffle"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/controls"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="3"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_prev"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="1dp"
|
||||||
|
android:layout_marginEnd="1dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_prev"
|
||||||
|
android:src="@drawable/ic_skip_previous"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_play_pause"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
android:layout_marginStart="1dp"
|
||||||
|
android:layout_marginEnd="1dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_play_pause"
|
||||||
|
android:src="@drawable/ic_play"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_next"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="1dp"
|
||||||
|
android:layout_marginEnd="1dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_next"
|
||||||
|
android:src="@drawable/ic_skip_next"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_repeat"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="1dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/widget_content_desc_repeat"
|
||||||
|
android:src="@drawable/ic_repeat"
|
||||||
|
android:tint="@color/widget_icon_tint" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user