Merge pull request #620 from ultrasonic/cc

CacheCleaner to Kotlin
This commit is contained in:
Nite 2021-11-02 22:29:57 +01:00 committed by GitHub
commit 210ae35ee0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 260 additions and 359 deletions

View File

@ -70,7 +70,7 @@ style:
excludeImportStatements: false
MagicNumber:
# 100 common in percentage, 1000 in milliseconds
ignoreNumbers: ['-1', '0', '1', '2', '10', '100', '256', '512', '1000', '1024']
ignoreNumbers: ['-1', '0', '1', '2', '5', '10', '100', '256', '512', '1000', '1024']
ignoreEnums: true
ignorePropertyDeclaration: true
UnnecessaryAbstractClass:

View File

@ -1,6 +1,7 @@
package org.moire.ultrasonic.fragment;
import android.os.Bundle;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -25,7 +26,6 @@ import org.moire.ultrasonic.subsonic.VideoPlayer;
import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FragmentBackgroundTask;
import org.moire.ultrasonic.util.Pair;
import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.EntryAdapter;
@ -341,7 +341,7 @@ public class BookmarksFragment extends Fragment {
@Override
protected void done(Pair<MusicDirectory, Boolean> result)
{
MusicDirectory musicDirectory = result.getFirst();
MusicDirectory musicDirectory = result.first;
List<MusicDirectory.Entry> entries = musicDirectory.getChildren();
int songCount = 0;
@ -371,7 +371,7 @@ public class BookmarksFragment extends Fragment {
deleteButton.setVisibility(View.GONE);
playNowButton.setVisibility(View.GONE);
if (listSize == 0 || result.getFirst().getChildren().size() < listSize)
if (listSize == 0 || result.first.getChildren().size() < listSize)
{
albumButtons.setVisibility(View.GONE);
}

View File

@ -1,300 +0,0 @@
package org.moire.ultrasonic.util;
import android.os.AsyncTask;
import android.os.StatFs;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Playlist;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.Downloader;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Responsible for cleaning up files from the offline download cache on the filesystem.
*/
public class CacheCleaner
{
private static final long MIN_FREE_SPACE = 500 * 1024L * 1024L;
public CacheCleaner()
{
}
public void clean()
{
try
{
new BackgroundCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
catch (Exception ex)
{
// If an exception is thrown, assume we execute correctly the next time
Timber.w(ex, "Exception in CacheCleaner.clean");
}
}
public void cleanSpace()
{
try
{
new BackgroundSpaceCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
catch (Exception ex)
{
// If an exception is thrown, assume we execute correctly the next time
Timber.w(ex,"Exception in CacheCleaner.cleanSpace");
}
}
public void cleanPlaylists(List<Playlist> playlists)
{
try
{
new BackgroundPlaylistsCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, playlists);
}
catch (Exception ex)
{
// If an exception is thrown, assume we execute correctly the next time
Timber.w(ex, "Exception in CacheCleaner.cleanPlaylists");
}
}
private static void deleteEmptyDirs(Iterable<File> dirs, Collection<File> doNotDelete)
{
for (File dir : dirs)
{
if (doNotDelete.contains(dir))
{
continue;
}
File[] children = dir.listFiles();
if (children != null)
{
// No songs left in the folder
if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath()))
{
// Delete Artwork files
Util.delete(FileUtil.getAlbumArtFile(dir));
children = dir.listFiles();
}
// Delete empty directory
if (children != null && children.length == 0)
{
Util.delete(dir);
}
}
}
}
private static long getMinimumDelete(List<File> files)
{
if (files.isEmpty())
{
return 0L;
}
long cacheSizeBytes = Settings.getCacheSizeMB() * 1024L * 1024L;
long bytesUsedBySubsonic = 0L;
for (File file : files)
{
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);
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));
return bytesToDelete;
}
private static void deleteFiles(Collection<File> files, Collection<File> doNotDelete, 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 (!doNotDelete.contains(file) && !file.getName().equals(Constants.ALBUM_ART_FILE))
{
long size = file.length();
if (Util.delete(file))
{
bytesDeleted += size;
}
}
}
}
Timber.i("Deleted: %s", Util.formatBytes(bytesDeleted));
}
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.");
if (isCacheFile)
{
files.add(file);
}
}
else
{
// Depth-first
for (File child : FileUtil.listFiles(file))
{
findCandidatesForDeletion(child, files, dirs);
}
dirs.add(file);
}
}
private static void sortByAscendingModificationTime(List<File> files)
{
Collections.sort(files, (a, b) -> Long.compare(a.lastModified(), b.lastModified()));
}
private static Set<File> findFilesToNotDelete()
{
Set<File> filesToNotDelete = new HashSet<>(5);
Lazy<Downloader> downloader = inject(Downloader.class);
for (DownloadFile downloadFile : downloader.getValue().getAll())
{
filesToNotDelete.add(downloadFile.getPartialFile());
filesToNotDelete.add(downloadFile.getCompleteOrSaveFile());
}
filesToNotDelete.add(FileUtil.getMusicDirectory());
return filesToNotDelete;
}
private static class BackgroundCleanup extends AsyncTask<Void, Void, Void>
{
@Override
protected Void doInBackground(Void... params)
{
try
{
Thread.currentThread().setName("BackgroundCleanup");
List<File> files = new ArrayList<>();
List<File> dirs = new ArrayList<>();
findCandidatesForDeletion(FileUtil.getMusicDirectory(), files, dirs);
sortByAscendingModificationTime(files);
Set<File> filesToNotDelete = findFilesToNotDelete();
deleteFiles(files, filesToNotDelete, getMinimumDelete(files), true);
deleteEmptyDirs(dirs, filesToNotDelete);
}
catch (RuntimeException x)
{
Timber.e(x, "Error in cache cleaning.");
}
return null;
}
}
private static class BackgroundSpaceCleanup extends AsyncTask<Void, Void, Void>
{
@Override
protected Void doInBackground(Void... params)
{
try
{
Thread.currentThread().setName("BackgroundSpaceCleanup");
List<File> files = new ArrayList<>();
List<File> dirs = new ArrayList<>();
findCandidatesForDeletion(FileUtil.getMusicDirectory(), files, dirs);
long bytesToDelete = getMinimumDelete(files);
if (bytesToDelete > 0L)
{
sortByAscendingModificationTime(files);
Set<File> filesToNotDelete = findFilesToNotDelete();
deleteFiles(files, filesToNotDelete, bytesToDelete, false);
}
}
catch (RuntimeException x)
{
Timber.e(x, "Error in cache cleaning.");
}
return null;
}
}
private static class BackgroundPlaylistsCleanup extends AsyncTask<List<Playlist>, Void, Void>
{
@Override
protected Void doInBackground(List<Playlist>... params)
{
try
{
Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
Thread.currentThread().setName("BackgroundPlaylistsCleanup");
String server = activeServerProvider.getValue().getActiveServer().getName();
SortedSet<File> playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(server));
List<Playlist> playlists = params[0];
for (Playlist playlist : playlists)
{
playlistFiles.remove(FileUtil.getPlaylistFile(server, playlist.getName()));
}
for (File playlist : playlistFiles)
{
playlist.delete();
}
}
catch (RuntimeException x)
{
Timber.e(x, "Error in playlist cache cleaning.");
}
return null;
}
}
}

