This commit is contained in:
tzugen 2021-11-01 17:07:18 +01:00
parent 28ef67a210
commit c9e276dc76
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
27 changed files with 76 additions and 1093 deletions

View File

@ -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

View File

@ -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",

View File

@ -3,10 +3,7 @@
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$!append &amp;&amp; !playNext &amp;&amp; !unpin &amp;&amp; !background</ID>
<ID>ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"</ID>
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference() &amp;&amp; Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.JELLY_BEAN &amp;&amp; ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
@ -21,14 +18,12 @@
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile)</ID>
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)</ID>
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.&lt;no name provided&gt;$String.format("%s\n\n%s", Util.getShareGreeting(), result.url)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s &gt; %s", suffix, transcodedSuffix)</ID>
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
@ -39,23 +34,18 @@
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array&lt;ServerSetting&gt;, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -&gt; Unit), private val serverEditRequestedCallback: ((Int) -&gt; Unit) )</ID>
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$10</ID>
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$60</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$60000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1024L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$256</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$3</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
<ID>MagicNumber:SongView.kt$SongView$3</ID>
<ID>MagicNumber:SongView.kt$SongView$4</ID>
<ID>MagicNumber:SongView.kt$SongView$60</ID>
<ID>MagicNumber:TrackCollectionFragment.kt$TrackCollectionFragment$10</ID>
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>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 )</ID>
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>

View File

@ -119,7 +119,6 @@ dependencies {
testImplementation testing.mockitoKotlin
testImplementation testing.robolectric
implementation other.dexter
implementation other.timber
}

View File

@ -15,17 +15,6 @@
file="../../../../.gradle/caches/transforms-3/e9d816753daf5450613abd98ccf3b80c/transformed/jetified-timber-4.7.1/jars/lint.jar"/>
</issue>
<issue
id="ScopedStorage"
message="WRITE_EXTERNAL_STORAGE no longer provides write access when targeting Android 10, unless you use `requestLegacyExternalStorage`"
errorLine1=" &lt;uses-permission android:name=&quot;android.permission.WRITE_EXTERNAL_STORAGE&quot;/>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="10"
column="36"/>
</issue>
<issue
id="IncludeLayoutParam"
message="Layout parameter `layout_gravity` ignored unless both `layout_width` and `layout_height` are also specified on `&lt;include>` tag"
@ -114,17 +103,6 @@
column="9"/>
</issue>
<issue
id="ExportedContentProvider"
message="Exported content providers can provide access to potentially sensitive data"
errorLine1=" &lt;provider"
errorLine2=" ~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="146"
column="10"/>
</issue>
<issue
id="ExportedReceiver"
message="Exported receiver does not require permission"
@ -169,17 +147,6 @@
column="9"/>
</issue>
<issue
id="UseCompoundDrawables"
message="This tag and its children can be replaced by one `&lt;TextView/>` and a compound drawable"
errorLine1="&lt;LinearLayout xmlns:a=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/main/res/layout/filepicker_item_file_lister.xml"
line="2"
column="2"/>
</issue>
<issue
id="Overdraw"
message="Possible overdraw: Root element paints background `@drawable/appwidget_dark_bg_trans` with a theme that also paints a background (inferred theme is `@style/NoActionBar`)"
@ -1064,27 +1031,27 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="476"
line="453"
column="14"/>
<location
file="src/main/res/values-cs/strings.xml"
line="448"
line="426"
column="14"/>
<location
file="src/main/res/values-es/strings.xml"
line="470"
line="449"
column="14"/>
<location
file="src/main/res/values-fr/strings.xml"
line="459"
line="438"
column="14"/>
<location
file="src/main/res/values-hu/strings.xml"
line="454"
line="433"
column="14"/>
<location
file="src/main/res/values-nl/strings.xml"
line="470"
line="449"
column="14"/>
<location
file="src/main/res/values-pl/strings.xml"
@ -1096,15 +1063,15 @@
column="14"/>
<location
file="src/main/res/values-pt-rBR/strings.xml"
line="463"
line="442"
column="14"/>
<location
file="src/main/res/values-ru/strings.xml"
line="471"
line="450"
column="14"/>
<location
file="src/main/res/values-zh-rCN/strings.xml"
line="452"
line="432"
column="14"/>
</issue>
@ -1678,17 +1645,6 @@
column="22"/>
</issue>
<issue
id="ContentDescription"
message="Missing `contentDescription` attribute on image"
errorLine1=" &lt;ImageView"
errorLine2=" ~~~~~~~~~">
<location
file="src/main/res/layout/filepicker_item_file_lister.xml"
line="10"
column="6"/>
</issue>
<issue
id="ContentDescription"
message="Missing `contentDescription` attribute on image"

