mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-03-12 17:40:04 +01:00
API 30
This commit is contained in:
parent
28ef67a210
commit
c9e276dc76
@ -2,7 +2,7 @@ version: 3
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/android:api-29
|
- image: circleci/android:api-30
|
||||||
working_directory: ~/ultrasonic
|
working_directory: ~/ultrasonic
|
||||||
environment:
|
environment:
|
||||||
JVM_OPTS: -Xmx3200m
|
JVM_OPTS: -Xmx3200m
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext.versions = [
|
ext.versions = [
|
||||||
minSdk : 21,
|
minSdk : 21,
|
||||||
targetSdk : 29,
|
targetSdk : 30,
|
||||||
compileSdk : 29,
|
compileSdk : 30,
|
||||||
// You need to run ./gradlew wrapper after updating the version
|
// You need to run ./gradlew wrapper after updating the version
|
||||||
gradle : '7.2',
|
gradle : '7.2',
|
||||||
|
|
||||||
@ -39,7 +39,6 @@ ext.versions = [
|
|||||||
kluent : "1.68",
|
kluent : "1.68",
|
||||||
apacheCodecs : "1.15",
|
apacheCodecs : "1.15",
|
||||||
robolectric : "4.6.1",
|
robolectric : "4.6.1",
|
||||||
dexter : "6.2.3",
|
|
||||||
timber : "4.7.1",
|
timber : "4.7.1",
|
||||||
fastScroll : "2.0.1",
|
fastScroll : "2.0.1",
|
||||||
colorPicker : "2.2.3",
|
colorPicker : "2.2.3",
|
||||||
@ -86,7 +85,6 @@ ext.other = [
|
|||||||
koinAndroid : "io.insert-koin:koin-android:$versions.koin",
|
koinAndroid : "io.insert-koin:koin-android:$versions.koin",
|
||||||
koinViewModel : "io.insert-koin:koin-android-viewmodel:$versions.koin",
|
koinViewModel : "io.insert-koin:koin-android-viewmodel:$versions.koin",
|
||||||
picasso : "com.squareup.picasso:picasso:$versions.picasso",
|
picasso : "com.squareup.picasso:picasso:$versions.picasso",
|
||||||
dexter : "com.karumi:dexter:$versions.dexter",
|
|
||||||
timber : "com.jakewharton.timber:timber:$versions.timber",
|
timber : "com.jakewharton.timber:timber:$versions.timber",
|
||||||
fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll",
|
fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll",
|
||||||
sortListView : "com.github.tzugen:drag-sort-listview:$versions.sortListView",
|
sortListView : "com.github.tzugen:drag-sort-listview:$versions.sortListView",
|
||||||
|
@ -3,10 +3,7 @@
|
|||||||
<ManuallySuppressedIssues></ManuallySuppressedIssues>
|
<ManuallySuppressedIssues></ManuallySuppressedIssues>
|
||||||
<CurrentIssues>
|
<CurrentIssues>
|
||||||
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background</ID>
|
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !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() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
|
|
||||||
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</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: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 enableButtons()</ID>
|
||||||
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</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.BufferTask$String.format("BufferTask (%s)", downloadFile)</ID>
|
||||||
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%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$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
|
||||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.<no name provided>$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("%02d.", trackNumber)</ID>
|
||||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
|
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
|
||||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix)</ID>
|
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix)</ID>
|
||||||
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
||||||
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</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: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: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: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>
|
<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>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
|
||||||
<ID>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) )</ID>
|
<ID>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) )</ID>
|
||||||
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</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.<no name provided>$60000</ID>
|
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$60000</ID>
|
||||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</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$8</ID>
|
||||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L</ID>
|
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L</ID>
|
||||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</ID>
|
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</ID>
|
||||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L</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$3</ID>
|
||||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>
|
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>
|
||||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
||||||
<ID>MagicNumber:SongView.kt$SongView$3</ID>
|
<ID>MagicNumber:SongView.kt$SongView$3</ID>
|
||||||
<ID>MagicNumber:SongView.kt$SongView$4</ID>
|
<ID>MagicNumber:SongView.kt$SongView$4</ID>
|
||||||
<ID>MagicNumber:SongView.kt$SongView$60</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: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: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>
|
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
||||||
|
@ -119,7 +119,6 @@ dependencies {
|
|||||||
testImplementation testing.mockitoKotlin
|
testImplementation testing.mockitoKotlin
|
||||||
testImplementation testing.robolectric
|
testImplementation testing.robolectric
|
||||||
|
|
||||||
implementation other.dexter
|
|
||||||
implementation other.timber
|
implementation other.timber
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,17 +15,6 @@
|
|||||||
file="../../../../.gradle/caches/transforms-3/e9d816753daf5450613abd98ccf3b80c/transformed/jetified-timber-4.7.1/jars/lint.jar"/>
|
file="../../../../.gradle/caches/transforms-3/e9d816753daf5450613abd98ccf3b80c/transformed/jetified-timber-4.7.1/jars/lint.jar"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ScopedStorage"
|
|
||||||
message="WRITE_EXTERNAL_STORAGE no longer provides write access when targeting Android 10, unless you use `requestLegacyExternalStorage`"
|
|
||||||
errorLine1=" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/AndroidManifest.xml"
|
|
||||||
line="10"
|
|
||||||
column="36"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="IncludeLayoutParam"
|
id="IncludeLayoutParam"
|
||||||
message="Layout parameter `layout_gravity` ignored unless both `layout_width` and `layout_height` are also specified on `<include>` tag"
|
message="Layout parameter `layout_gravity` ignored unless both `layout_width` and `layout_height` are also specified on `<include>` tag"
|
||||||
@ -114,17 +103,6 @@
|
|||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ExportedContentProvider"
|
|
||||||
message="Exported content providers can provide access to potentially sensitive data"
|
|
||||||
errorLine1=" <provider"
|
|
||||||
errorLine2=" ~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/AndroidManifest.xml"
|
|
||||||
line="146"
|
|
||||||
column="10"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="ExportedReceiver"
|
id="ExportedReceiver"
|
||||||
message="Exported receiver does not require permission"
|
message="Exported receiver does not require permission"
|
||||||
@ -169,17 +147,6 @@
|
|||||||
column="9"/>
|
column="9"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="UseCompoundDrawables"
|
|
||||||
message="This tag and its children can be replaced by one `<TextView/>` and a compound drawable"
|
|
||||||
errorLine1="<LinearLayout xmlns:a="http://schemas.android.com/apk/res/android""
|
|
||||||
errorLine2=" ~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/filepicker_item_file_lister.xml"
|
|
||||||
line="2"
|
|
||||||
column="2"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="Overdraw"
|
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`)"
|
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=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values/strings.xml"
|
file="src/main/res/values/strings.xml"
|
||||||
line="476"
|
line="453"
|
||||||
column="14"/>
|
column="14"/>
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values-cs/strings.xml"
|
file="src/main/res/values-cs/strings.xml"
|
||||||
line="448"
|
line="426"
|
||||||
column="14"/>
|
column="14"/>
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values-es/strings.xml"
|
file="src/main/res/values-es/strings.xml"
|
||||||
line="470"
|
line="449"
|
||||||
column="14"/>
|
column="14"/>
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values-fr/strings.xml"
|
file="src/main/res/values-fr/strings.xml"
|
||||||
line="459"
|
line="438"
|
||||||
column="14"/>
|
column="14"/>
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values-hu/strings.xml"
|
file="src/main/res/values-hu/strings.xml"
|
||||||
line="454"
|
line="433"
|
||||||
column="14"/>
|
column="14"/>
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values-nl/strings.xml"
|
file="src/main/res/values-nl/strings.xml"
|
||||||
line="470"
|
line="449"
|
||||||
column="14"/>
|
column="14"/>
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values-pl/strings.xml"
|
file="src/main/res/values-pl/strings.xml"
|
||||||
@ -1096,15 +1063,15 @@
|
|||||||
column="14"/>
|
column="14"/>
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values-pt-rBR/strings.xml"
|
file="src/main/res/values-pt-rBR/strings.xml"
|
||||||
line="463"
|
line="442"
|
||||||
column="14"/>
|
column="14"/>
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values-ru/strings.xml"
|
file="src/main/res/values-ru/strings.xml"
|
||||||
line="471"
|
line="450"
|
||||||
column="14"/>
|
column="14"/>
|
||||||
<location
|
<location
|
||||||
file="src/main/res/values-zh-rCN/strings.xml"
|
file="src/main/res/values-zh-rCN/strings.xml"
|
||||||
line="452"
|
line="432"
|
||||||
column="14"/>
|
column="14"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
@ -1678,17 +1645,6 @@
|
|||||||
column="22"/>
|
column="22"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="ContentDescription"
|
|
||||||
message="Missing `contentDescription` attribute on image"
|
|
||||||
errorLine1=" <ImageView"
|
|
||||||
errorLine2=" ~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/filepicker_item_file_lister.xml"
|
|
||||||
line="10"
|
|
||||||
column="6"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="ContentDescription"
|
id="ContentDescription"
|
||||||
message="Missing `contentDescription` attribute on image"
|
message="Missing `contentDescription` attribute on image"
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<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.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||||
@ -29,6 +28,7 @@
|
|||||||
android:label="@string/common.appname"
|
android:label="@string/common.appname"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:supportsRtl="false"
|
android:supportsRtl="false"
|
||||||
|
android:preserveLegacyExternalStorage="true"
|
||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
<meta-data android:name="com.google.android.gms.car.application"
|
<meta-data android:name="com.google.android.gms.car.application"
|
||||||
@ -145,7 +145,8 @@
|
|||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".provider.SearchSuggestionProvider"
|
android:name=".provider.SearchSuggestionProvider"
|
||||||
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/>
|
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"
|
||||||
|
tools:ignore="ExportedContentProvider" />
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receiver.A2dpIntentReceiver"
|
android:name=".receiver.A2dpIntentReceiver"
|
||||||
|
@ -45,10 +45,8 @@ import org.moire.ultrasonic.service.MediaPlayerController
|
|||||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.FileUtil
|
|
||||||
import org.moire.ultrasonic.util.NowPlayingEventDistributor
|
import org.moire.ultrasonic.util.NowPlayingEventDistributor
|
||||||
import org.moire.ultrasonic.util.NowPlayingEventListener
|
import org.moire.ultrasonic.util.NowPlayingEventListener
|
||||||
import org.moire.ultrasonic.util.PermissionUtil
|
|
||||||
import org.moire.ultrasonic.util.ServerColor
|
import org.moire.ultrasonic.util.ServerColor
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.SubsonicUncaughtExceptionHandler
|
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
|
* The main Activity of Ultrasonic which loads all other screens as Fragments
|
||||||
*/
|
*/
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
class NavigationActivity : AppCompatActivity() {
|
class NavigationActivity : AppCompatActivity() {
|
||||||
private var chatMenuItem: MenuItem? = null
|
private var chatMenuItem: MenuItem? = null
|
||||||
private var bookmarksMenuItem: MenuItem? = null
|
private var bookmarksMenuItem: MenuItem? = null
|
||||||
@ -83,7 +82,6 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
private val imageLoaderProvider: ImageLoaderProvider by inject()
|
private val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||||
private val nowPlayingEventDistributor: NowPlayingEventDistributor by inject()
|
private val nowPlayingEventDistributor: NowPlayingEventDistributor by inject()
|
||||||
private val themeChangedEventDistributor: ThemeChangedEventDistributor by inject()
|
private val themeChangedEventDistributor: ThemeChangedEventDistributor by inject()
|
||||||
private val permissionUtil: PermissionUtil by inject()
|
|
||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
private val serverRepository: ServerSettingDao by inject()
|
private val serverRepository: ServerSettingDao by inject()
|
||||||
|
|
||||||
@ -93,7 +91,6 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
setUncaughtExceptionHandler()
|
setUncaughtExceptionHandler()
|
||||||
permissionUtil.onForegroundApplicationStarted(this)
|
|
||||||
Util.applyTheme(this)
|
Util.applyTheme(this)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -240,7 +237,6 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener)
|
nowPlayingEventDistributor.unsubscribe(nowPlayingEventListener)
|
||||||
themeChangedEventDistributor.unsubscribe(themeChangedEventListener)
|
themeChangedEventDistributor.unsubscribe(themeChangedEventListener)
|
||||||
imageLoaderProvider.clearImageLoader()
|
imageLoaderProvider.clearImageLoader()
|
||||||
permissionUtil.onForegroundApplicationStopped()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
@ -353,10 +349,6 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun loadSettings() {
|
private fun loadSettings() {
|
||||||
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
|
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() {
|
private fun exit() {
|
||||||
|
@ -7,7 +7,6 @@ import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
|||||||
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
import org.moire.ultrasonic.util.MediaSessionEventDistributor
|
||||||
import org.moire.ultrasonic.util.MediaSessionHandler
|
import org.moire.ultrasonic.util.MediaSessionHandler
|
||||||
import org.moire.ultrasonic.util.NowPlayingEventDistributor
|
import org.moire.ultrasonic.util.NowPlayingEventDistributor
|
||||||
import org.moire.ultrasonic.util.PermissionUtil
|
|
||||||
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
|
import org.moire.ultrasonic.util.ThemeChangedEventDistributor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,7 +15,6 @@ import org.moire.ultrasonic.util.ThemeChangedEventDistributor
|
|||||||
val applicationModule = module {
|
val applicationModule = module {
|
||||||
single { ActiveServerProvider(get()) }
|
single { ActiveServerProvider(get()) }
|
||||||
single { ImageLoaderProvider(androidContext()) }
|
single { ImageLoaderProvider(androidContext()) }
|
||||||
single { PermissionUtil(androidContext()) }
|
|
||||||
single { NowPlayingEventDistributor() }
|
single { NowPlayingEventDistributor() }
|
||||||
single { ThemeChangedEventDistributor() }
|
single { ThemeChangedEventDistributor() }
|
||||||
single { MediaSessionEventDistributor() }
|
single { MediaSessionEventDistributor() }
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package org.moire.ultrasonic.filepicker
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
interface OnFileSelectedListener {
|
|
||||||
fun onFileSelected(file: File?, path: String?)
|
|
||||||
}
|
|
@ -1,11 +1,15 @@
|
|||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.fragment
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.DocumentsContract
|
||||||
import android.provider.SearchRecentSuggestions
|
import android.provider.SearchRecentSuggestions
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.StringRes
|
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.get
|
||||||
import org.koin.java.KoinJavaComponent.inject
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.featureflags.Feature
|
import org.moire.ultrasonic.featureflags.Feature
|
||||||
import org.moire.ultrasonic.featureflags.FeatureStorage
|
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.fragment.FragmentTitle.Companion.setTitle
|
||||||
import org.moire.ultrasonic.log.FileLoggerTree
|
import org.moire.ultrasonic.log.FileLoggerTree
|
||||||
import org.moire.ultrasonic.log.FileLoggerTree.Companion.deleteLogFiles
|
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.service.MediaPlayerController
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.FileUtil.defaultMusicDirectory
|
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.FileUtil.ultrasonicDirectory
|
||||||
import org.moire.ultrasonic.util.MediaSessionHandler
|
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
|
||||||
import org.moire.ultrasonic.util.Settings.preferences
|
import org.moire.ultrasonic.util.Settings.preferences
|
||||||
import org.moire.ultrasonic.util.Settings.shareGreeting
|
import org.moire.ultrasonic.util.Settings.shareGreeting
|
||||||
@ -55,6 +55,7 @@ import timber.log.Timber
|
|||||||
/**
|
/**
|
||||||
* Shows main app settings.
|
* Shows main app settings.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
class SettingsFragment :
|
class SettingsFragment :
|
||||||
PreferenceFragmentCompat(),
|
PreferenceFragmentCompat(),
|
||||||
OnSharedPreferenceChangeListener,
|
OnSharedPreferenceChangeListener,
|
||||||
@ -92,9 +93,6 @@ class SettingsFragment :
|
|||||||
private val mediaPlayerControllerLazy = inject<MediaPlayerController>(
|
private val mediaPlayerControllerLazy = inject<MediaPlayerController>(
|
||||||
MediaPlayerController::class.java
|
MediaPlayerController::class.java
|
||||||
)
|
)
|
||||||
private val permissionUtil = inject<PermissionUtil>(
|
|
||||||
PermissionUtil::class.java
|
|
||||||
)
|
|
||||||
private val themeChangedEventDistributor = inject<ThemeChangedEventDistributor>(
|
private val themeChangedEventDistributor = inject<ThemeChangedEventDistributor>(
|
||||||
ThemeChangedEventDistributor::class.java
|
ThemeChangedEventDistributor::class.java
|
||||||
)
|
)
|
||||||
@ -169,6 +167,21 @@ class SettingsFragment :
|
|||||||
update()
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
val preferences = preferences
|
val preferences = preferences
|
||||||
@ -229,29 +242,20 @@ class SettingsFragment :
|
|||||||
cacheLocation!!.summary = Settings.cacheLocation
|
cacheLocation!!.summary = Settings.cacheLocation
|
||||||
cacheLocation!!.onPreferenceClickListener =
|
cacheLocation!!.onPreferenceClickListener =
|
||||||
Preference.OnPreferenceClickListener {
|
Preference.OnPreferenceClickListener {
|
||||||
// If the user tries to change the cache location,
|
val isDefault = Settings.cacheLocation == defaultMusicDirectory.path
|
||||||
// we must first check to see if we have write access.
|
|
||||||
requestInitialPermission(
|
// Choose a directory using the system's file picker.
|
||||||
requireActivity()
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
) {
|
|
||||||
if (it) {
|
if (!isDefault && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val filePickerDialog = createFilePickerDialog(
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, defaultMusicDirectory.path)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
intent.addFlags(RW_FLAG)
|
||||||
|
intent.addFlags(PERSISTABLE_FLAG)
|
||||||
|
|
||||||
|
startActivityForResult(intent, SELECT_CACHE_ACTIVITY)
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -419,20 +423,15 @@ class SettingsFragment :
|
|||||||
sendBluetoothAlbumArt!!.isEnabled = enabled
|
sendBluetoothAlbumArt!!.isEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setCacheLocation(path: String) {
|
private fun setCacheLocation(uri: Uri) {
|
||||||
val dir = File(path)
|
if (uri.path != null) {
|
||||||
if (!ensureDirectoryExistsAndIsReadWritable(dir)) {
|
cacheLocation!!.summary = uri.path
|
||||||
permissionUtil.value.handlePermissionFailed {
|
Settings.cacheLocation = uri.path!!
|
||||||
val currentPath = Settings.cacheLocation
|
|
||||||
cacheLocation!!.summary = currentPath
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cacheLocation!!.summary = path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear download queue.
|
// Clear download queue.
|
||||||
mediaPlayerControllerLazy.value.clear()
|
mediaPlayerControllerLazy.value.clear()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setDebugLogToFile(writeLog: Boolean) {
|
private fun setDebugLogToFile(writeLog: Boolean) {
|
||||||
if (writeLog) {
|
if (writeLog) {
|
||||||
@ -471,4 +470,11 @@ class SettingsFragment :
|
|||||||
.create().show()
|
.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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import android.content.Context
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import android.util.Pair
|
||||||
import java.io.BufferedWriter
|
import java.io.BufferedWriter
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
@ -24,7 +25,6 @@ import java.util.Locale
|
|||||||
import java.util.SortedSet
|
import java.util.SortedSet
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import org.koin.java.KoinJavaComponent
|
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -43,10 +43,6 @@ object FileUtil {
|
|||||||
const val SUFFIX_SMALL = ".jpeg-small"
|
const val SUFFIX_SMALL = ".jpeg-small"
|
||||||
private const val UNNAMED = "unnamed"
|
private const val UNNAMED = "unnamed"
|
||||||
|
|
||||||
private val permissionUtil = KoinJavaComponent.inject<PermissionUtil>(
|
|
||||||
PermissionUtil::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getSongFile(song: MusicDirectory.Entry): File {
|
fun getSongFile(song: MusicDirectory.Entry): File {
|
||||||
val dir = getAlbumDirectory(song)
|
val dir = getAlbumDirectory(song)
|
||||||
|
|
||||||
@ -237,15 +233,13 @@ object FileUtil {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
val ultrasonicDirectory: File
|
val ultrasonicDirectory: File
|
||||||
get() {
|
get() {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File(
|
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File(
|
||||||
Environment.getExternalStorageDirectory(),
|
Environment.getExternalStorageDirectory(),
|
||||||
"Android/data/org.moire.ultrasonic"
|
"Android/data/org.moire.ultrasonic"
|
||||||
) else UApp.applicationContext().getExternalFilesDir(null)!!
|
) 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
|
@JvmStatic
|
||||||
val defaultMusicDirectory: File
|
val defaultMusicDirectory: File
|
||||||
get() = getOrCreateDirectory("music")
|
get() = getOrCreateDirectory("music")
|
||||||
@ -256,38 +250,39 @@ object FileUtil {
|
|||||||
val path = Settings.cacheLocation
|
val path = Settings.cacheLocation
|
||||||
val dir = File(path)
|
val dir = File(path)
|
||||||
val hasAccess = ensureDirectoryExistsAndIsReadWritable(dir)
|
val hasAccess = ensureDirectoryExistsAndIsReadWritable(dir)
|
||||||
if (!hasAccess) permissionUtil.value.handlePermissionFailed(null)
|
return if (hasAccess.second) dir else defaultMusicDirectory
|
||||||
return if (hasAccess) dir else defaultMusicDirectory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Suppress("ReturnCount")
|
@Suppress("ReturnCount")
|
||||||
fun ensureDirectoryExistsAndIsReadWritable(dir: File?): Boolean {
|
fun ensureDirectoryExistsAndIsReadWritable(dir: File?): Pair<Boolean, Boolean> {
|
||||||
|
val noAccess = Pair(false, false)
|
||||||
|
|
||||||
if (dir == null) {
|
if (dir == null) {
|
||||||
return false
|
return noAccess
|
||||||
}
|
}
|
||||||
if (dir.exists()) {
|
if (dir.exists()) {
|
||||||
if (!dir.isDirectory) {
|
if (!dir.isDirectory) {
|
||||||
Timber.w("%s exists but is not a directory.", dir)
|
Timber.w("%s exists but is not a directory.", dir)
|
||||||
return false
|
return noAccess
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (dir.mkdirs()) {
|
if (dir.mkdirs()) {
|
||||||
Timber.i("Created directory %s", dir)
|
Timber.i("Created directory %s", dir)
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Failed to create directory %s", dir)
|
Timber.w("Failed to create directory %s", dir)
|
||||||
return false
|
return noAccess
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!dir.canRead()) {
|
if (!dir.canRead()) {
|
||||||
Timber.w("No read permission for directory %s", dir)
|
Timber.w("No read permission for directory %s", dir)
|
||||||
return false
|
return noAccess
|
||||||
}
|
}
|
||||||
if (!dir.canWrite()) {
|
if (!dir.canWrite()) {
|
||||||
Timber.w("No write permission for directory %s", dir)
|
Timber.w("No write permission for directory %s", dir)
|
||||||
return false
|
return Pair(true, false)
|
||||||
}
|
}
|
||||||
return true
|
return Pair(true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -380,28 +380,6 @@
|
|||||||
<string name="settings.debug.log_keep">Zachovat soubory</string>
|
<string name="settings.debug.log_keep">Zachovat soubory</string>
|
||||||
<string name="settings.debug.log_delete">Smazat soubory</string>
|
<string name="settings.debug.log_delete">Smazat soubory</string>
|
||||||
<string name="settings.debug.log_deleted">Smazat soubory logů.</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.label">Nakonfigurované servery</string>
|
||||||
<string name="server_selector.delete_confirmation">Opravdu chcete odebrat server?</string>
|
<string name="server_selector.delete_confirmation">Opravdu chcete odebrat server?</string>
|
||||||
|
@ -405,28 +405,7 @@
|
|||||||
<string name="settings.debug.log_deleted">Archivos de registro eliminados.</string>
|
<string name="settings.debug.log_deleted">Archivos de registro eliminados.</string>
|
||||||
<string name="notification.downloading_title">Descargando medios en segundo plano…</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.label">Servidores configurados</string>
|
||||||
<string name="server_selector.delete_confirmation">¿Seguro que deseas borrar el servidor?</string>
|
<string name="server_selector.delete_confirmation">¿Seguro que deseas borrar el servidor?</string>
|
||||||
|
@ -394,28 +394,7 @@
|
|||||||
<string name="settings.debug.log_keep">Conserver les fichiers</string>
|
<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_delete">Supprimer les fichiers</string>
|
||||||
<string name="settings.debug.log_deleted">Fichiers de log supprimés</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 l’application. 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 l’application : %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.label">Serveurs configurés</string>
|
||||||
<string name="server_selector.delete_confirmation">Êtes-vous sûr de vouloir supprimer ce serveur ?</string>
|
<string name="server_selector.delete_confirmation">Êtes-vous sûr de vouloir supprimer ce serveur ?</string>
|
||||||
|
@ -392,28 +392,7 @@
|
|||||||
<string name="settings.debug.log_keep">Fájlok megtartása</string>
|
<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_delete">Fájlok törlése</string>
|
||||||
<string name="settings.debug.log_deleted">Naplófájlok törölve.</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.label">Beállított szerverek</string>
|
||||||
<string name="server_selector.delete_confirmation">Biztosan törölni szeretnéd a szervert?</string>
|
<string name="server_selector.delete_confirmation">Biztosan törölni szeretnéd a szervert?</string>
|
||||||
|
@ -405,28 +405,7 @@
|
|||||||
<string name="settings.debug.log_deleted">De logboeken zijn verwijderd.</string>
|
<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="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.label">Ingestelde servers:</string>
|
||||||
<string name="server_selector.delete_confirmation">Weet je zeker dat je deze server wilt verwijderen?</string>
|
<string name="server_selector.delete_confirmation">Weet je zeker dat je deze server wilt verwijderen?</string>
|
||||||
|
@ -398,28 +398,7 @@
|
|||||||
<string name="settings.debug.log_deleted">Arquivos de log excluídos.</string>
|
<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="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.label">Servidores Configurados</string>
|
||||||
<string name="server_selector.delete_confirmation">Quer realmente excluir o servidor?</string>
|
<string name="server_selector.delete_confirmation">Quer realmente excluir o servidor?</string>
|
||||||
|
@ -394,28 +394,7 @@
|
|||||||
<string name="settings.debug.log_keep">Сохранить файлы</string>
|
<string name="settings.debug.log_keep">Сохранить файлы</string>
|
||||||
<string name="settings.debug.log_delete">Удалить файлы</string>
|
<string name="settings.debug.log_delete">Удалить файлы</string>
|
||||||
<string name="settings.debug.log_deleted">Удаленные файлы журналов.</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.label">Настроенные серверы</string>
|
||||||
<string name="server_selector.delete_confirmation">Вы уверены, что хотите удалить сервер?</string>
|
<string name="server_selector.delete_confirmation">Вы уверены, что хотите удалить сервер?</string>
|
||||||
|
@ -394,27 +394,7 @@
|
|||||||
<string name="settings.debug.log_deleted">删除日志文件</string>
|
<string name="settings.debug.log_deleted">删除日志文件</string>
|
||||||
<string name="notification.downloading_title">在后台下载媒体…</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.label">配置服务器</string>
|
||||||
<string name="server_selector.delete_confirmation">您确定要删除此服务器吗?</string>
|
<string name="server_selector.delete_confirmation">您确定要删除此服务器吗?</string>
|
||||||
|
@ -411,29 +411,6 @@
|
|||||||
<string name="settings.debug.log_deleted">Deleted log files.</string>
|
<string name="settings.debug.log_deleted">Deleted log files.</string>
|
||||||
<string name="notification.downloading_title">Downloading media in the background…</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.label">Configured servers</string>
|
||||||
<string name="server_selector.delete_confirmation">Are you sure you want to delete the server?</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>
|
<string name="server_editor.label">Editing server</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user