mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 04:28:35 +00:00
Add a cache for GIFs.
This commit is contained in:
committed by
Cody Henthorne
parent
8e020c05f6
commit
c84de8fa60
@@ -18,7 +18,6 @@ package org.thoughtcrime.securesms;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.SystemClock;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -171,6 +170,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||||||
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
||||||
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
||||||
.addNonBlocking(EmojiSource::refresh)
|
.addNonBlocking(EmojiSource::refresh)
|
||||||
|
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||||
.addPostRender(this::initializeExpiringMessageManager)
|
.addPostRender(this::initializeExpiringMessageManager)
|
||||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
|||||||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
||||||
import org.thoughtcrime.securesms.util.Hex;
|
import org.thoughtcrime.securesms.util.Hex;
|
||||||
import org.thoughtcrime.securesms.util.IasKeyStore;
|
import org.thoughtcrime.securesms.util.IasKeyStore;
|
||||||
|
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
|
||||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||||
@@ -98,6 +99,7 @@ public class ApplicationDependencies {
|
|||||||
private static volatile TextSecureSessionStore sessionStore;
|
private static volatile TextSecureSessionStore sessionStore;
|
||||||
private static volatile TextSecurePreKeyStore preKeyStore;
|
private static volatile TextSecurePreKeyStore preKeyStore;
|
||||||
private static volatile SignalSenderKeyStore senderKeyStore;
|
private static volatile SignalSenderKeyStore senderKeyStore;
|
||||||
|
private static volatile GiphyMp4Cache giphyMp4Cache;
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
public static void init(@NonNull Application application, @NonNull Provider provider) {
|
public static void init(@NonNull Application application, @NonNull Provider provider) {
|
||||||
@@ -551,6 +553,17 @@ public class ApplicationDependencies {
|
|||||||
return senderKeyStore;
|
return senderKeyStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @NonNull GiphyMp4Cache getGiphyMp4Cache() {
|
||||||
|
if (giphyMp4Cache == null) {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
if (giphyMp4Cache == null) {
|
||||||
|
giphyMp4Cache = provider.provideGiphyMp4Cache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return giphyMp4Cache;
|
||||||
|
}
|
||||||
|
|
||||||
public interface Provider {
|
public interface Provider {
|
||||||
@NonNull GroupsV2Operations provideGroupsV2Operations();
|
@NonNull GroupsV2Operations provideGroupsV2Operations();
|
||||||
@NonNull SignalServiceAccountManager provideSignalServiceAccountManager();
|
@NonNull SignalServiceAccountManager provideSignalServiceAccountManager();
|
||||||
@@ -583,5 +596,6 @@ public class ApplicationDependencies {
|
|||||||
@NonNull TextSecureSessionStore provideSessionStore();
|
@NonNull TextSecureSessionStore provideSessionStore();
|
||||||
@NonNull TextSecurePreKeyStore providePreKeyStore();
|
@NonNull TextSecurePreKeyStore providePreKeyStore();
|
||||||
@NonNull SignalSenderKeyStore provideSenderKeyStore();
|
@NonNull SignalSenderKeyStore provideSenderKeyStore();
|
||||||
|
@NonNull GiphyMp4Cache provideGiphyMp4Cache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
|||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||||
@@ -284,6 +284,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
|||||||
return new SignalSenderKeyStore(context);
|
return new SignalSenderKeyStore(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull GiphyMp4Cache provideGiphyMp4Cache() {
|
||||||
|
return new GiphyMp4Cache(ByteUnit.MEGABYTES.toBytes(16));
|
||||||
|
}
|
||||||
|
|
||||||
private @NonNull WebSocketFactory provideWebSocketFactory(@NonNull SignalWebSocketHealthMonitor healthMonitor) {
|
private @NonNull WebSocketFactory provideWebSocketFactory(@NonNull SignalWebSocketHealthMonitor healthMonitor) {
|
||||||
return new WebSocketFactory() {
|
return new WebSocketFactory() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.util.storage;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
import org.signal.core.util.StreamUtil;
|
import org.signal.core.util.StreamUtil;
|
||||||
@@ -33,8 +34,8 @@ public final class FileStorage {
|
|||||||
@NonNull InputStream inputStream,
|
@NonNull InputStream inputStream,
|
||||||
@NonNull String directoryName,
|
@NonNull String directoryName,
|
||||||
@NonNull String fileNameBase,
|
@NonNull String fileNameBase,
|
||||||
@NonNull String extension
|
@NonNull String extension)
|
||||||
) throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
File directory = context.getDir(directoryName, Context.MODE_PRIVATE);
|
File directory = context.getDir(directoryName, Context.MODE_PRIVATE);
|
||||||
File file = File.createTempFile(fileNameBase, "." + extension, directory);
|
File file = File.createTempFile(fileNameBase, "." + extension, directory);
|
||||||
@@ -47,7 +48,8 @@ public final class FileStorage {
|
|||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static @NonNull InputStream read(@NonNull Context context,
|
public static @NonNull InputStream read(@NonNull Context context,
|
||||||
@NonNull String directoryName,
|
@NonNull String directoryName,
|
||||||
@NonNull String filename) throws IOException
|
@NonNull String filename)
|
||||||
|
throws IOException
|
||||||
{
|
{
|
||||||
File directory = context.getDir(directoryName, Context.MODE_PRIVATE);
|
File directory = context.getDir(directoryName, Context.MODE_PRIVATE);
|
||||||
File file = new File(directory, filename);
|
File file = new File(directory, filename);
|
||||||
@@ -80,6 +82,17 @@ public final class FileStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note that you will always get a file back, but that file may not exist on disk.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
public static @NonNull File getFile(@NonNull Context context,
|
||||||
|
@NonNull String directoryName,
|
||||||
|
@NonNull String filename)
|
||||||
|
{
|
||||||
|
return new File(context.getDir(directoryName, Context.MODE_PRIVATE), filename);
|
||||||
|
}
|
||||||
|
|
||||||
private static @NonNull OutputStream getOutputStream(@NonNull Context context, File outputFile) throws IOException {
|
private static @NonNull OutputStream getOutputStream(@NonNull Context context, File outputFile) throws IOException {
|
||||||
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
|
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
|
||||||
return ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).second;
|
return ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).second;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.video.exo;
|
package org.thoughtcrime.securesms.video.exo;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -10,6 +11,9 @@ import com.google.android.exoplayer2.upstream.DataSource;
|
|||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
|
||||||
|
import org.signal.core.util.ThreadUtil;
|
||||||
|
import org.signal.core.util.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.net.ChunkedDataFetcher;
|
import org.thoughtcrime.securesms.net.ChunkedDataFetcher;
|
||||||
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
@@ -28,9 +32,10 @@ public class ChunkedDataSource implements DataSource {
|
|||||||
private final OkHttpClient okHttpClient;
|
private final OkHttpClient okHttpClient;
|
||||||
private final TransferListener transferListener;
|
private final TransferListener transferListener;
|
||||||
|
|
||||||
private DataSpec dataSpec;
|
private DataSpec dataSpec;
|
||||||
private volatile InputStream inputStream;
|
private GiphyMp4Cache.ReadData cacheEntry;
|
||||||
private volatile Exception exception;
|
|
||||||
|
private volatile Exception exception;
|
||||||
|
|
||||||
ChunkedDataSource(@NonNull OkHttpClient okHttpClient, @Nullable TransferListener listener) {
|
ChunkedDataSource(@NonNull OkHttpClient okHttpClient, @Nullable TransferListener listener) {
|
||||||
this.okHttpClient = okHttpClient;
|
this.okHttpClient = okHttpClient;
|
||||||
@@ -46,57 +51,73 @@ public class ChunkedDataSource implements DataSource {
|
|||||||
this.dataSpec = dataSpec;
|
this.dataSpec = dataSpec;
|
||||||
this.exception = null;
|
this.exception = null;
|
||||||
|
|
||||||
if (inputStream != null) {
|
if (cacheEntry != null) {
|
||||||
inputStream.close();
|
cacheEntry.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.inputStream = null;
|
// XXX Android can't handle all videos starting at once, so this randomly offsets them
|
||||||
|
|
||||||
CountDownLatch countDownLatch = new CountDownLatch(1);
|
|
||||||
ChunkedDataFetcher fetcher = new ChunkedDataFetcher(okHttpClient);
|
|
||||||
|
|
||||||
fetcher.fetch(this.dataSpec.uri.toString(), dataSpec.length, new ChunkedDataFetcher.Callback() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(InputStream stream) {
|
|
||||||
inputStream = stream;
|
|
||||||
countDownLatch.countDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(Exception e) {
|
|
||||||
exception = e;
|
|
||||||
countDownLatch.countDown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
countDownLatch.await(30, TimeUnit.SECONDS);
|
Thread.sleep((long) (Math.random() * 750));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
throw new IOException(e);
|
// Exoplayer sometimes interrupts the thread
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exception != null) {
|
Context context = ApplicationDependencies.getApplication();
|
||||||
throw new IOException(exception);
|
GiphyMp4Cache cache = ApplicationDependencies.getGiphyMp4Cache();
|
||||||
|
|
||||||
|
cacheEntry = cache.read(context, dataSpec.uri);
|
||||||
|
|
||||||
|
if (cacheEntry == null) {
|
||||||
|
CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||||
|
ChunkedDataFetcher fetcher = new ChunkedDataFetcher(okHttpClient);
|
||||||
|
|
||||||
|
fetcher.fetch(this.dataSpec.uri.toString(), dataSpec.length, new ChunkedDataFetcher.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(InputStream stream) {
|
||||||
|
try {
|
||||||
|
cacheEntry = cache.write(context, dataSpec.uri, stream);
|
||||||
|
} catch (IOException e) {
|
||||||
|
exception = e;
|
||||||
|
}
|
||||||
|
countDownLatch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Exception e) {
|
||||||
|
exception = e;
|
||||||
|
countDownLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
countDownLatch.await(30, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception != null) {
|
||||||
|
throw new IOException(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheEntry == null) {
|
||||||
|
throw new IOException("Timed out waiting for download.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transferListener != null) {
|
||||||
|
transferListener.onTransferStart(this, dataSpec, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataSpec.length != C.LENGTH_UNSET && dataSpec.length - dataSpec.position <= 0) {
|
||||||
|
throw new EOFException("No more data");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputStream == null) {
|
return cacheEntry.getLength();
|
||||||
throw new IOException("Timed out waiting for input stream");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transferListener != null) {
|
|
||||||
transferListener.onTransferStart(this, dataSpec, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataSpec.length != C.LENGTH_UNSET && dataSpec.length - dataSpec.position <= 0) {
|
|
||||||
throw new EOFException("No more data");
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataSpec.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException {
|
public int read(@NonNull byte[] buffer, int offset, int readLength) throws IOException {
|
||||||
int read = inputStream.read(buffer, offset, readLength);
|
int read = cacheEntry.getInputStream().read(buffer, offset, readLength);
|
||||||
|
|
||||||
if (read > 0 && transferListener != null) {
|
if (read > 0 && transferListener != null) {
|
||||||
transferListener.onBytesTransferred(this, dataSpec, false, read);
|
transferListener.onBytesTransferred(this, dataSpec, false, read);
|
||||||
@@ -112,9 +133,10 @@ public class ChunkedDataSource implements DataSource {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
if (inputStream != null) {
|
if (cacheEntry != null) {
|
||||||
inputStream.close();
|
cacheEntry.release();
|
||||||
}
|
}
|
||||||
|
cacheEntry = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package org.thoughtcrime.securesms.video.exo
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import org.signal.core.util.StreamUtil
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.util.storage.FileStorage
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple disk cache for MP4 GIFS. While entries are stored on disk, the data has lifecycle of a single application session and will be cleared every app
|
||||||
|
* start. This lets us keep stuff simple and maintain all of our metadata and state in memory.
|
||||||
|
*
|
||||||
|
* Features
|
||||||
|
* - Write entire files into the cache
|
||||||
|
* - Keep entries that are actively being read in the cache by maintaining locks on entries
|
||||||
|
* - When the cache is over the size limit, inactive entries will be evicted in LRU order.
|
||||||
|
*/
|
||||||
|
class GiphyMp4Cache(private val maxSize: Long) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(GiphyMp4Cache::class.java)
|
||||||
|
private val DATA_LOCK = Object()
|
||||||
|
private const val DIRECTORY = "mp4gif_cache"
|
||||||
|
private const val PREFIX = "entry_"
|
||||||
|
private const val EXTENSION = "mp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val lockedUris: MutableSet<Uri> = mutableSetOf()
|
||||||
|
private val uriToEntry: MutableMap<Uri, Entry> = mutableMapOf()
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun onAppStart(context: Context) {
|
||||||
|
synchronized(DATA_LOCK) {
|
||||||
|
lockedUris.clear()
|
||||||
|
for (file in FileStorage.getAllFiles(context, DIRECTORY, PREFIX)) {
|
||||||
|
if (!file.delete()) {
|
||||||
|
Log.w(TAG, "Failed to delete: " + file.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun write(context: Context, uri: Uri, inputStream: InputStream): ReadData {
|
||||||
|
synchronized(DATA_LOCK) {
|
||||||
|
lockedUris.add(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
val filename: String = FileStorage.save(context, inputStream, DIRECTORY, PREFIX, EXTENSION)
|
||||||
|
val size = FileStorage.getFile(context, DIRECTORY, filename).length()
|
||||||
|
|
||||||
|
synchronized(DATA_LOCK) {
|
||||||
|
uriToEntry[uri] = Entry(
|
||||||
|
uri = uri,
|
||||||
|
filename = filename,
|
||||||
|
size = size,
|
||||||
|
lastAccessed = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return readFromStorage(context, uri) ?: throw IOException("Could not find file immediately after writing!")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun read(context: Context, uri: Uri): ReadData? {
|
||||||
|
synchronized(DATA_LOCK) {
|
||||||
|
lockedUris.add(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
readFromStorage(context, uri)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun readFromStorage(context: Context, uri: Uri): ReadData? {
|
||||||
|
val entry: Entry = synchronized(DATA_LOCK) {
|
||||||
|
uriToEntry[uri]
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
val length: Long = FileStorage.getFile(context, DIRECTORY, entry.filename).length()
|
||||||
|
val inputStream: InputStream = FileStorage.read(context, DIRECTORY, entry.filename)
|
||||||
|
return ReadData(inputStream, length) { onEntryReleased(context, uri) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEntryReleased(context: Context, uri: Uri) {
|
||||||
|
synchronized(DATA_LOCK) {
|
||||||
|
lockedUris.remove(uri)
|
||||||
|
|
||||||
|
var totalSize: Long = calculateTotalSize(uriToEntry)
|
||||||
|
|
||||||
|
if (totalSize > maxSize) {
|
||||||
|
val evictCandidatesInLruOrder: MutableList<Entry> = ArrayList(
|
||||||
|
uriToEntry.entries
|
||||||
|
.filter { e -> !lockedUris.contains(e.key) }
|
||||||
|
.map { e -> e.value }
|
||||||
|
.sortedBy { e -> e.lastAccessed }
|
||||||
|
)
|
||||||
|
|
||||||
|
while (totalSize > maxSize && evictCandidatesInLruOrder.isNotEmpty()) {
|
||||||
|
val toEvict: Entry = evictCandidatesInLruOrder.removeAt(0)
|
||||||
|
|
||||||
|
if (!FileStorage.getFile(context, DIRECTORY, toEvict.filename).delete()) {
|
||||||
|
Log.w(TAG, "Failed to delete ${toEvict.filename}")
|
||||||
|
}
|
||||||
|
|
||||||
|
uriToEntry.remove(toEvict.uri)
|
||||||
|
|
||||||
|
totalSize = calculateTotalSize(uriToEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateTotalSize(data: Map<Uri, Entry>): Long {
|
||||||
|
return data.values.map { e -> e.size }.reduceOrNull { sum, size -> sum + size } ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interface Lease {
|
||||||
|
fun release()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class Entry(val uri: Uri, val filename: String, val size: Long, val lastAccessed: Long)
|
||||||
|
|
||||||
|
data class ReadData(val inputStream: InputStream, val length: Long, val lease: Lease) {
|
||||||
|
fun release() {
|
||||||
|
StreamUtil.close(inputStream)
|
||||||
|
lease.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp">
|
android:layout_height="0dp">
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user