From e32b3461c9de1a4f087819e9b117873d1d9eb9a2 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 31 Oct 2021 13:07:48 +0100 Subject: [PATCH 01/14] Remove global scope use --- .../org/moire/ultrasonic/data/ActiveServerProvider.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index 4cfab1a9..be4af76f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -2,8 +2,8 @@ package org.moire.ultrasonic.data import androidx.lifecycle.MutableLiveData import androidx.room.Room +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -24,7 +24,7 @@ import timber.log.Timber */ class ActiveServerProvider( private val repository: ServerSettingDao -) { +) : CoroutineScope by CoroutineScope(Dispatchers.IO) { private var cachedServer: ServerSetting? = null private var cachedDatabase: MetaDatabase? = null private var cachedServerId: Int? = null @@ -83,7 +83,7 @@ class ActiveServerProvider( return } - GlobalScope.launch(Dispatchers.IO) { + launch { val serverId = repository.findByIndex(index)?.id ?: 0 setActiveServerId(serverId) } @@ -133,7 +133,7 @@ class ActiveServerProvider( * Sets the minimum Subsonic API version of the current server. */ fun setMinimumApiVersion(apiVersion: String) { - GlobalScope.launch(Dispatchers.IO) { + launch { if (cachedServer != null) { cachedServer!!.minimumApiVersion = apiVersion repository.update(cachedServer!!) From aece29559e4a2dc482c5e71979daa5112d203a45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 11:02:54 +0000 Subject: [PATCH 02/14] Bump lifecycle-viewmodel-ktx from 2.2.0 to 2.4.0 Bumps lifecycle-viewmodel-ktx from 2.2.0 to 2.4.0. --- updated-dependencies: - dependency-name: androidx.lifecycle:lifecycle-viewmodel-ktx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index a18b5d87..accab18a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -23,7 +23,7 @@ ext.versions = [ room : "2.3.0", kotlin : "1.5.31", kotlinxCoroutines : "1.5.2-native-mt", - viewModelKtx : "2.2.0", + viewModelKtx : "2.4.0", retrofit : "2.6.4", jackson : "2.9.5", From 34c39365131c91236b42a96dbc13c7c8b14ff883 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 11:03:00 +0000 Subject: [PATCH 03/14] Bump core-ktx from 1.5.0 to 1.7.0 Bumps core-ktx from 1.5.0 to 1.7.0. --- updated-dependencies: - dependency-name: androidx.core:core-ktx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index a18b5d87..5cecd975 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -7,7 +7,7 @@ ext.versions = [ navigation : "2.3.5", gradlePlugin : "4.2.2", - androidxcore : "1.5.0", + androidxcore : "1.7.0", ktlint : "0.37.1", ktlintGradle : "10.2.0", detekt : "1.18.1", From 416bc57eea2129c64cf230b055ec0108ffa0e0b9 Mon Sep 17 00:00:00 2001 From: tzugen <67737443+tzugen@users.noreply.github.com> Date: Mon, 1 Nov 2021 12:28:04 +0100 Subject: [PATCH 04/14] 1.6.0 --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 5cecd975..1531c568 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -7,7 +7,7 @@ ext.versions = [ navigation : "2.3.5", gradlePlugin : "4.2.2", - androidxcore : "1.7.0", + androidxcore : "1.6.0", ktlint : "0.37.1", ktlintGradle : "10.2.0", detekt : "1.18.1", From a58e541ccc8234722320aa4e7760a8a0e58d7496 Mon Sep 17 00:00:00 2001 From: tzugen <67737443+tzugen@users.noreply.github.com> Date: Mon, 1 Nov 2021 13:05:26 +0100 Subject: [PATCH 05/14] Update dependencies.gradle --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index accab18a..6d43ee7b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -23,7 +23,7 @@ ext.versions = [ room : "2.3.0", kotlin : "1.5.31", kotlinxCoroutines : "1.5.2-native-mt", - viewModelKtx : "2.4.0", + viewModelKtx : "2.3.0", retrofit : "2.6.4", jackson : "2.9.5", From e6624ada9a5c5a8dc599af67ef30904c0ca665bd Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 1 Nov 2021 14:12:35 +0100 Subject: [PATCH 06/14] Unnecesary null-check --- .../main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt index 348e0e6f..1d899bd4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt @@ -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 From 4fb4ab18dac6e3d15933983cfa4ae3cecdc638c4 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 1 Nov 2021 14:13:25 +0100 Subject: [PATCH 07/14] Unused argument --- .../kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index a95838cd..1addcf40 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -178,7 +178,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { } ) .setNegativeButton(getString(R.string.common_cancel)) { - dialogInterface, i -> + dialogInterface, _ -> dialogInterface.dismiss() } .setBottomSpace(DIALOG_PADDING) From 8c99c84a9062820aa098c1bbdd71df9f03449e87 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 1 Nov 2021 14:14:12 +0100 Subject: [PATCH 08/14] Default arguments --- .../org/moire/ultrasonic/service/MediaPlayerController.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 0e13666c..c24e9e88 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -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) From dfb356196591b7e901accf2deac9217e43f47b97 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 1 Nov 2021 14:20:57 +0100 Subject: [PATCH 09/14] Remove custom Pair implementation --- .../fragment/BookmarksFragment.java | 6 +-- .../java/org/moire/ultrasonic/util/Pair.java | 47 ------------------- 2 files changed, 3 insertions(+), 50 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/Pair.java diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java index 0b350604..36897f5f 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/BookmarksFragment.java @@ -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 result) { - MusicDirectory musicDirectory = result.getFirst(); + MusicDirectory musicDirectory = result.first; List 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); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Pair.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Pair.java deleted file mode 100644 index 4bedd194..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Pair.java +++ /dev/null @@ -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 . - - Copyright 2009 (C) Sindre Mehus - */ -package org.moire.ultrasonic.util; - -import java.io.Serializable; - -/** - * @author Sindre Mehus - */ -public class Pair 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; - } -} From f085a8ab65e35611f312c4f8e1bd87a1fd83af9f Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 1 Nov 2021 14:22:30 +0100 Subject: [PATCH 10/14] Transform CacheCleaner to Kotlin --- detekt-config.yml | 2 +- .../moire/ultrasonic/util/CacheCleaner.java | 300 ------------------ .../org/moire/ultrasonic/util/CacheCleaner.kt | 240 ++++++++++++++ 3 files changed, 241 insertions(+), 301 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt diff --git a/detekt-config.yml b/detekt-config.yml index c9117d88..8729b6e3 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -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: diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java deleted file mode 100644 index e3a48c70..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java +++ /dev/null @@ -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 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 dirs, Collection 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 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 files, Collection 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 files, List 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 files) - { - Collections.sort(files, (a, b) -> Long.compare(a.lastModified(), b.lastModified())); - } - - private static Set findFilesToNotDelete() - { - Set filesToNotDelete = new HashSet<>(5); - - Lazy 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 - { - @Override - protected Void doInBackground(Void... params) - { - try - { - Thread.currentThread().setName("BackgroundCleanup"); - List files = new ArrayList<>(); - List dirs = new ArrayList<>(); - - findCandidatesForDeletion(FileUtil.getMusicDirectory(), files, dirs); - sortByAscendingModificationTime(files); - - Set 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 - { - @Override - protected Void doInBackground(Void... params) - { - try - { - Thread.currentThread().setName("BackgroundSpaceCleanup"); - List files = new ArrayList<>(); - List dirs = new ArrayList<>(); - findCandidatesForDeletion(FileUtil.getMusicDirectory(), files, dirs); - - long bytesToDelete = getMinimumDelete(files); - if (bytesToDelete > 0L) - { - sortByAscendingModificationTime(files); - Set 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, Void, Void> - { - @Override - protected Void doInBackground(List... params) - { - try - { - Lazy activeServerProvider = inject(ActiveServerProvider.class); - Thread.currentThread().setName("BackgroundPlaylistsCleanup"); - String server = activeServerProvider.getValue().getActiveServer().getName(); - SortedSet playlistFiles = FileUtil.listFiles(FileUtil.getPlaylistDirectory(server)); - List 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; - } - } -} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt new file mode 100644 index 00000000..f54673d5 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt @@ -0,0 +1,240 @@ +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) { + 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() { + override fun doInBackground(vararg params: Void?): Void? { + try { + Thread.currentThread().name = "BackgroundCleanup" + val files: MutableList = ArrayList() + val dirs: MutableList = 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() { + override fun doInBackground(vararg params: Void?): Void? { + try { + Thread.currentThread().name = "BackgroundSpaceCleanup" + val files: MutableList = ArrayList() + val dirs: MutableList = ArrayList() + findCandidatesForDeletion(musicDirectory, files, dirs) + val bytesToDelete = getMinimumDelete(files) + if (bytesToDelete > 0L) { + 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, Void?, Void?>() { + override fun doInBackground(vararg params: List): Void? { + try { + val activeServerProvider = inject( + 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, doNotDelete: Collection) { + 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): 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, + doNotDelete: Collection, + 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, + dirs: MutableList + ) { + if (file.isFile && (isPartial(file) || isComplete(file))) { + files.add(file) + } else { + // Depth-first + for (child in listFiles(file)) { + findCandidatesForDeletion(child, files, dirs) + } + dirs.add(file) + } + } + + private fun sortByAscendingModificationTime(files: MutableList) { + files.sortWith { a: File, b: File -> + a.lastModified().compareTo(b.lastModified()) + } + } + + private fun findFilesToNotDelete(): Set { + val filesToNotDelete: MutableSet = HashSet(5) + val downloader = inject( + Downloader::class.java + ) + for (downloadFile in downloader.value.all) { + filesToNotDelete.add(downloadFile.partialFile) + filesToNotDelete.add(downloadFile.completeOrSaveFile) + } + filesToNotDelete.add(musicDirectory) + return filesToNotDelete + } + } +} From 4e3102f131aea9c44210d5727a247c73faccec63 Mon Sep 17 00:00:00 2001 From: Nite Date: Tue, 2 Nov 2021 22:19:09 +0100 Subject: [PATCH 11/14] Fixed condition for directory listing Minor cleanup --- .../org/moire/ultrasonic/util/CacheCleaner.kt | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt index f54673d5..2342db04 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt @@ -57,11 +57,14 @@ class CacheCleaner { override fun doInBackground(vararg params: Void?): Void? { try { Thread.currentThread().name = "BackgroundCleanup" + val files: MutableList = ArrayList() val dirs: MutableList = ArrayList() + findCandidatesForDeletion(musicDirectory, files, dirs) sortByAscendingModificationTime(files) val filesToNotDelete = findFilesToNotDelete() + deleteFiles(files, filesToNotDelete, getMinimumDelete(files), true) deleteEmptyDirs(dirs, filesToNotDelete) } catch (all: RuntimeException) { @@ -75,15 +78,18 @@ class CacheCleaner { override fun doInBackground(vararg params: Void?): Void? { try { Thread.currentThread().name = "BackgroundSpaceCleanup" + val files: MutableList = ArrayList() val dirs: MutableList = ArrayList() + findCandidatesForDeletion(musicDirectory, files, dirs) + val bytesToDelete = getMinimumDelete(files) - if (bytesToDelete > 0L) { - sortByAscendingModificationTime(files) - val filesToNotDelete = findFilesToNotDelete() - deleteFiles(files, filesToNotDelete, bytesToDelete, false) - } + 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.") } @@ -97,13 +103,17 @@ class CacheCleaner { val activeServerProvider = inject( 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() } @@ -118,9 +128,8 @@ class CacheCleaner { private const val MIN_FREE_SPACE = 500 * 1024L * 1024L private fun deleteEmptyDirs(dirs: Iterable, doNotDelete: Collection) { for (dir in dirs) { - if (doNotDelete.contains(dir)) { - continue - } + if (doNotDelete.contains(dir)) continue + var children = dir.listFiles() if (children != null) { // No songs left in the folder @@ -139,11 +148,11 @@ class CacheCleaner { } private fun getMinimumDelete(files: List): Long { - if (files.isEmpty()) { - return 0L - } + if (files.isEmpty()) return 0L + val cacheSizeBytes = cacheSizeMB * 1024L * 1024L var bytesUsedBySubsonic = 0L + for (file in files) { bytesUsedBySubsonic += file.length() } @@ -166,6 +175,7 @@ class CacheCleaner { 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 } @@ -184,10 +194,9 @@ class CacheCleaner { bytesToDelete: Long, deletePartials: Boolean ) { - if (files.isEmpty()) { - return - } + if (files.isEmpty()) return var bytesDeleted = 0L + for (file in files) { if (!deletePartials && bytesDeleted > bytesToDelete) break if (bytesToDelete > bytesDeleted || deletePartials && isPartial(file)) { @@ -209,7 +218,7 @@ class CacheCleaner { ) { if (file.isFile && (isPartial(file) || isComplete(file))) { files.add(file) - } else { + } else if (file.isDirectory) { // Depth-first for (child in listFiles(file)) { findCandidatesForDeletion(child, files, dirs) @@ -226,13 +235,16 @@ class CacheCleaner { private fun findFilesToNotDelete(): Set { val filesToNotDelete: MutableSet = HashSet(5) + val downloader = inject( Downloader::class.java ) + for (downloadFile in downloader.value.all) { filesToNotDelete.add(downloadFile.partialFile) filesToNotDelete.add(downloadFile.completeOrSaveFile) } + filesToNotDelete.add(musicDirectory) return filesToNotDelete } From a66d07ae84c11e0c7770b42077d42743f0007a19 Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 1 Nov 2021 15:09:53 +0100 Subject: [PATCH 12/14] Use modern network APIs --- .../org/moire/ultrasonic/util/Settings.kt | 6 +-- .../kotlin/org/moire/ultrasonic/util/Util.kt | 50 +++++++++++++++++-- ultrasonic/src/main/res/values/strings.xml | 4 +- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index 7a45b123..fd541dd6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -9,7 +9,6 @@ package org.moire.ultrasonic.util import android.content.Context import android.content.SharedPreferences -import android.net.ConnectivityManager import android.os.Build import androidx.preference.PreferenceManager import java.util.regex.Pattern @@ -70,12 +69,9 @@ object Settings { @JvmStatic val maxBitRate: Int get() { - val manager = Util.getConnectivityManager() - val networkInfo = manager.activeNetworkInfo ?: return 0 - val wifi = networkInfo.type == ConnectivityManager.TYPE_WIFI val preferences = preferences return preferences.getString( - if (wifi) Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI + if (Util.networkInfo().unmetered) Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI else Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, "0" )!!.toInt() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt index c6a8af0b..26ae9369 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt @@ -22,9 +22,13 @@ import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED import android.net.Uri import android.net.wifi.WifiManager import android.net.wifi.WifiManager.WifiLock +import android.os.Build import android.os.Bundle import android.os.Environment import android.os.Parcelable @@ -374,14 +378,45 @@ object Util { return null } + /** + * Check if a usable network for downloading media is available + * + * @return Boolean + */ @JvmStatic fun isNetworkConnected(): Boolean { - val manager = getConnectivityManager() - val networkInfo = manager.activeNetworkInfo - val connected = networkInfo != null && networkInfo.isConnected - val wifiConnected = connected && networkInfo!!.type == ConnectivityManager.TYPE_WIFI + val info = networkInfo() + val isUnmetered = info.unmetered val wifiRequired = Settings.isWifiRequiredForDownload - return connected && (!wifiRequired || wifiConnected) + return info.connected && (!wifiRequired || isUnmetered) + } + + /** + * Query connectivity status + * + * @return NetworkInfo object + */ + @Suppress("DEPRECATION") + fun networkInfo(): NetworkInfo { + val manager = getConnectivityManager() + val info = NetworkInfo() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network: Network? = manager.activeNetwork + val capabilities = manager.getNetworkCapabilities(network) + + if (capabilities != null) { + info.unmetered = capabilities.hasCapability(NET_CAPABILITY_NOT_METERED) + info.connected = capabilities.hasCapability(NET_CAPABILITY_INTERNET) + } + } else { + val networkInfo = manager.activeNetworkInfo + if (networkInfo != null) { + info.unmetered = networkInfo.type == ConnectivityManager.TYPE_WIFI + info.connected = networkInfo.isConnected + } + } + return info } @JvmStatic @@ -921,4 +956,9 @@ object Util { val context = appContext() return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager } + + data class NetworkInfo( + var connected: Boolean = false, + var unmetered: Boolean = false + ) } diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index a4f4e6f2..bbf382a2 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -339,8 +339,8 @@ 4 seconds 4.5 seconds 5 seconds - Only stream media if connected to Wi-Fi - Wi-Fi Streaming Only + Only download media on unmetered connections + Download on Wi-Fi only %1$s%2$s %d kbps 0 B From aac73cd6d774c64f003851af3e3e2da66f7876aa Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 3 Nov 2021 12:55:20 +0100 Subject: [PATCH 13/14] Further cleanup maxBitrate function --- .../org/moire/ultrasonic/util/Settings.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt index fd541dd6..d7c8545f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Settings.kt @@ -69,14 +69,21 @@ object Settings { @JvmStatic val maxBitRate: Int get() { - val preferences = preferences - return preferences.getString( - if (Util.networkInfo().unmetered) Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI - else Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, - "0" - )!!.toInt() + val network = Util.networkInfo() + + if (!network.connected) return 0 + + if (network.unmetered) { + return maxWifiBitRate + } else { + return maxMobileBitRate + } } + private var maxWifiBitRate by StringIntSetting(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI) + + private var maxMobileBitRate by StringIntSetting(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE) + @JvmStatic val preloadCount: Int get() { From c29b8ebe0eac9c9f966c1eceed5743c88f3b4181 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Nov 2021 17:16:07 +0000 Subject: [PATCH 14/14] Bump mockito-kotlin from 3.2.0 to 4.0.0 Bumps [mockito-kotlin](https://github.com/mockito/mockito-kotlin) from 3.2.0 to 4.0.0. - [Release notes](https://github.com/mockito/mockito-kotlin/releases) - [Commits](https://github.com/mockito/mockito-kotlin/compare/3.2.0...4.0.0) --- updated-dependencies: - dependency-name: org.mockito.kotlin:mockito-kotlin dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index a18b5d87..a5630f07 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -35,7 +35,7 @@ ext.versions = [ junit4 : "4.13.2", junit5 : "5.8.1", mockito : "4.0.0", - mockitoKotlin : "3.2.0", + mockitoKotlin : "4.0.0", kluent : "1.68", apacheCodecs : "1.15", robolectric : "4.6.1",