Skip to content

Commit 4c38322

Browse files
authored
Merge pull request #199 from tobexyz/chore/stabelize
Chore/stabelize
2 parents 98abc79 + 6b06651 commit 4c38322

41 files changed

Lines changed: 3137 additions & 591 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

yaacc/build.gradle

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ dependencies {
4242
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
4343
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
4444
implementation 'org.mp4parser:isoparser:1.9.56'
45-
//https://developer.android.com/jetpack/androidx/migrate/artifact-mappings
46-
//FIXMEimplementation 'androidx.documentfile:documentfile:1.1.0'
47-
//https://medium.com/swlh/sample-for-android-storage-access-framework-aka-scoped-storage-for-basic-use-cases-3ee4fee404fc
45+
4846
}
4947

5048
android {

yaacc/src/main/java/de/yaacc/Yaacc.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import de.yaacc.player.PlayerService;
4646
import de.yaacc.upnp.UpnpClient;
4747
import de.yaacc.upnp.server.YaaccUpnpServerService;
48+
import de.yaacc.util.SAFCacheManager;
4849
import de.yaacc.util.NotificationId;
4950
import de.yaacc.util.SafPermissionManager;
5051
import de.yaacc.util.ShutdownTimerListener;
@@ -145,11 +146,8 @@ public void exit() {
145146
}
146147

147148
private void clearCache() {
148-
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
149-
Set<String> tobeDeleted = preferences.getAll().keySet().stream().filter(k -> k.startsWith(getApplicationContext().getString(R.string.settings_duration_format_key))).collect(Collectors.toSet());
150-
SharedPreferences.Editor edit = preferences.edit();
151-
tobeDeleted.forEach(it -> edit.remove(it));
152-
edit.commit();
149+
// Trim cache to recommended size using LRU
150+
SAFCacheManager.getInstance(this).trimCache();
153151
}
154152

155153
public void createNotificationChannel() {

yaacc/src/main/java/de/yaacc/browser/BrowseContentItemAdapter.java

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ public class BrowseContentItemAdapter extends RecyclerView.Adapter<BrowseContent
7373
private ContentListFragment contentListFragment;
7474
private RecyclerView contentList;
7575
private ProgressBar progressBar;
76+
private SharedPreferences sharedPreferences;
77+
private boolean showThumbnails;
7678

7779

7880
public BrowseContentItemAdapter(ContentListFragment contentListFragment, RecyclerView contentList, UpnpClient upnpClient, ProgressBar progressBar) {
@@ -83,6 +85,9 @@ public BrowseContentItemAdapter(ContentListFragment contentListFragment, Recycle
8385
asyncTasks = new ArrayList<>();
8486
allItemsFetched = false;
8587
this.upnpClient = upnpClient;
88+
// Cache SharedPreferences lookup
89+
this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
90+
this.showThumbnails = sharedPreferences.getBoolean(context.getString(R.string.settings_thumbnails_chkbx), true);
8691
}
8792

8893
@Override
@@ -209,9 +214,6 @@ public BrowseContentItemAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
209214

210215
@Override
211216
public void onBindViewHolder(final BrowseContentItemAdapter.ViewHolder holder, final int listPosition) {
212-
SharedPreferences preferences = PreferenceManager
213-
.getDefaultSharedPreferences(context);
214-
215217
DIDLObject currentObject = (DIDLObject) getItem(listPosition);
216218
holder.name.setText(currentObject.getTitle());
217219

@@ -253,9 +255,7 @@ public void onBindViewHolder(final BrowseContentItemAdapter.ViewHolder holder, f
253255
holder.play.setVisibility(View.VISIBLE);
254256
holder.download.setVisibility(View.VISIBLE);
255257
holder.playlistAdd.setVisibility(View.VISIBLE);
256-
if (preferences.getBoolean(
257-
context.getString(R.string.settings_thumbnails_chkbx),
258-
true)) {
258+
if (showThumbnails) {
259259
DIDLObject.Property<URI> albumArtProperties = ((AudioItem) currentObject)
260260
.getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class);
261261
if (null != albumArtProperties) {
@@ -270,21 +270,17 @@ public void onBindViewHolder(final BrowseContentItemAdapter.ViewHolder holder, f
270270
holder.play.setVisibility(View.VISIBLE);
271271
holder.download.setVisibility(View.VISIBLE);
272272
holder.playlistAdd.setVisibility(View.GONE);
273-
if (preferences.getBoolean(
274-
context.getString(R.string.settings_thumbnails_chkbx),
275-
true))
273+
if (showThumbnails)
276274
iconDownloadTask.executeOnExecutor(((Yaacc) getContext().getApplicationContext()).getContentLoadExecutor(),
277-
Uri.parse(((ImageItem) currentObject)
275+
Uri.parse(currentObject
278276
.getFirstResource().getValue()));
279277
} else if (currentObject instanceof VideoItem) {
280278
holder.icon.setImageDrawable(ThemeHelper.tintDrawable(getContext().getResources().getDrawable(R.drawable.ic_baseline_movie_48, getContext().getTheme()), getContext().getTheme()));
281279
holder.playAll.setVisibility(View.VISIBLE);
282280
holder.play.setVisibility(View.VISIBLE);
283281
holder.download.setVisibility(View.VISIBLE);
284282
holder.playlistAdd.setVisibility(View.VISIBLE);
285-
if (preferences.getBoolean(
286-
context.getString(R.string.settings_thumbnails_chkbx),
287-
true)) {
283+
if (showThumbnails) {
288284
DIDLObject.Property<URI> albumArtProperties = ((VideoItem) currentObject)
289285
.getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class);
290286
if (null != albumArtProperties) {

yaacc/src/main/java/de/yaacc/player/AVTransportPlayer.java

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import android.content.Intent;
2525
import android.content.SharedPreferences;
2626
import android.graphics.Bitmap;
27+
import android.graphics.BitmapFactory;
2728
import android.net.Uri;
2829
import android.os.Handler;
2930
import android.os.IBinder;
@@ -32,14 +33,20 @@
3233
import android.support.v4.media.session.MediaSessionCompat;
3334
import android.widget.Toast;
3435

36+
import androidx.annotation.Nullable;
3537
import androidx.media3.common.MediaItem;
3638
import androidx.media3.common.Player;
39+
import androidx.media3.common.util.BitmapLoader;
3740
import androidx.media3.common.util.UnstableApi;
3841
import androidx.media3.session.MediaSession;
3942
import androidx.media3.session.SessionCommand;
4043
import androidx.media3.session.SessionCommands;
4144
import androidx.media3.ui.PlayerNotificationManager;
4245

46+
import com.google.common.util.concurrent.Futures;
47+
import com.google.common.util.concurrent.ListenableFuture;
48+
import com.google.common.util.concurrent.SettableFuture;
49+
4350
import org.fourthline.cling.model.action.ActionInvocation;
4451
import org.fourthline.cling.model.message.UpnpResponse;
4552
import org.fourthline.cling.model.meta.Device;
@@ -89,6 +96,7 @@
8996
import de.yaacc.upnp.server.http.YaaccUpnpServerContentHttpHandler;
9097
import de.yaacc.util.InterfaceResolutionHelper;
9198
import de.yaacc.util.YaaccLogger;
99+
import de.yaacc.util.image.IconDownloadCacheHandler;
92100
import de.yaacc.util.image.ImageDownloader;
93101

94102
/**
@@ -113,7 +121,7 @@ public class AVTransportPlayer extends AbstractPlayer {
113121
private int consecutivePositionFailures = 0;
114122

115123
// Retry tracking for critical commands
116-
private static final int MAX_RETRIES = 3;
124+
private static final int MAX_RETRIES = 30;
117125
private final Map<String, Integer> commandRetries = new HashMap<>();
118126

119127

@@ -150,10 +158,62 @@ public AVTransportPlayer(UpnpClient upnpClient) {
150158

151159
// Initialize Media3 Player wrapper
152160
playerWrapper = new AVTransportPlayerWrapper(this, null);
161+
BitmapLoader bitmapLoader = new BitmapLoader() {
162+
@Override
163+
public ListenableFuture<Bitmap> decodeBitmap(byte[] data) {
164+
SettableFuture<Bitmap> future = SettableFuture.create();
165+
try {
166+
Bitmap bitmap = android.graphics.BitmapFactory.decodeByteArray(data, 0, data.length);
167+
future.set(bitmap);
168+
} catch (Exception e) {
169+
future.setException(e);
170+
}
171+
return future;
172+
}
173+
174+
@Override
175+
public ListenableFuture<Bitmap> loadBitmap(Uri uri) {
176+
return loadBitmap(uri, null);
177+
}
178+
179+
@Override
180+
public ListenableFuture<Bitmap> loadBitmap(Uri uri, @Nullable BitmapFactory.Options options) {
181+
YaaccLogger.e(getClass().getName(), "BitmapLoader.loadBitmap called with uri: " + uri);
182+
183+
// Check cache first
184+
IconDownloadCacheHandler cache = IconDownloadCacheHandler.getInstance();
185+
Bitmap cachedBitmap = cache.getBitmap(uri, 512, 512);
186+
if (cachedBitmap != null) {
187+
YaaccLogger.e(getClass().getName(), "Returning cached bitmap: " + cachedBitmap.getWidth() + "x" + cachedBitmap.getHeight());
188+
return Futures.immediateFuture(cachedBitmap);
189+
}
190+
191+
SettableFuture<Bitmap> future = SettableFuture.create();
192+
// Load bitmap in background using ImageDownloader
193+
((Yaacc) getContext().getApplicationContext()).getContentLoadExecutor().execute(() -> {
194+
try {
195+
YaaccLogger.e(getClass().getName(), "Loading bitmap from: " + uri);
196+
Bitmap bitmap = new ImageDownloader().retrieveImageWithCertainSize(uri, 512, 512);
197+
if (bitmap != null) {
198+
cache.addBitmap(uri, 512, 512, bitmap);
199+
}
200+
YaaccLogger.e(getClass().getName(), "Bitmap loaded: " + (bitmap != null ? bitmap.getWidth() + "x" + bitmap.getHeight() : "null"));
201+
future.set(bitmap);
202+
YaaccLogger.e(getClass().getName(), "Future.set() called");
203+
} catch (Exception e) {
204+
YaaccLogger.e(getClass().getName(), "Failed to load bitmap", e);
205+
future.setException(e);
206+
}
207+
});
208+
return future;
209+
}
210+
153211

212+
};
154213
// Create Media3 MediaSession for the wrapper
155214
media3Session = new MediaSession.Builder(getContext(), playerWrapper)
156215
.setId("avtransport_" + id)
216+
// Don't set BitmapLoader - let notification manager handle it via getCurrentLargeIcon()
157217
.setCallback(new MediaSession.Callback() {
158218
@Override
159219
public MediaSession.ConnectionResult onConnect(MediaSession session,
@@ -210,6 +270,40 @@ public CharSequence getCurrentContentText(Player player) {
210270
@Override
211271
public Bitmap getCurrentLargeIcon(Player player,
212272
PlayerNotificationManager.BitmapCallback callback) {
273+
// Get album art URI from AVTransportPlayer (includes cover.jpg fallback)
274+
URI albumArtJavaUri = getAlbumArt();
275+
YaaccLogger.e(getClass().getName(), "getCurrentLargeIcon called, albumArtUri: " + albumArtJavaUri);
276+
277+
if (albumArtJavaUri != null) {
278+
android.net.Uri artworkUri = android.net.Uri.parse(albumArtJavaUri.toString());
279+
280+
// Check cache first - return immediately if available
281+
IconDownloadCacheHandler cache = IconDownloadCacheHandler.getInstance();
282+
Bitmap cachedBitmap = cache.getBitmap(artworkUri, 512, 512);
283+
if (cachedBitmap != null) {
284+
YaaccLogger.e(getClass().getName(), "Returning cached bitmap synchronously: " + cachedBitmap.getWidth() + "x" + cachedBitmap.getHeight());
285+
return cachedBitmap;
286+
}
287+
288+
// Load bitmap in background thread and use callback
289+
((Yaacc) getContext().getApplicationContext()).getContentLoadExecutor().execute(() -> {
290+
try {
291+
YaaccLogger.e(getClass().getName(), "Loading bitmap from: " + artworkUri);
292+
Bitmap bitmap = new ImageDownloader().retrieveImageWithCertainSize(artworkUri, 512, 512);
293+
if (bitmap != null) {
294+
cache.addBitmap(artworkUri, 512, 512, bitmap);
295+
YaaccLogger.e(getClass().getName(), "Bitmap loaded, calling callback: " + bitmap.getWidth() + "x" + bitmap.getHeight());
296+
callback.onBitmap(bitmap);
297+
} else {
298+
YaaccLogger.e(getClass().getName(), "Bitmap is null");
299+
}
300+
} catch (Exception e) {
301+
YaaccLogger.e(getClass().getName(), "Failed to load album art", e);
302+
}
303+
});
304+
} else {
305+
YaaccLogger.e(getClass().getName(), "albumArtUri is null");
306+
}
213307
return null;
214308
}
215309
})
@@ -631,6 +725,11 @@ private void proceedWithSetURI(PlayableItem playableItem, Service<?, ?> service)
631725
}
632726
DIDLObject.Property<URI> albumArtUriProperty = playableItem.getItem() == null ? null : playableItem.getItem().getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class);
633727
albumArtUri = (albumArtUriProperty == null) ? null : albumArtUriProperty.getValue();
728+
729+
// Trigger notification update with new album art
730+
if (albumArtUri != null) {
731+
updateMetadataInternal();
732+
}
634733

635734
InternalSetAVTransportURI setAVTransportURI = new InternalSetAVTransportURI(
636735
service, modifyProxyUrlWithDeviceId(playableItem.getUri().toString()), actionState, metadata,
@@ -910,6 +1009,35 @@ public void success(ActionInvocation actioninvocation) {
9101009
executorService.execute(actionCallback);
9111010
}
9121011

1012+
@Override
1013+
public Bitmap getIcon() {
1014+
// Try to get album art from cache only (don't block on download)
1015+
if (albumArtUri != null) {
1016+
IconDownloadCacheHandler cache = IconDownloadCacheHandler.getInstance();
1017+
Bitmap albumArt = cache.getBitmap(android.net.Uri.parse(albumArtUri.toString()), 512, 512);
1018+
if (albumArt != null) {
1019+
return albumArt;
1020+
}
1021+
1022+
// Trigger async download for next notification update
1023+
android.net.Uri artworkUri = android.net.Uri.parse(albumArtUri.toString());
1024+
((Yaacc) getContext().getApplicationContext()).getContentLoadExecutor().execute(() -> {
1025+
try {
1026+
Bitmap bitmap = new ImageDownloader().retrieveImageWithCertainSize(artworkUri, 512, 512);
1027+
if (bitmap != null) {
1028+
cache.addBitmap(artworkUri, 512, 512, bitmap);
1029+
// Trigger notification update by updating metadata
1030+
updateMetadataInternal();
1031+
}
1032+
} catch (Exception e) {
1033+
YaaccLogger.w(getClass().getName(), "Failed to load album art", e);
1034+
}
1035+
});
1036+
}
1037+
// Fall back to device icon
1038+
return super.getIcon();
1039+
}
1040+
9131041
@Override
9141042
protected void doResume() {
9151043
// For UPnP, just send Play command to resume from current position
@@ -1018,8 +1146,10 @@ public void received(ActionInvocation actioninvocation, TransportInfo info) {
10181146
return;
10191147
}
10201148

1021-
// If not playing and we haven't exceeded retry limit, try Play command again
1022-
if (info.getCurrentTransportState() != TransportState.PLAYING && playRetryCount < MAX_PLAY_RETRIES) {
1149+
// Only retry Play if we think we should be playing (not paused by user)
1150+
if (info.getCurrentTransportState() != TransportState.PLAYING &&
1151+
isPlaying() &&
1152+
playRetryCount < MAX_RETRIES) {
10231153
playRetryCount++;
10241154
YaaccLogger.d(getClass().getName(), "Renderer not playing, sending Play command again (attempt " + playRetryCount + ")");
10251155
executeCommand(new TimerTask() {
@@ -1088,7 +1218,7 @@ protected void getPositionInfo() {
10881218

10891219
// Track device-not-found as position failure
10901220
consecutivePositionFailures++;
1091-
if (consecutivePositionFailures >= 3 && isPlaying()) {
1221+
if (consecutivePositionFailures >= MAX_RETRIES && isPlaying()) {
10921222
YaaccLogger.w(getClass().getName(), "Device lost, stopping playback");
10931223
consecutivePositionFailures = 0;
10941224
stop();
@@ -1122,7 +1252,7 @@ public void failure(ActionInvocation actioninvocation,
11221252
YaaccLogger.w(getClass().getName(), "Position query failed " + consecutivePositionFailures + " times");
11231253

11241254
// After 3 consecutive failures, check device state to see if track ended
1125-
if (consecutivePositionFailures >= 3 && isPlaying()) {
1255+
if (consecutivePositionFailures >= MAX_RETRIES && isPlaying()) {
11261256
YaaccLogger.w(getClass().getName(), "Position query failed 3 times, checking transport state");
11271257
consecutivePositionFailures = 0;
11281258
getTransportInfo();

yaacc/src/main/java/de/yaacc/player/AVTransportPlayerActivity.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import android.net.Uri;
2525
import android.os.Bundle;
2626
import android.os.IBinder;
27-
import de.yaacc.util.YaaccLogger;
2827
import android.view.KeyEvent;
2928
import android.view.Menu;
3029
import android.view.MenuItem;
@@ -56,6 +55,7 @@
5655
import de.yaacc.util.AboutActivity;
5756
import de.yaacc.util.ThemeHelper;
5857
import de.yaacc.util.YaaccLogActivity;
58+
import de.yaacc.util.YaaccLogger;
5959
import de.yaacc.util.image.ImageDownloadTask;
6060

6161
/**
@@ -144,15 +144,15 @@ protected void onDestroy() {
144144

145145
protected void initialize() {
146146
Player player = getPlayer();
147-
ImageButton btnPrev = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlPrev);
148-
ImageButton btnNext = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlNext);
149-
ImageButton btnStop = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlStop);
150-
ImageButton btnPlay = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlPlay);
151-
ImageButton btnPause = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlPause);
152-
ImageButton btnPlaylist = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlPlaylist);
153-
ImageButton btnExit = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlExit);
154-
ImageButton btnFf = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlFastForward);
155-
ImageButton btnFr = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlFastRewind);
147+
ImageButton btnPrev = findViewById(R.id.avtransportPlayerActivityControlPrev);
148+
ImageButton btnNext = findViewById(R.id.avtransportPlayerActivityControlNext);
149+
ImageButton btnStop = findViewById(R.id.avtransportPlayerActivityControlStop);
150+
ImageButton btnPlay = findViewById(R.id.avtransportPlayerActivityControlPlay);
151+
ImageButton btnPause = findViewById(R.id.avtransportPlayerActivityControlPause);
152+
ImageButton btnPlaylist = findViewById(R.id.avtransportPlayerActivityControlPlaylist);
153+
ImageButton btnExit = findViewById(R.id.avtransportPlayerActivityControlExit);
154+
ImageButton btnFf = findViewById(R.id.avtransportPlayerActivityControlFastForward);
155+
ImageButton btnFr = findViewById(R.id.avtransportPlayerActivityControlFastRewind);
156156
if (player == null) {
157157
btnPrev.setActivated(false);
158158
btnNext.setActivated(false);
@@ -420,7 +420,7 @@ private void doSetTrackInfo() {
420420
position.setText(getPlayer().getPositionString());
421421
TextView next = findViewById(R.id.avtransportPlayerActivityNextItem);
422422
next.setText(getPlayer().getNextItemTitle());
423-
ImageView albumArtView = (ImageView) findViewById(R.id.avtransportPlayerActivityImageView);
423+
ImageView albumArtView = findViewById(R.id.avtransportPlayerActivityImageView);
424424
URI albumArtUri = getPlayer().getAlbumArt();
425425

426426
if (null != albumArtUri) {

0 commit comments

Comments
 (0)