View File

@ -1,47 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.util;
import java.io.Serializable;
/**
* @author Sindre Mehus
*/
public class Pair<S, T> implements Serializable
{
private static final long serialVersionUID = -8903987928477888234L;
private final S first;
private final T second;
public Pair(S first, T second)
{
this.first = first;
this.second = second;
}
public S getFirst()
{
return first;
}
public T getSecond()
{
return second;
}
}

View File

@ -178,7 +178,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
}
)
.setNegativeButton(getString(R.string.common_cancel)) {
dialogInterface, i ->
dialogInterface, _ ->
dialogInterface.dismiss()
}
.setBottomSpace(DIALOG_PADDING)

View File

@ -31,7 +31,7 @@ class BitmapUtils {
if (entry == null) return null
val albumArtFile = FileUtil.getAlbumArtFile(entry)
val bitmap: Bitmap? = null
if (albumArtFile != null && albumArtFile.exists()) {
if (albumArtFile.exists()) {
return getBitmapFromDisk(albumArtFile.path, size, bitmap)
}
return null

View File

@ -260,12 +260,8 @@ class MediaPlayerController(
}
@Synchronized
fun clear() {
clear(true)
}
@Synchronized
fun clear(serialize: Boolean) {
@JvmOverloads
fun clear(serialize: Boolean = true) {
val mediaPlayerService = runningInstance
if (mediaPlayerService != null) {
mediaPlayerService.clear(serialize)

View File

@ -0,0 +1,252 @@
package org.moire.ultrasonic.util
import android.os.AsyncTask
import android.os.StatFs
import java.io.File
import java.util.ArrayList
import java.util.HashSet
import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Playlist
import org.moire.ultrasonic.service.Downloader
import org.moire.ultrasonic.util.FileUtil.getAlbumArtFile
import org.moire.ultrasonic.util.FileUtil.getPlaylistDirectory
import org.moire.ultrasonic.util.FileUtil.getPlaylistFile
import org.moire.ultrasonic.util.FileUtil.listFiles
import org.moire.ultrasonic.util.FileUtil.musicDirectory
import org.moire.ultrasonic.util.Settings.cacheSizeMB
import org.moire.ultrasonic.util.Util.delete
import org.moire.ultrasonic.util.Util.formatBytes
import timber.log.Timber
/**
* Responsible for cleaning up files from the offline download cache on the filesystem.
*/
class CacheCleaner {
fun clean() {
try {
BackgroundCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
} catch (all: Exception) {
// If an exception is thrown, assume we execute correctly the next time
Timber.w(all, "Exception in CacheCleaner.clean")
}
}
fun cleanSpace() {
try {
BackgroundSpaceCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
} catch (all: Exception) {
// If an exception is thrown, assume we execute correctly the next time
Timber.w(all, "Exception in CacheCleaner.cleanSpace")
}
}
fun cleanPlaylists(playlists: List<Playlist>) {
try {
BackgroundPlaylistsCleanup().executeOnExecutor(
AsyncTask.THREAD_POOL_EXECUTOR,
playlists
)
} catch (all: Exception) {
// If an exception is thrown, assume we execute correctly the next time
Timber.w(all, "Exception in CacheCleaner.cleanPlaylists")
}
}
private class BackgroundCleanup : AsyncTask<Void?, Void?, Void?>() {
override fun doInBackground(vararg params: Void?): Void? {
try {
Thread.currentThread().name = "BackgroundCleanup"
val files: MutableList<File> = ArrayList()
val dirs: MutableList<File> = ArrayList()
findCandidatesForDeletion(musicDirectory, files, dirs)
sortByAscendingModificationTime(files)
val filesToNotDelete = findFilesToNotDelete()
deleteFiles(files, filesToNotDelete, getMinimumDelete(files), true)
deleteEmptyDirs(dirs, filesToNotDelete)
} catch (all: RuntimeException) {
Timber.e(all, "Error in cache cleaning.")
}
return null
}
}
private class BackgroundSpaceCleanup : AsyncTask<Void?, Void?, Void?>() {
override fun doInBackground(vararg params: Void?): Void? {
try {
Thread.currentThread().name = "BackgroundSpaceCleanup"
val files: MutableList<File> = ArrayList()
val dirs: MutableList<File> = ArrayList()
findCandidatesForDeletion(musicDirectory, files, dirs)
val bytesToDelete = getMinimumDelete(files)
if (bytesToDelete <= 0L) return null
sortByAscendingModificationTime(files)
val filesToNotDelete = findFilesToNotDelete()
deleteFiles(files, filesToNotDelete, bytesToDelete, false)
} catch (all: RuntimeException) {
Timber.e(all, "Error in cache cleaning.")
}
return null
}
}
private class BackgroundPlaylistsCleanup : AsyncTask<List<Playlist>, Void?, Void?>() {
override fun doInBackground(vararg params: List<Playlist>): Void? {
try {
val activeServerProvider = inject<ActiveServerProvider>(
ActiveServerProvider::class.java
)
Thread.currentThread().name = "BackgroundPlaylistsCleanup"
val server = activeServerProvider.value.getActiveServer().name
val playlistFiles = listFiles(getPlaylistDirectory(server))
val playlists = params[0]
for ((_, name) in playlists) {
playlistFiles.remove(getPlaylistFile(server, name))
}
for (playlist in playlistFiles) {
playlist.delete()
}
} catch (all: RuntimeException) {
Timber.e(all, "Error in playlist cache cleaning.")
}
return null
}
}
companion object {
private const val MIN_FREE_SPACE = 500 * 1024L * 1024L
private fun deleteEmptyDirs(dirs: Iterable<File>, doNotDelete: Collection<File>) {
for (dir in dirs) {
if (doNotDelete.contains(dir)) continue
var children = dir.listFiles()
if (children != null) {
// No songs left in the folder
if (children.size == 1 && children[0].path == getAlbumArtFile(dir).path) {
// Delete Artwork files
delete(getAlbumArtFile(dir))
children = dir.listFiles()
}
// Delete empty directory
if (children != null && children.isEmpty()) {
delete(dir)
}
}
}
}
private fun getMinimumDelete(files: List<File>): Long {
if (files.isEmpty()) return 0L
val cacheSizeBytes = cacheSizeMB * 1024L * 1024L
var bytesUsedBySubsonic = 0L
for (file in files) {
bytesUsedBySubsonic += file.length()
}
// Ensure that file system is not more than 95% full.
val stat = StatFs(files[0].path)
val bytesTotalFs = stat.blockCountLong * stat.blockSizeLong
val bytesAvailableFs = stat.availableBlocksLong * stat.blockSizeLong
val bytesUsedFs = bytesTotalFs - bytesAvailableFs
val minFsAvailability = bytesTotalFs - MIN_FREE_SPACE
val bytesToDeleteCacheLimit = (bytesUsedBySubsonic - cacheSizeBytes).coerceAtLeast(0L)
val bytesToDeleteFsLimit = (bytesUsedFs - minFsAvailability).coerceAtLeast(0L)
val bytesToDelete = bytesToDeleteCacheLimit.coerceAtLeast(bytesToDeleteFsLimit)
Timber.i(
"File system : %s of %s available",
formatBytes(bytesAvailableFs),
formatBytes(bytesTotalFs)
)
Timber.i("Cache limit : %s", formatBytes(cacheSizeBytes))
Timber.i("Cache size before : %s", formatBytes(bytesUsedBySubsonic))
Timber.i("Minimum to delete : %s", formatBytes(bytesToDelete))
return bytesToDelete
}
private fun isPartial(file: File): Boolean {
return file.name.endsWith(".partial") || file.name.contains(".partial.")
}
private fun isComplete(file: File): Boolean {
return file.name.endsWith(".complete") || file.name.contains(".complete.")
}
@Suppress("NestedBlockDepth")
private fun deleteFiles(
files: Collection<File>,
doNotDelete: Collection<File>,
bytesToDelete: Long,
deletePartials: Boolean
) {
if (files.isEmpty()) return
var bytesDeleted = 0L
for (file in files) {
if (!deletePartials && bytesDeleted > bytesToDelete) break
if (bytesToDelete > bytesDeleted || deletePartials && isPartial(file)) {
if (!doNotDelete.contains(file) && file.name != Constants.ALBUM_ART_FILE) {
val size = file.length()
if (delete(file)) {
bytesDeleted += size
}
}
}
}
Timber.i("Deleted: %s", formatBytes(bytesDeleted))
}
private fun findCandidatesForDeletion(
file: File,
files: MutableList<File>,
dirs: MutableList<File>
) {
if (file.isFile && (isPartial(file) || isComplete(file))) {
files.add(file)
} else if (file.isDirectory) {
// Depth-first
for (child in listFiles(file)) {
findCandidatesForDeletion(child, files, dirs)
}
dirs.add(file)
}
}
private fun sortByAscendingModificationTime(files: MutableList<File>) {
files.sortWith { a: File, b: File ->
a.lastModified().compareTo(b.lastModified())
}
}
private fun findFilesToNotDelete(): Set<File> {
val filesToNotDelete: MutableSet<File> = HashSet(5)
val downloader = inject<Downloader>(
Downloader::class.java
)
for (downloadFile in downloader.value.all) {
filesToNotDelete.add(downloadFile.partialFile)
filesToNotDelete.add(downloadFile.completeOrSaveFile)
}
filesToNotDelete.add(musicDirectory)
return filesToNotDelete
}
}
}