2015-07-26 18:15:07 +02:00
|
|
|
package org.moire.ultrasonic.util;
|
2012-02-26 21:25:13 +01:00
|
|
|
|
2013-12-04 07:36:02 +01:00
|
|
|
import android.os.AsyncTask;
|
|
|
|
import android.os.StatFs;
|
2020-09-30 14:47:59 +02:00
|
|
|
import timber.log.Timber;
|
2013-12-04 07:36:02 +01:00
|
|
|
|
2020-09-18 09:37:19 +02:00
|
|
|
import org.moire.ultrasonic.data.ActiveServerProvider;
|
2015-07-26 18:15:07 +02:00
|
|
|
import org.moire.ultrasonic.domain.Playlist;
|
|
|
|
import org.moire.ultrasonic.service.DownloadFile;
|
2020-06-23 18:40:44 +02:00
|
|
|
import org.moire.ultrasonic.service.Downloader;
|
2013-12-04 07:36:02 +01:00
|
|
|
|
2012-02-26 21:25:13 +01:00
|
|
|
import java.io.File;
|
|
|
|
import java.util.ArrayList;
|
2013-12-04 07:36:02 +01:00
|
|
|
import java.util.Collection;
|
2012-02-26 21:25:13 +01:00
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.HashSet;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.Set;
|
2013-05-16 09:59:55 +02:00
|
|
|
import java.util.SortedSet;
|
2012-02-26 21:25:13 +01:00
|
|
|
|
2020-06-22 18:35:58 +02:00
|
|
|
import kotlin.Lazy;
|
|
|
|
|
2020-09-18 09:37:19 +02:00
|
|
|
import static org.koin.java.KoinJavaComponent.inject;
|
2020-06-22 18:35:58 +02:00
|
|
|
|
2020-06-26 15:18:14 +02:00
|
|
|
/**
|
2021-05-27 12:15:56 +02:00
|
|
|
* Responsible for cleaning up files from the offline download cache on the filesystem.
|
2020-06-26 15:18:14 +02:00
|
|
|
*/
|
2013-12-04 07:36:02 +01:00
|
|
|
public class CacheCleaner
|
|
|
|
{
|
2013-05-16 09:59:55 +02:00
|
|
|
private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L;
|
2012-02-26 21:25:13 +01:00
|
|
|
|
2021-05-21 18:50:57 +02:00
|
|
|
public CacheCleaner()
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
|
|
|
}
|
2012-02-26 21:25:13 +01:00
|
|
|
|
2013-12-04 07:36:02 +01:00
|
|
|
public void clean()
|
|
|
|
{
|
2013-12-11 20:45:21 +01:00
|
|
|
try
|
|
|
|
{
|
|
|
|
new BackgroundCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
|
|
|
// If an exception is thrown, assume we execute correctly the next time
|
2020-09-30 14:47:59 +02:00
|
|
|
Timber.w(ex, "Exception in CacheCleaner.clean");
|
2013-12-11 20:45:21 +01:00
|
|
|
}
|
2013-12-04 07:36:02 +01:00
|
|
|
}
|
2012-02-26 21:25:13 +01:00
|
|
|
|
2013-12-04 07:36:02 +01:00
|
|
|
public void cleanSpace()
|
|
|
|
{
|
2013-12-11 20:45:21 +01:00
|
|
|
try
|
|
|
|
{
|
|
|
|
new BackgroundSpaceCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
|
|
|
// If an exception is thrown, assume we execute correctly the next time
|
2020-09-30 14:47:59 +02:00
|
|
|
Timber.w(ex,"Exception in CacheCleaner.cleanSpace");
|
2013-12-11 20:45:21 +01:00
|
|
|
}
|
2013-05-16 09:59:55 +02:00
|
|
|
}
|
2013-12-04 07:36:02 +01:00
|
|
|
|
|
|
|
public void cleanPlaylists(List<Playlist> playlists)
|
|
|
|
{
|
2013-12-11 20:45:21 +01:00
|
|
|
try
|
|
|
|
{
|
|
|
|
new BackgroundPlaylistsCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, playlists);
|
|
|
|
}
|
|
|
|
catch (Exception ex)
|
|
|
|
{
|
|
|
|
// If an exception is thrown, assume we execute correctly the next time
|
2020-09-30 14:47:59 +02:00
|
|
|
Timber.w(ex, "Exception in CacheCleaner.cleanPlaylists");
|
2013-12-11 20:45:21 +01:00
|
|
|
}
|
2013-05-16 09:59:55 +02:00
|
|
|
}
|
|
|
|
|
2021-05-21 19:11:26 +02:00
|
|
|
private static void deleteEmptyDirs(Iterable<File> dirs, Collection<File> doNotDelete)
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
|
|
|
for (File dir : dirs)
|
|
|
|
{
|
2014-01-27 00:47:13 +01:00
|
|
|
if (doNotDelete.contains(dir))
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
File[] children = dir.listFiles();
|
|
|
|
|
|
|
|
if (children != null)
|
|
|
|
{
|
|
|
|
// No songs left in the folder
|
2021-05-09 10:57:36 +02:00
|
|
|
if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath()))
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
2021-05-09 10:57:36 +02:00
|
|
|
Util.delete(FileUtil.getAlbumArtFile(dir));
|
2013-12-04 07:36:02 +01:00
|
|
|
children = dir.listFiles();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete empty directory
|
|
|
|
if (children != null && children.length == 0)
|
|
|
|
{
|
|
|
|
Util.delete(dir);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-21 19:11:26 +02:00
|
|
|
private static long getMinimumDelete(List<File> files)
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
2013-12-30 09:33:39 +01:00
|
|
|
if (files.isEmpty())
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
2013-05-16 09:59:55 +02:00
|
|
|
return 0L;
|
|
|
|
}
|
2013-12-04 07:36:02 +01:00
|
|
|
|
2021-05-09 08:35:15 +02:00
|
|
|
long cacheSizeBytes = Util.getCacheSizeMB() * 1024L * 1024L;
|
2013-12-04 07:36:02 +01:00
|
|
|
long bytesUsedBySubsonic = 0L;
|
2014-01-27 00:47:13 +01:00
|
|
|
|
2013-12-04 07:36:02 +01:00
|
|
|
for (File file : files)
|
|
|
|
{
|
|
|
|
bytesUsedBySubsonic += file.length();
|
|
|
|
}
|
|
|
|
|
2013-05-16 09:59:55 +02:00
|
|
|
// Ensure that file system is not more than 95% full.
|
2013-12-04 07:36:02 +01:00
|
|
|
StatFs stat = new StatFs(files.get(0).getPath());
|
|
|
|
long bytesTotalFs = (long) stat.getBlockCount() * (long) stat.getBlockSize();
|
|
|
|
long bytesAvailableFs = (long) stat.getAvailableBlocks() * (long) stat.getBlockSize();
|
|
|
|
long bytesUsedFs = bytesTotalFs - bytesAvailableFs;
|
|
|
|
long minFsAvailability = bytesTotalFs - MIN_FREE_SPACE;
|
|
|
|
|
|
|
|
long bytesToDeleteCacheLimit = Math.max(bytesUsedBySubsonic - cacheSizeBytes, 0L);
|
|
|
|
long bytesToDeleteFsLimit = Math.max(bytesUsedFs - minFsAvailability, 0L);
|
|
|
|
long bytesToDelete = Math.max(bytesToDeleteCacheLimit, bytesToDeleteFsLimit);
|
|
|
|
|
2020-09-30 14:47:59 +02:00
|
|
|
Timber.i("File system : %s of %s available", Util.formatBytes(bytesAvailableFs), Util.formatBytes(bytesTotalFs));
|
|
|
|
Timber.i("Cache limit : %s", Util.formatBytes(cacheSizeBytes));
|
|
|
|
Timber.i("Cache size before : %s", Util.formatBytes(bytesUsedBySubsonic));
|
|
|
|
Timber.i("Minimum to delete : %s", Util.formatBytes(bytesToDelete));
|
2013-12-04 07:36:02 +01:00
|
|
|
|
2013-05-16 09:59:55 +02:00
|
|
|
return bytesToDelete;
|
|
|
|
}
|
|
|
|
|
2014-01-27 00:47:13 +01:00
|
|
|
private static void deleteFiles(Collection<File> files, Collection<File> doNotDelete, long bytesToDelete, boolean deletePartials)
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
|
|
|
if (files.isEmpty())
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
long bytesDeleted = 0L;
|
|
|
|
for (File file : files)
|
|
|
|
{
|
|
|
|
if (!deletePartials && bytesDeleted > bytesToDelete) break;
|
|
|
|
|
|
|
|
if (bytesToDelete > bytesDeleted || (deletePartials && (file.getName().endsWith(".partial") || file.getName().contains(".partial."))))
|
|
|
|
{
|
2014-01-27 00:47:13 +01:00
|
|
|
if (!doNotDelete.contains(file) && !file.getName().equals(Constants.ALBUM_ART_FILE))
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
|
|
|
long size = file.length();
|
2014-01-27 00:47:13 +01:00
|
|
|
|
2013-12-04 07:36:02 +01:00
|
|
|
if (Util.delete(file))
|
|
|
|
{
|
|
|
|
bytesDeleted += size;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-21 18:50:57 +02:00
|
|
|
Timber.i("Deleted: %s", Util.formatBytes(bytesDeleted));
|
2013-12-04 07:36:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private static void findCandidatesForDeletion(File file, List<File> files, List<File> dirs)
|
|
|
|
{
|
|
|
|
if (file.isFile())
|
|
|
|
{
|
|
|
|
String name = file.getName();
|
|
|
|
boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete.");
|
2014-01-27 00:47:13 +01:00
|
|
|
|
2013-12-04 07:36:02 +01:00
|
|
|
if (isCacheFile)
|
|
|
|
{
|
|
|
|
files.add(file);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Depth-first
|
|
|
|
for (File child : FileUtil.listFiles(file))
|
|
|
|
{
|
|
|
|
findCandidatesForDeletion(child, files, dirs);
|
|
|
|
}
|
2014-01-27 00:47:13 +01:00
|
|
|
|
2013-12-04 07:36:02 +01:00
|
|
|
dirs.add(file);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void sortByAscendingModificationTime(List<File> files)
|
|
|
|
{
|
2021-05-21 19:11:26 +02:00
|
|
|
Collections.sort(files, (a, b) -> Long.compare(a.lastModified(), b.lastModified()));
|
2013-12-04 07:36:02 +01:00
|
|
|
}
|
|
|
|
|
2021-05-21 19:11:26 +02:00
|
|
|
private static Set<File> findFilesToNotDelete()
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
2021-05-21 19:11:26 +02:00
|
|
|
Set<File> filesToNotDelete = new HashSet<>(5);
|
|
|
|
|
|
|
|
Lazy<Downloader> downloader = inject(Downloader.class);
|
2013-12-04 07:36:02 +01:00
|
|
|
|
2020-06-23 18:40:44 +02:00
|
|
|
for (DownloadFile downloadFile : downloader.getValue().getDownloads())
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
2014-01-27 00:47:13 +01:00
|
|
|
filesToNotDelete.add(downloadFile.getPartialFile());
|
2021-04-17 12:25:21 +02:00
|
|
|
filesToNotDelete.add(downloadFile.getCompleteOrSaveFile());
|
2013-12-04 07:36:02 +01:00
|
|
|
}
|
|
|
|
|
2021-05-09 10:57:36 +02:00
|
|
|
filesToNotDelete.add(FileUtil.getMusicDirectory());
|
2014-01-27 00:47:13 +01:00
|
|
|
return filesToNotDelete;
|
2013-12-04 07:36:02 +01:00
|
|
|
}
|
|
|
|
|
2021-05-21 19:11:26 +02:00
|
|
|
private static class BackgroundCleanup extends AsyncTask<Void, Void, Void>
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
2013-05-16 09:59:55 +02:00
|
|
|
@Override
|
2013-12-04 07:36:02 +01:00
|
|
|
protected Void doInBackground(Void... params)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2014-01-25 10:41:45 +01:00
|
|
|
Thread.currentThread().setName("BackgroundCleanup");
|
2021-05-21 19:11:26 +02:00
|
|
|
List<File> files = new ArrayList<>();
|
|
|
|
List<File> dirs = new ArrayList<>();
|
2013-05-16 09:59:55 +02:00
|
|
|
|
2021-05-09 10:57:36 +02:00
|
|
|
findCandidatesForDeletion(FileUtil.getMusicDirectory(), files, dirs);
|
2013-05-16 09:59:55 +02:00
|
|
|
sortByAscendingModificationTime(files);
|
|
|
|
|
2014-01-27 00:47:13 +01:00
|
|
|
Set<File> filesToNotDelete = findFilesToNotDelete();
|
2013-05-16 09:59:55 +02:00
|
|
|
|
2014-01-27 00:47:13 +01:00
|
|
|
deleteFiles(files, filesToNotDelete, getMinimumDelete(files), true);
|
|
|
|
deleteEmptyDirs(dirs, filesToNotDelete);
|
2013-12-04 07:36:02 +01:00
|
|
|
}
|
|
|
|
catch (RuntimeException x)
|
|
|
|
{
|
2020-09-30 14:47:59 +02:00
|
|
|
Timber.e(x, "Error in cache cleaning.");
|
2013-05-16 09:59:55 +02:00
|
|
|
}
|
2013-07-17 10:59:58 +02:00
|
|
|
|
2013-05-16 09:59:55 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2013-07-17 10:59:58 +02:00
|
|
|
|
2021-05-21 19:11:26 +02:00
|
|
|
private static class BackgroundSpaceCleanup extends AsyncTask<Void, Void, Void>
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
2013-05-16 09:59:55 +02:00
|
|
|
@Override
|
2013-12-04 07:36:02 +01:00
|
|
|
protected Void doInBackground(Void... params)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2014-01-25 10:41:45 +01:00
|
|
|
Thread.currentThread().setName("BackgroundSpaceCleanup");
|
2021-05-21 19:11:26 +02:00
|
|
|
List<File> files = new ArrayList<>();
|
|
|
|
List<File> dirs = new ArrayList<>();
|
2021-05-09 10:57:36 +02:00
|
|
|
findCandidatesForDeletion(FileUtil.getMusicDirectory(), files, dirs);
|
2013-07-17 10:59:58 +02:00
|
|
|
|
2013-05-16 09:59:55 +02:00
|
|
|
long bytesToDelete = getMinimumDelete(files);
|
2013-12-04 07:36:02 +01:00
|
|
|
if (bytesToDelete > 0L)
|
|
|
|
{
|
2013-05-16 09:59:55 +02:00
|
|
|
sortByAscendingModificationTime(files);
|
2014-01-27 00:47:13 +01:00
|
|
|
Set<File> filesToNotDelete = findFilesToNotDelete();
|
|
|
|
deleteFiles(files, filesToNotDelete, bytesToDelete, false);
|
2013-05-16 09:59:55 +02:00
|
|
|
}
|
2013-12-04 07:36:02 +01:00
|
|
|
}
|
|
|
|
catch (RuntimeException x)
|
|
|
|
{
|
2020-09-30 14:47:59 +02:00
|
|
|
Timber.e(x, "Error in cache cleaning.");
|
2013-05-16 09:59:55 +02:00
|
|
|
}
|
2013-07-17 10:59:58 +02:00
|
|
|
|
2013-05-16 09:59:55 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2013-07-17 10:59:58 +02:00
|
|
|
|
2021-05-21 19:11:26 +02:00
|
|
|
private static class BackgroundPlaylistsCleanup extends AsyncTask<List<Playlist>, Void, Void>
|
2013-12-04 07:36:02 +01:00
|
|
|
{
|
2013-05-16 09:59:55 +02:00
|
|
|
@Override
|
2013-12-04 07:36:02 +01:00
|
|
|
protected Void doInBackground(List<Playlist>... params)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2021-05-21 19:11:26 +02:00
|
|
|
Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
|
2014-01-25 10:41:45 +01:00
|
|
|
Thread.currentThread().setName("BackgroundPlaylistsCleanup");
|
2020-09-18 09:37:19 +02:00
|
|
|
String server = activeServerProvider.getValue().getActiveServer().getName();
|
2021-05-09 10:57:36 +02:00
|
|
|
SortedSet<File> playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(server));
|
2013-05-16 09:59:55 +02:00
|
|
|
List<Playlist> playlists = params[0];
|
2013-12-04 07:36:02 +01:00
|
|
|
for (Playlist playlist : playlists)
|
|
|
|
{
|
2021-05-09 10:57:36 +02:00
|
|
|
playlistFiles.remove(FileUtil.getPlaylistFile(server, playlist.getName()));
|
2013-07-17 10:59:58 +02:00
|
|
|
}
|
|
|
|
|
2013-12-04 07:36:02 +01:00
|
|
|
for (File playlist : playlistFiles)
|
|
|
|
{
|
2013-07-17 10:59:58 +02:00
|
|
|
playlist.delete();
|
2013-05-16 09:59:55 +02:00
|
|
|
}
|
2013-12-04 07:36:02 +01:00
|
|
|
}
|
|
|
|
catch (RuntimeException x)
|
|
|
|
{
|
2020-09-30 14:47:59 +02:00
|
|
|
Timber.e(x, "Error in playlist cache cleaning.");
|
2013-05-16 09:59:55 +02:00
|
|
|
}
|
2013-07-17 10:59:58 +02:00
|
|
|
|
2013-05-16 09:59:55 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|