diff --git a/.github/workflows/github_release.yml b/.github/workflows/github_release.yml
index 7591281f..d026f23a 100644
--- a/.github/workflows/github_release.yml
+++ b/.github/workflows/github_release.yml
@@ -3,7 +3,7 @@ name: Github Release Workflow
on:
push:
tags:
- - '[0-9]+.[0-9]+.[0-9]+'
+ - 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
build:
diff --git a/.gitignore b/.gitignore
index 1047677c..48ad5d8f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,5 @@
.vscode/settings.json
# release / debug files
tempus-release-key.jks
-app/tempo/
\ No newline at end of file
+app/tempo/
+app/notquitemy/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 02e4e59a..76c83d17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,25 @@
***This log is for this fork to detail updates since 3.9.0 from the main repo.***
+## [3.17.14](https://github.com/eddyizm/tempo/releases/tag/v3.17.14) (2025-10-16)
+## What's Changed
+* fix: General build warning and playback issues by @le-firehawk in https://github.com/eddyizm/tempo/pull/167
+* fix: persist album sort preference by @eddyizm in https://github.com/eddyizm/tempo/pull/168
+* Fix album parse empty date field by @eddyizm in https://github.com/eddyizm/tempo/pull/171
+* fix: Include shuffle/repeat controls in f-droid build's media notific… by @le-firehawk in https://github.com/eddyizm/tempo/pull/174
+* fix: limits image size to prevent widget crash #172 by @eddyizm in https://github.com/eddyizm/tempo/pull/175
+
+**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.17.0...v3.17.14
+
+## [3.17.0](https://github.com/eddyizm/tempo/releases/tag/v3.17.0) (2025-10-10)
+## What's Changed
+* chore: adding screenshot and docs for 4 icons/buttons in player control by @eddyizm in https://github.com/eddyizm/tempo/pull/162
+* Update Polish translation by @skajmer in https://github.com/eddyizm/tempo/pull/160
+* feat: Make all objects in Tempo references for quick access by @le-firehawk in https://github.com/eddyizm/tempo/pull/158
+* fix: Glide module incorrectly encoding IPv6 addresses by @le-firehawk in https://github.com/eddyizm/tempo/pull/159
+
+**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.16.6...v3.17.0
+
## [3.16.6](https://github.com/eddyizm/tempo/releases/tag/v3.16.6) (2025-10-08)
## What's Changed
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempo/pull/151
diff --git a/README.md b/README.md
index 44caa050..07bdc1ac 100644
--- a/README.md
+++ b/README.md
@@ -25,11 +25,21 @@ Tempo does not rely on magic algorithms to decide what you should listen to. Ins
## 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`
+`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.
-Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md)
+### 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](CHANGELOG.md)
Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
@@ -49,13 +59,11 @@ Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempo, expanding your audio entertainment options.
- **Transcoding Support**: Activate transcoding of tracks on your Subsonic server, allowing you to set a transcoding profile for optimized streaming directly from the app. This feature requires support from your Subsonic server.
- **Android Auto Support**: Enjoy your favorite music on the go with full Android Auto integration, allowing you to seamlessly control and listen to your tracks directly from your mobile device while driving.
+- **Multiple Libraries**: Tempo handles multi-library setups gracefully. They are displayed as Library folders.
-## Sponsors
+## Credits
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
@@ -90,6 +98,16 @@ Tempo is an open-source project developed and maintained solely by me. I would l
+## Contributing
+
+Please fork and open PR's against the development branch. Make sure your PR builds successfully.
+
+If there is an UI change, please include a before/after screenshot and a short video/gif if that helps elaborating the fix/feature in the PR.
+
+Currently there are no tests but I would love to start on some unit tests.
+
+Not a hard requirement but any new feature/change should ideally include an update to the nacent documention.
+
## 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.
diff --git a/USAGE.md b/USAGE.md
index 7cf5c25b..9ce9496b 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -57,10 +57,30 @@ This app works with any service that implements the Subsonic API, including:
## Main Features
### Library View
-**TODO**
+
+**Multi-library**
+
+Tempo handles multi-library setups gracefully. They are displayed as Library folders.
+
+However, if you want to limit or change libraries you could use a workaround, if your server supports it.
+
+You can create multiple users , one for each library, and save each of them in Tempo app.
### Now Playing Screen
-**TODO**
+
+On the main player control screen, tapping on the artwork will reveal a small collection of 4 buttons/icons.
+
+
+
+
+*marked the icons with numbers for clarity*
+
+1. Downloads the track (there is a notification if the android screen but not a pop toast currently )
+2. Adds track to playlist - pops up playlist dialog.
+3. Adds tracks to the queue via instant mix function
+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
diff --git a/app/build.gradle b/app/build.gradle
index 43bbc967..6d9e1884 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,8 +10,8 @@ android {
minSdkVersion 24
targetSdk 35
- versionCode 34
- versionName '3.16.6'
+ versionCode 36
+ versionName '3.17.14'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
javaCompileOptions {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 816683ca..b8d72d8b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -42,6 +42,16 @@
+
+
+
+
+
+
+
+ {
+ private static final int DEFAULT_TIMEOUT_MS = 2500;
+
+ @Override
+ public boolean handles(@NonNull String model) {
+ return model.startsWith("http://") || model.startsWith("https://");
+ }
+
+ @Override
+ public LoadData buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) {
+ if (!handles(model)) {
+ return null;
+ }
+ return new LoadData<>(new ObjectKey(model), new IPv6StreamFetcher(model));
+ }
+
+ private static class IPv6StreamFetcher implements DataFetcher {
+ private final String model;
+ private InputStream stream;
+ private HttpURLConnection connection;
+
+ IPv6StreamFetcher(String model) {
+ this.model = model;
+ }
+
+ @Override
+ public void loadData(@NonNull Priority priority, @NonNull DataCallback 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 getDataClass() {
+ return InputStream.class;
+ }
+
+ @NonNull
+ @Override
+ public DataSource getDataSource() {
+ return DataSource.REMOTE;
+ }
+ }
+
+ public static class Factory implements ModelLoaderFactory {
+ @NonNull
+ @Override
+ public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) {
+ return new IPv6StringLoader();
+ }
+
+ @Override
+ public void teardown() {
+ // No-op
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/Chronology.kt b/app/src/main/java/com/cappielloantonio/tempo/model/Chronology.kt
index 18a77bfa..a3b142e2 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/model/Chronology.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/model/Chronology.kt
@@ -8,18 +8,18 @@ import androidx.room.PrimaryKey
import com.cappielloantonio.tempo.subsonic.models.Child
import com.cappielloantonio.tempo.util.Preferences
import kotlinx.parcelize.Parcelize
-import java.util.*
+import java.util.Date
@Keep
@Parcelize
@Entity(tableName = "chronology")
-class Chronology(@PrimaryKey override val id: String) : Child(id) {
+class Chronology(
+ @PrimaryKey override val id: String,
@ColumnInfo(name = "timestamp")
- var timestamp: Long = System.currentTimeMillis()
-
+ var timestamp: Long = System.currentTimeMillis(),
@ColumnInfo(name = "server")
- var server: String? = null
-
+ var server: String? = null,
+) : Child(id) {
constructor(mediaItem: MediaItem) : this(mediaItem.mediaMetadata.extras!!.getString("id")!!) {
parentId = mediaItem.mediaMetadata.extras!!.getString("parentId")
isDir = mediaItem.mediaMetadata.extras!!.getBoolean("isDir")
diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/Download.kt b/app/src/main/java/com/cappielloantonio/tempo/model/Download.kt
index e5cc34b3..0c54e1cd 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/model/Download.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/model/Download.kt
@@ -10,19 +10,17 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
@Entity(tableName = "download")
-class Download(@PrimaryKey override val id: String) : Child(id) {
+class Download(
+ @PrimaryKey override val id: String,
@ColumnInfo(name = "playlist_id")
- var playlistId: String? = null
-
+ var playlistId: String? = null,
@ColumnInfo(name = "playlist_name")
- var playlistName: String? = null
-
+ var playlistName: String? = null,
@ColumnInfo(name = "download_state", defaultValue = "1")
- var downloadState: Int = 0
-
+ var downloadState: Int = 0,
@ColumnInfo(name = "download_uri", defaultValue = "")
- var downloadUri: String? = null
-
+ var downloadUri: String? = null,
+) : Child(id) {
constructor(child: Child) : this(child.id) {
parentId = child.parentId
isDir = child.isDir
@@ -62,5 +60,5 @@ class Download(@PrimaryKey override val id: String) : Child(id) {
@Keep
data class DownloadStack(
var id: String,
- var view: String?
+ var view: String?,
)
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/Queue.kt b/app/src/main/java/com/cappielloantonio/tempo/model/Queue.kt
index ca2300c2..87840178 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/model/Queue.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/model/Queue.kt
@@ -10,20 +10,18 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
@Entity(tableName = "queue")
-class Queue(override val id: String) : Child(id) {
+class Queue(
+ override val id: String,
@PrimaryKey
@ColumnInfo(name = "track_order")
- var trackOrder: Int = 0
-
+ var trackOrder: Int = 0,
@ColumnInfo(name = "last_play")
- var lastPlay: Long = 0
-
+ var lastPlay: Long = 0,
@ColumnInfo(name = "playing_changed")
- var playingChanged: Long = 0
-
+ var playingChanged: Long = 0,
@ColumnInfo(name = "stream_id")
- var streamId: String? = null
-
+ var streamId: String? = null,
+) : Child(id) {
constructor(child: Child) : this(child.id) {
parentId = child.parentId
isDir = child.isDir
diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java
index 6dc8d3e3..bcc358b5 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AlbumRepository.java
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.repository;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
+import android.util.Log;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.interfaces.DecadesCallback;
@@ -31,14 +32,22 @@ public class AlbumRepository {
.enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
- if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
+ if (response.isSuccessful()
+ && response.body() != null
+ && response.body().getSubsonicResponse().getAlbumList2() != null
+ && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
+
listLiveAlbums.setValue(response.body().getSubsonicResponse().getAlbumList2().getAlbums());
+ } else {
+ Log.e("AlbumRepository", "API Error on getAlbums. HTTP Code: " + response.code());
+ listLiveAlbums.setValue(new ArrayList<>());
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull Throwable t) {
-
+ Log.e("AlbumRepository", "Network Failure on getAlbums: " + t.getMessage());
+ listLiveAlbums.setValue(new ArrayList<>());
}
});
diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java
index f87cd577..66d0a185 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/repository/PlaylistRepository.java
@@ -80,6 +80,33 @@ public class PlaylistRepository {
return listLivePlaylistSongs;
}
+ public MutableLiveData getPlaylist(String id) {
+ MutableLiveData playlistLiveData = new MutableLiveData<>();
+
+ App.getSubsonicClientInstance(false)
+ .getPlaylistClient()
+ .getPlaylist(id)
+ .enqueue(new Callback() {
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) {
+ if (response.isSuccessful()
+ && response.body() != null
+ && response.body().getSubsonicResponse().getPlaylist() != null) {
+ playlistLiveData.setValue(response.body().getSubsonicResponse().getPlaylist());
+ } else {
+ playlistLiveData.setValue(null);
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull Throwable t) {
+ playlistLiveData.setValue(null);
+ }
+ });
+
+ return playlistLiveData;
+ }
+
public void addSongToPlaylist(String playlistId, ArrayList songsId) {
if (songsId.isEmpty()) {
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt
index f05238ce..3f9868bd 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/RetrofitClient.kt
@@ -2,22 +2,28 @@ package com.cappielloantonio.tempo.subsonic
import com.cappielloantonio.tempo.App
import com.cappielloantonio.tempo.subsonic.utils.CacheUtil
+import com.cappielloantonio.tempo.subsonic.utils.EmptyDateTypeAdapter
import com.google.gson.GsonBuilder
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
+import java.util.Date
import java.util.concurrent.TimeUnit
class RetrofitClient(subsonic: Subsonic) {
var retrofit: Retrofit
init {
+ val gson = GsonBuilder()
+ .registerTypeAdapter(Date::class.java, EmptyDateTypeAdapter())
+ .setLenient()
+ .create()
+
retrofit = Retrofit.Builder()
.baseUrl(subsonic.url)
- .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create()))
- .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
+ .addConverterFactory(GsonConverterFactory.create(gson))
.client(getOkHttpClient())
.build()
}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumID3.kt
index 3072a3a4..95c67da2 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumID3.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumID3.kt
@@ -4,38 +4,36 @@ import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
-import java.time.Instant
-import java.time.LocalDate
-import java.util.*
+import java.util.Date
@Keep
@Parcelize
-open class AlbumID3 : Parcelable {
- var id: String? = null
- var name: String? = null
- var artist: String? = null
- var artistId: String? = null
+open class AlbumID3(
+ var id: String? = null,
+ var name: String? = null,
+ var artist: String? = null,
+ var artistId: String? = null,
@SerializedName("coverArt")
- var coverArtId: String? = null
- var songCount: Int? = 0
- var duration: Int? = 0
- var playCount: Long? = 0
- var created: Date? = null
- var starred: Date? = null
- var year: Int = 0
- var genre: String? = null
- var played: Date? = Date(0)
- var userRating: Int? = 0
- var recordLabels: List? = null
- var musicBrainzId: String? = null
- var genres: List? = null
- var artists: List? = null
- var displayArtist: String? = null
- var releaseTypes: List? = null
- var moods: List? = null
- var sortName: String? = null
- var originalReleaseDate: ItemDate? = null
- var releaseDate: ItemDate? = null
- var isCompilation: Boolean? = null
- var discTitles: List? = null
-}
\ No newline at end of file
+ var coverArtId: String? = null,
+ var songCount: Int? = 0,
+ var duration: Int? = 0,
+ var playCount: Long? = 0,
+ var created: Date? = null,
+ var starred: Date? = null,
+ var year: Int = 0,
+ var genre: String? = null,
+ var played: Date? = Date(0),
+ var userRating: Int? = 0,
+ var recordLabels: List? = null,
+ var musicBrainzId: String? = null,
+ var genres: List? = null,
+ var artists: List? = null,
+ var displayArtist: String? = null,
+ var releaseTypes: List? = null,
+ var moods: List? = null,
+ var sortName: String? = null,
+ var originalReleaseDate: ItemDate? = null,
+ var releaseDate: ItemDate? = null,
+ var isCompilation: Boolean? = null,
+ var discTitles: List? = null,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumWithSongsID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumWithSongsID3.kt
index 79c56092..8498e777 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumWithSongsID3.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/AlbumWithSongsID3.kt
@@ -7,7 +7,7 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
-class AlbumWithSongsID3 : AlbumID3(), Parcelable {
+class AlbumWithSongsID3(
@SerializedName("song")
- var songs: List? = null
-}
\ No newline at end of file
+ var songs: List? = null,
+) : AlbumID3(), Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Artist.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Artist.kt
index a13d169d..22aa527b 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Artist.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Artist.kt
@@ -7,10 +7,10 @@ import java.util.Date
@Keep
@Parcelize
-class Artist : Parcelable {
- var id: String? = null
- var name: String? = null
- var starred: Date? = null
- var userRating: Int? = null
- var averageRating: Double? = null
-}
\ No newline at end of file
+class Artist(
+ var id: String? = null,
+ var name: String? = null,
+ var starred: Date? = null,
+ var userRating: Int? = null,
+ var averageRating: Double? = null,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistID3.kt
index a17f4aa3..ccf4ee7e 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistID3.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistID3.kt
@@ -4,15 +4,15 @@ import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
-import java.util.*
+import java.util.Date
@Keep
@Parcelize
-open class ArtistID3 : Parcelable {
- var id: String? = null
- var name: String? = null
+open class ArtistID3(
+ var id: String? = null,
+ var name: String? = null,
@SerializedName("coverArt")
- var coverArtId: String? = null
- var albumCount = 0
- var starred: Date? = null
-}
\ No newline at end of file
+ var coverArtId: String? = null,
+ var albumCount: Int = 0,
+ var starred: Date? = null,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt
index c22c8207..2e21e111 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt
@@ -7,7 +7,7 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
-class ArtistWithAlbumsID3 : ArtistID3(), Parcelable {
+class ArtistWithAlbumsID3(
@SerializedName("album")
- var albums: List? = null
-}
\ No newline at end of file
+ var albums: List? = null,
+) : ArtistID3(), Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Directory.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Directory.kt
index 45395ede..f189589e 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Directory.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Directory.kt
@@ -8,15 +8,15 @@ import java.util.Date
@Keep
@Parcelize
-class Directory : Parcelable {
+class Directory(
@SerializedName("child")
- var children: List? = null
- var id: String? = null
+ var children: List? = null,
+ var id: String? = null,
@SerializedName("parent")
- var parentId: String? = null
- var name: String? = null
- var starred: Date? = null
- var userRating: Int? = null
- var averageRating: Double? = null
- var playCount: Long? = null
-}
\ No newline at end of file
+ var parentId: String? = null,
+ var name: String? = null,
+ var starred: Date? = null,
+ var userRating: Int? = null,
+ var averageRating: Double? = null,
+ var playCount: Long? = null,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/DiscTitle.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/DiscTitle.kt
index 2910d4bf..32caa8cc 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/DiscTitle.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/DiscTitle.kt
@@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
-open class DiscTitle : Parcelable {
- var disc: Int? = null
- var title: String? = null
-}
\ No newline at end of file
+open class DiscTitle(
+ var disc: Int? = null,
+ var title: String? = null,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Genre.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Genre.kt
index cb1b7719..1db88c40 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Genre.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Genre.kt
@@ -7,9 +7,9 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
-class Genre : Parcelable {
+class Genre(
@SerializedName("value")
- var genre: String? = null
- var songCount = 0
- var albumCount = 0
-}
\ No newline at end of file
+ var genre: String? = null,
+ var songCount: Int = 0,
+ var albumCount: Int = 0,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStation.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStation.kt
index 0d312ce0..07f00c5b 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStation.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/InternetRadioStation.kt
@@ -6,9 +6,9 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
-class InternetRadioStation : Parcelable {
- var id: String? = null
- var name: String? = null
- var streamUrl: String? = null
- var homePageUrl: String? = null
-}
\ No newline at end of file
+class InternetRadioStation(
+ var id: String? = null,
+ var name: String? = null,
+ var streamUrl: String? = null,
+ var homePageUrl: String? = null,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemDate.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemDate.kt
index 5de2fbc6..385b7fd7 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemDate.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemDate.kt
@@ -9,11 +9,11 @@ import java.util.Locale
@Keep
@Parcelize
-open class ItemDate : Parcelable {
- var year: Int? = null
- var month: Int? = null
- var day: Int? = null
-
+open class ItemDate(
+ var year: Int? = null,
+ var month: Int? = null,
+ var day: Int? = null,
+) : Parcelable {
fun getFormattedDate(): String? {
if (year == null && month == null && day == null) return null
@@ -22,8 +22,7 @@ open class ItemDate : Parcelable {
SimpleDateFormat("yyyy", Locale.getDefault())
} else if (day == null) {
SimpleDateFormat("MMMM yyyy", Locale.getDefault())
- }
- else{
+ } else {
SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault())
}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemGenre.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemGenre.kt
index ce164fb6..971809ff 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemGenre.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ItemGenre.kt
@@ -6,6 +6,6 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
-open class ItemGenre : Parcelable {
- var name: String? = null
-}
\ No newline at end of file
+open class ItemGenre(
+ var name: String? = null,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MusicFolder.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MusicFolder.kt
index 7c277df4..e31bf7c8 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MusicFolder.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/MusicFolder.kt
@@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
-class MusicFolder : Parcelable {
- var id: String? = null
- var name: String? = null
-}
\ No newline at end of file
+class MusicFolder(
+ var id: String? = null,
+ var name: String? = null,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NowPlayingEntry.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NowPlayingEntry.kt
index e5391872..bd69808c 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NowPlayingEntry.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/NowPlayingEntry.kt
@@ -8,10 +8,9 @@ import kotlinx.parcelize.Parcelize
@Parcelize
class NowPlayingEntry(
@SerializedName("_id")
- override val id: String
-) : Child(id) {
- var username: String? = null
- var minutesAgo = 0
- var playerId = 0
- var playerName: String? = null
-}
\ No newline at end of file
+ override val id: String,
+ var username: String? = null,
+ var minutesAgo: Int = 0,
+ var playerId: Int = 0,
+ var playerName: String? = null,
+) : Child(id)
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlist.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlist.kt
index 2d85271e..926c2390 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlist.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlist.kt
@@ -7,8 +7,9 @@ import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
-import java.util.*
+import java.util.Date
@Keep
@Parcelize
@@ -16,27 +17,56 @@ import java.util.*
open class Playlist(
@PrimaryKey
@ColumnInfo(name = "id")
- open var id: String
-) : Parcelable {
+ open var id: String,
@ColumnInfo(name = "name")
- var name: String? = null
+ var name: String? = null,
+ @ColumnInfo(name = "duration")
+ var duration: Long = 0,
+ @ColumnInfo(name = "coverArt")
+ var coverArtId: String? = null,
+) : Parcelable {
@Ignore
+ @IgnoredOnParcel
var comment: String? = null
@Ignore
+ @IgnoredOnParcel
var owner: String? = null
@Ignore
+ @IgnoredOnParcel
@SerializedName("public")
var isUniversal: Boolean? = null
@Ignore
+ @IgnoredOnParcel
var songCount: Int = 0
- @ColumnInfo(name = "duration")
- var duration: Long = 0
@Ignore
+ @IgnoredOnParcel
var created: Date? = null
@Ignore
+ @IgnoredOnParcel
var changed: Date? = null
- @ColumnInfo(name = "coverArt")
- var coverArtId: String? = null
@Ignore
+ @IgnoredOnParcel
var allowedUsers: List? = null
+ @Ignore
+ constructor(
+ id: String,
+ name: String?,
+ comment: String?,
+ owner: String?,
+ isUniversal: Boolean?,
+ songCount: Int,
+ duration: Long,
+ created: Date?,
+ changed: Date?,
+ coverArtId: String?,
+ allowedUsers: List?,
+ ) : this(id, name, duration, coverArtId) {
+ this.comment = comment
+ this.owner = owner
+ this.isUniversal = isUniversal
+ this.songCount = songCount
+ this.created = created
+ this.changed = changed
+ this.allowedUsers = allowedUsers
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PlaylistWithSongs.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PlaylistWithSongs.kt
index 92e194c8..350dcbd4 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PlaylistWithSongs.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PlaylistWithSongs.kt
@@ -9,8 +9,7 @@ import kotlinx.parcelize.Parcelize
@Parcelize
class PlaylistWithSongs(
@SerializedName("_id")
- override var id: String
-) : Playlist(id), Parcelable {
+ override var id: String,
@SerializedName("entry")
- var entries: List? = null
-}
\ No newline at end of file
+ var entries: List? = null,
+) : Playlist(id), Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlists.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlists.kt
index 8aa271a4..34079c76 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlists.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Playlists.kt
@@ -6,5 +6,5 @@ import com.google.gson.annotations.SerializedName
@Keep
class Playlists(
@SerializedName("playlist")
- var playlists: List? = null
+ var playlists: List? = null,
)
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastChannel.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastChannel.kt
index 088ed97e..b4d124ff 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastChannel.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastChannel.kt
@@ -3,20 +3,21 @@ package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
-class PodcastChannel : Parcelable {
+class PodcastChannel(
@SerializedName("episode")
- var episodes: List? = null
- var id: String? = null
- var url: String? = null
- var title: String? = null
- var description: String? = null
+ var episodes: List? = null,
+ var id: String? = null,
+ var url: String? = null,
+ var title: String? = null,
+ var description: String? = null,
@SerializedName("coverArt")
- var coverArtId: String? = null
- var originalImageUrl: String? = null
- var status: String? = null
- var errorMessage: String? = null
-}
\ No newline at end of file
+ var coverArtId: String? = null,
+ var originalImageUrl: String? = null,
+ var status: String? = null,
+ var errorMessage: String? = null,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastEpisode.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastEpisode.kt
index f8893224..fc3fab21 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastEpisode.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/PodcastEpisode.kt
@@ -3,37 +3,38 @@ package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.util.*
@Keep
@Parcelize
-class PodcastEpisode : Parcelable {
- var id: String? = null
+class PodcastEpisode(
+ var id: String? = null,
@SerializedName("parent")
- var parentId: String? = null
- var isDir = false
- var title: String? = null
- var album: String? = null
- var artist: String? = null
- var year: Int? = null
- var genre: String? = null
+ var parentId: String? = null,
+ var isDir: Boolean = false,
+ var title: String? = null,
+ var album: String? = null,
+ var artist: String? = null,
+ var year: Int? = null,
+ var genre: String? = null,
@SerializedName("coverArt")
- var coverArtId: String? = null
- var size: Long? = null
- var contentType: String? = null
- var suffix: String? = null
- var duration: Int? = null
+ var coverArtId: String? = null,
+ var size: Long? = null,
+ var contentType: String? = null,
+ var suffix: String? = null,
+ var duration: Int? = null,
@SerializedName("bitRate")
- var bitrate: Int? = null
- var path: String? = null
- var isVideo: Boolean = false
- var created: Date? = null
- var artistId: String? = null
- var type: String? = null
- var streamId: String? = null
- var channelId: String? = null
- var description: String? = null
- var status: String? = null
- var publishDate: Date? = null
-}
\ No newline at end of file
+ var bitrate: Int? = null,
+ var path: String? = null,
+ var isVideo: Boolean = false,
+ var created: Date? = null,
+ var artistId: String? = null,
+ var type: String? = null,
+ var streamId: String? = null,
+ var channelId: String? = null,
+ var description: String? = null,
+ var status: String? = null,
+ var publishDate: Date? = null,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/RecordLabel.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/RecordLabel.kt
index 687f3fd7..52531d90 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/RecordLabel.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/RecordLabel.kt
@@ -2,10 +2,11 @@ package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
+import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
-open class RecordLabel : Parcelable {
- var name: String? = null
-}
\ No newline at end of file
+open class RecordLabel(
+ var name: String? = null,
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Share.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Share.kt
index 193cefc8..986e4b50 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Share.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/Share.kt
@@ -3,20 +3,21 @@ package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
-import java.util.*
+import java.util.Date
@Keep
@Parcelize
-class Share : Parcelable {
+data class Share(
@SerializedName("entry")
- var entries: List? = null
- var id: String? = null
- var url: String? = null
- var description: String? = null
- var username: String? = null
- var created: Date? = null
- var expires: Date? = null
- var lastVisited: Date? = null
- var visitCount = 0
-}
\ No newline at end of file
+ var entries: List? = null,
+ var id: String? = null,
+ var url: String? = null,
+ var description: String? = null,
+ var username: String? = null,
+ var created: Date? = null,
+ var expires: Date? = null,
+ var lastVisited: Date? = null,
+ var visitCount: Int = 0
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/CacheUtil.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/CacheUtil.java
index 047b0010..69dc9dd4 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/CacheUtil.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/CacheUtil.java
@@ -38,21 +38,36 @@ public class CacheUtil {
return chain.proceed(request);
};
+
private boolean isConnected() {
ConnectivityManager connectivityManager = (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-
- if (connectivityManager != null) {
- Network network = connectivityManager.getActiveNetwork();
-
- if (network != null) {
- NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
-
- if (capabilities != null) {
- return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
- }
- }
+ if (connectivityManager == null) {
+ return false;
}
- return false;
+ Network network = connectivityManager.getActiveNetwork();
+ if (network == null) {
+ return false;
+ }
+
+ NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
+ if (capabilities == null) {
+ return false;
+ }
+
+ boolean hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ if (!hasInternet) {
+ return false;
+ }
+
+ boolean hasAppropriateTransport = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
+ || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+ || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
+ if (!hasAppropriateTransport) {
+ return false;
+ }
+
+ return true;
}
+
}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/EmptyDateTypeAdapter.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/EmptyDateTypeAdapter.kt
new file mode 100644
index 00000000..bcdd5ee8
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/utils/EmptyDateTypeAdapter.kt
@@ -0,0 +1,42 @@
+package com.cappielloantonio.tempo.subsonic.utils
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonParseException
+import java.lang.reflect.Type
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+// This adapter handles Date objects, returning null if the JSON string is empty or unparsable.
+class EmptyDateTypeAdapter : JsonDeserializer {
+
+ // Define the date formats expected from the Subsonic server.
+ private val dateFormats: List = listOf(
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") },
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") },
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }
+ )
+
+ @Throws(JsonParseException::class)
+ override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Date? {
+ val jsonString = json.asString.trim()
+
+ if (jsonString.isEmpty()) {
+ return null
+ }
+
+ for (format in dateFormats) {
+ try {
+ return format.parse(jsonString)
+ } catch (e: ParseException) {
+ // Ignore and try the next format
+ }
+ }
+
+ return null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java
index db76b98d..9aa558f8 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java
@@ -37,6 +37,8 @@ import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog;
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment;
+import com.cappielloantonio.tempo.util.AssetLinkNavigator;
+import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
@@ -60,6 +62,8 @@ public class MainActivity extends BaseActivity {
private BottomNavigationView bottomNavigationView;
public NavController navController;
private BottomSheetBehavior bottomSheetBehavior;
+ private AssetLinkNavigator assetLinkNavigator;
+ private AssetLinkUtil.AssetLink pendingAssetLink;
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
private Intent pendingDownloadPlaybackIntent;
@@ -76,6 +80,7 @@ public class MainActivity extends BaseActivity {
setContentView(view);
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
+ assetLinkNavigator = new AssetLinkNavigator(this);
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
connectivityStatusReceiverManager(true);
@@ -311,6 +316,24 @@ public class MainActivity extends BaseActivity {
public void goFromLogin() {
setBottomSheetInPeek(mainViewModel.isQueueLoaded());
goToHome();
+ consumePendingAssetLink();
+ }
+
+ public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink) {
+ openAssetLink(assetLink, true);
+ }
+
+ public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink, boolean collapsePlayer) {
+ if (!isUserAuthenticated()) {
+ pendingAssetLink = assetLink;
+ return;
+ }
+ if (collapsePlayer) {
+ setBottomSheetInPeek(true);
+ }
+ if (assetLinkNavigator != null) {
+ assetLinkNavigator.open(assetLink);
+ }
}
public void quit() {
@@ -443,6 +466,7 @@ public class MainActivity extends BaseActivity {
|| intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) {
pendingDownloadPlaybackIntent = new Intent(intent);
}
+ handleAssetLinkIntent(intent);
}
private void consumePendingPlaybackIntent() {
@@ -452,6 +476,35 @@ public class MainActivity extends BaseActivity {
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)) {
@@ -500,4 +553,4 @@ public class MainActivity extends BaseActivity {
MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem);
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java
index 1f360c80..69583a40 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCatalogueAdapter.java
@@ -20,6 +20,7 @@ import com.cappielloantonio.tempo.util.MusicUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.Date;
import java.util.List;
public class AlbumCatalogueAdapter extends RecyclerView.Adapter implements Filterable {
@@ -152,12 +153,20 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter album.getName() != null ? album.getName() : "",
+ String.CASE_INSENSITIVE_ORDER
+ ));
break;
case Constants.ALBUM_ORDER_BY_ARTIST:
- albums.sort(Comparator.comparing(AlbumID3::getArtist, Comparator.nullsLast(Comparator.naturalOrder())));
+ albums.sort(Comparator.comparing(
+ album -> album.getArtist() != null ? album.getArtist() : "",
+ String.CASE_INSENSITIVE_ORDER
+ ));
break;
case Constants.ALBUM_ORDER_BY_YEAR:
albums.sort(Comparator.comparing(AlbumID3::getYear));
@@ -166,15 +175,23 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter album.getCreated() != null ? album.getCreated() : new Date(0),
+ Comparator.nullsLast(Date::compareTo)
+ ));
Collections.reverse(albums);
break;
case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED:
- albums.sort(Comparator.comparing(AlbumID3::getPlayed));
+ albums.sort(Comparator.comparing(
+ album -> album.getPlayed() != null ? album.getPlayed() : new Date(0),
+ Comparator.nullsLast(Date::compareTo)
+ ));
Collections.reverse(albums);
break;
case Constants.ALBUM_ORDER_BY_MOST_PLAYED:
- albums.sort(Comparator.comparing(AlbumID3::getPlayCount));
+ albums.sort(Comparator.comparing(
+ album -> album.getPlayCount() != null ? album.getPlayCount() : 0L
+ ));
Collections.reverse(albums);
break;
}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/TrackInfoDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/TrackInfoDialog.java
index f866c250..e6b91f01 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/TrackInfoDialog.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/TrackInfoDialog.java
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.os.Bundle;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
@@ -10,6 +11,7 @@ import androidx.media3.common.MediaMetadata;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogTrackInfoBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
+import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
@@ -21,6 +23,11 @@ public class TrackInfoDialog extends DialogFragment {
private DialogTrackInfoBinding bind;
private final MediaMetadata mediaMetadata;
+ private AssetLinkUtil.AssetLink songLink;
+ private AssetLinkUtil.AssetLink albumLink;
+ private AssetLinkUtil.AssetLink artistLink;
+ private AssetLinkUtil.AssetLink genreLink;
+ private AssetLinkUtil.AssetLink yearLink;
public TrackInfoDialog(MediaMetadata mediaMetadata) {
this.mediaMetadata = mediaMetadata;
@@ -52,6 +59,8 @@ public class TrackInfoDialog extends DialogFragment {
}
private void setTrackInfo() {
+ genreLink = null;
+ yearLink = null;
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
bind.trakArtistInfoTextView.setText(
mediaMetadata.artist != null
@@ -61,17 +70,41 @@ public class TrackInfoDialog extends DialogFragment {
: "");
if (mediaMetadata.extras != null) {
+ songLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_SONG, mediaMetadata.extras.getString("id"));
+ albumLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ALBUM, mediaMetadata.extras.getString("albumId"));
+ artistLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ARTIST, mediaMetadata.extras.getString("artistId"));
+ genreLink = AssetLinkUtil.parseLinkString(mediaMetadata.extras.getString("assetLinkGenre"));
+ yearLink = AssetLinkUtil.parseLinkString(mediaMetadata.extras.getString("assetLinkYear"));
+
CustomGlideRequest.Builder
.from(requireContext(), mediaMetadata.extras.getString("coverArtId", ""), CustomGlideRequest.ResourceType.Song)
.build()
.into(bind.trackCoverInfoImageView);
- bind.titleValueSector.setText(mediaMetadata.extras.getString("title", getString(R.string.label_placeholder)));
- bind.albumValueSector.setText(mediaMetadata.extras.getString("album", getString(R.string.label_placeholder)));
- bind.artistValueSector.setText(mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder)));
+ bindAssetLink(bind.trackCoverInfoImageView, albumLink != null ? albumLink : songLink);
+ bindAssetLink(bind.trakTitleInfoTextView, songLink);
+ bindAssetLink(bind.trakArtistInfoTextView, artistLink != null ? artistLink : songLink);
+
+ String titleValue = mediaMetadata.extras.getString("title", getString(R.string.label_placeholder));
+ String albumValue = mediaMetadata.extras.getString("album", getString(R.string.label_placeholder));
+ String artistValue = mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder));
+ String genreValue = mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder));
+ int yearValue = mediaMetadata.extras.getInt("year", 0);
+
+ if (genreLink == null && genreValue != null && !genreValue.isEmpty() && !getString(R.string.label_placeholder).contentEquals(genreValue)) {
+ genreLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_GENRE, genreValue);
+ }
+
+ if (yearLink == null && yearValue != 0) {
+ yearLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(yearValue));
+ }
+
+ bind.titleValueSector.setText(titleValue);
+ bind.albumValueSector.setText(albumValue);
+ bind.artistValueSector.setText(artistValue);
bind.trackNumberValueSector.setText(mediaMetadata.extras.getInt("track", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("track", 0)) : getString(R.string.label_placeholder));
- bind.yearValueSector.setText(mediaMetadata.extras.getInt("year", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("year", 0)) : getString(R.string.label_placeholder));
- bind.genreValueSector.setText(mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder)));
+ bind.yearValueSector.setText(yearValue != 0 ? String.valueOf(yearValue) : getString(R.string.label_placeholder));
+ bind.genreValueSector.setText(genreValue);
bind.sizeValueSector.setText(mediaMetadata.extras.getLong("size", 0) != 0 ? MusicUtil.getReadableByteCount(mediaMetadata.extras.getLong("size", 0)) : getString(R.string.label_placeholder));
bind.contentTypeValueSector.setText(mediaMetadata.extras.getString("contentType", getString(R.string.label_placeholder)));
bind.suffixValueSector.setText(mediaMetadata.extras.getString("suffix", getString(R.string.label_placeholder)));
@@ -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.pathValueSector.setText(mediaMetadata.extras.getString("path", getString(R.string.label_placeholder)));
bind.discNumberValueSector.setText(mediaMetadata.extras.getInt("discNumber", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("discNumber", 0)) : getString(R.string.label_placeholder));
+
+ bindAssetLink(bind.titleValueSector, songLink);
+ bindAssetLink(bind.albumValueSector, albumLink);
+ bindAssetLink(bind.artistValueSector, artistLink);
+ bindAssetLink(bind.genreValueSector, genreLink);
+ bindAssetLink(bind.yearValueSector, yearLink);
}
}
@@ -135,4 +174,31 @@ public class TrackInfoDialog extends DialogFragment {
bind.trakTranscodingInfoTextView.setText(info);
}
}
+
+ private void bindAssetLink(android.view.View view, AssetLinkUtil.AssetLink assetLink) {
+ if (view == null) return;
+ if (assetLink == null) {
+ AssetLinkUtil.clearLinkAppearance(view);
+ view.setOnClickListener(null);
+ view.setOnLongClickListener(null);
+ view.setClickable(false);
+ view.setLongClickable(false);
+ return;
+ }
+
+ view.setClickable(true);
+ view.setLongClickable(true);
+ AssetLinkUtil.applyLinkAppearance(view);
+ view.setOnClickListener(v -> {
+ dismissAllowingStateLoss();
+ boolean collapse = !AssetLinkUtil.TYPE_SONG.equals(assetLink.type);
+ ((com.cappielloantonio.tempo.ui.activity.MainActivity) requireActivity()).openAssetLink(assetLink, collapse);
+ });
+ view.setOnLongClickListener(v -> {
+ AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
+ Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show();
+ return true;
+ });
+ }
+
}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumCatalogueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumCatalogueFragment.java
index bb62e939..4061cccd 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumCatalogueFragment.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumCatalogueFragment.java
@@ -32,17 +32,19 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
import com.cappielloantonio.tempo.util.Constants;
+import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.AlbumCatalogueViewModel;
@OptIn(markerClass = UnstableApi.class)
public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
- private static final String TAG = "ArtistCatalogueFragment";
+ private static final String TAG = "AlbumCatalogueFragment";
private FragmentAlbumCatalogueBinding bind;
private MainActivity activity;
private AlbumCatalogueViewModel albumCatalogueViewModel;
private AlbumCatalogueAdapter albumAdapter;
+ private String currentSortOrder;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -115,7 +117,10 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
albumAdapter = new AlbumCatalogueAdapter(this, true);
albumAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
bind.albumCatalogueRecyclerView.setAdapter(albumAdapter);
- albumCatalogueViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> albumAdapter.setItems(albums));
+ albumCatalogueViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> {
+ albumAdapter.setItems(albums);
+ applySavedSortOrder();
+ });
bind.albumCatalogueRecyclerView.setOnTouchListener((v, event) -> {
hideKeyboard(v);
@@ -137,6 +142,44 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
});
}
+ private void applySavedSortOrder() {
+ String savedSortOrder = Preferences.getAlbumSortOrder();
+ currentSortOrder = savedSortOrder;
+ albumAdapter.sort(savedSortOrder);
+ updateSortIndicator();
+ }
+
+ private void updateSortIndicator() {
+ if (bind == null) return;
+
+ String sortText = getSortDisplayText(currentSortOrder);
+ bind.albumListSortTextView.setText(sortText);
+ bind.albumListSortTextView.setVisibility(View.VISIBLE);
+ }
+
+ private String getSortDisplayText(String sortOrder) {
+ if (sortOrder == null) return "";
+
+ switch (sortOrder) {
+ case Constants.ALBUM_ORDER_BY_NAME:
+ return getString(R.string.menu_sort_name);
+ case Constants.ALBUM_ORDER_BY_ARTIST:
+ return getString(R.string.menu_group_by_artist);
+ case Constants.ALBUM_ORDER_BY_YEAR:
+ return getString(R.string.menu_sort_year);
+ case Constants.ALBUM_ORDER_BY_RANDOM:
+ return getString(R.string.menu_sort_random);
+ case Constants.ALBUM_ORDER_BY_RECENTLY_ADDED:
+ return getString(R.string.menu_sort_recently_added);
+ case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED:
+ return getString(R.string.menu_sort_recently_played);
+ case Constants.ALBUM_ORDER_BY_MOST_PLAYED:
+ return getString(R.string.menu_sort_most_played);
+ default:
+ return "";
+ }
+ }
+
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.toolbar_menu, menu);
@@ -172,26 +215,29 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
popup.getMenuInflater().inflate(menuResource, popup.getMenu());
popup.setOnMenuItemClickListener(menuItem -> {
+ String newSortOrder = null;
+
if (menuItem.getItemId() == R.id.menu_album_sort_name) {
- albumAdapter.sort(Constants.ALBUM_ORDER_BY_NAME);
- return true;
+ newSortOrder = Constants.ALBUM_ORDER_BY_NAME;
} else if (menuItem.getItemId() == R.id.menu_album_sort_artist) {
- albumAdapter.sort(Constants.ALBUM_ORDER_BY_ARTIST);
- return true;
+ newSortOrder = Constants.ALBUM_ORDER_BY_ARTIST;
} else if (menuItem.getItemId() == R.id.menu_album_sort_year) {
- albumAdapter.sort(Constants.ALBUM_ORDER_BY_YEAR);
- return true;
+ newSortOrder = Constants.ALBUM_ORDER_BY_YEAR;
} else if (menuItem.getItemId() == R.id.menu_album_sort_random) {
- albumAdapter.sort(Constants.ALBUM_ORDER_BY_RANDOM);
- return true;
+ newSortOrder = Constants.ALBUM_ORDER_BY_RANDOM;
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_added) {
- albumAdapter.sort(Constants.ALBUM_ORDER_BY_RECENTLY_ADDED);
- return true;
+ newSortOrder = Constants.ALBUM_ORDER_BY_RECENTLY_ADDED;
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_played) {
- albumAdapter.sort(Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED);
- return true;
+ newSortOrder = Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED;
} else if (menuItem.getItemId() == R.id.menu_album_sort_most_played) {
- albumAdapter.sort(Constants.ALBUM_ORDER_BY_MOST_PLAYED);
+ newSortOrder = Constants.ALBUM_ORDER_BY_MOST_PLAYED;
+ }
+
+ if (newSortOrder != null) {
+ currentSortOrder = newSortOrder;
+ albumAdapter.sort(newSortOrder);
+ Preferences.setAlbumSortOrder(newSortOrder);
+ updateSortIndicator();
return true;
}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java
index d8fe82cc..9bf95803 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumPageFragment.java
@@ -35,6 +35,7 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
+import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
@@ -177,8 +178,35 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
bind.albumNameLabel.setText(album.getName());
bind.albumArtistLabel.setText(album.getArtist());
+ AssetLinkUtil.applyLinkAppearance(bind.albumArtistLabel);
+ AssetLinkUtil.AssetLink artistLink = buildArtistLink(album);
+ bind.albumArtistLabel.setOnLongClickListener(v -> {
+ if (artistLink != null) {
+ AssetLinkUtil.copyToClipboard(requireContext(), artistLink);
+ Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, artistLink.id), Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ return false;
+ });
bind.albumReleaseYearLabel.setText(album.getYear() != 0 ? String.valueOf(album.getYear()) : "");
- 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));
if (album.getGenre() != null && !album.getGenre().isEmpty()) {
bind.albumGenresTextview.setText(album.getGenre());
@@ -351,4 +379,23 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
private void setMediaBrowserListenableFuture() {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
}
-}
\ No newline at end of file
+
+ private void openYearLink(int year) {
+ AssetLinkUtil.AssetLink link = buildYearLink(year);
+ if (link != null) {
+ activity.openAssetLink(link);
+ }
+ }
+
+ private AssetLinkUtil.AssetLink buildYearLink(int year) {
+ if (year <= 0) return null;
+ return AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year));
+ }
+
+ private AssetLinkUtil.AssetLink buildArtistLink(AlbumID3 album) {
+ if (album == null || album.getArtistId() == null || album.getArtistId().isEmpty()) {
+ return null;
+ }
+ return AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ARTIST, album.getArtistId());
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java
index 6841f247..e2bca343 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java
@@ -195,6 +195,7 @@ public class PlayerBottomSheetFragment extends Fragment {
}
}
+
private void setMediaControllerUI(MediaBrowser mediaBrowser) {
if (mediaBrowser.getMediaMetadata().extras != null) {
switch (mediaBrowser.getMediaMetadata().extras.getString("type", Constants.MEDIA_TYPE_MUSIC)) {
diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java
index e63436c7..e3155b56 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java
@@ -13,9 +13,10 @@ import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
+import android.widget.RatingBar;
import android.widget.TextView;
import android.widget.ToggleButton;
-import android.widget.RatingBar;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
@@ -41,12 +42,14 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager;
+import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
import com.cappielloantonio.tempo.viewmodel.RatingViewModel;
import com.google.android.material.chip.Chip;
+import com.google.android.material.chip.ChipGroup;
import com.google.android.material.elevation.SurfaceColors;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@@ -76,6 +79,10 @@ public class PlayerControllerFragment extends Fragment {
private ImageButton playerTrackInfo;
private LinearLayout ratingContainer;
private ImageButton equalizerButton;
+ private ChipGroup assetLinkChipGroup;
+ private Chip playerSongLinkChip;
+ private Chip playerAlbumLinkChip;
+ private Chip playerArtistLinkChip;
private MainActivity activity;
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
@@ -139,6 +146,10 @@ public class PlayerControllerFragment extends Fragment {
songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar);
ratingContainer = bind.getRoot().findViewById(R.id.rating_container);
equalizerButton = bind.getRoot().findViewById(R.id.player_open_equalizer_button);
+ assetLinkChipGroup = bind.getRoot().findViewById(R.id.asset_link_chip_group);
+ playerSongLinkChip = bind.getRoot().findViewById(R.id.asset_link_song_chip);
+ playerAlbumLinkChip = bind.getRoot().findViewById(R.id.asset_link_album_chip);
+ playerArtistLinkChip = bind.getRoot().findViewById(R.id.asset_link_artist_chip);
checkAndSetRatingContainerVisibility();
}
@@ -219,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
? View.VISIBLE
: View.GONE);
+
+ updateAssetLinkChips(mediaMetadata);
}
private void setMediaInfo(MediaMetadata mediaMetadata) {
@@ -259,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) {
initPlaybackSpeedButton(mediaBrowser);
@@ -548,4 +665,4 @@ public class PlayerControllerFragment extends Fragment {
isServiceBound = false;
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java
index 90e32793..39ba4394 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/SongBottomSheetDialog.java
@@ -30,6 +30,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
+import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
@@ -39,6 +40,8 @@ import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import com.google.android.material.chip.Chip;
+import com.google.android.material.chip.ChipGroup;
import com.google.common.util.concurrent.ListenableFuture;
import android.content.Intent;
@@ -56,6 +59,13 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
private TextView downloadButton;
private TextView removeButton;
+ private ChipGroup assetLinkChipGroup;
+ private Chip songLinkChip;
+ private Chip albumLinkChip;
+ private Chip artistLinkChip;
+ private AssetLinkUtil.AssetLink currentSongLink;
+ private AssetLinkUtil.AssetLink currentAlbumLink;
+ private AssetLinkUtil.AssetLink currentArtistLink;
private ListenableFuture mediaBrowserListenableFuture;
@@ -109,6 +119,11 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
TextView artistSong = view.findViewById(R.id.song_artist_text_view);
artistSong.setText(songBottomSheetViewModel.getSong().getArtist());
+ initAssetLinkChips(view);
+ bindAssetLinkView(coverSong, currentSongLink);
+ bindAssetLinkView(titleSong, currentSongLink);
+ bindAssetLinkView(artistSong, currentArtistLink != null ? currentArtistLink : currentSongLink);
+
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null);
favoriteToggle.setOnClickListener(v -> {
@@ -282,6 +297,95 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
}
}
+ private void initAssetLinkChips(View root) {
+ assetLinkChipGroup = root.findViewById(R.id.asset_link_chip_group);
+ songLinkChip = root.findViewById(R.id.asset_link_song_chip);
+ albumLinkChip = root.findViewById(R.id.asset_link_album_chip);
+ artistLinkChip = root.findViewById(R.id.asset_link_artist_chip);
+
+ currentSongLink = bindAssetLinkChip(songLinkChip, AssetLinkUtil.TYPE_SONG, song.getId());
+ currentAlbumLink = bindAssetLinkChip(albumLinkChip, AssetLinkUtil.TYPE_ALBUM, song.getAlbumId());
+ currentArtistLink = bindAssetLinkChip(artistLinkChip, AssetLinkUtil.TYPE_ARTIST, song.getArtistId());
+ syncAssetLinkGroupVisibility();
+ }
+
+ private AssetLinkUtil.AssetLink bindAssetLinkChip(@Nullable Chip chip, String type, @Nullable String id) {
+ if (chip == null) return null;
+ if (id == null || id.isEmpty()) {
+ clearAssetLinkChip(chip);
+ return null;
+ }
+
+ String label = getString(AssetLinkUtil.getLabelRes(type));
+ AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.buildAssetLink(type, id);
+ if (assetLink == null) {
+ clearAssetLinkChip(chip);
+ return null;
+ }
+
+ chip.setText(getString(R.string.asset_link_chip_text, label, assetLink.id));
+ chip.setVisibility(View.VISIBLE);
+
+ chip.setOnClickListener(v -> {
+ if (assetLink != null) {
+ ((MainActivity) requireActivity()).openAssetLink(assetLink);
+ }
+ });
+
+ chip.setOnLongClickListener(v -> {
+ if (assetLink != null) {
+ AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
+ Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ });
+
+ return assetLink;
+ }
+
+ private void clearAssetLinkChip(@Nullable Chip chip) {
+ if (chip == null) return;
+ chip.setVisibility(View.GONE);
+ chip.setText("");
+ chip.setOnClickListener(null);
+ chip.setOnLongClickListener(null);
+ }
+
+ private void syncAssetLinkGroupVisibility() {
+ if (assetLinkChipGroup == null) return;
+ boolean hasVisible = false;
+ for (int i = 0; i < assetLinkChipGroup.getChildCount(); i++) {
+ View child = assetLinkChipGroup.getChildAt(i);
+ if (child.getVisibility() == View.VISIBLE) {
+ hasVisible = true;
+ break;
+ }
+ }
+ assetLinkChipGroup.setVisibility(hasVisible ? View.VISIBLE : View.GONE);
+ }
+
+ private void bindAssetLinkView(@Nullable View view, @Nullable AssetLinkUtil.AssetLink assetLink) {
+ if (view == null) return;
+ if (assetLink == null) {
+ AssetLinkUtil.clearLinkAppearance(view);
+ view.setOnClickListener(null);
+ view.setOnLongClickListener(null);
+ view.setClickable(false);
+ view.setLongClickable(false);
+ return;
+ }
+
+ view.setClickable(true);
+ view.setLongClickable(true);
+ AssetLinkUtil.applyLinkAppearance(view);
+ view.setOnClickListener(v -> ((MainActivity) requireActivity()).openAssetLink(assetLink, !AssetLinkUtil.TYPE_SONG.equals(assetLink.type)));
+ view.setOnLongClickListener(v -> {
+ AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
+ Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show();
+ return true;
+ });
+ }
+
private void initializeMediaBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
@@ -293,4 +397,4 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
private void refreshShares() {
homeViewModel.refreshShares(requireActivity());
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkNavigator.java b/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkNavigator.java
new file mode 100644
index 00000000..9d3ba966
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkNavigator.java
@@ -0,0 +1,188 @@
+package com.cappielloantonio.tempo.util;
+
+import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavController;
+import androidx.navigation.NavOptions;
+
+import com.cappielloantonio.tempo.BuildConfig;
+import com.cappielloantonio.tempo.R;
+import com.cappielloantonio.tempo.repository.AlbumRepository;
+import com.cappielloantonio.tempo.repository.ArtistRepository;
+import com.cappielloantonio.tempo.repository.PlaylistRepository;
+import com.cappielloantonio.tempo.repository.SongRepository;
+import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
+import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
+import com.cappielloantonio.tempo.subsonic.models.Child;
+import com.cappielloantonio.tempo.subsonic.models.Playlist;
+import com.cappielloantonio.tempo.subsonic.models.Genre;
+import com.cappielloantonio.tempo.ui.activity.MainActivity;
+import com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog.SongBottomSheetDialog;
+import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
+
+public final class AssetLinkNavigator {
+ private final MainActivity activity;
+ private final SongRepository songRepository = new SongRepository();
+ private final AlbumRepository albumRepository = new AlbumRepository();
+ private final ArtistRepository artistRepository = new ArtistRepository();
+ private final PlaylistRepository playlistRepository = new PlaylistRepository();
+
+ public AssetLinkNavigator(@NonNull MainActivity activity) {
+ this.activity = activity;
+ }
+
+ public void open(@Nullable AssetLinkUtil.AssetLink assetLink) {
+ if (assetLink == null) {
+ return;
+ }
+ switch (assetLink.type) {
+ case AssetLinkUtil.TYPE_SONG:
+ openSong(assetLink.id);
+ break;
+ case AssetLinkUtil.TYPE_ALBUM:
+ openAlbum(assetLink.id);
+ break;
+ case AssetLinkUtil.TYPE_ARTIST:
+ openArtist(assetLink.id);
+ break;
+ case AssetLinkUtil.TYPE_PLAYLIST:
+ openPlaylist(assetLink.id);
+ break;
+ case AssetLinkUtil.TYPE_GENRE:
+ openGenre(assetLink.id);
+ break;
+ case AssetLinkUtil.TYPE_YEAR:
+ openYear(assetLink.id);
+ break;
+ default:
+ Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
+ break;
+ }
+ }
+
+ private void openSong(@NonNull String id) {
+ MutableLiveData liveData = songRepository.getSong(id);
+ Observer observer = new Observer() {
+ @Override
+ public void onChanged(Child child) {
+ liveData.removeObserver(this);
+ if (child == null) {
+ Toast.makeText(activity, R.string.asset_link_error_song, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ SongBottomSheetViewModel viewModel = new ViewModelProvider(activity).get(SongBottomSheetViewModel.class);
+ viewModel.setSong(child);
+ SongBottomSheetDialog dialog = new SongBottomSheetDialog();
+ Bundle args = new Bundle();
+ args.putParcelable(Constants.TRACK_OBJECT, child);
+ dialog.setArguments(args);
+ dialog.show(activity.getSupportFragmentManager(), null);
+ }
+ };
+ liveData.observe(activity, observer);
+ }
+
+ private void openAlbum(@NonNull String id) {
+ MutableLiveData liveData = albumRepository.getAlbum(id);
+ Observer observer = new Observer() {
+ @Override
+ public void onChanged(AlbumID3 album) {
+ liveData.removeObserver(this);
+ if (album == null) {
+ Toast.makeText(activity, R.string.asset_link_error_album, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putParcelable(Constants.ALBUM_OBJECT, album);
+ navigateSafely(R.id.albumPageFragment, args);
+ }
+ };
+ liveData.observe(activity, observer);
+ }
+
+ private void openArtist(@NonNull String id) {
+ MutableLiveData liveData = artistRepository.getArtist(id);
+ Observer observer = new Observer() {
+ @Override
+ public void onChanged(ArtistID3 artist) {
+ liveData.removeObserver(this);
+ if (artist == null) {
+ Toast.makeText(activity, R.string.asset_link_error_artist, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putParcelable(Constants.ARTIST_OBJECT, artist);
+ navigateSafely(R.id.artistPageFragment, args);
+ }
+ };
+ liveData.observe(activity, observer);
+ }
+
+ private void openPlaylist(@NonNull String id) {
+ MutableLiveData liveData = playlistRepository.getPlaylist(id);
+ Observer observer = new Observer() {
+ @Override
+ public void onChanged(Playlist playlist) {
+ liveData.removeObserver(this);
+ if (playlist == null) {
+ Toast.makeText(activity, R.string.asset_link_error_playlist, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putParcelable(Constants.PLAYLIST_OBJECT, playlist);
+ navigateSafely(R.id.playlistPageFragment, args);
+ }
+ };
+ liveData.observe(activity, observer);
+ }
+
+ private void openGenre(@NonNull String genreName) {
+ String trimmed = genreName.trim();
+ if (trimmed.isEmpty()) {
+ Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Genre genre = new Genre();
+ genre.setGenre(trimmed);
+ genre.setSongCount(0);
+ genre.setAlbumCount(0);
+ Bundle args = new Bundle();
+ args.putParcelable(Constants.GENRE_OBJECT, genre);
+ args.putString(Constants.MEDIA_BY_GENRE, Constants.MEDIA_BY_GENRE);
+ navigateSafely(R.id.songListPageFragment, args);
+ }
+
+ private void openYear(@NonNull String yearValue) {
+ try {
+ int year = Integer.parseInt(yearValue.trim());
+ Bundle args = new Bundle();
+ args.putInt("year_object", year);
+ args.putString(Constants.MEDIA_BY_YEAR, Constants.MEDIA_BY_YEAR);
+ navigateSafely(R.id.songListPageFragment, args);
+ } catch (NumberFormatException ex) {
+ Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void navigateSafely(int destinationId, @Nullable Bundle args) {
+ activity.runOnUiThread(() -> {
+ NavController navController = activity.navController;
+ if (navController == null) {
+ return;
+ }
+ if (navController.getCurrentDestination() != null
+ && navController.getCurrentDestination().getId() == destinationId) {
+ navController.navigate(destinationId, args, new NavOptions.Builder().setLaunchSingleTop(true).build());
+ } else {
+ navController.navigate(destinationId, args);
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkUtil.java
new file mode 100644
index 00000000..1609a88a
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/tempo/util/AssetLinkUtil.java
@@ -0,0 +1,188 @@
+package com.cappielloantonio.tempo.util;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.core.content.ContextCompat;
+
+import com.cappielloantonio.tempo.R;
+
+import java.util.Objects;
+
+import com.google.android.material.color.MaterialColors;
+
+public final class AssetLinkUtil {
+ public static final String SCHEME = "tempo";
+ public static final String HOST_ASSET = "asset";
+
+ public static final String TYPE_SONG = "song";
+ public static final String TYPE_ALBUM = "album";
+ public static final String TYPE_ARTIST = "artist";
+ public static final String TYPE_PLAYLIST = "playlist";
+ public static final String TYPE_GENRE = "genre";
+ public static final String TYPE_YEAR = "year";
+
+ private AssetLinkUtil() {
+ }
+
+ @Nullable
+ public static AssetLink parse(@Nullable Intent intent) {
+ if (intent == null) return null;
+ return parse(intent.getData());
+ }
+
+ @Nullable
+ public static AssetLink parse(@Nullable Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+
+ if (!SCHEME.equalsIgnoreCase(uri.getScheme())) {
+ return null;
+ }
+
+ String host = uri.getHost();
+ if (!HOST_ASSET.equalsIgnoreCase(host)) {
+ return null;
+ }
+
+ if (uri.getPathSegments().size() < 2) {
+ return null;
+ }
+
+ String type = uri.getPathSegments().get(0);
+ String id = uri.getPathSegments().get(1);
+ if (TextUtils.isEmpty(type) || TextUtils.isEmpty(id)) {
+ return null;
+ }
+
+ if (!isSupportedType(type)) {
+ return null;
+ }
+
+ return new AssetLink(type, id, uri);
+ }
+
+ public static boolean isSupportedType(@Nullable String type) {
+ if (type == null) return false;
+ switch (type) {
+ case TYPE_SONG:
+ case TYPE_ALBUM:
+ case TYPE_ARTIST:
+ case TYPE_PLAYLIST:
+ case TYPE_GENRE:
+ case TYPE_YEAR:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @NonNull
+ public static Uri buildUri(@NonNull String type, @NonNull String id) {
+ return new Uri.Builder()
+ .scheme(SCHEME)
+ .authority(HOST_ASSET)
+ .appendPath(type)
+ .appendPath(id)
+ .build();
+ }
+
+ @Nullable
+ public static String buildLink(@Nullable String type, @Nullable String id) {
+ if (TextUtils.isEmpty(type) || TextUtils.isEmpty(id) || !isSupportedType(type)) {
+ return null;
+ }
+ return buildUri(Objects.requireNonNull(type), Objects.requireNonNull(id)).toString();
+ }
+
+ @Nullable
+ public static AssetLink buildAssetLink(@Nullable String type, @Nullable String id) {
+ String link = buildLink(type, id);
+ return parseLinkString(link);
+ }
+
+ @Nullable
+ public static AssetLink parseLinkString(@Nullable String link) {
+ if (TextUtils.isEmpty(link)) {
+ return null;
+ }
+ return parse(Uri.parse(link));
+ }
+
+ public static void copyToClipboard(@NonNull Context context, @NonNull AssetLink assetLink) {
+ ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ if (clipboardManager == null) {
+ return;
+ }
+ ClipData clipData = ClipData.newPlainText(context.getString(R.string.asset_link_clipboard_label), assetLink.uri.toString());
+ clipboardManager.setPrimaryClip(clipData);
+ }
+
+ @StringRes
+ public static int getLabelRes(@NonNull String type) {
+ switch (type) {
+ case TYPE_SONG:
+ return R.string.asset_link_label_song;
+ case TYPE_ALBUM:
+ return R.string.asset_link_label_album;
+ case TYPE_ARTIST:
+ return R.string.asset_link_label_artist;
+ case TYPE_PLAYLIST:
+ return R.string.asset_link_label_playlist;
+ case TYPE_GENRE:
+ return R.string.asset_link_label_genre;
+ case TYPE_YEAR:
+ return R.string.asset_link_label_year;
+ default:
+ return R.string.asset_link_label_unknown;
+ }
+ }
+
+ public static void applyLinkAppearance(@NonNull View view) {
+ if (view instanceof TextView) {
+ TextView textView = (TextView) view;
+ if (textView.getTag(R.id.tag_link_original_color) == null) {
+ textView.setTag(R.id.tag_link_original_color, textView.getCurrentTextColor());
+ }
+ int accent = MaterialColors.getColor(view, com.google.android.material.R.attr.colorPrimary,
+ ContextCompat.getColor(view.getContext(), android.R.color.holo_blue_light));
+ textView.setTextColor(accent);
+ }
+ }
+
+ public static void clearLinkAppearance(@NonNull View view) {
+ if (view instanceof TextView) {
+ TextView textView = (TextView) view;
+ Object original = textView.getTag(R.id.tag_link_original_color);
+ if (original instanceof Integer) {
+ textView.setTextColor((Integer) original);
+ } else {
+ int defaultColor = MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnSurface,
+ ContextCompat.getColor(view.getContext(), android.R.color.primary_text_light));
+ textView.setTextColor(defaultColor);
+ }
+ }
+ }
+
+ public static final class AssetLink {
+ public final String type;
+ public final String id;
+ public final Uri uri;
+
+ AssetLink(@NonNull String type, @NonNull String id, @NonNull Uri uri) {
+ this.type = type;
+ this.id = id;
+ this.uri = uri;
+ }
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioWriter.java b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioWriter.java
index cf0f768e..efd97350 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioWriter.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/util/ExternalAudioWriter.java
@@ -17,6 +17,9 @@ 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;
@@ -102,35 +105,76 @@ public class ExternalAudioWriter {
ExternalDownloadMetadataStore.remove(metadataKey);
return;
}
- String scheme = mediaUri.getScheme();
- if (scheme == null || (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) {
- notifyFailure(context, "Unsupported 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 {
- 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);
+ 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;
}
- String mimeType = connection.getContentType();
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()) {
@@ -146,7 +190,6 @@ public class ExternalAudioWriter {
String fileName = sanitized + "." + extension;
DocumentFile existingFile = findFile(directory, fileName);
- long remoteLength = connection.getContentLengthLong();
Long recordedSize = ExternalDownloadMetadataStore.getSize(metadataKey);
if (existingFile != null && existingFile.exists()) {
long localLength = existingFile.length();
@@ -175,7 +218,7 @@ public class ExternalAudioWriter {
}
Uri targetUri = targetFile.getUri();
- try (InputStream in = connection.getInputStream();
+ try (InputStream in = openInputStream(context, mediaUri, scheme, connection, sourceFile);
OutputStream out = context.getContentResolver().openOutputStream(targetUri)) {
if (out == null) {
notifyFailure(context, "Cannot open output stream.");
@@ -319,4 +362,32 @@ public class ExternalAudioWriter {
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
}
+
+ private static InputStream openInputStream(Context context,
+ Uri mediaUri,
+ String scheme,
+ HttpURLConnection connection,
+ File sourceFile) throws IOException {
+ switch (scheme) {
+ case "http":
+ case "https":
+ if (connection == null) {
+ throw new IOException("Connection not initialized");
+ }
+ return connection.getInputStream();
+ case "content":
+ InputStream contentStream = context.getContentResolver().openInputStream(mediaUri);
+ if (contentStream == null) {
+ throw new IOException("Cannot open content stream");
+ }
+ return contentStream;
+ case "file":
+ if (sourceFile == null || !sourceFile.exists()) {
+ throw new IOException("Missing source file");
+ }
+ return new FileInputStream(sourceFile);
+ default:
+ throw new IOException("Unsupported scheme " + scheme);
+ }
+ }
}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java
index 9bd7fcad..558aa218 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java
@@ -74,6 +74,12 @@ public class MappingUtil {
bundle.putInt("originalWidth", media.getOriginalWidth() != null ? media.getOriginalWidth() : 0);
bundle.putInt("originalHeight", media.getOriginalHeight() != null ? media.getOriginalHeight() : 0);
bundle.putString("uri", uri.toString());
+ bundle.putString("assetLinkSong", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, media.getId()));
+ bundle.putString("assetLinkAlbum", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, media.getAlbumId()));
+ bundle.putString("assetLinkArtist", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, media.getArtistId()));
+ bundle.putString("assetLinkGenre", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_GENRE, media.getGenre()));
+ Integer year = media.getYear();
+ bundle.putString("assetLinkYear", year != null && year != 0 ? AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year)) : null);
return new MediaItem.Builder()
.setMediaId(media.getId())
diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt
index 9baaca1c..4815fb83 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt
@@ -77,6 +77,8 @@ object Preferences {
private const val EQUALIZER_BAND_LEVELS = "equalizer_band_levels"
private const val MINI_SHUFFLE_BUTTON_VISIBILITY = "mini_shuffle_button_visibility"
private const val ALBUM_DETAIL = "album_detail"
+ private const val ALBUM_SORT_ORDER = "album_sort_order"
+ private const val DEFAULT_ALBUM_SORT_ORDER = Constants.ALBUM_ORDER_BY_NAME
@JvmStatic
fun getServer(): String? {
@@ -644,4 +646,14 @@ object Preferences {
fun showAlbumDetail(): Boolean {
return App.getInstance().preferences.getBoolean(ALBUM_DETAIL, false)
}
+
+ @JvmStatic
+ fun getAlbumSortOrder(): String {
+ return App.getInstance().preferences.getString(ALBUM_SORT_ORDER, DEFAULT_ALBUM_SORT_ORDER) ?: DEFAULT_ALBUM_SORT_ORDER
+ }
+
+ @JvmStatic
+ fun setAlbumSortOrder(sortOrder: String) {
+ App.getInstance().preferences.edit().putString(ALBUM_SORT_ORDER, sortOrder).apply()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider.java b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider.java
index 06986145..93a1a7e2 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider.java
@@ -5,17 +5,20 @@ 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 android.app.PendingIntent;
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";
@@ -28,7 +31,7 @@ public class WidgetProvider extends AppWidgetProvider {
public void onUpdate(Context ctx, AppWidgetManager mgr, int[] ids) {
for (int id : ids) {
RemoteViews rv = WidgetUpdateManager.chooseBuild(ctx, id);
- attachIntents(ctx, rv, id);
+ attachIntents(ctx, rv, id, null, null, null);
mgr.updateAppWidget(id, rv);
}
}
@@ -50,16 +53,23 @@ public class WidgetProvider extends AppWidgetProvider {
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);
+ 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);
+ 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,
@@ -97,9 +107,31 @@ public class WidgetProvider extends AppWidgetProvider {
rv.setOnClickPendingIntent(R.id.btn_shuffle, shuffle);
rv.setOnClickPendingIntent(R.id.btn_repeat, repeat);
- PendingIntent launch = TaskStackBuilder.create(ctx)
- .addNextIntentWithParentStack(new Intent(ctx, MainActivity.class))
- .getPendingIntent(requestCodeBase + 10, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent launch = buildMainActivityPendingIntent(ctx, requestCodeBase + 10, null);
rv.setOnClickPendingIntent(R.id.root, launch);
+
+ PendingIntent songPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 20, songLink);
+ PendingIntent artistPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 21, artistLink);
+ PendingIntent albumPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 22, albumLink);
+
+ PendingIntent fallback = launch;
+ rv.setOnClickPendingIntent(R.id.album_art, songPending != null ? songPending : fallback);
+ rv.setOnClickPendingIntent(R.id.title, songPending != null ? songPending : fallback);
+ rv.setOnClickPendingIntent(R.id.subtitle,
+ artistPending != null ? artistPending : (songPending != null ? songPending : fallback));
+ rv.setOnClickPendingIntent(R.id.album, albumPending != null ? albumPending : fallback);
+ }
+
+ private static PendingIntent buildMainActivityPendingIntent(Context ctx, int requestCode, @Nullable String link) {
+ Intent intent;
+ if (!TextUtils.isEmpty(link)) {
+ intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link), ctx, MainActivity.class);
+ } else {
+ intent = new Intent(ctx, MainActivity.class);
+ }
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ TaskStackBuilder stackBuilder = TaskStackBuilder.create(ctx);
+ stackBuilder.addNextIntentWithParentStack(intent);
+ return stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
}
}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java
index 4132511e..f159c526 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java
@@ -4,8 +4,9 @@ import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
-import android.text.TextUtils;
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;
@@ -17,6 +18,7 @@ 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;
@@ -25,6 +27,8 @@ import java.util.concurrent.ExecutionException;
public final class WidgetUpdateManager {
+ private static final int WIDGET_SAFE_ART_SIZE = 512;
+
public static void updateFromState(Context ctx,
String title,
String artist,
@@ -34,7 +38,10 @@ public final class WidgetUpdateManager {
boolean shuffleEnabled,
int repeatMode,
long positionMs,
- long durationMs) {
+ 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 = "";
@@ -46,7 +53,7 @@ public final class WidgetUpdateManager {
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);
+ WidgetProvider.attachIntents(ctx, rv, id, songLink, albumLink, artistLink);
mgr.updateAppWidget(id, rv);
}
}
@@ -56,7 +63,7 @@ public final class WidgetUpdateManager {
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);
+ WidgetProvider.attachIntents(ctx, rv, id, null, null, null);
mgr.updateAppWidget(id, rv);
}
}
@@ -70,7 +77,10 @@ public final class WidgetUpdateManager {
boolean shuffleEnabled,
int repeatMode,
long positionMs,
- long durationMs) {
+ 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;
@@ -79,12 +89,15 @@ public final class WidgetUpdateManager {
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(),
+ WIDGET_SAFE_ART_SIZE,
new CustomTarget() {
@Override
public void onResourceReady(Bitmap resource, Transition super Bitmap> transition) {
@@ -93,7 +106,7 @@ public final class WidgetUpdateManager {
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);
+ WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
mgr.updateAppWidget(id, rv);
}
}
@@ -105,7 +118,7 @@ public final class WidgetUpdateManager {
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);
+ WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
mgr.updateAppWidget(id, rv);
}
}
@@ -117,7 +130,7 @@ public final class WidgetUpdateManager {
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);
+ WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
mgr.updateAppWidget(id, rv);
}
}
@@ -133,6 +146,7 @@ public final class WidgetUpdateManager {
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)
@@ -140,10 +154,26 @@ public final class WidgetUpdateManager {
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 = mi.mediaMetadata.extras.getString("coverArtId");
+ 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();
@@ -159,7 +189,10 @@ public final class WidgetUpdateManager {
c.getShuffleModeEnabled(),
c.getRepeatMode(),
position,
- duration);
+ duration,
+ songLink,
+ albumLink,
+ artistLink);
c.release();
} catch (ExecutionException | InterruptedException ignored) {
}
@@ -273,4 +306,4 @@ public final class WidgetUpdateManager {
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml
new file mode 100644
index 00000000..5592db28
--- /dev/null
+++ b/app/src/main/res/drawable/ic_link.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/layout/bottom_sheet_song_dialog.xml b/app/src/main/res/layout/bottom_sheet_song_dialog.xml
index 2105f941..7cf98440 100644
--- a/app/src/main/res/layout/bottom_sheet_song_dialog.xml
+++ b/app/src/main/res/layout/bottom_sheet_song_dialog.xml
@@ -68,6 +68,14 @@
+
+
-
\ No newline at end of file
+
diff --git a/app/src/main/res/layout/fragment_album_catalogue.xml b/app/src/main/res/layout/fragment_album_catalogue.xml
index c0928866..71977634 100644
--- a/app/src/main/res/layout/fragment_album_catalogue.xml
+++ b/app/src/main/res/layout/fragment_album_catalogue.xml
@@ -41,23 +41,40 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
-
+ app:layout_constraintEnd_toEndOf="parent">
+
+
+
+
+
+
-
@@ -87,4 +103,3 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
-
diff --git a/app/src/main/res/layout/inner_fragment_player_controller_layout.xml b/app/src/main/res/layout/inner_fragment_player_controller_layout.xml
index 99e2c90f..29747587 100644
--- a/app/src/main/res/layout/inner_fragment_player_controller_layout.xml
+++ b/app/src/main/res/layout/inner_fragment_player_controller_layout.xml
@@ -57,6 +57,17 @@
+
+
+ app:layout_constraintTop_toBottomOf="@+id/player_asset_link_row" />
-
\ No newline at end of file
+
diff --git a/app/src/main/res/layout/view_asset_link_row.xml b/app/src/main/res/layout/view_asset_link_row.xml
new file mode 100644
index 00000000..7060db54
--- /dev/null
+++ b/app/src/main/res/layout/view_asset_link_row.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 1dc81873..5c7e8b6c 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -88,6 +88,9 @@
Wymaganewymagany jest prefiks http lub httpsPobieranie
+ Wyłącz serce
+ Włącz serce
+ Ładowanie…Wybierz dwa lub więcej filtrówFiltryFiltruj wykonawców
@@ -213,8 +216,9 @@
AnulujUtwórzDodaj do playlisty
- Dodano piosenkę do playlisty
- Nie udało się dodać piosenki do playlisty
+ Dodano piosenki do playlisty
+ Nie udało się dodać piosenek do playlisty
+ Pominięto wszystkie piosenki jako duplikaty%1$d utworów • %2$sDługość • %1$sPrzytrzymaj aby usunąć
@@ -281,6 +285,8 @@
Tempo jest otwarto-źródłowym i lekkim klientem muzycznym dla Subsonic, stworzonym i zbudowanym natywnie dla Androida.O aplikacjiAlways on display
+ Zezwalaj na dodawania duplikatów do playlist
+ Jeżeli włączone, duplikaty nie będą sprawdzane podczas dodawania do playlisty.Format transkodowaniaJeżeli włączone, Tempo nie będzię wymuszał pobierania utworu z ustawieniami transkodowania wybranymi poniżej.Priorytetyzuj ustawienia serwera używanego do strumieniowania w pobieraniach
@@ -296,6 +302,8 @@
Priorytet przy transkodowaniu utworu danego serwerowiStrategia buforowaniaAby zmiany przyniosły efekt, musisz ręcznie zrestartować aplikację.
+ Wybierz folder dla pobranych plików muzycznych
+ Wyczyść folder pobieraniaPozwala muzyce odtwarzać się dalej po końcu playlisty, odtwarza podobne piosenkiOdtwarzanie bez przerwyRozmiar cache dla okładek
@@ -304,6 +312,9 @@
Zatwierdzenie nieodwracalnie usunie wszystkie zapisane elementyUsuń zapisane elementyPamięć do pobierania
+ Utworzono folder pobierania.
+ Wybrano folder pobierania
+ Ustaw folder pobieraniaZmień ustawienia audioKorektor systemowyhttps://github.com/eddyizm/tempo
@@ -312,6 +323,7 @@
https://github.com/eddyizm/tempo/discussionsDołącz do dyskusji i wsparcia społecznościWsparcie użytkowników
+ Skanowanie: naliczono %1$d utworówRozdzielczość obrazówJęzykWyloguj
@@ -332,6 +344,8 @@
Timer synchronizacjiJeżeli włączone, użytkownik będzie miał możliwość zapisania kolejki i będzie miał możliwość załadowania jej stanu przy otwarciu aplikacji.Synchronizuj kolejkę odtwarzania dla tego użytkownika [Niedokończone]
+ Pokaż przycisk odtwarzania losowego
+ Jeżeli włączone, pokazuje przycisk losowego odtwarzania, i usuwa przycisk serca w mini odtwarzaczuPokaż radioJeżeli włączone, widoczna będzie sekcja radia. Zrestartuj aplikację aby, zmiany przyniosły pełny efekt.Automatyczne pobieranie tesktów
@@ -366,6 +380,7 @@
MotywDaneOgólne
+ PlaylistyOcenyWzmocnienie głośności przy ponownym odtwarzaniuScrobble
@@ -454,6 +469,17 @@
unDrawSpecjalne podziękowania dla unDraw bez którego ilustracji nie mogliśmy uczynić tej aplikacji jeszcze piękniejszą.https://undraw.co/
+ Widget Tempo
+ Nie odtwarza
+ Otwórz Tempo
+ 0:00
+ 0:00
+ Okładka albumu
+ Play lub pauza
+ Następny utwór
+ Poprzedni utwór
+ Przełącznik odtwarzania losowego
+ Zmień tryb powtarzania%d album do zsynchronizowania %d albumów do zsynchrpnizowania
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
new file mode 100644
index 00000000..c29aa81f
--- /dev/null
+++ b/app/src/main/res/values/ids.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3834a4bb..74548d7b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -410,6 +410,22 @@
Update shareExpiration date: %1$sSharing is not supported or not enabled
+ Tempo asset link
+ Song UID
+ Album UID
+ Artist UID
+ Playlist UID
+ Genre UID
+ Year UID
+ Asset UID
+ Unsupported asset link
+ Song could not be opened
+ Album could not be opened
+ Artist could not be opened
+ Playlist could not be opened
+ %1$s • %2$s
+ Copied %1$s to clipboard
+ Asset link: %1$sDescriptionExpiration dateCancel
diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt
index 27214f90..c669a7d3 100644
--- a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt
+++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt
@@ -14,15 +14,19 @@ import androidx.media3.common.*
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.TrackGroupArray
import androidx.media3.exoplayer.trackselection.TrackSelectionArray
import androidx.media3.session.*
import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.tempo.R
+import com.cappielloantonio.tempo.repository.QueueRepository
import com.cappielloantonio.tempo.ui.activity.MainActivity
+import com.cappielloantonio.tempo.util.AssetLinkUtil
import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.DownloadUtil
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
+import com.cappielloantonio.tempo.util.MappingUtil
import com.cappielloantonio.tempo.util.Preferences
import com.cappielloantonio.tempo.util.ReplayGainUtil
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
@@ -83,6 +87,7 @@ class MediaService : MediaLibraryService() {
initializeCustomCommands()
initializePlayer()
initializeMediaLibrarySession()
+ restorePlayerFromQueue()
initializePlayerListener()
initializeEqualizerManager()
@@ -118,15 +123,17 @@ class MediaService : MediaLibraryService() {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
- shuffleCommands.forEach { commandButton ->
- // TODO: Aggiungere i comandi personalizzati
- // commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
+ (shuffleCommands + repeatCommands).forEach { commandButton ->
+ commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
}
- return MediaSession.ConnectionResult.accept(
- availableSessionCommands.build(),
- connectionResult.availablePlayerCommands
- )
+ customLayout = buildCustomLayout(session.player)
+
+ return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
+ .setAvailableSessionCommands(availableSessionCommands.build())
+ .setAvailablePlayerCommands(connectionResult.availablePlayerCommands)
+ .setCustomLayout(customLayout)
+ .build()
}
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
@@ -225,7 +232,7 @@ class MediaService : MediaLibraryService() {
private fun initializePlayer() {
player = ExoPlayer.Builder(this)
.setRenderersFactory(getRenderersFactory())
- .setMediaSourceFactory(DynamicMediaSourceFactory(this))
+ .setMediaSourceFactory(getMediaSourceFactory())
.setAudioAttributes(AudioAttributes.DEFAULT, true)
.setHandleAudioBecomingNoisy(true)
.setWakeMode(C.WAKE_MODE_NETWORK)
@@ -268,6 +275,33 @@ class MediaService : MediaLibraryService() {
}
}
+ private fun restorePlayerFromQueue() {
+ if (player.mediaItemCount > 0) return
+
+ val queueRepository = QueueRepository()
+ val storedQueue = queueRepository.media
+ if (storedQueue.isNullOrEmpty()) return
+
+ val mediaItems = MappingUtil.mapMediaItems(storedQueue)
+ if (mediaItems.isEmpty()) return
+
+ val lastIndex = try {
+ queueRepository.lastPlayedMediaIndex
+ } catch (_: Exception) {
+ 0
+ }.coerceIn(0, mediaItems.size - 1)
+
+ val lastPosition = try {
+ queueRepository.lastPlayedMediaTimestamp
+ } catch (_: Exception) {
+ 0L
+ }.let { if (it < 0L) 0L else it }
+
+ player.setMediaItems(mediaItems, lastIndex, lastPosition)
+ player.prepare()
+ updateWidget()
+ }
+
private fun initializePlayerListener() {
player.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
@@ -398,7 +432,7 @@ class MediaService : MediaLibraryService() {
.build()
}
- private fun ignoreFuture(customLayout: ListenableFuture) {
+ private fun ignoreFuture(@Suppress("UNUSED_PARAMETER") customLayout: ListenableFuture) {
/* Do nothing. */
}
@@ -421,7 +455,14 @@ class MediaService : MediaLibraryService() {
?: mi?.mediaMetadata?.extras?.getString("artist")
val album = mi?.mediaMetadata?.albumTitle?.toString()
?: mi?.mediaMetadata?.extras?.getString("album")
- val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
+ val extras = mi?.mediaMetadata?.extras
+ val coverId = extras?.getString("coverArtId")
+ val songLink = extras?.getString("assetLinkSong")
+ ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
+ val albumLink = extras?.getString("assetLinkAlbum")
+ ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
+ val artistLink = extras?.getString("assetLinkArtist")
+ ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
WidgetUpdateManager.updateFromState(
@@ -434,7 +475,10 @@ class MediaService : MediaLibraryService() {
player.shuffleModeEnabled,
player.repeatMode,
position,
- duration
+ duration,
+ songLink,
+ albumLink,
+ artistLink
)
}
@@ -453,6 +497,7 @@ class MediaService : MediaLibraryService() {
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
+ private fun getMediaSourceFactory(): MediaSource.Factory = DynamicMediaSourceFactory(this)
}
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt
index a01a2644..1bc0b15f 100644
--- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt
+++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt
@@ -295,11 +295,6 @@ open class MediaLibrarySessionCallback(
args: Bundle
): ListenableFuture {
- val mediaItemId = args.getString(
- MediaConstants.EXTRA_KEY_MEDIA_ID,
- session.player.currentMediaItem?.mediaId
- )
-
when (customCommand.customAction) {
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> {
session.player.shuffleModeEnabled = true
@@ -398,4 +393,4 @@ open class MediaLibrarySessionCallback(
): ListenableFuture>> {
return MediaBrowserTree.search(query)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt
index 51292761..2ff81ac4 100644
--- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt
+++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt
@@ -8,6 +8,7 @@ import android.os.Binder
import android.os.IBinder
import android.os.Handler
import android.os.Looper
+import androidx.core.content.ContextCompat
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.AudioAttributes
@@ -21,10 +22,13 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.tempo.repository.AutomotiveRepository
+import com.cappielloantonio.tempo.repository.QueueRepository
import com.cappielloantonio.tempo.ui.activity.MainActivity
+import com.cappielloantonio.tempo.util.AssetLinkUtil
import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.DownloadUtil
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
+import com.cappielloantonio.tempo.util.MappingUtil
import com.cappielloantonio.tempo.util.Preferences
import com.cappielloantonio.tempo.util.ReplayGainUtil
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
@@ -70,9 +74,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
initializeRepository()
initializePlayer()
- initializeCastPlayer()
initializeMediaLibrarySession()
+ restorePlayerFromQueue()
initializePlayerListener()
+ initializeCastPlayer()
initializeEqualizerManager()
setPlayer(
@@ -142,12 +147,20 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
player.repeatMode = Preferences.getRepeatMode()
}
+ @Suppress("DEPRECATION")
private fun initializeCastPlayer() {
if (GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
) {
- castPlayer = CastPlayer(CastContext.getSharedInstance(this))
- castPlayer.setSessionAvailabilityListener(this)
+ CastContext.getSharedInstance(this, ContextCompat.getMainExecutor(this))
+ .addOnSuccessListener { castContext ->
+ castPlayer = CastPlayer(castContext)
+ castPlayer.setSessionAvailabilityListener(this@MediaService)
+
+ if (castPlayer.isCastSessionAvailable && this::mediaLibrarySession.isInitialized) {
+ setPlayer(player, castPlayer)
+ }
+ }
}
}
@@ -165,6 +178,33 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
.build()
}
+ private fun restorePlayerFromQueue() {
+ if (player.mediaItemCount > 0) return
+
+ val queueRepository = QueueRepository()
+ val storedQueue = queueRepository.media
+ if (storedQueue.isNullOrEmpty()) return
+
+ val mediaItems = MappingUtil.mapMediaItems(storedQueue)
+ if (mediaItems.isEmpty()) return
+
+ val lastIndex = try {
+ queueRepository.lastPlayedMediaIndex
+ } catch (_: Exception) {
+ 0
+ }.coerceIn(0, mediaItems.size - 1)
+
+ val lastPosition = try {
+ queueRepository.lastPlayedMediaTimestamp
+ } catch (_: Exception) {
+ 0L
+ }.let { if (it < 0L) 0L else it }
+
+ player.setMediaItems(mediaItems, lastIndex, lastPosition)
+ player.prepare()
+ updateWidget()
+ }
+
private fun createLibrarySessionCallback(): MediaLibrarySessionCallback {
return MediaLibrarySessionCallback(this, automotiveRepository)
}
@@ -262,7 +302,14 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
?: mi?.mediaMetadata?.extras?.getString("artist")
val album = mi?.mediaMetadata?.albumTitle?.toString()
?: mi?.mediaMetadata?.extras?.getString("album")
- val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
+ val extras = mi?.mediaMetadata?.extras
+ val coverId = extras?.getString("coverArtId")
+ val songLink = extras?.getString("assetLinkSong")
+ ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
+ val albumLink = extras?.getString("assetLinkAlbum")
+ ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
+ val artistLink = extras?.getString("assetLinkArtist")
+ ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
WidgetUpdateManager.updateFromState(
@@ -275,7 +322,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
player.shuffleModeEnabled,
player.repeatMode,
position,
- duration
+ duration,
+ songLink,
+ albumLink,
+ artistLink
)
}
diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/util/Flavors.java b/app/src/tempo/java/com/cappielloantonio/tempo/util/Flavors.java
index 4bed2921..1ec0cd92 100644
--- a/app/src/tempo/java/com/cappielloantonio/tempo/util/Flavors.java
+++ b/app/src/tempo/java/com/cappielloantonio/tempo/util/Flavors.java
@@ -2,13 +2,16 @@ package com.cappielloantonio.tempo.util;
import android.content.Context;
+import androidx.core.content.ContextCompat;
+
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
public class Flavors {
+ @SuppressWarnings("deprecation")
public static void initializeCastContext(Context context) {
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS)
- CastContext.getSharedInstance(context);
+ CastContext.getSharedInstance(context, ContextCompat.getMainExecutor(context));
}
}
diff --git a/mockup/usage/player_icons.png b/mockup/usage/player_icons.png
new file mode 100644
index 00000000..59d6fa4f
Binary files /dev/null and b/mockup/usage/player_icons.png differ