diff --git a/dependencies.gradle b/dependencies.gradle index 520aa0a4..24bb2e52 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.6.0", ktlint : "0.37.1", ktlintGradle : "10.2.0", detekt : "1.18.1", @@ -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.3.0", retrofit : "2.6.4", jackson : "2.9.5", @@ -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", 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/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/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/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; - } -} 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!!) 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) 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 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 0dcb7fd9..0e4650c1 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) 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..2342db04 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt @@ -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) { + 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) 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, 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 if (file.isDirectory) { + // 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 + } + } +} 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..d7c8545f 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,17 +69,21 @@ 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 - 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() { 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