diff --git a/.circleci/config.yml b/.circleci/config.yml
index 24fe852b..ef2b3d17 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -2,7 +2,7 @@ version: 3
jobs:
build:
docker:
- - image: circleci/android:api-29
+ - image: circleci/android:api-30
working_directory: ~/ultrasonic
environment:
JVM_OPTS: -Xmx3200m
diff --git a/dependencies.gradle b/dependencies.gradle
index a18b5d87..301e7193 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,7 +1,7 @@
ext.versions = [
minSdk : 21,
- targetSdk : 29,
- compileSdk : 29,
+ targetSdk : 30,
+ compileSdk : 30,
// You need to run ./gradlew wrapper after updating the version
gradle : '7.2',
@@ -39,7 +39,6 @@ ext.versions = [
kluent : "1.68",
apacheCodecs : "1.15",
robolectric : "4.6.1",
- dexter : "6.2.3",
timber : "4.7.1",
fastScroll : "2.0.1",
colorPicker : "2.2.3",
@@ -86,7 +85,6 @@ ext.other = [
koinAndroid : "io.insert-koin:koin-android:$versions.koin",
koinViewModel : "io.insert-koin:koin-android-viewmodel:$versions.koin",
picasso : "com.squareup.picasso:picasso:$versions.picasso",
- dexter : "com.karumi:dexter:$versions.dexter",
timber : "com.jakewharton.timber:timber:$versions.timber",
fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll",
sortListView : "com.github.tzugen:drag-sort-listview:$versions.sortListView",
diff --git a/detekt-baseline.xml b/detekt-baseline.xml
index a85a67ab..8adb941d 100644
--- a/detekt-baseline.xml
+++ b/detekt-baseline.xml
@@ -3,10 +3,7 @@
ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background
- ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"
- ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )
ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()
- ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)
ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)
ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()
ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)
@@ -21,14 +18,12 @@
ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile)
ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)
ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)
- ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.<no name provided>$String.format("%s\n\n%s", Util.getShareGreeting(), result.url)
ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)
ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)
ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix)
LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment
LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()
LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)
- LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)
LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)
LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)
LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )
@@ -39,23 +34,18 @@
LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)
LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) )
MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192
- MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$10
- MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$60
MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$60000
MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000
- MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1024L
MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8
MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L
MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L
MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L
- MagicNumber:MediaPlayerService.kt$MediaPlayerService$256
MagicNumber:MediaPlayerService.kt$MediaPlayerService$3
MagicNumber:MediaPlayerService.kt$MediaPlayerService$4
MagicNumber:RESTMusicService.kt$RESTMusicService$206
MagicNumber:SongView.kt$SongView$3
MagicNumber:SongView.kt$SongView$4
MagicNumber:SongView.kt$SongView$60
- MagicNumber:TrackCollectionFragment.kt$TrackCollectionFragment$10
NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()
NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )
NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()
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/build.gradle b/ultrasonic/build.gradle
index 7835f863..16645705 100644
--- a/ultrasonic/build.gradle
+++ b/ultrasonic/build.gradle
@@ -119,7 +119,6 @@ dependencies {
testImplementation testing.mockitoKotlin
testImplementation testing.robolectric
- implementation other.dexter
implementation other.timber
}
diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml
index d74e6655..63eb8137 100644
--- a/ultrasonic/lint-baseline.xml
+++ b/ultrasonic/lint-baseline.xml
@@ -1,71 +1,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -154,17 +89,6 @@
column="9"/>
-
-
-
-
-
-
-
-
-
-
-
-
@@ -315,7 +217,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
@@ -385,51 +287,51 @@
column="13"/>
@@ -444,51 +346,51 @@
column="13"/>
@@ -501,6 +403,65 @@
file="src/main/res/values/strings.xml"
line="131"
column="13"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ message="The resource `R.string.parser_reading` appears to be unused"
+ errorLine1=" <string name="parser.reading">Reading from server.</string>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ message="The resource `R.string.parser_reading_done` appears to be unused"
+ errorLine1=" <string name="parser.reading_done">Reading from server. Done!</string>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -798,51 +700,51 @@
column="13"/>
-
+
@@ -857,51 +759,51 @@
column="13"/>
-
+
@@ -938,51 +840,51 @@
column="13"/>
-
+
@@ -997,51 +899,51 @@
column="13"/>
-
+
@@ -1056,51 +958,51 @@
column="13"/>
-
+
@@ -1115,51 +1017,51 @@
column="13"/>
-
+
@@ -1181,72 +1083,50 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1989,7 +1792,7 @@
errorLine2=" ~~~~~~~~~">
@@ -2000,7 +1803,7 @@
errorLine2=" ~~~~~~~~~">
@@ -2011,7 +1814,7 @@
errorLine2=" ~~~~~~~~~">
@@ -2022,7 +1825,7 @@
errorLine2=" ~~~~~~~~~">
@@ -2033,7 +1836,7 @@
errorLine2=" ~~~~~~~~~">
@@ -2231,7 +2034,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~">
@@ -2257,15 +2060,4 @@
column="10"/>
-
-
-
-
diff --git a/ultrasonic/src/main/AndroidManifest.xml b/ultrasonic/src/main/AndroidManifest.xml
index 8b709fbd..0f18c785 100644
--- a/ultrasonic/src/main/AndroidManifest.xml
+++ b/ultrasonic/src/main/AndroidManifest.xml
@@ -7,7 +7,6 @@
-
@@ -29,6 +28,7 @@
android:label="@string/common.appname"
android:usesCleartextTraffic="true"
android:supportsRtl="false"
+ android:preserveLegacyExternalStorage="true"
tools:ignore="UnusedAttribute">
+ android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"
+ tools:ignore="ExportedContentProvider" />
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/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
index a92bfcc4..2af515b4 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt
@@ -48,7 +48,6 @@ import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.NowPlayingEventDistributor
import org.moire.ultrasonic.util.NowPlayingEventListener
-import org.moire.ultrasonic.util.PermissionUtil
import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.SubsonicUncaughtExceptionHandler
@@ -60,6 +59,7 @@ import timber.log.Timber
/**
* The main Activity of Ultrasonic which loads all other screens as Fragments
*/
+@Suppress("TooManyFunctions")
class NavigationActivity : AppCompatActivity() {
private var chatMenuItem: MenuItem? = null
private var bookmarksMenuItem: MenuItem? = null
@@ -83,7 +83,6 @@ class NavigationActivity : AppCompatActivity() {
private val imageLoaderProvider: ImageLoaderProvider by inject()
private val nowPlayingEventDistributor: NowPlayingEventDistributor by inject()
private val themeChangedEventDistributor: ThemeChangedEventDistributor by inject()
- private val permissionUtil: PermissionUtil by inject()
private val activeServerProvider: ActiveServerProvider by inject()
private val serverRepository: ServerSettingDao by inject()
@@ -93,7 +92,6 @@ class NavigationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setUncaughtExceptionHandler()
- permissionUtil.onForegroundApplicationStarted(this)
Util.applyTheme(this)
super.onCreate(savedInstanceState)
@@ -240,7 +238,6 @@ class NavigationActivity : AppCompatActivity() {
nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener)
themeChangedEventDistributor.unsubscribe(themeChangedEventListener)
imageLoaderProvider.clearImageLoader()
- permissionUtil.onForegroundApplicationStopped()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
@@ -353,10 +350,6 @@ class NavigationActivity : AppCompatActivity() {
private fun loadSettings() {
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
- val preferences = Settings.preferences
- if (!preferences.contains(Constants.PREFERENCES_KEY_CACHE_LOCATION)) {
- Settings.cacheLocation = FileUtil.defaultMusicDirectory.path
- }
}
private fun exit() {
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt
index e44e0774..044235b0 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt
@@ -7,7 +7,6 @@ import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.MediaSessionEventDistributor
import org.moire.ultrasonic.util.MediaSessionHandler
import org.moire.ultrasonic.util.NowPlayingEventDistributor
-import org.moire.ultrasonic.util.PermissionUtil
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
/**
@@ -16,7 +15,6 @@ import org.moire.ultrasonic.util.ThemeChangedEventDistributor
val applicationModule = module {
single { ActiveServerProvider(get()) }
single { ImageLoaderProvider(androidContext()) }
- single { PermissionUtil(androidContext()) }
single { NowPlayingEventDistributor() }
single { ThemeChangedEventDistributor() }
single { MediaSessionEventDistributor() }
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerAdapter.kt
deleted file mode 100644
index 5c4dcbc7..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerAdapter.kt
+++ /dev/null
@@ -1,229 +0,0 @@
-package org.moire.ultrasonic.filepicker
-
-import android.content.Context
-import android.graphics.drawable.Drawable
-import android.os.Environment
-import android.text.TextUtils
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
-import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.widget.AppCompatEditText
-import androidx.recyclerview.widget.RecyclerView
-import java.io.File
-import java.util.LinkedList
-import org.moire.ultrasonic.R
-import org.moire.ultrasonic.util.Util
-import timber.log.Timber
-
-/**
- * Adapter for the RecyclerView which handles listing, navigating and picking files
- * @author this implementation is loosely based on the work of Yogesh Sundaresan,
- * original license: http://www.apache.org/licenses/LICENSE-2.0
- */
-internal class FilePickerAdapter(view: FilePickerView) :
- RecyclerView.Adapter() {
-
- private var data: MutableList = LinkedList()
- var defaultDirectory: File = Environment.getExternalStorageDirectory()
- var initialDirectory: File = Environment.getExternalStorageDirectory()
- lateinit var selectedDirectoryChanged: (String, Boolean) -> Unit
- var selectedDirectory: File = defaultDirectory
- private set
-
- private var context: Context? = null
- private var listerView: FilePickerView? = view
- private var isRealDirectory: Boolean = false
-
- private var folderIcon: Drawable? = null
- private var upIcon: Drawable? = null
- private var sdIcon: Drawable? = null
-
- init {
- this.context = view.context
- upIcon = Util.getDrawableFromAttribute(context, R.attr.filepicker_subdirectory_up)
- folderIcon = Util.getDrawableFromAttribute(context, R.attr.filepicker_folder)
- sdIcon = Util.getDrawableFromAttribute(context, R.attr.filepicker_sd_card)
- }
-
- fun start() {
- fileLister(initialDirectory)
- }
-
- private fun fileLister(currentDirectory: File) {
- var fileList = LinkedList()
- val storages: List?
- val storagePaths: List?
- storages = context!!.getExternalFilesDirs(null).filterNotNull()
- storagePaths = storages.map { i -> i.absolutePath }
-
- if (currentDirectory.absolutePath == "/" ||
- currentDirectory.absolutePath == "/storage" ||
- currentDirectory.absolutePath == "/storage/emulated" ||
- currentDirectory.absolutePath == "/mnt"
- ) {
- isRealDirectory = false
- fileList = getKitKatStorageItems(storages)
- } else {
- isRealDirectory = true
- val files = currentDirectory.listFiles()
- files?.forEach { file ->
- if (file.isDirectory) {
- fileList.add(FileListItem(file, file.name, folderIcon!!))
- }
- }
- }
-
- data = LinkedList(fileList)
-
- data.sortWith { f1, f2 ->
- if (f1.file!!.isDirectory && f2.file!!.isDirectory)
- f1.name.compareTo(f2.name, ignoreCase = true)
- else if (f1.file!!.isDirectory && !f2.file!!.isDirectory)
- -1
- else if (!f1.file!!.isDirectory && f2.file!!.isDirectory)
- 1
- else if (!f1.file!!.isDirectory && !f2.file!!.isDirectory)
- f1.name.compareTo(f2.name, ignoreCase = true)
- else
- 0
- }
-
- selectedDirectory = currentDirectory
- selectedDirectoryChanged.invoke(
- if (isRealDirectory) selectedDirectory.absolutePath
- else context!!.getString(R.string.filepicker_available_drives),
- isRealDirectory
- )
-
- // Add the "Up" navigation to the list
- if (currentDirectory.absolutePath != "/" && isRealDirectory) {
- // If we are on KitKat or later, only the default App folder is usable, so we can't
- // navigate the SD card. Jump to the root if "Up" is selected.
- if (storagePaths.indexOf(currentDirectory.absolutePath) > 0)
- data.add(0, FileListItem(File("/"), "..", upIcon!!))
- else
- data.add(0, FileListItem(selectedDirectory.parentFile!!, "..", upIcon!!))
- }
-
- notifyDataSetChanged()
- listerView!!.scrollToPosition(0)
- }
-
- private fun getKitKatStorageItems(storages: List): LinkedList {
- val fileList = LinkedList()
- if (storages.isNotEmpty()) {
- for ((index, file) in storages.withIndex()) {
- var path = file.absolutePath
- path = path.replace("/Android/data/([a-zA-Z_][.\\w]*)/files".toRegex(), "")
- if (index == 0) {
- fileList.add(
- FileListItem(
- File(path),
- context!!.getString(R.string.filepicker_internal, path),
- sdIcon!!
- )
- )
- } else {
- fileList.add(
- FileListItem(
- file,
- context!!.getString(R.string.filepicker_default_app_folder, path),
- sdIcon!!
- )
- )
- }
- }
- }
- return fileList
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileListHolder {
- return FileListHolder(
- LayoutInflater.from(context).inflate(
- R.layout.filepicker_item_file_lister, listerView, false
- )
- )
- }
-
- override fun onBindViewHolder(holder: FileListHolder, position: Int) {
- val actualFile = data[position]
-
- holder.name.text = actualFile.name
- holder.icon.setImageDrawable(actualFile.icon)
- }
-
- override fun getItemCount(): Int {
- return data.size
- }
-
- fun goToDefault() {
- fileLister(defaultDirectory)
- }
-
- fun createNewFolder() {
- val view = View.inflate(context, R.layout.filepicker_dialog_create_folder, null)
- val editText = view.findViewById(R.id.edittext)
- val builder = AlertDialog.Builder(context!!)
- .setView(view)
- .setTitle(context!!.getString(R.string.filepicker_enter_folder_name))
- .setPositiveButton(context!!.getString(R.string.filepicker_create)) { _, _ -> }
- val dialog = builder.create()
- dialog.show()
-
- dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
- val name = editText.text!!.toString()
-
- if (TextUtils.isEmpty(name)) {
- Util.toast(context!!, context!!.getString(R.string.filepicker_name_invalid))
- } else {
- val file = File(selectedDirectory, name)
-
- if (file.exists()) {
- Util.toast(context!!, context!!.getString(R.string.filepicker_already_exists))
- } else {
- dialog.dismiss()
- if (file.mkdirs()) {
- fileLister(file)
- } else {
- Util.toast(
- context!!,
- context!!.getString(R.string.filepicker_create_folder_failed)
- )
- }
- }
- }
- }
- }
-
- internal inner class FileListItem(
- fileParameter: File,
- nameParameter: String,
- iconParameter: Drawable
- ) {
- var file: File? = fileParameter
- var name: String = nameParameter
- var icon: Drawable? = iconParameter
- }
-
- internal inner class FileListHolder(
- itemView: View
- ) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
-
- var name: TextView = itemView.findViewById(R.id.name)
- var icon: ImageView = itemView.findViewById(R.id.icon)
-
- init {
- itemView.findViewById(R.id.layout).setOnClickListener(this)
- }
-
- override fun onClick(v: View) {
- val clickedFile = data[adapterPosition]
- selectedDirectory = clickedFile.file!!
- fileLister(clickedFile.file!!)
- Timber.d(clickedFile.file!!.absolutePath)
- }
- }
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerDialog.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerDialog.kt
deleted file mode 100644
index 5c743136..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerDialog.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package org.moire.ultrasonic.filepicker
-
-import android.content.Context
-import android.content.DialogInterface.BUTTON_NEGATIVE
-import android.content.DialogInterface.BUTTON_NEUTRAL
-import android.content.DialogInterface.BUTTON_POSITIVE
-import android.view.LayoutInflater
-import android.widget.Button
-import android.widget.TextView
-import androidx.appcompat.app.AlertDialog
-import org.moire.ultrasonic.R
-
-/**
- * This dialog can be used to pick a file / folder from the filesystem.
- * Currently only supports folders.
- * @author this implementation is loosely based on the work of Yogesh Sundaresan,
- * original license: http://www.apache.org/licenses/LICENSE-2.0
- */
-class FilePickerDialog {
-
- private var alertDialog: AlertDialog? = null
- private var filePickerView: FilePickerView? = null
- private var onFileSelectedListener: OnFileSelectedListener? = null
- private var currentPath: TextView? = null
- private var newFolderButton: Button? = null
-
- private constructor(context: Context) {
- alertDialog = AlertDialog.Builder(context).create()
- initialize(context)
- }
-
- private constructor(context: Context, themeResId: Int) {
- alertDialog = AlertDialog.Builder(context, themeResId).create()
- initialize(context)
- }
-
- private fun initialize(context: Context) {
- val view = LayoutInflater.from(context).inflate(R.layout.filepicker_dialog_main, null)
-
- alertDialog!!.setView(view)
- filePickerView = view.findViewById(R.id.file_list_view)
- currentPath = view.findViewById(R.id.current_path)
-
- newFolderButton = view.findViewById(R.id.filepicker_create_folder)
- newFolderButton!!.setOnClickListener { filePickerView!!.createNewFolder() }
-
- alertDialog!!.setTitle(context.getString(R.string.filepicker_select_folder))
-
- alertDialog!!.setButton(BUTTON_POSITIVE, context.getString(R.string.filepicker_select)) {
- dialogInterface, _ ->
- dialogInterface.dismiss()
- if (onFileSelectedListener != null)
- onFileSelectedListener!!.onFileSelected(
- filePickerView!!.selected, filePickerView!!.selected.absolutePath
- )
- }
- alertDialog!!.setButton(BUTTON_NEUTRAL, context.getString(R.string.filepicker_default)) {
- _, _ ->
- filePickerView!!.goToDefaultDirectory()
- }
- alertDialog!!.setButton(BUTTON_NEGATIVE, context.getString(R.string.common_cancel)) {
- dialogInterface, _ ->
- dialogInterface.dismiss()
- }
- }
-
- /**
- * Display the FilePickerDialog
- */
- fun show() {
- filePickerView!!.start { currentDirectory, isRealPath ->
- run {
- currentPath?.text = currentDirectory
- newFolderButton!!.isEnabled = isRealPath
- }
- }
- alertDialog!!.show()
- alertDialog!!.getButton(BUTTON_NEUTRAL).setOnClickListener {
- filePickerView!!.goToDefaultDirectory()
- }
- }
-
- /**
- * Listener to know which file/directory is selected
- *
- * @param onFileSelectedListener Instance of the Listener
- */
- fun setOnFileSelectedListener(onFileSelectedListener: OnFileSelectedListener) {
- this.onFileSelectedListener = onFileSelectedListener
- }
-
- /**
- * Set the initial directory to show the list of files in that directory
- *
- * @param path String denoting to the directory
- */
- fun setDefaultDirectory(path: String) {
- filePickerView!!.setDefaultDirectory(path)
- }
-
- fun setInitialDirectory(path: String) {
- filePickerView!!.setInitialDirectory(path)
- }
-
- companion object {
- /**
- * Creates a default instance of FilePickerDialog
- *
- * @param context Context of the App
- * @return Instance of FileListerDialog
- */
- fun createFilePickerDialog(context: Context): FilePickerDialog {
- return FilePickerDialog(context)
- }
- }
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerView.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerView.kt
deleted file mode 100644
index a94f86be..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/FilePickerView.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package org.moire.ultrasonic.filepicker
-
-import android.content.Context
-import android.util.AttributeSet
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import java.io.File
-
-/**
- * RecyclerView containing the file list of a directory
- * @author this implementation is loosely based on the work of Yogesh Sundaresan,
- * original license: http://www.apache.org/licenses/LICENSE-2.0
- */
-internal class FilePickerView : RecyclerView {
-
- private var adapter: FilePickerAdapter? = null
-
- val selected: File
- get() = adapter!!.selectedDirectory
-
- constructor(context: Context) : super(context) {
- initialize()
- }
-
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
- initialize()
- }
-
- constructor(
- context: Context,
- attrs: AttributeSet?,
- defStyle: Int
- ) : super(context, attrs, defStyle) {
- initialize()
- }
-
- private fun initialize() {
- layoutManager = LinearLayoutManager(context, VERTICAL, false)
- adapter = FilePickerAdapter(this)
- }
-
- fun start(selectedDirectoryChangedListener: (String, Boolean) -> Unit) {
- setAdapter(adapter)
- adapter?.selectedDirectoryChanged = selectedDirectoryChangedListener
- adapter!!.start()
- }
-
- fun setDefaultDirectory(file: File) {
- adapter!!.defaultDirectory = file
- }
-
- fun setDefaultDirectory(path: String) {
- setDefaultDirectory(File(path))
- }
-
- fun setInitialDirectory(path: String) {
- adapter!!.initialDirectory = File(path)
- }
-
- fun goToDefaultDirectory() {
- adapter!!.goToDefault()
- }
-
- fun createNewFolder() {
- adapter!!.createNewFolder()
- }
-}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/OnFileSelectedListener.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/OnFileSelectedListener.kt
deleted file mode 100644
index 54d3b5de..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/filepicker/OnFileSelectedListener.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.moire.ultrasonic.filepicker
-
-import java.io.File
-
-interface OnFileSelectedListener {
- fun onFileSelected(file: File?, path: String?)
-}
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/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt
index 392c04a4..6f26b174 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt
@@ -1,11 +1,16 @@
package org.moire.ultrasonic.fragment
+import android.app.Activity
import android.app.AlertDialog
import android.content.DialogInterface
+import android.content.Intent
+import android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import android.net.Uri
import android.os.Build
import android.os.Bundle
+import android.provider.DocumentsContract
import android.provider.SearchRecentSuggestions
import android.view.View
import androidx.annotation.StringRes
@@ -16,16 +21,13 @@ import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
-import java.io.File
-import kotlin.math.ceil
import org.koin.core.component.KoinComponent
import org.koin.java.KoinJavaComponent.get
import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.R
+import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage
-import org.moire.ultrasonic.filepicker.FilePickerDialog.Companion.createFilePickerDialog
-import org.moire.ultrasonic.filepicker.OnFileSelectedListener
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.log.FileLoggerTree
import org.moire.ultrasonic.log.FileLoggerTree.Companion.deleteLogFiles
@@ -37,11 +39,8 @@ import org.moire.ultrasonic.provider.SearchSuggestionProvider
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil.defaultMusicDirectory
-import org.moire.ultrasonic.util.FileUtil.ensureDirectoryExistsAndIsReadWritable
import org.moire.ultrasonic.util.FileUtil.ultrasonicDirectory
import org.moire.ultrasonic.util.MediaSessionHandler
-import org.moire.ultrasonic.util.PermissionUtil
-import org.moire.ultrasonic.util.PermissionUtil.Companion.requestInitialPermission
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Settings.preferences
import org.moire.ultrasonic.util.Settings.shareGreeting
@@ -51,6 +50,8 @@ import org.moire.ultrasonic.util.TimeSpanPreference
import org.moire.ultrasonic.util.TimeSpanPreferenceDialogFragmentCompat
import org.moire.ultrasonic.util.Util.toast
import timber.log.Timber
+import java.io.File
+import kotlin.math.ceil
/**
* Shows main app settings.
@@ -92,9 +93,6 @@ class SettingsFragment :
private val mediaPlayerControllerLazy = inject(
MediaPlayerController::class.java
)
- private val permissionUtil = inject(
- PermissionUtil::class.java
- )
private val themeChangedEventDistributor = inject(
ThemeChangedEventDistributor::class.java
)
@@ -169,6 +167,26 @@ class SettingsFragment :
update()
}
+ override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
+ if (requestCode == SELECT_CACHE_ACTIVITY && resultCode == Activity.RESULT_OK) {
+ // The result data contains a URI for the document or directory that
+ // the user selected.
+ resultData?.data?.also { uri ->
+ // Perform operations on the document using its URI.
+ val contentResolver = UApp.applicationContext().contentResolver
+
+ val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+// Check for the freshest data.
+ contentResolver.takePersistableUriPermission(uri, takeFlags)
+
+ setCacheLocation(uri)
+
+ }
+ }
+ }
+
+
override fun onResume() {
super.onResume()
val preferences = preferences
@@ -229,29 +247,19 @@ class SettingsFragment :
cacheLocation!!.summary = Settings.cacheLocation
cacheLocation!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
- // If the user tries to change the cache location,
- // we must first check to see if we have write access.
- requestInitialPermission(
- requireActivity()
- ) {
- if (it) {
- val filePickerDialog = createFilePickerDialog(
- requireContext()
- )
- filePickerDialog.setDefaultDirectory(defaultMusicDirectory.path)
- filePickerDialog.setInitialDirectory(cacheLocation!!.summary.toString())
- filePickerDialog.setOnFileSelectedListener(object :
- OnFileSelectedListener {
- override fun onFileSelected(file: File?, path: String?) {
- if (path != null) {
- Settings.cacheLocation = path
- setCacheLocation(path)
- }
- }
- })
- filePickerDialog.show()
- }
+ val isDefault = Settings.cacheLocation == defaultMusicDirectory.path
+
+ // Choose a directory using the system's file picker.
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
+
+ if (!isDefault && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, defaultMusicDirectory.path)
}
+
+ intent.addFlags(FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
+ startActivityForResult(intent, SELECT_CACHE_ACTIVITY)
+
+
true
}
}
@@ -419,19 +427,14 @@ class SettingsFragment :
sendBluetoothAlbumArt!!.isEnabled = enabled
}
- private fun setCacheLocation(path: String) {
- val dir = File(path)
- if (!ensureDirectoryExistsAndIsReadWritable(dir)) {
- permissionUtil.value.handlePermissionFailed {
- val currentPath = Settings.cacheLocation
- cacheLocation!!.summary = currentPath
- }
- } else {
- cacheLocation!!.summary = path
- }
+ private fun setCacheLocation(uri: Uri) {
+ if (uri.path != null) {
+ cacheLocation!!.summary = uri.path
+ Settings.cacheLocation = uri.path!!
- // Clear download queue.
- mediaPlayerControllerLazy.value.clear()
+ // Clear download queue.
+ mediaPlayerControllerLazy.value.clear()
+ }
}
private fun setDebugLogToFile(writeLog: Boolean) {
@@ -471,4 +474,8 @@ class SettingsFragment :
.create().show()
}
}
+
+ companion object {
+ const val SELECT_CACHE_ACTIVITY = 161161
+ }
}
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 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)
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
+ }
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt
index f9a03051..2f10ebe7 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt
@@ -11,6 +11,7 @@ import android.content.Context
import android.os.Build
import android.os.Environment
import android.text.TextUtils
+import android.util.Pair
import java.io.BufferedWriter
import java.io.File
import java.io.FileInputStream
@@ -24,9 +25,10 @@ import java.util.Locale
import java.util.SortedSet
import java.util.TreeSet
import java.util.regex.Pattern
-import org.koin.java.KoinJavaComponent
+import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory
+import org.moire.ultrasonic.service.MediaPlayerController
import timber.log.Timber
object FileUtil {
@@ -43,10 +45,6 @@ object FileUtil {
const val SUFFIX_SMALL = ".jpeg-small"
private const val UNNAMED = "unnamed"
- private val permissionUtil = KoinJavaComponent.inject(
- PermissionUtil::class.java
- )
-
fun getSongFile(song: MusicDirectory.Entry): File {
val dir = getAlbumDirectory(song)
@@ -237,15 +235,13 @@ object FileUtil {
@JvmStatic
val ultrasonicDirectory: File
get() {
+ @Suppress("DEPRECATION")
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File(
Environment.getExternalStorageDirectory(),
"Android/data/org.moire.ultrasonic"
) else UApp.applicationContext().getExternalFilesDir(null)!!
}
- // After Android M, the location of the files must be queried differently.
- // GetExternalFilesDir will always return a directory which Ultrasonic
- // can access without any extra privileges.
@JvmStatic
val defaultMusicDirectory: File
get() = getOrCreateDirectory("music")
@@ -256,38 +252,39 @@ object FileUtil {
val path = Settings.cacheLocation
val dir = File(path)
val hasAccess = ensureDirectoryExistsAndIsReadWritable(dir)
- if (!hasAccess) permissionUtil.value.handlePermissionFailed(null)
- return if (hasAccess) dir else defaultMusicDirectory
+ return if (hasAccess.second) dir else defaultMusicDirectory
}
@JvmStatic
@Suppress("ReturnCount")
- fun ensureDirectoryExistsAndIsReadWritable(dir: File?): Boolean {
+ fun ensureDirectoryExistsAndIsReadWritable(dir: File?): Pair {
+ val noAccess = Pair(false, false)
+
if (dir == null) {
- return false
+ return noAccess
}
if (dir.exists()) {
if (!dir.isDirectory) {
Timber.w("%s exists but is not a directory.", dir)
- return false
+ return noAccess
}
} else {
if (dir.mkdirs()) {
Timber.i("Created directory %s", dir)
} else {
Timber.w("Failed to create directory %s", dir)
- return false
+ return noAccess
}
}
if (!dir.canRead()) {
Timber.w("No read permission for directory %s", dir)
- return false
+ return noAccess
}
if (!dir.canWrite()) {
Timber.w("No write permission for directory %s", dir)
- return false
+ return Pair(true, false)
}
- return true
+ return Pair(true, true)
}
/**
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/PermissionUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/PermissionUtil.kt
deleted file mode 100644
index e39154c0..00000000
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/PermissionUtil.kt
+++ /dev/null
@@ -1,255 +0,0 @@
-package org.moire.ultrasonic.util
-
-import android.Manifest
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.os.Handler
-import android.os.Looper
-import androidx.core.content.PermissionChecker
-import com.karumi.dexter.Dexter
-import com.karumi.dexter.MultiplePermissionsReport
-import com.karumi.dexter.PermissionToken
-import com.karumi.dexter.listener.PermissionRequest
-import com.karumi.dexter.listener.multi.MultiplePermissionsListener
-import org.moire.ultrasonic.R
-import org.moire.ultrasonic.util.FileUtil.defaultMusicDirectory
-import timber.log.Timber
-
-/**
- * Contains static functions for Permission handling
- */
-class PermissionUtil(private val applicationContext: Context) {
- private var activityContext: Context? = null
-
- fun onForegroundApplicationStarted(context: Context?) {
- activityContext = context
- }
-
- fun onForegroundApplicationStopped() {
- activityContext = null
- }
-
- /**
- * This function can be used to handle file access permission failures.
- *
- * It will check if the failure is because the necessary permissions aren't available,
- * and it will request them, if necessary.
- *
- * @param callback callback function to execute after the permission request is finished
- */
- fun handlePermissionFailed(callback: ((Boolean) -> Unit)?) {
- val currentCachePath = Settings.cacheLocation
- val defaultCachePath = defaultMusicDirectory.path
-
- // Ultrasonic can do nothing about this error when the Music Directory is already set to the default.
- if (currentCachePath.compareTo(defaultCachePath) == 0) return
-
- if (PermissionChecker.checkSelfPermission(
- applicationContext,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- ) == PermissionChecker.PERMISSION_DENIED ||
- PermissionChecker.checkSelfPermission(
- applicationContext,
- Manifest.permission.READ_EXTERNAL_STORAGE
- ) == PermissionChecker.PERMISSION_DENIED
- ) {
- // While we request permission, the Music Directory is temporarily reset to its default location
- Settings.cacheLocation = defaultMusicDirectory.path
- // If the application is not running, we can't notify the user
- if (activityContext == null) return
- requestFailedPermission(activityContext!!, currentCachePath, callback)
- } else {
- Settings.cacheLocation = defaultMusicDirectory.path
- // If the application is not running, we can't notify the user
- if (activityContext != null) {
- Handler(Looper.getMainLooper()).post {
- showWarning(
- activityContext!!,
- activityContext!!.getString(R.string.permissions_message_box_title),
- activityContext!!.getString(R.string.permissions_access_error),
- null
- )
- }
- }
- callback?.invoke(false)
- }
- }
-
- companion object {
- /**
- * This function requests permission to access the filesystem.
- * It can be used to request the permission initially, e.g. when the user decides to
- * use a non-default folder for the cache
- * @param context context for the operation
- * @param callback callback function to execute after the permission request is finished
- */
- @JvmStatic
- fun requestInitialPermission(
- context: Context,
- callback: ((Boolean) -> Unit)?
- ) {
- Dexter.withContext(context)
- .withPermissions(
- Manifest.permission.WRITE_EXTERNAL_STORAGE,
- Manifest.permission.READ_EXTERNAL_STORAGE
- )
- .withListener(object : MultiplePermissionsListener {
- override fun onPermissionsChecked(report: MultiplePermissionsReport) {
- if (report.areAllPermissionsGranted()) {
- Timber.i("R/W permission granted for external storage")
- callback?.invoke(true)
- return
- }
- if (report.isAnyPermissionPermanentlyDenied) {
- Timber.i(
- "R/W permission is permanently denied for external storage"
- )
- showSettingsDialog(context)
- callback?.invoke(false)
- return
- }
- Timber.i("R/W permission is missing for external storage")
- showWarning(
- context,
- context.getString(R.string.permissions_message_box_title),
- context.getString(R.string.permissions_rationale_description_initial),
- null
- )
- callback?.invoke(false)
- }
-
- override fun onPermissionRationaleShouldBeShown(
- permissions: List,
- token: PermissionToken
- ) {
- showWarning(
- context,
- context.getString(R.string.permissions_rationale_title),
- context.getString(R.string.permissions_rationale_description_initial),
- token
- )
- }
- }).withErrorListener { error ->
- Timber.e(
- "An error has occurred during checking permissions with Dexter: %s",
- error.toString()
- )
- }
- .check()
- }
-
- private fun requestFailedPermission(
- context: Context,
- cacheLocation: String?,
- callback: ((Boolean) -> Unit)?
- ) {
- Dexter.withContext(context)
- .withPermissions(
- Manifest.permission.WRITE_EXTERNAL_STORAGE,
- Manifest.permission.READ_EXTERNAL_STORAGE
- )
- .withListener(object : MultiplePermissionsListener {
- override fun onPermissionsChecked(report: MultiplePermissionsReport) {
- if (report.areAllPermissionsGranted()) {
- Timber.i("Permission granted to use cache directory %s", cacheLocation)
-
- if (cacheLocation != null) {
- Settings.cacheLocation = cacheLocation
- }
- callback?.invoke(true)
- return
- }
- if (report.isAnyPermissionPermanentlyDenied) {
- Timber.i(
- "R/W permission for cache directory %s was permanently denied",
- cacheLocation
- )
- showSettingsDialog(context)
- callback?.invoke(false)
- return
- }
- Timber.i(
- "At least one permission is missing to use directory %s ",
- cacheLocation
- )
- Settings.cacheLocation = defaultMusicDirectory.path
- showWarning(
- context, context.getString(R.string.permissions_message_box_title),
- context.getString(R.string.permissions_permission_missing), null
- )
- callback?.invoke(false)
- }
-
- override fun onPermissionRationaleShouldBeShown(
- permissions: List,
- token: PermissionToken
- ) {
- showWarning(
- context,
- context.getString(R.string.permissions_rationale_title),
- context.getString(R.string.permissions_rationale_description_failed),
- token
- )
- }
- }).withErrorListener { error ->
- Timber.e(
- "An error has occurred during checking permissions with Dexter: %s",
- error.toString()
- )
- }
- .check()
- }
-
- private fun showSettingsDialog(ctx: Context) {
-
- val builder = Util.createDialog(
- context = ctx,
- android.R.drawable.ic_dialog_alert,
- ctx.getString(R.string.permissions_permanent_denial_title),
- ctx.getString(R.string.permissions_permanent_denial_description)
- )
-
- builder.setPositiveButton(ctx.getString(R.string.permissions_open_settings)) {
- dialog, _ ->
- dialog.cancel()
- openSettings(ctx)
- }
-
- builder.setNegativeButton(ctx.getString(R.string.common_cancel)) { dialog, _ ->
- Settings.cacheLocation = defaultMusicDirectory.path
- dialog.cancel()
- }
-
- builder.show()
- }
-
- private fun openSettings(context: Context) {
- val i = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- i.addCategory(Intent.CATEGORY_DEFAULT)
- i.data = Uri.parse("package:" + context.packageName)
- context.startActivity(i)
- }
-
- private fun showWarning(
- context: Context,
- title: String,
- text: String,
- token: PermissionToken?
- ) {
-
- val builder = Util.createDialog(
- context = context,
- android.R.drawable.ic_dialog_alert,
- title,
- text
- )
-
- builder.setPositiveButton(context.getString(R.string.common_ok)) { dialog, _ ->
- dialog.cancel()
- token?.continuePermissionRequest()
- }
- builder.show()
- }
- }
-}
diff --git a/ultrasonic/src/main/res/layout/filepicker_dialog_create_folder.xml b/ultrasonic/src/main/res/layout/filepicker_dialog_create_folder.xml
deleted file mode 100644
index 3737694e..00000000
--- a/ultrasonic/src/main/res/layout/filepicker_dialog_create_folder.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout/filepicker_dialog_main.xml b/ultrasonic/src/main/res/layout/filepicker_dialog_main.xml
deleted file mode 100644
index fb860179..00000000
--- a/ultrasonic/src/main/res/layout/filepicker_dialog_main.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout/filepicker_item_file_lister.xml b/ultrasonic/src/main/res/layout/filepicker_item_file_lister.xml
deleted file mode 100644
index 96fa314e..00000000
--- a/ultrasonic/src/main/res/layout/filepicker_item_file_lister.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml
index 071e0266..b6fd82f8 100644
--- a/ultrasonic/src/main/res/values-cs/strings.xml
+++ b/ultrasonic/src/main/res/values-cs/strings.xml
@@ -380,28 +380,6 @@
Zachovat soubory
Smazat soubory
Smazat soubory logů.
- Ultrasonic nemá přístup k odkládacím souborům hudby. Umístění odkládacího adresáře bylo změněno na výchozí hodnotu.
- Varování
- Ultrasonic vyžaduje práva čtení/zápisu do hudebního odkládacího adresáře. Umístění odkládacího adresáře bylo změněno na výchozí hodnotu.
- Vyžádání oprávnění
- Ultrasonic vyžaduje práva čtení/zápisu do hudebního odkládacího adresáře.\nPovolte aplikaci Ultrasonic přístup do souborového systému.
- Oprávnění dlouhodobě zamítnuto
- Ultrasonic vyžaduje práva čtení/zápisu do hudebního odkládacího adresáře. Tyto zle povolit v nastavení aplikace. Pokud tuto žádost zamítnete, bude použit výchozí odkládací adresář.
- Otevřít nastavení
- Pro změnu umístění odkládacího adresáře potřebuje Ultrasonic práva čtení/zápisu do souborového systému.
-
- Vybrat adresář
- Vytvořit nový adresář
- Selhání vytvoření nového adresáře
- %1$s (interní)
- Výchozí adresář aplikace na %1$s (externí)
- Zadat jméno adresáře
- Vytvořit
- Zadejte platné jméno adresáře
- Tento adresář již existuje.\nZadejte prosím jiné jméno adresáře
- Vybrat
- Použít výchozí
- Dostupná úložiště:
Nakonfigurované servery
Opravdu chcete odebrat server?
diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml
index c176ee3f..d51c4d79 100644
--- a/ultrasonic/src/main/res/values-es/strings.xml
+++ b/ultrasonic/src/main/res/values-es/strings.xml
@@ -405,28 +405,7 @@
Archivos de registro eliminados.
Descargando medios en segundo plano…
- Ultrasonic no puede acceder a la caché de los ficheros de música. La ubicación de la caché se restableció a la ruta predeterminada.
- Atención
- Ultrasonic necesita permiso de lectura / escritura para el directorio caché de música. El directorio caché se restableció a su valor predeterminado.
- Solicitud de permisos
- Ultrasonic necesita permiso de lectura / escritura para el directorio caché de música. Por favor permite a Ultrasonic acceder al sistema de ficheros.
- Permisos denegados permanentemente
- Ultrasonic necesita acceso de lectura / escritura a la ubicación de la caché. Puedes otorgarlos en la configuración de la aplicación. Si rechazas esta solicitud, la ubicación de la caché se restablecerá a su valor predeterminado.
- Abrir configuración
- Para poder cambiar la ubicación de la caché, Ultrasonic necesita permiso de lectura / escritura en el sistema de archivos.
- Selecciona una carpeta
- Crear nueva carpeta
- Fallo al crear una nueva carpeta
- %1$s (Almacenamiento interno)
- Carpeta predeterminada de la aplicación en %1$s (Almacenamiento externo)
- Introduce el nombre de la carpeta
- Crear
- Por favor introduce un nombre de carpeta válido
- Esta carpeta ya existe.\nPor favor proporciona otro nombre para la carpeta
- Seleccionar
- Usar predeterminado
- Unidades disponibles:
Servidores configurados
¿Seguro que deseas borrar el servidor?
diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml
index 9b75c926..392cef16 100644
--- a/ultrasonic/src/main/res/values-fr/strings.xml
+++ b/ultrasonic/src/main/res/values-fr/strings.xml
@@ -394,28 +394,7 @@
Conserver les fichiers
Supprimer les fichiers
Fichiers de log supprimés
- Ultrasonic ne peut pas accéder au cache. Le répertoire de cache a été réinitialisé sur le chemin par défaut.
- Attention
- Ultrasonic requiert les droits de lecture/écriture sur le répertoire de cache. Le répertoire de cache a été réinitialisé à la valeur par défaut.
- Demande de permission
- Ultrasonic requiert les droits de lecture/écriture sur le répertoire de cache. Veuillez autoriser Ultrasonic à accéder au système de fichiers.
- Permissions refusées de manière permanente
- Ultrasonic requiert les droits de lecture/écriture sur le répertoire de cache. Vous pouvez les activer dans les paramètres Android de l’application. Si vous rejetez cette permission, le répertoire par défaut sera utilisé pour le cache.
- Ouvrir les paramètres
- Afin de pouvoir modifier le répertoire de cache, Ultrasonic requiert les droits de lecture/écriture sur le système de fichiers.
- Sélectionner un dossier
- Créer un dossier
- Impossible de créer un dossier
- %1$s (Interne)
- Répertoire par défaut de l’application : %1$s (Mémoire externe)
- Saisir le nom du dossier
- Créer
- Veuillez entrer un nom de dossier valide
- Ce dossier existe déjà.\nVeuillez donner un autre nom
- Sélectionner
- Utiliser la valeur par défaut
- Emplacements de stockage disponibles :
Serveurs configurés
Êtes-vous sûr de vouloir supprimer ce serveur ?
diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml
index f1aa2a85..a0791890 100644
--- a/ultrasonic/src/main/res/values-hu/strings.xml
+++ b/ultrasonic/src/main/res/values-hu/strings.xml
@@ -392,28 +392,7 @@
Fájlok megtartása
Fájlok törlése
Naplófájlok törölve.
- Az Ultrasonic nem éri el a zenei fájl gyorsítótárat. A gyorsítótár helye visszaállítva az alapbeállításra.
- Figyelem
- Az Ultrasonic működéséhez írás/olvasás hozzáférés szükséges a zenei fájl gyorsítótárhoz. A gyorsítótár helye visszaállítva az alapbeállításra.
- Jogosultság kérés
- Az Ultrasonic működéséhez írás/olvasás hozzáférés szükséges a zenei fájl gyorsítótárhoz.\nKérlek, adj hozzáférést az Ultrasonicnak a fájlrendszerhez.
- A jogosultság visszautasítva
- Az Ultrasonic működéséhez írás/olvasás hozzáférés szükséges a zenei fájl gyorsítótárhoz. Ez a beállítás az alkalmazásbeállítások között módosítható. Ha elutasítod ezt a kérést, a gyorsítótár helye visszaáll az alapbeállításra.
- Beállítások megnyitása
- A gyorsítótár helyének megváltoztatásához az Ultrasonicnak írás/olvasás hozzáférésre van szüksége a fájlrendszerhez.
- Mappa kiválasztása
- Új mappa létrehozása
- Az új mappa létrehozása nem sikerült
- %1$s (Belső)
- Alapértelmezett alkalmazásmappa a %1$s tárolón (Külső)
- A mappa neve
- Létrehoz
- Kérjük, adj meg egy érvényes mappanevet
- Ilyen nevű mappa már létezik.\nKérjük, adj meg más nevet.
- Választ
- Alapért.
- Elérhető tárolók:
Beállított szerverek
Biztosan törölni szeretnéd a szervert?
diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml
index 0876e987..0ce8cce4 100644
--- a/ultrasonic/src/main/res/values-nl/strings.xml
+++ b/ultrasonic/src/main/res/values-nl/strings.xml
@@ -405,28 +405,7 @@
De logboeken zijn verwijderd.
Bezig met downloaden van media op de achtergrond…
- Ultrasonic heeft geen toegang tot de muziekcache. De cachelocatie is teruggezet op de standaardlocatie.
- Waarschuwing
- Ultrasonic heeft lees- en schrijfrechten nodig op de muziekcachemap. De cachemap is teruggezet op de standaardmap.
- Rechtenverzoek
- Ultrasonic heeft lees- en schrijfrechten nodig op de muziekcachemap.\nGeef Ultrasonic toegang tot het bestandssysteem.
- De rechten zijn afgewezen
- Ultrasonic heeft lees- en schrijfrechten nodig op de cachemap. Je kunt deze rechten verlenen in de appinstellingen. Als je dit verzoek afwijst, wordt de standaardmap gebruikt.
- Instellingen openen
- Ultrasonic heeft lees- en schrijfrechten nodig op het bestandssysteem om de cachemap te kunnen wijzigen.
- Kies een map
- Nieuwe map maken
- De map kan niet worden aangemaakt
- %1$s (intern)
- De standaard appmap op %1$s (extern)
- Voer een mapnaam in
- Maken
- Voer een geldige mapnaam in
- Deze map bestaat al.\nGeef de map een andere naam.
- Kiezen
- Standaard gebruiken
- Beschikbare schijven:
Ingestelde servers:
Weet je zeker dat je deze server wilt verwijderen?
diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml
index 41dd11fa..2767a907 100644
--- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml
+++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml
@@ -398,28 +398,7 @@
Arquivos de log excluídos.
Baixado mídia em segundo plano…
- O Ultrasonic não pôde acessar o cache dos arquivos de música. O local do cache foi redefinido para o caminho padrão.
- Atenção
- O Ultrasonic precisa de permissão de leitura/escrita no diretório de cache das músicas. O diretório de cache foi redefinido para seu valor padrão.
- Pedido de permissão
- O Ultrasonic precisa de permissão de leitura/escrita no diretório de cache das músicas.\nPermita que o Ultrasonic acesse o sistema de arquivos.
- Permissões negadas permanentemente
- O Ultrasonic precisa de acesso de leitura/escrita no local do cache. Você pode concedê-los nas configurações do aplicativo. Se você rejeitar esta solicitação, a pasta padrão será usada como local do cache.
- Abrir configurações
- Para poder alterar a localização do cache, o Ultrasonic precisa de permissão de leitura/escrita no sistema de arquivos.
- Selecionar uma pasta
- Criar uma nova pasta
- Erro ao criar a nova pasta
- %1$s (Interno)
- Pasta padrão do aplicativo %1$s (Externo)
- Digite o nome da pasta
- Criar
- Digite um nome válido para a pasta
- Esta pasta já existe.\nDigite outro nome para a pasta
- Selecionar
- Usar o padrão
- Unidades disponíveis:
Servidores Configurados
Quer realmente excluir o servidor?
diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml
index 2c4091ea..d13d3181 100644
--- a/ultrasonic/src/main/res/values-ru/strings.xml
+++ b/ultrasonic/src/main/res/values-ru/strings.xml
@@ -394,28 +394,7 @@
Сохранить файлы
Удалить файлы
Удаленные файлы журналов.
- Ultrasonic не может получить доступ к кэшу музыкальных файлов. Местоположение кэша было сброшено на путь по умолчанию.
- Внимание
- Ultrasonic требуется разрешение на чтение/запись в директории музыкального кэша. Каталог кэша был сброшен на значение по умолчанию.
- Запрос разрешения
- Ultrasonic требуется разрешение на чтение/запись в директории музыкального кэша.\nПожалуйста, разрешите Ultrasonic доступ к файловой системе.
- Разрешениях навсегда отклонены
- Ultrasonic необходим доступ на чтение/запись к местоположению кэша. Вы можете предоставить их в настройках приложения. Если вы отклоните этот запрос, в качестве местоположения кэша будет использоваться папка по умолчанию.
- Открыть настройки
- Чтобы иметь возможность изменять местоположение кэша, Ultrasonic необходимо разрешение на чтение/запись в файловой системе.
- Выберите папку
- Создать новую папку
- Не удалось создать новую папку
- %1$s(Внутренний)
- Папка приложения по умолчанию %1$s (Внешняя)
- Введите имя папки
- Создать
- Пожалуйста, введите правильное имя папки
- Эта папка уже существует.\nПожалуйста, укажите другое имя для папки
- Выбрать
- Использовать по умолчанию
- Доступные диски:
Настроенные серверы
Вы уверены, что хотите удалить сервер?
diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml
index 00860b89..6596db6a 100644
--- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml
+++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml
@@ -394,27 +394,7 @@
删除日志文件
在后台下载媒体…
- Ultrasonic 无法访问音乐文件缓存,缓存位置已重置为默认路径。
- 警告
- Ultrasonic 需要对音乐缓存目录的读/写权限,缓存位置已重置为默认路径。
- 需要权限
- Ultrasonic 需要对音乐缓存目录具有读/写权限。\n请允许 Ultrasonic 访问文件系统。
- Ultrasonic 需要对音乐缓存目录具有读/写权限。您可以在应用程序设置中授予该权限,否则将以默认路径作为缓存目录。
- 打开设置
- 为了更改缓存位置,Ultrasonic 需要对文件系统具有读/写权限。
- 选择文件夹
- 创建文件夹
- 无法创建文件夹
- %1$s (内置)
- 默认应用文件夹 %1$s (外置)
- 输入文件夹名称
- 创建
- 请输入一个有效的文件夹名称
- 该文件夹已存在。\n请为该文件夹提供另一个名称
- 选择
- 使用默认值
- 可用驱动器:
配置服务器
您确定要删除此服务器吗?
diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml
index a4f4e6f2..d886cd68 100644
--- a/ultrasonic/src/main/res/values/strings.xml
+++ b/ultrasonic/src/main/res/values/strings.xml
@@ -411,29 +411,6 @@
Deleted log files.
Downloading media in the background…
- Ultrasonic can\'t access the music file cache. Cache location was reset to the default path.
- Warning
- Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.
- Permission request
- Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.
- Permissions permanently denied
- Ultrasonic needs Read/Write access to the cache location. You can grant them in app settings. If you reject this request, the default folder will be used as the cache location.
- Open settings
- To be able to change the cache location, Ultrasonic needs read/write permission to the filesystem.
-
- Select a folder
- Create new folder
- Failed to create new folder
- %1$s (Internal)
- Default app folder on %1$s (External)
- Enter the folder name
- Create
- Please enter a valid folder name
- This folder already exists.\nPlease provide another name for the folder
- Select
- Use default
- Available drives:
-
Configured servers
Are you sure you want to delete the server?
Editing server