mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 06:28:27 +02:00
perf(audio): reuse media session worker and bound metadata retention
This commit is contained in:
1 parent
9c0b22e2c8
commit
a57b4504de
5 files changed
+234
-34
No files matched your search
@@ -73,4 +73,5 @@ android {
|
||||
dependencies {
|
||||
implementation("androidx.media:media:1.8.0")
|
||||
implementation("androidx.core:core:1.19.1")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
}
|
||||
Vendored
+29
-14
@@ -42,7 +42,6 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -100,11 +99,12 @@ public class AudioService extends MediaBrowserServiceCompat {
|
||||
| PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
|
||||
| PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED;
|
||||
|
||||
static AudioService instance;
|
||||
static volatile AudioService instance;
|
||||
private volatile boolean destroyed;
|
||||
private static PendingIntent contentIntent;
|
||||
private static ServiceListener listener;
|
||||
private static List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
|
||||
private static final Map<String, MediaMetadataCompat> mediaMetadataCache = new HashMap<>();
|
||||
private static final PinnedMetadataCache<MediaMetadataCompat> mediaMetadataCache = new PinnedMetadataCache<>(256);
|
||||
|
||||
public static void init(ServiceListener listener) {
|
||||
AudioService.listener = listener;
|
||||
@@ -164,7 +164,10 @@ public class AudioService extends MediaBrowserServiceCompat {
|
||||
}
|
||||
}
|
||||
MediaMetadataCompat mediaMetadata = builder.build();
|
||||
mediaMetadataCache.put(mediaId, mediaMetadata);
|
||||
synchronized (mediaMetadataCache) {
|
||||
if (destroyed) throw new IllegalStateException("Audio service is no longer attached");
|
||||
mediaMetadataCache.put(mediaId, mediaMetadata);
|
||||
}
|
||||
return mediaMetadata;
|
||||
}
|
||||
|
||||
@@ -359,6 +362,10 @@ public class AudioService extends MediaBrowserServiceCompat {
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
synchronized (mediaMetadataCache) {
|
||||
destroyed = true;
|
||||
mediaMetadataCache.clear();
|
||||
}
|
||||
super.onDestroy();
|
||||
if (listener != null) {
|
||||
listener.onDestroy();
|
||||
@@ -367,7 +374,6 @@ public class AudioService extends MediaBrowserServiceCompat {
|
||||
mediaMetadata = null;
|
||||
artBitmap = null;
|
||||
queue.clear();
|
||||
mediaMetadataCache.clear();
|
||||
controls.clear();
|
||||
artBitmapCache.evictAll();
|
||||
compactActionIndices = null;
|
||||
@@ -800,7 +806,11 @@ public class AudioService extends MediaBrowserServiceCompat {
|
||||
* Updates queue.
|
||||
* Gets called from background thread.
|
||||
*/
|
||||
synchronized void setQueue(List<MediaSessionCompat.QueueItem> queue) {
|
||||
synchronized void setQueue(List<MediaSessionCompat.QueueItem> queue, Map<String, MediaMetadataCompat> metadata) {
|
||||
synchronized (mediaMetadataCache) {
|
||||
if (destroyed) throw new IllegalStateException("Audio service is no longer attached");
|
||||
mediaMetadataCache.setQueue(metadata);
|
||||
}
|
||||
AudioService.queue = queue;
|
||||
mediaSession.setQueue(queue);
|
||||
}
|
||||
@@ -822,6 +832,10 @@ public class AudioService extends MediaBrowserServiceCompat {
|
||||
* - https://9to5google.com/2020/08/02/android-11-lockscreen-art/
|
||||
*/
|
||||
synchronized void setMetadata(MediaMetadataCompat mediaMetadata) {
|
||||
synchronized (mediaMetadataCache) {
|
||||
if (destroyed) throw new IllegalStateException("Audio service is no longer attached");
|
||||
mediaMetadataCache.setCurrent(mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID), mediaMetadata);
|
||||
}
|
||||
String artCacheFilePath = mediaMetadata.getString("artCacheFile");
|
||||
if (artCacheFilePath != null) {
|
||||
// Load local files and network images, cached in files
|
||||
@@ -838,6 +852,7 @@ public class AudioService extends MediaBrowserServiceCompat {
|
||||
artBitmap = null;
|
||||
}
|
||||
}
|
||||
if (destroyed) throw new IllegalStateException("Audio service is no longer attached");
|
||||
this.mediaMetadata = mediaMetadata;
|
||||
mediaSession.setMetadata(mediaMetadata);
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
@@ -907,19 +922,19 @@ public class AudioService extends MediaBrowserServiceCompat {
|
||||
@Override
|
||||
public void onAddQueueItem(MediaDescriptionCompat description) {
|
||||
if (listener == null) return;
|
||||
listener.onAddQueueItem(getMediaMetadata(description.getMediaId()));
|
||||
listener.onAddQueueItem(description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddQueueItem(MediaDescriptionCompat description, int index) {
|
||||
if (listener == null) return;
|
||||
listener.onAddQueueItemAt(getMediaMetadata(description.getMediaId()), index);
|
||||
listener.onAddQueueItemAt(description, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveQueueItem(MediaDescriptionCompat description) {
|
||||
if (listener == null) return;
|
||||
listener.onRemoveQueueItem(getMediaMetadata(description.getMediaId()));
|
||||
listener.onRemoveQueueItem(description);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1141,7 +1156,7 @@ public class AudioService extends MediaBrowserServiceCompat {
|
||||
|
||||
public void onPlayMediaItem(final MediaDescriptionCompat description) {
|
||||
if (listener == null) return;
|
||||
listener.onPlayMediaItem(getMediaMetadata(description.getMediaId()));
|
||||
listener.onPlayMediaItem(description);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1172,9 +1187,9 @@ public class AudioService extends MediaBrowserServiceCompat {
|
||||
void onSetRepeatMode(int repeatMode);
|
||||
void onSetShuffleMode(int shuffleMode);
|
||||
void onCustomAction(String action, Bundle extras);
|
||||
void onAddQueueItem(MediaMetadataCompat metadata);
|
||||
void onAddQueueItemAt(MediaMetadataCompat metadata, int index);
|
||||
void onRemoveQueueItem(MediaMetadataCompat metadata);
|
||||
void onAddQueueItem(MediaDescriptionCompat description);
|
||||
void onAddQueueItemAt(MediaDescriptionCompat description, int index);
|
||||
void onRemoveQueueItem(MediaDescriptionCompat description);
|
||||
void onRemoveQueueItemAt(int index);
|
||||
void onSetPlaybackSpeed(float speed);
|
||||
void onSetCaptioningEnabled(boolean enabled);
|
||||
@@ -1185,7 +1200,7 @@ public class AudioService extends MediaBrowserServiceCompat {
|
||||
// NON-STANDARD METHODS
|
||||
//
|
||||
|
||||
void onPlayMediaItem(MediaMetadataCompat metadata);
|
||||
void onPlayMediaItem(MediaDescriptionCompat description);
|
||||
void onTaskRemoved();
|
||||
void onClose();
|
||||
void onDestroy();
|
||||
|
||||
Vendored
+84
-20
@@ -34,6 +34,7 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
@@ -131,6 +132,7 @@ public class AudioServicePlugin implements FlutterPlugin, ActivityAware {
|
||||
|
||||
private static final String CHANNEL_CLIENT = "com.ryanheise.audio_service.client.methods";
|
||||
private static final String CHANNEL_HANDLER = "com.ryanheise.audio_service.handler.methods";
|
||||
private static final String DESCRIPTION_METADATA = "com.ryanheise.audio_service.metadata";
|
||||
|
||||
private static final Set<ClientInterface> clientInterfaces = new HashSet<>();
|
||||
private static ClientInterface mainClientInterface;
|
||||
@@ -520,6 +522,8 @@ public class AudioServicePlugin implements FlutterPlugin, ActivityAware {
|
||||
public MethodChannel channel;
|
||||
private AudioTrack silenceAudioTrack;
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private final ExecutorService mediaWorker = Executors.newSingleThreadExecutor();
|
||||
private volatile boolean destroyed;
|
||||
private List<MethodInvocation> methodInvocationQueue = new LinkedList<MethodInvocation>();
|
||||
|
||||
public AudioHandlerInterface(BinaryMessenger messenger) {
|
||||
@@ -702,8 +706,8 @@ public class AudioServicePlugin implements FlutterPlugin, ActivityAware {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayMediaItem(MediaMetadataCompat metadata) {
|
||||
invokeMethod("playMediaItem", mapOf("mediaItem", mediaMetadata2raw(metadata)));
|
||||
public void onPlayMediaItem(MediaDescriptionCompat description) {
|
||||
invokeMediaItemMethod("playMediaItem", description, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -712,20 +716,48 @@ public class AudioServicePlugin implements FlutterPlugin, ActivityAware {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddQueueItem(MediaMetadataCompat metadata) {
|
||||
invokeMethod("addQueueItem", mapOf("mediaItem", mediaMetadata2raw(metadata)));
|
||||
public void onAddQueueItem(MediaDescriptionCompat description) {
|
||||
invokeMediaItemMethod("addQueueItem", description, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddQueueItemAt(MediaMetadataCompat metadata, int index) {
|
||||
invokeMethod("insertQueueItem", mapOf(
|
||||
"mediaItem", mediaMetadata2raw(metadata),
|
||||
"index", index));
|
||||
public void onAddQueueItemAt(MediaDescriptionCompat description, int index) {
|
||||
invokeMediaItemMethod("insertQueueItem", description, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveQueueItem(MediaMetadataCompat metadata) {
|
||||
invokeMethod("removeQueueItem", mapOf("mediaItem", mediaMetadata2raw(metadata)));
|
||||
public void onRemoveQueueItem(MediaDescriptionCompat description) {
|
||||
invokeMediaItemMethod("removeQueueItem", description, null);
|
||||
}
|
||||
|
||||
private void invokeMediaItemMethod(String method, MediaDescriptionCompat description, Integer index) {
|
||||
MediaMetadataCompat metadata = AudioService.getMediaMetadata(description.getMediaId());
|
||||
if (metadata == null && description.getExtras() != null) {
|
||||
metadata = description.getExtras().getParcelable(DESCRIPTION_METADATA);
|
||||
}
|
||||
if (metadata != null) {
|
||||
sendMediaItemMethod(method, mediaMetadata2raw(metadata), index);
|
||||
return;
|
||||
}
|
||||
// A browser can retain a description after its metadata leaves the
|
||||
// recent cache. Resolve it through the same Dart API as onLoadItem.
|
||||
invokeMethod("getMediaItem", mapOf("mediaId", description.getMediaId()), new Result() {
|
||||
@Override
|
||||
public void success(Object value) {
|
||||
if (destroyed || !(value instanceof Map)) return;
|
||||
Object item = ((Map<?, ?>)value).get("mediaItem");
|
||||
if (item instanceof Map) sendMediaItemMethod(method, (Map<?, ?>)item, index);
|
||||
}
|
||||
@Override
|
||||
public void error(String code, String message, Object details) { }
|
||||
@Override
|
||||
public void notImplemented() { }
|
||||
});
|
||||
}
|
||||
|
||||
private void sendMediaItemMethod(String method, Map<?, ?> item, Integer index) {
|
||||
invokeMethod(method, index == null ? mapOf("mediaItem", item)
|
||||
: mapOf("mediaItem", item, "index", index));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -835,11 +867,15 @@ public class AudioServicePlugin implements FlutterPlugin, ActivityAware {
|
||||
Map<?, ?> args = (Map<?, ?>)call.arguments;
|
||||
switch (call.method) {
|
||||
case "setMediaItem": {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
final AudioService service = AudioService.instance;
|
||||
mediaWorker.execute(() -> {
|
||||
try {
|
||||
if (destroyed || service == null || AudioService.instance != service) {
|
||||
throw new IllegalStateException("Audio service is no longer attached");
|
||||
}
|
||||
Map<?, ?> rawMediaItem = (Map<?, ?>)args.get("mediaItem");
|
||||
MediaMetadataCompat mediaMetadata = createMediaMetadata(rawMediaItem);
|
||||
AudioService.instance.setMetadata(mediaMetadata);
|
||||
MediaMetadataCompat mediaMetadata = createMediaMetadata(rawMediaItem, service);
|
||||
service.setMetadata(mediaMetadata);
|
||||
handler.post(() -> result.success(null));
|
||||
} catch (Exception e) {
|
||||
handler.post(() -> {
|
||||
@@ -850,11 +886,16 @@ public class AudioServicePlugin implements FlutterPlugin, ActivityAware {
|
||||
break;
|
||||
}
|
||||
case "setQueue": {
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
final AudioService service = AudioService.instance;
|
||||
mediaWorker.execute(() -> {
|
||||
try {
|
||||
if (destroyed || service == null || AudioService.instance != service) {
|
||||
throw new IllegalStateException("Audio service is no longer attached");
|
||||
}
|
||||
@SuppressWarnings("unchecked") List<Map<?, ?>> rawQueue = (List<Map<?, ?>>) args.get("queue");
|
||||
List<MediaSessionCompat.QueueItem> queue = raw2queue(rawQueue);
|
||||
AudioService.instance.setQueue(queue);
|
||||
Map<String, MediaMetadataCompat> metadata = new HashMap<>();
|
||||
List<MediaSessionCompat.QueueItem> queue = raw2queue(rawQueue, metadata, service);
|
||||
service.setQueue(queue, metadata);
|
||||
handler.post(() -> result.success(null));
|
||||
} catch (Exception e) {
|
||||
handler.post(() -> {
|
||||
@@ -1016,6 +1057,9 @@ public class AudioServicePlugin implements FlutterPlugin, ActivityAware {
|
||||
}
|
||||
|
||||
private void destroy() {
|
||||
destroyed = true;
|
||||
channel.setMethodCallHandler(null);
|
||||
mediaWorker.shutdown();
|
||||
if (silenceAudioTrack != null)
|
||||
silenceAudioTrack.release();
|
||||
}
|
||||
@@ -1129,8 +1173,12 @@ public class AudioServicePlugin implements FlutterPlugin, ActivityAware {
|
||||
}
|
||||
|
||||
private static MediaMetadataCompat createMediaMetadata(Map<?, ?> rawMediaItem) {
|
||||
return createMediaMetadata(rawMediaItem, AudioService.instance);
|
||||
}
|
||||
|
||||
private static MediaMetadataCompat createMediaMetadata(Map<?, ?> rawMediaItem, AudioService service) {
|
||||
//noinspection unchecked
|
||||
return AudioService.instance.createMediaMetadata(
|
||||
return service.createMediaMetadata(
|
||||
(String)rawMediaItem.get("id"),
|
||||
(String)rawMediaItem.get("title"),
|
||||
(String)rawMediaItem.get("album"),
|
||||
@@ -1178,16 +1226,32 @@ public class AudioServicePlugin implements FlutterPlugin, ActivityAware {
|
||||
|
||||
private static MediaBrowserCompat.MediaItem rawToMediaItem(Map<?, ?> rawMediaItem) {
|
||||
MediaMetadataCompat mediaMetadata = createMediaMetadata(rawMediaItem);
|
||||
final MediaDescriptionCompat description = addExtrasToMediaDescription(mediaMetadata.getDescription(), (Map<?, ?>)rawMediaItem.get("extras"));
|
||||
MediaDescriptionCompat description = addExtrasToMediaDescription(mediaMetadata.getDescription(), (Map<?, ?>)rawMediaItem.get("extras"));
|
||||
// Browser descriptions can outlive the bounded server cache. Keep their
|
||||
// metadata with the returned item instead of retaining every browsed ID.
|
||||
Bundle extras = new Bundle();
|
||||
if (description.getExtras() != null) extras.putAll(description.getExtras());
|
||||
extras.putParcelable(DESCRIPTION_METADATA, mediaMetadata);
|
||||
description = new MediaDescriptionCompat.Builder()
|
||||
.setMediaId(description.getMediaId())
|
||||
.setTitle(description.getTitle())
|
||||
.setSubtitle(description.getSubtitle())
|
||||
.setDescription(description.getDescription())
|
||||
.setIconBitmap(description.getIconBitmap())
|
||||
.setIconUri(description.getIconUri())
|
||||
.setMediaUri(description.getMediaUri())
|
||||
.setExtras(extras)
|
||||
.build();
|
||||
final Boolean playable = (Boolean)rawMediaItem.get("playable");
|
||||
return new MediaBrowserCompat.MediaItem(description, playable ? MediaBrowserCompat.MediaItem.FLAG_PLAYABLE : MediaBrowserCompat.MediaItem.FLAG_BROWSABLE);
|
||||
}
|
||||
|
||||
private static List<MediaSessionCompat.QueueItem> raw2queue(List<Map<?, ?>> rawQueue) {
|
||||
private static List<MediaSessionCompat.QueueItem> raw2queue(List<Map<?, ?>> rawQueue, Map<String, MediaMetadataCompat> metadata, AudioService service) {
|
||||
List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
|
||||
int i = 0;
|
||||
for (Map<?, ?> rawMediaItem : rawQueue) {
|
||||
MediaMetadataCompat mediaMetadata = createMediaMetadata(rawMediaItem);
|
||||
MediaMetadataCompat mediaMetadata = createMediaMetadata(rawMediaItem, service);
|
||||
metadata.put(mediaMetadata.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID), mediaMetadata);
|
||||
MediaDescriptionCompat description = addExtrasToMediaDescription(mediaMetadata.getDescription(), (Map<?, ?>)rawMediaItem.get("extras"));
|
||||
queue.add(new MediaSessionCompat.QueueItem(description, i));
|
||||
i++;
|
||||
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
package com.ryanheise.audioservice;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** Keeps current/queue items while bounding metadata retained from older browsers. */
|
||||
final class PinnedMetadataCache<T> {
|
||||
private final int recentLimit;
|
||||
private final Map<String, T> recent = new LinkedHashMap<>(16, 0.75f, true);
|
||||
private Map<String, T> queue = new HashMap<>();
|
||||
private String currentId;
|
||||
private T current;
|
||||
|
||||
PinnedMetadataCache(int recentLimit) {
|
||||
if (recentLimit < 1) throw new IllegalArgumentException("recentLimit must be positive");
|
||||
this.recentLimit = recentLimit;
|
||||
}
|
||||
|
||||
synchronized void put(String id, T metadata) {
|
||||
recent.put(id, metadata);
|
||||
while (recent.size() > recentLimit) {
|
||||
recent.remove(recent.keySet().iterator().next());
|
||||
}
|
||||
}
|
||||
|
||||
synchronized T get(String id) {
|
||||
if (id != null && id.equals(currentId)) return current;
|
||||
T metadata = queue.get(id);
|
||||
return metadata != null ? metadata : recent.get(id);
|
||||
}
|
||||
|
||||
synchronized void setCurrent(String id, T metadata) {
|
||||
currentId = id;
|
||||
current = metadata;
|
||||
}
|
||||
|
||||
synchronized void setQueue(Map<String, T> metadata) {
|
||||
queue = new HashMap<>(metadata);
|
||||
}
|
||||
|
||||
synchronized void clear() {
|
||||
recent.clear();
|
||||
queue.clear();
|
||||
currentId = null;
|
||||
current = null;
|
||||
}
|
||||
|
||||
synchronized int recentSize() {
|
||||
return recent.size();
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.ryanheise.audioservice;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
public class PinnedMetadataCacheTest {
|
||||
@Test
|
||||
public void queueLargerThanRecentLimitAndCurrentStayAvailableAfterChurn() {
|
||||
PinnedMetadataCache<String> cache = new PinnedMetadataCache<>(2);
|
||||
Map<String, String> queue = new HashMap<>();
|
||||
for (int i = 0; i < 10; i++) queue.put("queue-" + i, "metadata-" + i);
|
||||
cache.setQueue(queue);
|
||||
queue.clear();
|
||||
cache.setCurrent("playing", "current");
|
||||
for (int i = 0; i < 1000; i++) cache.put("browser-" + i, "old");
|
||||
for (int i = 0; i < 10; i++) assertEquals("metadata-" + i, cache.get("queue-" + i));
|
||||
assertEquals("current", cache.get("playing"));
|
||||
assertEquals(2, cache.recentSize());
|
||||
cache.setQueue(new HashMap<>());
|
||||
assertNull(cache.get("queue-0"));
|
||||
cache.clear();
|
||||
assertNull(cache.get("playing"));
|
||||
assertEquals(0, cache.recentSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recentBrowserReadsRefreshEvictionOrder() {
|
||||
PinnedMetadataCache<String> cache = new PinnedMetadataCache<>(2);
|
||||
cache.put("first", "a");
|
||||
cache.put("second", "b");
|
||||
assertEquals("a", cache.get("first"));
|
||||
cache.put("third", "c");
|
||||
assertNull(cache.get("second"));
|
||||
assertEquals("a", cache.get("first"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentBrowserAndWorkerWritesRemainBounded() throws Exception {
|
||||
PinnedMetadataCache<String> cache = new PinnedMetadataCache<>(16);
|
||||
cache.setCurrent("playing", "current");
|
||||
ExecutorService workers = Executors.newFixedThreadPool(4);
|
||||
List<Future<?>> results = new ArrayList<>();
|
||||
try {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
final String id = "item-" + i;
|
||||
results.add(workers.submit(() -> {
|
||||
cache.put(id, id);
|
||||
cache.get(id);
|
||||
}));
|
||||
}
|
||||
} finally {
|
||||
workers.shutdown();
|
||||
}
|
||||
org.junit.Assert.assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
for (Future<?> result : results) result.get();
|
||||
assertEquals(16, cache.recentSize());
|
||||
assertEquals("current", cache.get("playing"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user