View File

@ -7,7 +7,6 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
@ -29,6 +28,7 @@
android:label="@string/common.appname"
android:usesCleartextTraffic="true"
android:supportsRtl="false"
android:preserveLegacyExternalStorage="true"
tools:ignore="UnusedAttribute">
<meta-data android:name="com.google.android.gms.car.application"
@ -145,7 +145,8 @@
<provider
android:name=".provider.SearchSuggestionProvider"
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/>
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"
tools:ignore="ExportedContentProvider" />
<receiver
android:name=".receiver.A2dpIntentReceiver"

View File

@ -45,10 +45,8 @@ import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
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 +58,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 +82,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 +91,6 @@ class NavigationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setUncaughtExceptionHandler()
permissionUtil.onForegroundApplicationStarted(this)
Util.applyTheme(this)
super.onCreate(savedInstanceState)
@ -240,7 +237,6 @@ class NavigationActivity : AppCompatActivity() {
nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener)
themeChangedEventDistributor.unsubscribe(themeChangedEventListener)
imageLoaderProvider.clearImageLoader()
permissionUtil.onForegroundApplicationStopped()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
@ -353,10 +349,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() {

View File

@ -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() }

View File

@ -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<FilePickerAdapter.FileListHolder>() {
private var data: MutableList<FileListItem> = 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<FileListItem>()
val storages: List<File>?
val storagePaths: List<String>?
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<File>): LinkedList<FileListItem> {
val fileList = LinkedList<FileListItem>()
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<AppCompatEditText>(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<View>(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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -1,7 +0,0 @@
package org.moire.ultrasonic.filepicker
import java.io.File
interface OnFileSelectedListener {
fun onFileSelected(file: File?, path: String?)
}

View File

@ -1,11 +1,15 @@
package org.moire.ultrasonic.fragment
import android.app.Activity
import android.app.AlertDialog
import android.content.DialogInterface
import android.content.Intent
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
@ -22,10 +26,9 @@ 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 +40,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
@ -55,6 +55,7 @@ import timber.log.Timber
/**
* Shows main app settings.
*/
@Suppress("TooManyFunctions")
class SettingsFragment :
PreferenceFragmentCompat(),
OnSharedPreferenceChangeListener,
@ -92,9 +93,6 @@ class SettingsFragment :
private val mediaPlayerControllerLazy = inject<MediaPlayerController>(
MediaPlayerController::class.java
)
private val permissionUtil = inject<PermissionUtil>(
PermissionUtil::class.java
)
private val themeChangedEventDistributor = inject<ThemeChangedEventDistributor>(
ThemeChangedEventDistributor::class.java
)
@ -169,6 +167,21 @@ 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
contentResolver.takePersistableUriPermission(uri, RW_FLAG)
setCacheLocation(uri)
}
}
}
override fun onResume() {
super.onResume()
val preferences = preferences
@ -229,29 +242,20 @@ 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(RW_FLAG)
intent.addFlags(PERSISTABLE_FLAG)
startActivityForResult(intent, SELECT_CACHE_ACTIVITY)
true
}
}
@ -419,19 +423,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 +470,11 @@ class SettingsFragment :
.create().show()
}
}
companion object {
const val SELECT_CACHE_ACTIVITY = 161161
const val RW_FLAG = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
const val PERSISTABLE_FLAG = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
}
}

View File

