diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b8d72d8b..b7a386b7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -96,7 +96,12 @@
android:resource="@xml/widget_info"/>
-
+
diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt
index 60d641ce..39ac3f03 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt
+++ b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt
@@ -1,5 +1,6 @@
package com.cappielloantonio.tempo.model
+import android.content.ContentResolver
import android.net.Uri
import android.os.Bundle
import androidx.annotation.Keep
@@ -13,6 +14,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.cappielloantonio.tempo.glide.CustomGlideRequest
+import com.cappielloantonio.tempo.provider.AlbumArtContentProvider
import com.cappielloantonio.tempo.subsonic.models.Child
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode
@@ -197,7 +199,7 @@ class SessionMediaItem() {
fun getMediaItem(): MediaItem {
val uri: Uri = getStreamUri()
- val artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, getImageSize()))
+ val artworkUri = AlbumArtContentProvider.contentUri(coverArtId)
val bundle = Bundle()
bundle.putString("id", id)
diff --git a/app/src/main/java/com/cappielloantonio/tempo/provider/AlbumArtContentProvider.java b/app/src/main/java/com/cappielloantonio/tempo/provider/AlbumArtContentProvider.java
new file mode 100644
index 00000000..cace2db9
--- /dev/null
+++ b/app/src/main/java/com/cappielloantonio/tempo/provider/AlbumArtContentProvider.java
@@ -0,0 +1,149 @@
+package com.cappielloantonio.tempo.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.cappielloantonio.tempo.glide.CustomGlideRequest;
+import com.cappielloantonio.tempo.util.Preferences;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+public class AlbumArtContentProvider extends ContentProvider {
+ public static final String AUTHORITY = "com.cappielloantonio.tempo.provider";
+ public static final String ALBUM_ART = "albumArt";
+ private ExecutorService executor;
+
+ private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ static {
+ uriMatcher.addURI(AUTHORITY, "albumArt/*", 1);
+ }
+
+ public static Uri contentUri(String artworkId) {
+ return new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(AUTHORITY)
+ .appendPath(ALBUM_ART)
+ .appendPath(artworkId)
+ .build();
+ }
+
+ @Nullable
+ @Override
+ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
+ Context context = getContext();
+ String albumId = uri.getLastPathSegment();
+ Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize()));
+
+ try {
+ // use pipe to communicate between background thread and caller of openFile()
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ ParcelFileDescriptor readSide = pipe[0];
+ ParcelFileDescriptor writeSide = pipe[1];
+
+ // perform loading in background thread to avoid blocking UI
+ executor.execute(() -> {
+ try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
+
+ // request artwork from API using Glide
+ File file = Glide.with(context)
+ .asFile()
+ .load(artworkUri)
+ .diskCacheStrategy(DiskCacheStrategy.DATA)
+ .submit()
+ .get();
+
+ // copy artwork down pipe returned by ContentProvider
+ try (InputStream in = new FileInputStream(file)) {
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = in.read(buffer)) != -1) {
+ out.write(buffer, 0, bytesRead);
+ }
+ } catch (Exception e) {
+ writeSide.closeWithError("Failed to load image: " + e.getMessage());
+ }
+
+ } catch (Exception e) {
+ try {
+ writeSide.closeWithError("Failed to load image: " + e.getMessage());
+ } catch (IOException ignored) {}
+ }
+ });
+
+ return readSide;
+
+ } catch (IOException e) {
+ throw new FileNotFoundException("Could not create pipe: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ executor = Executors.newFixedThreadPool(
+ Math.max(2, Runtime.getRuntime().availableProcessors() / 2)
+ );
+ return true;
+ }
+
+ @Override
+ public void shutdown() {
+ if (executor != null) {
+ executor.shutdown();
+ try {
+ if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
+ executor.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ executor.shutdownNow();
+ }
+ }
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ return "";
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
+ return null;
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
+ return 0;
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
+ return 0;
+ }
+}
diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java
index fe24d81d..b509360e 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java
@@ -1,6 +1,7 @@
package com.cappielloantonio.tempo.repository;
+import android.content.ContentResolver;
import android.net.Uri;
import android.view.View;
@@ -22,6 +23,7 @@ import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.SessionMediaItem;
+import com.cappielloantonio.tempo.provider.AlbumArtContentProvider;
import com.cappielloantonio.tempo.service.DownloaderManager;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
@@ -70,7 +72,7 @@ public class AutomotiveRepository {
List mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) {
- Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
+ Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())
@@ -217,7 +219,7 @@ public class AutomotiveRepository {
List mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) {
- Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
+ Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())
@@ -272,7 +274,7 @@ public class AutomotiveRepository {
List mediaItems = new ArrayList<>();
for (ArtistID3 artist : artists) {
- Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize()));
+ Uri artworkUri = AlbumArtContentProvider.contentUri(artist.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(artist.getName())
@@ -397,7 +399,7 @@ public class AutomotiveRepository {
List children = response.body().getSubsonicResponse().getIndexes().getChildren();
for (Child song : children) {
- Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(song.getCoverArtId(), Preferences.getImageSize()));
+ Uri artworkUri = AlbumArtContentProvider.contentUri(song.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(song.getTitle())
@@ -451,7 +453,7 @@ public class AutomotiveRepository {
List mediaItems = new ArrayList<>();
for (Child child : directory.getChildren()) {
- Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(child.getCoverArtId(), Preferences.getImageSize()));
+ Uri artworkUri = AlbumArtContentProvider.contentUri(child.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(child.getTitle())
@@ -550,7 +552,7 @@ public class AutomotiveRepository {
List mediaItems = new ArrayList<>();
for (PodcastEpisode episode : episodes) {
- Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(episode.getCoverArtId(), Preferences.getImageSize()));
+ Uri artworkUri = AlbumArtContentProvider.contentUri(episode.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(episode.getTitle())
@@ -687,7 +689,7 @@ public class AutomotiveRepository {
List mediaItems = new ArrayList<>();
for (AlbumID3 album : albums) {
- Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
+ Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())
@@ -800,7 +802,7 @@ public class AutomotiveRepository {
if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) {
for (ArtistID3 artist : response.body().getSubsonicResponse().getSearchResult3().getArtists()) {
- Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize()));
+ Uri artworkUri = AlbumArtContentProvider.contentUri(artist.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(artist.getName())
@@ -822,7 +824,7 @@ public class AutomotiveRepository {
if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) {
for (AlbumID3 album : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) {
- Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
+ Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setTitle(album.getName())
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 a41e0983..71007c78 100644
--- a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java
+++ b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java
@@ -1,5 +1,6 @@
package com.cappielloantonio.tempo.util;
+import android.content.ContentResolver;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
@@ -15,6 +16,7 @@ import androidx.media3.common.HeartRating;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Download;
+import com.cappielloantonio.tempo.provider.AlbumArtContentProvider;
import com.cappielloantonio.tempo.repository.DownloadRepository;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
@@ -45,7 +47,7 @@ public class MappingUtil {
Uri artworkUri = null;
if (coverArtId != null) {
- artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, Preferences.getImageSize()));
+ artworkUri = AlbumArtContentProvider.contentUri(coverArtId);
}
Bundle bundle = new Bundle();
@@ -235,7 +237,7 @@ public class MappingUtil {
public static MediaItem mapMediaItem(PodcastEpisode podcastEpisode) {
Uri uri = getUri(podcastEpisode);
- Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(podcastEpisode.getCoverArtId(), Preferences.getImageSize()));
+ Uri artworkUri = AlbumArtContentProvider.contentUri(podcastEpisode.getCoverArtId());
Bundle bundle = new Bundle();
bundle.putString("id", podcastEpisode.getId());