2017-01-09 08:01:12 +01:00
|
|
|
package net.nullsum.audinaut.util;
|
2016-12-18 18:41:30 +01:00
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.Comparator;
|
|
|
|
import java.util.HashSet;
|
|
|
|
import java.util.List;
|
|
|
|
import java.util.Set;
|
|
|
|
|
|
|
|
import android.content.Context;
|
|
|
|
import android.util.Log;
|
|
|
|
import android.os.StatFs;
|
2017-01-09 08:01:12 +01:00
|
|
|
import net.nullsum.audinaut.domain.Playlist;
|
|
|
|
import net.nullsum.audinaut.service.DownloadFile;
|
|
|
|
import net.nullsum.audinaut.service.DownloadService;
|
|
|
|
import net.nullsum.audinaut.service.MediaStoreService;
|
2016-12-18 18:41:30 +01:00
|
|
|
|
|
|
|
import java.util.*;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @author Sindre Mehus
|
|
|
|
* @version $Id$
|
|
|
|
*/
|
|
|
|
public class CacheCleaner {
|
|
|
|
|
|
|
|
private static final String TAG = CacheCleaner.class.getSimpleName();
|
|
|
|
private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L;
|
|
|
|
private static final long MAX_COVER_ART_SPACE = 100 * 1024L * 1024L;
|
|
|
|
|
|
|
|
private final Context context;
|
|
|
|
private final DownloadService downloadService;
|
|
|
|
private final MediaStoreService mediaStore;
|
|
|
|
|
|
|
|
public CacheCleaner(Context context, DownloadService downloadService) {
|
|
|
|
this.context = context;
|
|
|
|
this.downloadService = downloadService;
|
|
|
|
this.mediaStore = new MediaStoreService(context);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void clean() {
|
|
|
|
new BackgroundCleanup(context).execute();
|
|
|
|
}
|
|
|
|
public void cleanSpace() {
|
|
|
|
new BackgroundSpaceCleanup(context).execute();
|
|
|
|
}
|
|
|
|
public void cleanPlaylists(List<Playlist> playlists) {
|
|
|
|
new BackgroundPlaylistsCleanup(context, playlists).execute();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void deleteEmptyDirs(List<File> dirs, Set<File> undeletable) {
|
|
|
|
for (File dir : dirs) {
|
|
|
|
if (undeletable.contains(dir)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
FileUtil.deleteEmptyDir(dir);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private long getMinimumDelete(List<File> files, List<File> pinned) {
|
|
|
|
if(files.size() == 0) {
|
|
|
|
return 0L;
|
|
|
|
}
|
|
|
|
|
|
|
|
long cacheSizeBytes = Util.getCacheSizeMB(context) * 1024L * 1024L;
|
|
|
|
|
|
|
|
long bytesUsedBySubsonic = 0L;
|
|
|
|
for (File file : files) {
|
|
|
|
bytesUsedBySubsonic += file.length();
|
|
|
|
}
|
|
|
|
for (File file : pinned) {
|
|
|
|
bytesUsedBySubsonic += file.length();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure that file system is not more than 95% full.
|
|
|
|
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);
|
|
|
|
|
|
|
|
Log.i(TAG, "File system : " + Util.formatBytes(bytesAvailableFs) + " of " + Util.formatBytes(bytesTotalFs) + " available");
|
|
|
|
Log.i(TAG, "Cache limit : " + Util.formatBytes(cacheSizeBytes));
|
|
|
|
Log.i(TAG, "Cache size before : " + Util.formatBytes(bytesUsedBySubsonic));
|
|
|
|
Log.i(TAG, "Minimum to delete : " + Util.formatBytes(bytesToDelete));
|
|
|
|
|
|
|
|
return bytesToDelete;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void deleteFiles(List<File> files, Set<File> undeletable, long bytesToDelete, boolean deletePartials) {
|
|
|
|
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.")))) {
|
|
|
|
if (!undeletable.contains(file) && !file.getName().equals(Constants.ALBUM_ART_FILE)) {
|
|
|
|
long size = file.length();
|
|
|
|
if (Util.delete(file)) {
|
|
|
|
bytesDeleted += size;
|
|
|
|
mediaStore.deleteFromMediaStore(file);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Log.i(TAG, "Deleted : " + Util.formatBytes(bytesDeleted));
|
|
|
|
}
|
|
|
|
|
|
|
|
private void findCandidatesForDeletion(File file, List<File> files, List<File> pinned, List<File> dirs) {
|
|
|
|
if (file.isFile()) {
|
|
|
|
String name = file.getName();
|
|
|
|
boolean isCacheFile = name.endsWith(".partial") || name.contains(".partial.") || name.endsWith(".complete") || name.contains(".complete.");
|
|
|
|
if (isCacheFile) {
|
|
|
|
files.add(file);
|
|
|
|
} else {
|
|
|
|
pinned.add(file);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Depth-first
|
|
|
|
for (File child : FileUtil.listFiles(file)) {
|
|
|
|
findCandidatesForDeletion(child, files, pinned, dirs);
|
|
|
|
}
|
|
|
|
dirs.add(file);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void sortByAscendingModificationTime(List<File> files) {
|
|
|
|
Collections.sort(files, new Comparator<File>() {
|
|
|
|
@Override
|
|
|
|
public int compare(File a, File b) {
|
|
|
|
if (a.lastModified() < b.lastModified()) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
if (a.lastModified() > b.lastModified()) {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private Set<File> findUndeletableFiles() {
|
|
|
|
Set<File> undeletable = new HashSet<File>(5);
|
|
|
|
|
|
|
|
for (DownloadFile downloadFile : downloadService.getDownloads()) {
|
|
|
|
undeletable.add(downloadFile.getPartialFile());
|
|
|
|
undeletable.add(downloadFile.getCompleteFile());
|
|
|
|
}
|
|
|
|
|
|
|
|
undeletable.add(FileUtil.getMusicDirectory(context));
|
|
|
|
return undeletable;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void cleanupCoverArt(Context context) {
|
|
|
|
File dir = FileUtil.getAlbumArtDirectory(context);
|
|
|
|
|
|
|
|
List<File> files = new ArrayList<File>();
|
|
|
|
long bytesUsed = 0L;
|
|
|
|
for(File file: dir.listFiles()) {
|
|
|
|
if(file.isFile()) {
|
|
|
|
files.add(file);
|
|
|
|
bytesUsed += file.length();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't waste time sorting if under limit already
|
|
|
|
if(bytesUsed < MAX_COVER_ART_SPACE) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
sortByAscendingModificationTime(files);
|
|
|
|
long bytesDeleted = 0L;
|
|
|
|
for(File file: files) {
|
|
|
|
// End as soon as the space used is below the threshold
|
|
|
|
if(bytesUsed < MAX_COVER_ART_SPACE) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
long bytes = file.length();
|
|
|
|
if(file.delete()) {
|
|
|
|
bytesUsed -= bytes;
|
|
|
|
bytesDeleted += bytes;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Log.i(TAG, "Deleted " + Util.formatBytes(bytesDeleted) + " worth of cover art");
|
|
|
|
}
|
|
|
|
|
|
|
|
private class BackgroundCleanup extends SilentBackgroundTask<Void> {
|
|
|
|
public BackgroundCleanup(Context context) {
|
|
|
|
super(context);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected Void doInBackground() {
|
|
|
|
if (downloadService == null) {
|
|
|
|
Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
List<File> files = new ArrayList<File>();
|
|
|
|
List<File> pinned = new ArrayList<File>();
|
|
|
|
List<File> dirs = new ArrayList<File>();
|
|
|
|
|
|
|
|
findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs);
|
|
|
|
sortByAscendingModificationTime(files);
|
|
|
|
|
|
|
|
Set<File> undeletable = findUndeletableFiles();
|
|
|
|
|
|
|
|
deleteFiles(files, undeletable, getMinimumDelete(files, pinned), true);
|
|
|
|
deleteEmptyDirs(dirs, undeletable);
|
|
|
|
|
|
|
|
// Make sure cover art directory does not grow too large
|
|
|
|
cleanupCoverArt(context);
|
|
|
|
} catch (RuntimeException x) {
|
|
|
|
Log.e(TAG, "Error in cache cleaning.", x);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private class BackgroundSpaceCleanup extends SilentBackgroundTask<Void> {
|
|
|
|
public BackgroundSpaceCleanup(Context context) {
|
|
|
|
super(context);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected Void doInBackground() {
|
|
|
|
if (downloadService == null) {
|
|
|
|
Log.e(TAG, "DownloadService not set. Aborting cache cleaning.");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
List<File> files = new ArrayList<File>();
|
|
|
|
List<File> pinned = new ArrayList<File>();
|
|
|
|
List<File> dirs = new ArrayList<File>();
|
|
|
|
findCandidatesForDeletion(FileUtil.getMusicDirectory(context), files, pinned, dirs);
|
|
|
|
|
|
|
|
long bytesToDelete = getMinimumDelete(files, pinned);
|
|
|
|
if(bytesToDelete > 0L) {
|
|
|
|
sortByAscendingModificationTime(files);
|
|
|
|
Set<File> undeletable = findUndeletableFiles();
|
|
|
|
deleteFiles(files, undeletable, bytesToDelete, false);
|
|
|
|
}
|
|
|
|
} catch (RuntimeException x) {
|
|
|
|
Log.e(TAG, "Error in cache cleaning.", x);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private class BackgroundPlaylistsCleanup extends SilentBackgroundTask<Void> {
|
|
|
|
private final List<Playlist> playlists;
|
|
|
|
|
|
|
|
public BackgroundPlaylistsCleanup(Context context, List<Playlist> playlists) {
|
|
|
|
super(context);
|
|
|
|
this.playlists = playlists;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected Void doInBackground() {
|
|
|
|
try {
|
|
|
|
String server = Util.getServerName(context);
|
|
|
|
SortedSet<File> playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(context, server));
|
|
|
|
for (Playlist playlist : playlists) {
|
|
|
|
playlistFiles.remove(FileUtil.getPlaylistFile(context, server, playlist.getName()));
|
|
|
|
}
|
|
|
|
|
|
|
|
for(File playlist : playlistFiles) {
|
|
|
|
playlist.delete();
|
|
|
|
}
|
|
|
|
} catch (RuntimeException x) {
|
|
|
|
Log.e(TAG, "Error in playlist cache cleaning.", x);
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|