@ -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,7 +25,6 @@ import java.util.Locale
import java.util.SortedSet
import java.util.TreeSet
import java.util.regex.Pattern
import org.koin.java.KoinJavaComponent
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory
import timber.log.Timber
@ -43,10 +43,6 @@ object FileUtil {
const val SUFFIX_SMALL = ".jpeg-small"
private const val UNNAMED = "unnamed"
private val permissionUtil = KoinJavaComponent.inject<PermissionUtil>(
PermissionUtil::class.java
)
fun getSongFile(song: MusicDirectory.Entry): File {
val dir = getAlbumDirectory(song)
@ -237,15 +233,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 +250,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<Boolean, Boolean> {
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)
}
/**

View File

@ -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<PermissionRequest>,
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<PermissionRequest>,
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()
}
}
}

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:inputType="text"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="match_parent"
a:layout_height="match_parent"
a:orientation="vertical">
<TextView
a:id="@+id/current_path"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:layout_alignParentTop="true"
a:layout_margin="20dp"
a:gravity="center_vertical"
a:text=""
a:textSize="18sp" />
<org.moire.ultrasonic.filepicker.FilePickerView
a:id="@+id/file_list_view"
a:layout_width="match_parent"
a:layout_height="match_parent"
a:layout_below="@id/current_path"
a:layout_above="@id/filepicker_create_folder"
a:scrollbars="vertical" />
<Button
a:id="@+id/filepicker_create_folder"
style="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_alignParentBottom="true"
a:layout_marginStart="10dp"
a:layout_marginTop="10dp"
a:layout_marginEnd="10dp"
a:layout_marginBottom="10dp"
a:drawableStart="?attr/filepicker_create_new_folder"
a:drawableLeft="?attr/filepicker_create_new_folder"
a:drawablePadding="10dp"
a:gravity="start|center_vertical"
a:text="@string/filepicker.create_folder" />
</RelativeLayout>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android"
a:id="@+id/layout"
a:padding="5dp"
a:layout_width="match_parent"
a:layout_height="wrap_content"
a:minHeight="?android:attr/listPreferredItemHeight"
a:orientation="horizontal">
<ImageView
a:id="@+id/icon"
a:layout_width="36dp"
a:layout_height="36dp"
a:layout_gravity="center_vertical" />
<TextView
a:id="@+id/name"
a:layout_width="wrap_content"
a:layout_height="wrap_content"
a:layout_gravity="center_vertical"
a:layout_marginStart="20dp"
a:gravity="center_vertical"
a:text=""
a:textSize="18sp" />
</LinearLayout>

View File

@ -380,28 +380,6 @@
<string name="settings.debug.log_keep">Zachovat soubory</string>
<string name="settings.debug.log_delete">Smazat soubory</string>
<string name="settings.debug.log_deleted">Smazat soubory logů.</string>
<string name="permissions.access_error">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.</string>
<string name="permissions.message_box_title">Varování</string>
<string name="permissions.permission_missing">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.</string>
<string name="permissions.rationale_title">Vyžádání oprávnění</string>
<string name="permissions.rationale_description_failed">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.</string>
<string name="permissions.permanent_denial_title">Oprávnění dlouhodobě zamítnuto</string>
<string name="permissions.permanent_denial_description">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ář.</string>
<string name="permissions.open_settings">Otevřít nastavení</string>
<string name="permissions.rationale_description_initial">Pro změnu umístění odkládacího adresáře potřebuje Ultrasonic práva čtení/zápisu do souborového systému.</string>
<string name="filepicker.select_folder">Vybrat adresář</string>
<string name="filepicker.create_folder">Vytvořit nový adresář</string>
<string name="filepicker.create_folder_failed">Selhání vytvoření nového adresáře</string>
<string name="filepicker.internal">%1$s (interní)</string>
<string name="filepicker.default_app_folder">Výchozí adresář aplikace na %1$s (externí)</string>
<string name="filepicker.enter_folder_name">Zadat jméno adresáře</string>
<string name="filepicker.create">Vytvořit</string>
<string name="filepicker.name_invalid">Zadejte platné jméno adresáře</string>
<string name="filepicker.already_exists">Tento adresář již existuje.\nZadejte prosím jiné jméno adresáře</string>
<string name="filepicker.select">Vybrat</string>
<string name="filepicker.default">Použít výchozí</string>
<string name="filepicker.available_drives">Dostupná úložiště:</string>
<string name="server_selector.label">Nakonfigurované servery</string>
<string name="server_selector.delete_confirmation">Opravdu chcete odebrat server?</string>

View File

@ -405,28 +405,7 @@
<string name="settings.debug.log_deleted">Archivos de registro eliminados.</string>
<string name="notification.downloading_title">Descargando medios en segundo plano…</string>
<string name="permissions.access_error">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.</string>
<string name="permissions.message_box_title">Atención</string>
<string name="permissions.permission_missing">Ultrasonic necesita permiso de lectura / escritura para el directorio caché de música. El directorio caché se restableció a su valor predeterminado.</string>
<string name="permissions.rationale_title">Solicitud de permisos</string>
<string name="permissions.rationale_description_failed">Ultrasonic necesita permiso de lectura / escritura para el directorio caché de música. Por favor permite a Ultrasonic acceder al sistema de ficheros.</string>
<string name="permissions.permanent_denial_title">Permisos denegados permanentemente</string>
<string name="permissions.permanent_denial_description">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.</string>
<string name="permissions.open_settings">Abrir configuración</string>
<string name="permissions.rationale_description_initial">Para poder cambiar la ubicación de la caché, Ultrasonic necesita permiso de lectura / escritura en el sistema de archivos.</string>
<string name="filepicker.select_folder">Selecciona una carpeta</string>
<string name="filepicker.create_folder">Crear nueva carpeta</string>
<string name="filepicker.create_folder_failed">Fallo al crear una nueva carpeta</string>
<string name="filepicker.internal">%1$s (Almacenamiento interno)</string>
<string name="filepicker.default_app_folder">Carpeta predeterminada de la aplicación en %1$s (Almacenamiento externo)</string>
<string name="filepicker.enter_folder_name">Introduce el nombre de la carpeta</string>
<string name="filepicker.create">Crear</string>
<string name="filepicker.name_invalid">Por favor introduce un nombre de carpeta válido</string>
<string name="filepicker.already_exists">Esta carpeta ya existe.\nPor favor proporciona otro nombre para la carpeta</string>
<string name="filepicker.select">Seleccionar</string>
<string name="filepicker.default">Usar predeterminado</string>
<string name="filepicker.available_drives">Unidades disponibles:</string>
<string name="server_selector.label">Servidores configurados</string>
<string name="server_selector.delete_confirmation">¿Seguro que deseas borrar el servidor?</string>

View File

@ -394,28 +394,7 @@
<string name="settings.debug.log_keep">Conserver les fichiers</string>
<string name="settings.debug.log_delete">Supprimer les fichiers</string>
<string name="settings.debug.log_deleted">Fichiers de log supprimés</string>
<string name="permissions.access_error">Ultrasonic ne peut pas accéder au cache. Le répertoire de cache a été réinitialisé sur le chemin par défaut.</string>
<string name="permissions.message_box_title">Attention</string>
<string name="permissions.permission_missing">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.</string>
<string name="permissions.rationale_title">Demande de permission</string>
<string name="permissions.rationale_description_failed">Ultrasonic requiert les droits de lecture/écriture sur le répertoire de cache. Veuillez autoriser Ultrasonic à accéder au système de fichiers.</string>
<string name="permissions.permanent_denial_title">Permissions refusées de manière permanente</string>
<string name="permissions.permanent_denial_description">Ultrasonic requiert les droits de lecture/écriture sur le répertoire de cache. Vous pouvez les activer dans les paramètres Android de lapplication. Si vous rejetez cette permission, le répertoire par défaut sera utilisé pour le cache.</string>
<string name="permissions.open_settings">Ouvrir les paramètres</string>
<string name="permissions.rationale_description_initial">Afin de pouvoir modifier le répertoire de cache, Ultrasonic requiert les droits de lecture/écriture sur le système de fichiers.</string>
<string name="filepicker.select_folder">Sélectionner un dossier</string>
<string name="filepicker.create_folder">Créer un dossier</string>
<string name="filepicker.create_folder_failed">Impossible de créer un dossier</string>
<string name="filepicker.internal">%1$s (Interne)</string>
<string name="filepicker.default_app_folder">Répertoire par défaut de lapplication : %1$s (Mémoire externe)</string>
<string name="filepicker.enter_folder_name">Saisir le nom du dossier</string>
<string name="filepicker.create">Créer</string>
<string name="filepicker.name_invalid">Veuillez entrer un nom de dossier valide</string>
<string name="filepicker.already_exists">Ce dossier existe déjà.\nVeuillez donner un autre nom</string>
<string name="filepicker.select">Sélectionner</string>
<string name="filepicker.default">Utiliser la valeur par défaut</string>
<string name="filepicker.available_drives">Emplacements de stockage disponibles:</string>
<string name="server_selector.label">Serveurs configurés</string>
<string name="server_selector.delete_confirmation">Êtes-vous sûr de vouloir supprimer ce serveur?</string>

View File

@ -392,28 +392,7 @@
<string name="settings.debug.log_keep">Fájlok megtartása</string>
<string name="settings.debug.log_delete">Fájlok törlése</string>
<string name="settings.debug.log_deleted">Naplófájlok törölve.</string>
<string name="permissions.access_error">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.</string>
<string name="permissions.message_box_title">Figyelem</string>
<string name="permissions.permission_missing">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.</string>
<string name="permissions.rationale_title">Jogosultság kérés</string>
<string name="permissions.rationale_description_failed">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.</string>
<string name="permissions.permanent_denial_title">A jogosultság visszautasítva</string>
<string name="permissions.permanent_denial_description">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.</string>
<string name="permissions.open_settings">Beállítások megnyitása</string>
<string name="permissions.rationale_description_initial">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.</string>
<string name="filepicker.select_folder">Mappa kiválasztása</string>
<string name="filepicker.create_folder">Új mappa létrehozása</string>
<string name="filepicker.create_folder_failed">Az új mappa létrehozása nem sikerült</string>
<string name="filepicker.internal">%1$s (Belső)</string>
<string name="filepicker.default_app_folder">Alapértelmezett alkalmazásmappa a %1$s tárolón (Külső)</string>
<string name="filepicker.enter_folder_name">A mappa neve</string>
<string name="filepicker.create">Létrehoz</string>
<string name="filepicker.name_invalid">Kérjük, adj meg egy érvényes mappanevet</string>
<string name="filepicker.already_exists">Ilyen nevű mappa már létezik.\nKérjük, adj meg más nevet.</string>
<string name="filepicker.select">Választ</string>
<string name="filepicker.default">Alapért.</string>
<string name="filepicker.available_drives">Elérhető tárolók:</string>
<string name="server_selector.label">Beállított szerverek</string>
<string name="server_selector.delete_confirmation">Biztosan törölni szeretnéd a szervert?</string>

View File

@ -405,28 +405,7 @@
<string name="settings.debug.log_deleted">De logboeken zijn verwijderd.</string>
<string name="notification.downloading_title">Bezig met downloaden van media op de achtergrond…</string>
<string name="permissions.access_error">Ultrasonic heeft geen toegang tot de muziekcache. De cachelocatie is teruggezet op de standaardlocatie.</string>
<string name="permissions.message_box_title">Waarschuwing</string>
<string name="permissions.permission_missing">Ultrasonic heeft lees- en schrijfrechten nodig op de muziekcachemap. De cachemap is teruggezet op de standaardmap.</string>
<string name="permissions.rationale_title">Rechtenverzoek</string>
<string name="permissions.rationale_description_failed">Ultrasonic heeft lees- en schrijfrechten nodig op de muziekcachemap.\nGeef Ultrasonic toegang tot het bestandssysteem.</string>
<string name="permissions.permanent_denial_title">De rechten zijn afgewezen</string>
<string name="permissions.permanent_denial_description">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.</string>
<string name="permissions.open_settings">Instellingen openen</string>
<string name="permissions.rationale_description_initial">Ultrasonic heeft lees- en schrijfrechten nodig op het bestandssysteem om de cachemap te kunnen wijzigen.</string>
<string name="filepicker.select_folder">Kies een map</string>
<string name="filepicker.create_folder">Nieuwe map maken</string>
<string name="filepicker.create_folder_failed">De map kan niet worden aangemaakt</string>
<string name="filepicker.internal">%1$s (intern)</string>
<string name="filepicker.default_app_folder">De standaard appmap op %1$s (extern)</string>
<string name="filepicker.enter_folder_name">Voer een mapnaam in</string>
<string name="filepicker.create">Maken</string>
<string name="filepicker.name_invalid">Voer een geldige mapnaam in</string>
<string name="filepicker.already_exists">Deze map bestaat al.\nGeef de map een andere naam.</string>
<string name="filepicker.select">Kiezen</string>
<string name="filepicker.default">Standaard gebruiken</string>
<string name="filepicker.available_drives">Beschikbare schijven:</string>
<string name="server_selector.label">Ingestelde servers:</string>
<string name="server_selector.delete_confirmation">Weet je zeker dat je deze server wilt verwijderen?</string>

View File

@ -398,28 +398,7 @@
<string name="settings.debug.log_deleted">Arquivos de log excluídos.</string>
<string name="notification.downloading_title">Baixado mídia em segundo plano…</string>
<string name="permissions.access_error">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.</string>
<string name="permissions.message_box_title">Atenção</string>
<string name="permissions.permission_missing">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.</string>
<string name="permissions.rationale_title">Pedido de permissão</string>
<string name="permissions.rationale_description_failed">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.</string>
<string name="permissions.permanent_denial_title">Permissões negadas permanentemente</string>
<string name="permissions.permanent_denial_description">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.</string>
<string name="permissions.open_settings">Abrir configurações</string>
<string name="permissions.rationale_description_initial">Para poder alterar a localização do cache, o Ultrasonic precisa de permissão de leitura/escrita no sistema de arquivos.</string>
<string name="filepicker.select_folder">Selecionar uma pasta</string>
<string name="filepicker.create_folder">Criar uma nova pasta</string>
<string name="filepicker.create_folder_failed">Erro ao criar a nova pasta</string>
<string name="filepicker.internal">%1$s (Interno)</string>
<string name="filepicker.default_app_folder">Pasta padrão do aplicativo %1$s (Externo)</string>
<string name="filepicker.enter_folder_name">Digite o nome da pasta</string>
<string name="filepicker.create">Criar</string>
<string name="filepicker.name_invalid">Digite um nome válido para a pasta</string>
<string name="filepicker.already_exists">Esta pasta já existe.\nDigite outro nome para a pasta</string>
<string name="filepicker.select">Selecionar</string>
<string name="filepicker.default">Usar o padrão</string>
<string name="filepicker.available_drives">Unidades disponíveis:</string>
<string name="server_selector.label">Servidores Configurados</string>
<string name="server_selector.delete_confirmation">Quer realmente excluir o servidor?</string>

View File

@ -394,28 +394,7 @@
<string name="settings.debug.log_keep">Сохранить файлы</string>
<string name="settings.debug.log_delete">Удалить файлы</string>
<string name="settings.debug.log_deleted">Удаленные файлы журналов.</string>
<string name="permissions.access_error">Ultrasonic не может получить доступ к кэшу музыкальных файлов. Местоположение кэша было сброшено на путь по умолчанию.</string>
<string name="permissions.message_box_title">Внимание</string>
<string name="permissions.permission_missing">Ultrasonic требуется разрешение на чтение/запись в директории музыкального кэша. Каталог кэша был сброшен на значение по умолчанию.</string>
<string name="permissions.rationale_title">Запрос разрешения</string>
<string name="permissions.rationale_description_failed">Ultrasonic требуется разрешение на чтение/запись в директории музыкального кэша.\nПожалуйста, разрешите Ultrasonic доступ к файловой системе.</string>
<string name="permissions.permanent_denial_title">Разрешениях навсегда отклонены</string>
<string name="permissions.permanent_denial_description">Ultrasonic необходим доступ на чтение/запись к местоположению кэша. Вы можете предоставить их в настройках приложения. Если вы отклоните этот запрос, в качестве местоположения кэша будет использоваться папка по умолчанию.</string>
<string name="permissions.open_settings">Открыть настройки</string>
<string name="permissions.rationale_description_initial">Чтобы иметь возможность изменять местоположение кэша, Ultrasonic необходимо разрешение на чтение/запись в файловой системе.</string>
<string name="filepicker.select_folder">Выберите папку</string>
<string name="filepicker.create_folder">Создать новую папку</string>
<string name="filepicker.create_folder_failed">Не удалось создать новую папку</string>
<string name="filepicker.internal">%1$s(Внутренний) </string>
<string name="filepicker.default_app_folder">Папка приложения по умолчанию %1$s (Внешняя)</string>
<string name="filepicker.enter_folder_name">Введите имя папки</string>
<string name="filepicker.create">Создать</string>
<string name="filepicker.name_invalid">Пожалуйста, введите правильное имя папки</string>
<string name="filepicker.already_exists">Эта папка уже существует.\nПожалуйста, укажите другое имя для папки</string>
<string name="filepicker.select">Выбрать</string>
<string name="filepicker.default">Использовать по умолчанию</string>
<string name="filepicker.available_drives">Доступные диски:</string>
<string name="server_selector.label">Настроенные серверы</string>
<string name="server_selector.delete_confirmation">Вы уверены, что хотите удалить сервер?</string>

View File

@ -394,27 +394,7 @@
<string name="settings.debug.log_deleted">删除日志文件</string>
<string name="notification.downloading_title">在后台下载媒体…</string>
<string name="permissions.access_error">Ultrasonic 无法访问音乐文件缓存,缓存位置已重置为默认路径。</string>
<string name="permissions.message_box_title">警告</string>
<string name="permissions.permission_missing">Ultrasonic 需要对音乐缓存目录的读/写权限,缓存位置已重置为默认路径。</string>
<string name="permissions.rationale_title">需要权限</string>
<string name="permissions.rationale_description_failed">Ultrasonic 需要对音乐缓存目录具有读/写权限。\n请允许 Ultrasonic 访问文件系统。</string>
<string name="permissions.permanent_denial_description">Ultrasonic 需要对音乐缓存目录具有读/写权限。您可以在应用程序设置中授予该权限,否则将以默认路径作为缓存目录。</string>
<string name="permissions.open_settings">打开设置</string>
<string name="permissions.rationale_description_initial">为了更改缓存位置Ultrasonic 需要对文件系统具有读/写权限。</string>
<string name="filepicker.select_folder">选择文件夹</string>
<string name="filepicker.create_folder">创建文件夹</string>
<string name="filepicker.create_folder_failed">无法创建文件夹</string>
<string name="filepicker.internal">%1$s (内置)</string>
<string name="filepicker.default_app_folder">默认应用文件夹 %1$s (外置)</string>
<string name="filepicker.enter_folder_name">输入文件夹名称</string>
<string name="filepicker.create">创建</string>
<string name="filepicker.name_invalid">请输入一个有效的文件夹名称</string>
<string name="filepicker.already_exists">该文件夹已存在。\n请为该文件夹提供另一个名称</string>
<string name="filepicker.select">选择</string>
<string name="filepicker.default">使用默认值</string>
<string name="filepicker.available_drives">可用驱动器:</string>
<string name="server_selector.label">配置服务器</string>
<string name="server_selector.delete_confirmation">您确定要删除此服务器吗?</string>

View File

@ -411,29 +411,6 @@
<string name="settings.debug.log_deleted">Deleted log files.</string>
<string name="notification.downloading_title">Downloading media in the background…</string>
<string name="permissions.access_error">Ultrasonic can\'t access the music file cache. Cache location was reset to the default path.</string>
<string name="permissions.message_box_title">Warning</string>
<string name="permissions.permission_missing">Ultrasonic needs read/write permission to the music cache directory. Cache directory was reset to its default value.</string>
<string name="permissions.rationale_title">Permission request</string>
<string name="permissions.rationale_description_failed">Ultrasonic needs read/write permission to the music cache directory.\nPlease allow Ultrasonic to access the filesystem.</string>
<string name="permissions.permanent_denial_title">Permissions permanently denied</string>
<string name="permissions.permanent_denial_description">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.</string>
<string name="permissions.open_settings">Open settings</string>
<string name="permissions.rationale_description_initial">To be able to change the cache location, Ultrasonic needs read/write permission to the filesystem.</string>
<string name="filepicker.select_folder">Select a folder</string>
<string name="filepicker.create_folder">Create new folder</string>
<string name="filepicker.create_folder_failed">Failed to create new folder</string>
<string name="filepicker.internal">%1$s (Internal)</string>
<string name="filepicker.default_app_folder">Default app folder on %1$s (External)</string>
<string name="filepicker.enter_folder_name">Enter the folder name</string>
<string name="filepicker.create">Create</string>
<string name="filepicker.name_invalid">Please enter a valid folder name</string>
<string name="filepicker.already_exists">This folder already exists.\nPlease provide another name for the folder</string>
<string name="filepicker.select">Select</string>
<string name="filepicker.default">Use default</string>
<string name="filepicker.available_drives">Available drives:</string>
<string name="server_selector.label">Configured servers</string>
<string name="server_selector.delete_confirmation">Are you sure you want to delete the server?</string>
<string name="server_editor.label">Editing server</string>