mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-25 05:10:55 +03:00
Implemented Storage Access Framework as Music Cache
This commit is contained in:
parent
a6e76e9d53
commit
1d0bb944e1
@ -42,6 +42,7 @@ ext.versions = [
|
||||
timber : "4.7.1",
|
||||
fastScroll : "2.0.1",
|
||||
colorPicker : "2.2.3",
|
||||
fsaf : "1.1"
|
||||
]
|
||||
|
||||
ext.gradlePlugins = [
|
||||
@ -89,6 +90,7 @@ ext.other = [
|
||||
fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll",
|
||||
sortListView : "com.github.tzugen:drag-sort-listview:$versions.sortListView",
|
||||
colorPickerView : "com.github.skydoves:colorpickerview:$versions.colorPicker",
|
||||
fsaf : "com.github.K1rakishou:Fuck-Storage-Access-Framework:$versions.fsaf",
|
||||
]
|
||||
|
||||
ext.testing = [
|
||||
|
@ -106,6 +106,7 @@ dependencies {
|
||||
implementation other.fastScroll
|
||||
implementation other.sortListView
|
||||
implementation other.colorPickerView
|
||||
implementation other.fsaf
|
||||
|
||||
kapt androidSupport.room
|
||||
|
||||
|
@ -8,8 +8,6 @@ import org.moire.ultrasonic.service.Supplier;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
@ -155,8 +153,7 @@ public class StreamProxy implements Runnable
|
||||
}
|
||||
|
||||
Timber.i("Processing request for file %s", localPath);
|
||||
File file = new File(localPath);
|
||||
if (!file.exists()) {
|
||||
if (!StorageFile.Companion.isPathExists(localPath)) {
|
||||
Timber.e("File %s does not exist", localPath);
|
||||
return false;
|
||||
}
|
||||
@ -194,12 +191,12 @@ public class StreamProxy implements Runnable
|
||||
while (isRunning && !client.isClosed())
|
||||
{
|
||||
// See if there's more to send
|
||||
File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteOrSaveFile() : downloadFile.getPartialFile();
|
||||
String file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteOrSaveFile() : downloadFile.getPartialFile();
|
||||
int cbSentThisBatch = 0;
|
||||
|
||||
if (file.exists())
|
||||
if (StorageFile.Companion.isPathExists(file))
|
||||
{
|
||||
FileInputStream input = new FileInputStream(file);
|
||||
InputStream input = StorageFile.Companion.getFromPath(file).getFileInputStream();
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -20,7 +20,6 @@ import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import java.io.File
|
||||
import kotlin.math.ceil
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.java.KoinJavaComponent.get
|
||||
@ -51,6 +50,7 @@ import org.moire.ultrasonic.util.TimeSpanPreference
|
||||
import org.moire.ultrasonic.util.TimeSpanPreferenceDialogFragmentCompat
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Shows main app settings.
|
||||
@ -167,17 +167,28 @@ class SettingsFragment :
|
||||
}
|
||||
|
||||
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
|
||||
if (
|
||||
requestCode != SELECT_CACHE_ACTIVITY ||
|
||||
resultCode != Activity.RESULT_OK ||
|
||||
resultData == null
|
||||
) return
|
||||
|
||||
contentResolver.takePersistableUriPermission(uri, RW_FLAG)
|
||||
val read = (resultData.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0
|
||||
val write = (resultData.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0
|
||||
val persist = (resultData.flags and Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0
|
||||
|
||||
setCacheLocation(uri)
|
||||
}
|
||||
// TODO Should we show an error?
|
||||
if (!read || !write || !persist) return
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,7 +249,9 @@ class SettingsFragment :
|
||||
}
|
||||
|
||||
private fun setupCacheLocationPreference() {
|
||||
cacheLocation!!.summary = Settings.cacheLocation
|
||||
// TODO add means to reset cache directory to its default value
|
||||
val uri = Uri.parse(Settings.cacheLocation)
|
||||
cacheLocation!!.summary = uri.path
|
||||
cacheLocation!!.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
val isDefault = Settings.cacheLocation == defaultMusicDirectory.path
|
||||
@ -400,6 +413,7 @@ class SettingsFragment :
|
||||
}
|
||||
|
||||
private fun setHideMedia(hide: Boolean) {
|
||||
// TODO this only hides the media files in the Ultrasonic dir and not in the music cache
|
||||
val nomediaDir = File(ultrasonicDirectory, ".nomedia")
|
||||
if (hide && !nomediaDir.exists()) {
|
||||
if (!nomediaDir.mkdir()) {
|
||||
@ -425,7 +439,7 @@ class SettingsFragment :
|
||||
private fun setCacheLocation(uri: Uri) {
|
||||
if (uri.path != null) {
|
||||
cacheLocation!!.summary = uri.path
|
||||
Settings.cacheLocation = uri.path!!
|
||||
Settings.cacheLocation = uri.toString()
|
||||
|
||||
// Clear download queue.
|
||||
mediaPlayerControllerLazy.value.clear()
|
||||
|
@ -5,8 +5,10 @@ import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.StorageFile
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
@Suppress("UtilityClassWithPublicConstructor")
|
||||
class BitmapUtils {
|
||||
@ -31,8 +33,8 @@ class BitmapUtils {
|
||||
if (entry == null) return null
|
||||
val albumArtFile = FileUtil.getAlbumArtFile(entry)
|
||||
val bitmap: Bitmap? = null
|
||||
if (albumArtFile.exists()) {
|
||||
return getBitmapFromDisk(albumArtFile.path, size, bitmap)
|
||||
if (albumArtFile != null && File(albumArtFile).exists()) {
|
||||
return getBitmapFromDisk(albumArtFile, size, bitmap)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@ -43,8 +45,8 @@ class BitmapUtils {
|
||||
): Bitmap? {
|
||||
val albumArtFile = FileUtil.getAlbumArtFile(filename)
|
||||
val bitmap: Bitmap? = null
|
||||
if (albumArtFile != null && albumArtFile.exists()) {
|
||||
return getBitmapFromDisk(albumArtFile.path, size, bitmap)
|
||||
if (albumArtFile != null && File(albumArtFile).exists()) {
|
||||
return getBitmapFromDisk(albumArtFile, size, bitmap)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import androidx.core.content.ContextCompat
|
||||
import com.squareup.picasso.LruCache
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.squareup.picasso.RequestCreator
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
@ -21,8 +20,10 @@ import org.moire.ultrasonic.api.subsonic.throwOnFailure
|
||||
import org.moire.ultrasonic.api.subsonic.toStreamResponse
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.StorageFile
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Our new image loader which uses Picasso as a backend.
|
||||
@ -161,7 +162,8 @@ class ImageLoader(
|
||||
val file = FileUtil.getAlbumArtFile(entry)
|
||||
|
||||
// Return if have a cache hit
|
||||
if (file.exists()) return
|
||||
if (file != null && File(file).exists()) return
|
||||
File(file!!).createNewFile()
|
||||
|
||||
// Can't load empty string ids
|
||||
val id = entry.coverArt
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.moire.ultrasonic.log
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
@ -8,6 +7,7 @@ import java.util.Locale
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* A Timber Tree which can be used to log to a file
|
||||
|
@ -9,12 +9,9 @@ package org.moire.ultrasonic.service
|
||||
|
||||
import android.text.TextUtils
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.RandomAccessFile
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
@ -25,6 +22,7 @@ import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.CacheCleaner
|
||||
import org.moire.ultrasonic.util.CancellableTask
|
||||
import org.moire.ultrasonic.util.StorageFile
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
@ -37,9 +35,9 @@ class DownloadFile(
|
||||
val song: MusicDirectory.Entry,
|
||||
private val save: Boolean
|
||||
) : KoinComponent, Identifiable {
|
||||
val partialFile: File
|
||||
val completeFile: File
|
||||
private val saveFile: File = FileUtil.getSongFile(song)
|
||||
val partialFile: String
|
||||
val completeFile: String
|
||||
private val saveFile: String = FileUtil.getSongFile(song)
|
||||
private var downloadTask: CancellableTask? = null
|
||||
var isFailed = false
|
||||
private var retryCount = MAX_RETRIES
|
||||
@ -65,8 +63,8 @@ class DownloadFile(
|
||||
val status: MutableLiveData<DownloadStatus> = MutableLiveData(DownloadStatus.IDLE)
|
||||
|
||||
init {
|
||||
partialFile = File(saveFile.parent, FileUtil.getPartialFile(saveFile.name))
|
||||
completeFile = File(saveFile.parent, FileUtil.getCompleteFile(saveFile.name))
|
||||
partialFile = FileUtil.getParentPath(saveFile) + "/" + FileUtil.getPartialFile(FileUtil.getNameFromPath(saveFile))
|
||||
completeFile = FileUtil.getParentPath(saveFile) + "/" + FileUtil.getCompleteFile(FileUtil.getNameFromPath(saveFile))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,14 +89,14 @@ class DownloadFile(
|
||||
}
|
||||
}
|
||||
|
||||
val completeOrSaveFile: File
|
||||
get() = if (saveFile.exists()) {
|
||||
val completeOrSaveFile: String
|
||||
get() = if (StorageFile.isPathExists(saveFile)) {
|
||||
saveFile
|
||||
} else {
|
||||
completeFile
|
||||
}
|
||||
|
||||
val completeOrPartialFile: File
|
||||
val completeOrPartialFile: String
|
||||
get() = if (isCompleteFileAvailable) {
|
||||
completeOrSaveFile
|
||||
} else {
|
||||
@ -106,15 +104,15 @@ class DownloadFile(
|
||||
}
|
||||
|
||||
val isSaved: Boolean
|
||||
get() = saveFile.exists()
|
||||
get() = StorageFile.isPathExists(saveFile)
|
||||
|
||||
@get:Synchronized
|
||||
val isCompleteFileAvailable: Boolean
|
||||
get() = saveFile.exists() || completeFile.exists()
|
||||
get() = StorageFile.isPathExists(saveFile) || StorageFile.isPathExists(completeFile)
|
||||
|
||||
@get:Synchronized
|
||||
val isWorkDone: Boolean
|
||||
get() = saveFile.exists() || completeFile.exists() && !save ||
|
||||
get() = StorageFile.isPathExists(saveFile) || StorageFile.isPathExists(completeFile) && !save ||
|
||||
saveWhenDone || completeWhenDone
|
||||
|
||||
@get:Synchronized
|
||||
@ -143,36 +141,24 @@ class DownloadFile(
|
||||
}
|
||||
|
||||
fun unpin() {
|
||||
if (saveFile.exists()) {
|
||||
if (!saveFile.renameTo(completeFile)) {
|
||||
Timber.w(
|
||||
"Renaming file failed. Original file: %s; Rename to: %s",
|
||||
saveFile.name, completeFile.name
|
||||
)
|
||||
}
|
||||
if (StorageFile.isPathExists(saveFile)) {
|
||||
StorageFile.rename(saveFile, completeFile)
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanup(): Boolean {
|
||||
var ok = true
|
||||
if (completeFile.exists() || saveFile.exists()) {
|
||||
if (StorageFile.isPathExists(completeFile) || StorageFile.isPathExists(saveFile)) {
|
||||
ok = Util.delete(partialFile)
|
||||
}
|
||||
|
||||
if (saveFile.exists()) {
|
||||
if (StorageFile.isPathExists(saveFile)) {
|
||||
ok = ok and Util.delete(completeFile)
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// In support of LRU caching.
|
||||
fun updateModificationDate() {
|
||||
updateModificationDate(saveFile)
|
||||
updateModificationDate(partialFile)
|
||||
updateModificationDate(completeFile)
|
||||
}
|
||||
|
||||
fun setPlaying(isPlaying: Boolean) {
|
||||
if (!isPlaying) doPendingRename()
|
||||
this.isPlaying = isPlaying
|
||||
@ -208,15 +194,15 @@ class DownloadFile(
|
||||
override fun execute() {
|
||||
|
||||
var inputStream: InputStream? = null
|
||||
var outputStream: FileOutputStream? = null
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
if (saveFile.exists()) {
|
||||
if (StorageFile.isPathExists(saveFile)) {
|
||||
Timber.i("%s already exists. Skipping.", saveFile)
|
||||
status.postValue(DownloadStatus.DONE)
|
||||
return
|
||||
}
|
||||
|
||||
if (completeFile.exists()) {
|
||||
if (StorageFile.isPathExists(completeFile)) {
|
||||
if (save) {
|
||||
if (isPlaying) {
|
||||
saveWhenDone = true
|
||||
@ -237,8 +223,10 @@ class DownloadFile(
|
||||
val duration = song.duration
|
||||
var fileLength: Long = 0
|
||||
|
||||
if (!partialFile.exists()) {
|
||||
fileLength = partialFile.length()
|
||||
if (!StorageFile.isPathExists(partialFile)) {
|
||||
fileLength = 0
|
||||
} else {
|
||||
fileLength = StorageFile.getFromPath(partialFile).length()
|
||||
}
|
||||
|
||||
needsDownloading = (
|
||||
@ -248,20 +236,17 @@ class DownloadFile(
|
||||
|
||||
if (needsDownloading) {
|
||||
// Attempt partial HTTP GET, appending to the file if it exists.
|
||||
val (inStream, partial) = musicService.getDownloadInputStream(
|
||||
song, partialFile.length(), desiredBitRate, save
|
||||
val (inStream, isPartial) = musicService.getDownloadInputStream(
|
||||
song, fileLength, desiredBitRate, save
|
||||
)
|
||||
|
||||
inputStream = inStream
|
||||
|
||||
if (partial) {
|
||||
Timber.i(
|
||||
"Executed partial HTTP GET, skipping %d bytes",
|
||||
partialFile.length()
|
||||
)
|
||||
if (isPartial) {
|
||||
Timber.i("Executed partial HTTP GET, skipping %d bytes", fileLength)
|
||||
}
|
||||
|
||||
outputStream = FileOutputStream(partialFile, partial)
|
||||
outputStream = StorageFile.getOrCreateFileFromPath(partialFile).getFileOutputStream(isPartial)
|
||||
|
||||
val len = inputStream.copyTo(outputStream) { totalBytesCopied ->
|
||||
setProgress(totalBytesCopied)
|
||||
@ -379,30 +364,6 @@ class DownloadFile(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateModificationDate(file: File) {
|
||||
if (file.exists()) {
|
||||
val ok = file.setLastModified(System.currentTimeMillis())
|
||||
if (!ok) {
|
||||
Timber.i(
|
||||
"Failed to set last-modified date on %s, trying alternate method",
|
||||
file
|
||||
)
|
||||
try {
|
||||
// Try alternate method to update last modified date to current time
|
||||
// Found at https://code.google.com/p/android/issues/detail?id=18624
|
||||
// According to the bug, this was fixed in Android 8.0 (API 26)
|
||||
val raf = RandomAccessFile(file, "rw")
|
||||
val length = raf.length()
|
||||
raf.setLength(length + 1)
|
||||
raf.setLength(length)
|
||||
raf.close()
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Failed to set last-modified date on %s", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun compareTo(other: Identifiable) = compareTo(other as DownloadFile)
|
||||
|
||||
fun compareTo(other: DownloadFile): Int {
|
||||
|
@ -19,7 +19,7 @@ import android.os.PowerManager
|
||||
import android.os.PowerManager.PARTIAL_WAKE_LOCK
|
||||
import android.os.PowerManager.WakeLock
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.io.File
|
||||
import org.moire.ultrasonic.util.StorageFile
|
||||
import java.net.URLEncoder
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
@ -37,6 +37,7 @@ import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.StreamProxy
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Represents a Media Player which uses the mobile's resources for playback
|
||||
@ -362,16 +363,17 @@ class LocalMediaPlayer : KoinComponent {
|
||||
try {
|
||||
downloadFile.setPlaying(false)
|
||||
|
||||
val file = downloadFile.completeOrPartialFile
|
||||
val file = StorageFile.getFromPath(downloadFile.completeOrPartialFile)
|
||||
val partial = !downloadFile.isCompleteFileAvailable
|
||||
|
||||
downloadFile.updateModificationDate()
|
||||
// TODO this won't work with SAF, we should use something else, e.g. a recent list
|
||||
// downloadFile.updateModificationDate()
|
||||
mediaPlayer.setOnCompletionListener(null)
|
||||
|
||||
setPlayerState(PlayerState.IDLE)
|
||||
setAudioAttributes(mediaPlayer)
|
||||
|
||||
var dataSource = file.path
|
||||
var dataSource: String? = null
|
||||
if (partial) {
|
||||
if (proxy == null) {
|
||||
proxy = StreamProxy(object : Supplier<DownloadFile?>() {
|
||||
@ -393,7 +395,14 @@ class LocalMediaPlayer : KoinComponent {
|
||||
|
||||
Timber.i("Preparing media player")
|
||||
|
||||
mediaPlayer.setDataSource(dataSource)
|
||||
if (dataSource != null) mediaPlayer.setDataSource(dataSource)
|
||||
else if (file.isRawFile()) mediaPlayer.setDataSource(file.getRawFilePath())
|
||||
else {
|
||||
val descriptor = file.getDocumentFileDescriptor("r")!!
|
||||
mediaPlayer.setDataSource(descriptor.fileDescriptor)
|
||||
descriptor.close()
|
||||
}
|
||||
|
||||
setPlayerState(PlayerState.PREPARING)
|
||||
|
||||
mediaPlayer.setOnBufferingUpdateListener { mp, percent ->
|
||||
@ -452,7 +461,7 @@ class LocalMediaPlayer : KoinComponent {
|
||||
@Synchronized
|
||||
private fun setupNext(downloadFile: DownloadFile) {
|
||||
try {
|
||||
val file = downloadFile.completeOrPartialFile
|
||||
val file = StorageFile.getFromPath(downloadFile.completeOrPartialFile)
|
||||
|
||||
// Release the media player if it is not our active player
|
||||
if (nextMediaPlayer != null && nextMediaPlayer != mediaPlayer) {
|
||||
@ -472,7 +481,12 @@ class LocalMediaPlayer : KoinComponent {
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
|
||||
nextMediaPlayer!!.setDataSource(file.path)
|
||||
if (file.isRawFile()) nextMediaPlayer!!.setDataSource(file.getRawFilePath())
|
||||
else {
|
||||
val descriptor = file.getDocumentFileDescriptor("r")!!
|
||||
nextMediaPlayer!!.setDataSource(descriptor.fileDescriptor)
|
||||
descriptor.close()
|
||||
}
|
||||
setNextPlayerState(PlayerState.PREPARING)
|
||||
nextMediaPlayer!!.setOnPreparedListener {
|
||||
try {
|
||||
@ -600,7 +614,7 @@ class LocalMediaPlayer : KoinComponent {
|
||||
private val autoStart: Boolean = true
|
||||
) : CancellableTask() {
|
||||
private val expectedFileSize: Long
|
||||
private val partialFile: File = downloadFile.partialFile
|
||||
private val partialFile: String = downloadFile.partialFile
|
||||
|
||||
override fun execute() {
|
||||
setPlayerState(PlayerState.DOWNLOADING)
|
||||
@ -616,7 +630,8 @@ class LocalMediaPlayer : KoinComponent {
|
||||
|
||||
private fun bufferComplete(): Boolean {
|
||||
val completeFileAvailable = downloadFile.isWorkDone
|
||||
val size = partialFile.length()
|
||||
val size = if (!StorageFile.isPathExists(partialFile)) 0
|
||||
else StorageFile.getFromPath(partialFile).length()
|
||||
|
||||
Timber.i(
|
||||
"Buffering %s (%d/%d, %s)",
|
||||
@ -649,7 +664,7 @@ class LocalMediaPlayer : KoinComponent {
|
||||
|
||||
private inner class CheckCompletionTask(downloadFile: DownloadFile?) : CancellableTask() {
|
||||
private val downloadFile: DownloadFile?
|
||||
private val partialFile: File?
|
||||
private val partialFile: String?
|
||||
override fun execute() {
|
||||
Thread.currentThread().name = "CheckCompletionTask"
|
||||
if (downloadFile == null) {
|
||||
@ -673,7 +688,10 @@ class LocalMediaPlayer : KoinComponent {
|
||||
val completeFileAvailable = downloadFile!!.isWorkDone
|
||||
val state = (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED)
|
||||
|
||||
Timber.i("Buffering next %s (%d)", partialFile, partialFile!!.length())
|
||||
val length = if (partialFile == null || !StorageFile.isPathExists(partialFile)) 0
|
||||
else StorageFile.getFromPath(partialFile).length()
|
||||
|
||||
Timber.i("Buffering next %s (%d)", partialFile, length)
|
||||
|
||||
return completeFileAvailable && state
|
||||
}
|
||||
|
@ -9,9 +9,7 @@ package org.moire.ultrasonic.service
|
||||
import android.media.MediaMetadataRetriever
|
||||
import java.io.BufferedReader
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import java.io.FileWriter
|
||||
import org.moire.ultrasonic.util.StorageFile
|
||||
import java.io.InputStream
|
||||
import java.io.Reader
|
||||
import java.lang.Math.min
|
||||
@ -43,6 +41,8 @@ import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
import java.io.FileReader
|
||||
import java.io.FileWriter
|
||||
|
||||
// TODO: There are quite a number of deeply nested and complicated functions in this class..
|
||||
// Simplify them :)
|
||||
@ -55,8 +55,8 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
val root = FileUtil.musicDirectory
|
||||
for (file in FileUtil.listFiles(root)) {
|
||||
if (file.isDirectory) {
|
||||
val index = Index(file.path)
|
||||
index.id = file.path
|
||||
val index = Index(file.getPath())
|
||||
index.id = file.getPath()
|
||||
index.index = file.name.substring(0, 1)
|
||||
index.name = file.name
|
||||
indexes.add(index)
|
||||
@ -100,14 +100,14 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
name: String?,
|
||||
refresh: Boolean
|
||||
): MusicDirectory {
|
||||
val dir = File(id)
|
||||
val dir = StorageFile.getFromPath(id)
|
||||
val result = MusicDirectory()
|
||||
result.name = dir.name
|
||||
|
||||
val seen: MutableCollection<String?> = HashSet()
|
||||
|
||||
for (file in FileUtil.listMediaFiles(dir)) {
|
||||
val filename = getName(file)
|
||||
val filename = getName(file.name, file.isDirectory)
|
||||
if (filename != null && !seen.contains(filename)) {
|
||||
seen.add(filename)
|
||||
result.addChild(createEntry(file, filename))
|
||||
@ -127,7 +127,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
val artistName = artistFile.name
|
||||
if (artistFile.isDirectory) {
|
||||
if (matchCriteria(criteria, artistName).also { closeness = it } > 0) {
|
||||
val artist = Artist(artistFile.path)
|
||||
val artist = Artist(artistFile.getPath())
|
||||
artist.index = artistFile.name.substring(0, 1)
|
||||
artist.name = artistName
|
||||
artist.closeness = closeness
|
||||
@ -205,10 +205,12 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
var line = buffer.readLine()
|
||||
if ("#EXTM3U" != line) return playlist
|
||||
while (buffer.readLine().also { line = it } != null) {
|
||||
val entryFile = File(line)
|
||||
val entryName = getName(entryFile)
|
||||
if (entryFile.exists() && entryName != null) {
|
||||
playlist.addChild(createEntry(entryFile, entryName))
|
||||
if (StorageFile.isPathExists(line)) {
|
||||
val entryFile = StorageFile.getFromPath(line)
|
||||
val entryName = getName(entryFile.name, entryFile.isDirectory)
|
||||
if (entryName != null) {
|
||||
playlist.addChild(createEntry(entryFile, entryName))
|
||||
}
|
||||
}
|
||||
}
|
||||
playlist
|
||||
@ -228,8 +230,8 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
try {
|
||||
fw.write("#EXTM3U\n")
|
||||
for (e in entries) {
|
||||
var filePath = FileUtil.getSongFile(e).absolutePath
|
||||
if (!File(filePath).exists()) {
|
||||
var filePath = FileUtil.getSongFile(e)
|
||||
if (!StorageFile.isPathExists(filePath)) {
|
||||
val ext = FileUtil.getExtension(filePath)
|
||||
val base = FileUtil.getBaseName(filePath)
|
||||
filePath = "$base.complete.$ext"
|
||||
@ -251,7 +253,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
|
||||
override fun getRandomSongs(size: Int): MusicDirectory {
|
||||
val root = FileUtil.musicDirectory
|
||||
val children: MutableList<File> = LinkedList()
|
||||
val children: MutableList<StorageFile> = LinkedList()
|
||||
listFilesRecursively(root, children)
|
||||
val result = MusicDirectory()
|
||||
if (children.isEmpty()) {
|
||||
@ -261,7 +263,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
val finalSize: Int = min(children.size, size)
|
||||
for (i in 0 until finalSize) {
|
||||
val file = children[i % children.size]
|
||||
result.addChild(createEntry(file, getName(file)))
|
||||
result.addChild(createEntry(file, getName(file.name, file.isDirectory)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -483,28 +485,27 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
|
||||
companion object {
|
||||
private val COMPILE = Pattern.compile(" ")
|
||||
private fun getName(file: File): String? {
|
||||
var name = file.name
|
||||
if (file.isDirectory) {
|
||||
return name
|
||||
private fun getName(fileName: String, isDirectory: Boolean): String? {
|
||||
if (isDirectory) {
|
||||
return fileName
|
||||
}
|
||||
if (name.endsWith(".partial") || name.contains(".partial.") ||
|
||||
name == Constants.ALBUM_ART_FILE
|
||||
if (fileName.endsWith(".partial") || fileName.contains(".partial.") ||
|
||||
fileName == Constants.ALBUM_ART_FILE
|
||||
) {
|
||||
return null
|
||||
}
|
||||
name = name.replace(".complete", "")
|
||||
val name = fileName.replace(".complete", "")
|
||||
return FileUtil.getBaseName(name)
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "ComplexMethod", "LongMethod", "NestedBlockDepth")
|
||||
private fun createEntry(file: File, name: String?): MusicDirectory.Entry {
|
||||
val entry = MusicDirectory.Entry(file.path)
|
||||
private fun createEntry(file: StorageFile, name: String?): MusicDirectory.Entry {
|
||||
val entry = MusicDirectory.Entry(file.getPath())
|
||||
entry.isDirectory = file.isDirectory
|
||||
entry.parent = file.parent
|
||||
entry.size = file.length()
|
||||
val root = FileUtil.musicDirectory.path
|
||||
entry.path = file.path.replaceFirst(
|
||||
entry.parent = file.getParent()!!.getPath()
|
||||
entry.size = if (file.isFile) file.length() else 0
|
||||
val root = FileUtil.musicDirectory.getPath()
|
||||
entry.path = file.getPath().replaceFirst(
|
||||
String.format(Locale.ROOT, "^%s/", root).toRegex(), ""
|
||||
)
|
||||
entry.title = name
|
||||
@ -520,7 +521,14 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
var hasVideo: String? = null
|
||||
try {
|
||||
val mmr = MediaMetadataRetriever()
|
||||
mmr.setDataSource(file.path)
|
||||
|
||||
if (file.isRawFile()) mmr.setDataSource(file.getRawFilePath())
|
||||
else {
|
||||
val descriptor = file.getDocumentFileDescriptor("r")!!
|
||||
mmr.setDataSource(descriptor.fileDescriptor)
|
||||
descriptor.close()
|
||||
}
|
||||
|
||||
artist = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
|
||||
album = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)
|
||||
title = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
||||
@ -533,8 +541,8 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
mmr.release()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
entry.artist = artist ?: file.parentFile!!.parentFile!!.name
|
||||
entry.album = album ?: file.parentFile!!.name
|
||||
entry.artist = artist ?: file.getParent()!!.getParent()!!.name
|
||||
entry.album = album ?: file.getParent()!!.name
|
||||
if (title != null) {
|
||||
entry.title = title
|
||||
}
|
||||
@ -589,8 +597,8 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
}
|
||||
entry.suffix = FileUtil.getExtension(file.name.replace(".complete", ""))
|
||||
val albumArt = FileUtil.getAlbumArtFile(entry)
|
||||
if (albumArt.exists()) {
|
||||
entry.coverArt = albumArt.path
|
||||
if (albumArt != null && StorageFile.isPathExists(albumArt)) {
|
||||
entry.coverArt = albumArt
|
||||
}
|
||||
return entry
|
||||
}
|
||||
@ -598,7 +606,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
@Suppress("NestedBlockDepth")
|
||||
private fun recursiveAlbumSearch(
|
||||
artistName: String,
|
||||
file: File,
|
||||
file: StorageFile,
|
||||
criteria: SearchCriteria,
|
||||
albums: MutableList<MusicDirectory.Entry>,
|
||||
songs: MutableList<MusicDirectory.Entry>
|
||||
@ -606,7 +614,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
var closeness: Int
|
||||
for (albumFile in FileUtil.listMediaFiles(file)) {
|
||||
if (albumFile.isDirectory) {
|
||||
val albumName = getName(albumFile)
|
||||
val albumName = getName(albumFile.name, albumFile.isDirectory)
|
||||
if (matchCriteria(criteria, albumName).also { closeness = it } > 0) {
|
||||
val album = createEntry(albumFile, albumName)
|
||||
album.artist = artistName
|
||||
@ -614,7 +622,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
albums.add(album)
|
||||
}
|
||||
for (songFile in FileUtil.listMediaFiles(albumFile)) {
|
||||
val songName = getName(songFile)
|
||||
val songName = getName(songFile.name, songFile.isDirectory)
|
||||
if (songFile.isDirectory) {
|
||||
recursiveAlbumSearch(artistName, songFile, criteria, albums, songs)
|
||||
} else if (matchCriteria(criteria, songName).also { closeness = it } > 0) {
|
||||
@ -626,7 +634,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val songName = getName(albumFile)
|
||||
val songName = getName(albumFile.name, albumFile.isDirectory)
|
||||
if (matchCriteria(criteria, songName).also { closeness = it } > 0) {
|
||||
val song = createEntry(albumFile, songName)
|
||||
song.artist = artistName
|
||||
@ -655,7 +663,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
return closeness
|
||||
}
|
||||
|
||||
private fun listFilesRecursively(parent: File, children: MutableList<File>) {
|
||||
private fun listFilesRecursively(parent: StorageFile, children: MutableList<StorageFile>) {
|
||||
for (file in FileUtil.listMediaFiles(parent)) {
|
||||
if (file.isFile) {
|
||||
children.add(file)
|
||||
|
@ -1,8 +1,9 @@
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.os.StatFs
|
||||
import java.io.File
|
||||
import android.system.Os
|
||||
import java.util.ArrayList
|
||||
import java.util.HashSet
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
@ -57,8 +58,8 @@ class CacheCleaner {
|
||||
override fun doInBackground(vararg params: Void?): Void? {
|
||||
try {
|
||||
Thread.currentThread().name = "BackgroundCleanup"
|
||||
val files: MutableList<File> = ArrayList()
|
||||
val dirs: MutableList<File> = ArrayList()
|
||||
val files: MutableList<StorageFile> = ArrayList()
|
||||
val dirs: MutableList<StorageFile> = ArrayList()
|
||||
findCandidatesForDeletion(musicDirectory, files, dirs)
|
||||
sortByAscendingModificationTime(files)
|
||||
val filesToNotDelete = findFilesToNotDelete()
|
||||
@ -75,8 +76,8 @@ class CacheCleaner {
|
||||
override fun doInBackground(vararg params: Void?): Void? {
|
||||
try {
|
||||
Thread.currentThread().name = "BackgroundSpaceCleanup"
|
||||
val files: MutableList<File> = ArrayList()
|
||||
val dirs: MutableList<File> = ArrayList()
|
||||
val files: MutableList<StorageFile> = ArrayList()
|
||||
val dirs: MutableList<StorageFile> = ArrayList()
|
||||
findCandidatesForDeletion(musicDirectory, files, dirs)
|
||||
val bytesToDelete = getMinimumDelete(files)
|
||||
if (bytesToDelete > 0L) {
|
||||
@ -116,29 +117,29 @@ class CacheCleaner {
|
||||
|
||||
companion object {
|
||||
private const val MIN_FREE_SPACE = 500 * 1024L * 1024L
|
||||
private fun deleteEmptyDirs(dirs: Iterable<File>, doNotDelete: Collection<File>) {
|
||||
private fun deleteEmptyDirs(dirs: Iterable<StorageFile>, doNotDelete: Collection<String>) {
|
||||
for (dir in dirs) {
|
||||
if (doNotDelete.contains(dir)) {
|
||||
if (doNotDelete.contains(dir.getPath())) {
|
||||
continue
|
||||
}
|
||||
var children = dir.listFiles()
|
||||
if (children != null) {
|
||||
// No songs left in the folder
|
||||
if (children.size == 1 && children[0].path == getAlbumArtFile(dir).path) {
|
||||
if (children.size == 1 && children[0].getPath() == getAlbumArtFile(dir.getPath())) {
|
||||
// Delete Artwork files
|
||||
delete(getAlbumArtFile(dir))
|
||||
delete(getAlbumArtFile(dir.getPath()))
|
||||
children = dir.listFiles()
|
||||
}
|
||||
|
||||
// Delete empty directory
|
||||
if (children != null && children.isEmpty()) {
|
||||
delete(dir)
|
||||
delete(dir.getPath())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMinimumDelete(files: List<File>): Long {
|
||||
private fun getMinimumDelete(files: List<StorageFile>): Long {
|
||||
if (files.isEmpty()) {
|
||||
return 0L
|
||||
}
|
||||
@ -149,11 +150,25 @@ class CacheCleaner {
|
||||
}
|
||||
|
||||
// Ensure that file system is not more than 95% full.
|
||||
val stat = StatFs(files[0].path)
|
||||
val bytesTotalFs = stat.blockCountLong * stat.blockSizeLong
|
||||
val bytesAvailableFs = stat.availableBlocksLong * stat.blockSizeLong
|
||||
val bytesUsedFs = bytesTotalFs - bytesAvailableFs
|
||||
val minFsAvailability = bytesTotalFs - MIN_FREE_SPACE
|
||||
val bytesUsedFs: Long
|
||||
val minFsAvailability: Long
|
||||
val bytesTotalFs: Long
|
||||
val bytesAvailableFs: Long
|
||||
if (files[0].isRawFile()) {
|
||||
val stat = StatFs(files[0].getRawFilePath())
|
||||
bytesTotalFs = stat.blockCountLong * stat.blockSizeLong
|
||||
bytesAvailableFs = stat.availableBlocksLong * stat.blockSizeLong
|
||||
bytesUsedFs = bytesTotalFs - bytesAvailableFs
|
||||
minFsAvailability = bytesTotalFs - MIN_FREE_SPACE
|
||||
} else {
|
||||
val descriptor = files[0].getDocumentFileDescriptor("r")!!
|
||||
val stat = Os.fstatvfs(descriptor.fileDescriptor)
|
||||
bytesTotalFs = stat.f_blocks * stat.f_bsize
|
||||
bytesAvailableFs = stat.f_bfree * stat.f_bsize
|
||||
bytesUsedFs = bytesTotalFs - bytesAvailableFs
|
||||
minFsAvailability = bytesTotalFs - MIN_FREE_SPACE
|
||||
descriptor.close()
|
||||
}
|
||||
val bytesToDeleteCacheLimit = (bytesUsedBySubsonic - cacheSizeBytes).coerceAtLeast(0L)
|
||||
val bytesToDeleteFsLimit = (bytesUsedFs - minFsAvailability).coerceAtLeast(0L)
|
||||
val bytesToDelete = bytesToDeleteCacheLimit.coerceAtLeast(bytesToDeleteFsLimit)
|
||||
@ -169,18 +184,18 @@ class CacheCleaner {
|
||||
return bytesToDelete
|
||||
}
|
||||
|
||||
private fun isPartial(file: File): Boolean {
|
||||
private fun isPartial(file: StorageFile): Boolean {
|
||||
return file.name.endsWith(".partial") || file.name.contains(".partial.")
|
||||
}
|
||||
|
||||
private fun isComplete(file: File): Boolean {
|
||||
private fun isComplete(file: StorageFile): Boolean {
|
||||
return file.name.endsWith(".complete") || file.name.contains(".complete.")
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
private fun deleteFiles(
|
||||
files: Collection<File>,
|
||||
doNotDelete: Collection<File>,
|
||||
files: Collection<StorageFile>,
|
||||
doNotDelete: Collection<String>,
|
||||
bytesToDelete: Long,
|
||||
deletePartials: Boolean
|
||||
) {
|
||||
@ -191,9 +206,9 @@ class CacheCleaner {
|
||||
for (file in files) {
|
||||
if (!deletePartials && bytesDeleted > bytesToDelete) break
|
||||
if (bytesToDelete > bytesDeleted || deletePartials && isPartial(file)) {
|
||||
if (!doNotDelete.contains(file) && file.name != Constants.ALBUM_ART_FILE) {
|
||||
if (!doNotDelete.contains(file.getPath()) && file.name != Constants.ALBUM_ART_FILE) {
|
||||
val size = file.length()
|
||||
if (delete(file)) {
|
||||
if (delete(file.getPath())) {
|
||||
bytesDeleted += size
|
||||
}
|
||||
}
|
||||
@ -203,9 +218,9 @@ class CacheCleaner {
|
||||
}
|
||||
|
||||
private fun findCandidatesForDeletion(
|
||||
file: File,
|
||||
files: MutableList<File>,
|
||||
dirs: MutableList<File>
|
||||
file: StorageFile,
|
||||
files: MutableList<StorageFile>,
|
||||
dirs: MutableList<StorageFile>
|
||||
) {
|
||||
if (file.isFile && (isPartial(file) || isComplete(file))) {
|
||||
files.add(file)
|
||||
@ -218,14 +233,14 @@ class CacheCleaner {
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortByAscendingModificationTime(files: MutableList<File>) {
|
||||
files.sortWith { a: File, b: File ->
|
||||
private fun sortByAscendingModificationTime(files: MutableList<StorageFile>) {
|
||||
files.sortWith { a: StorageFile, b: StorageFile ->
|
||||
a.lastModified().compareTo(b.lastModified())
|
||||
}
|
||||
}
|
||||
|
||||
private fun findFilesToNotDelete(): Set<File> {
|
||||
val filesToNotDelete: MutableSet<File> = HashSet(5)
|
||||
private fun findFilesToNotDelete(): Set<String> {
|
||||
val filesToNotDelete: MutableSet<String> = HashSet(5)
|
||||
val downloader = inject<Downloader>(
|
||||
Downloader::class.java
|
||||
)
|
||||
@ -233,7 +248,7 @@ class CacheCleaner {
|
||||
filesToNotDelete.add(downloadFile.partialFile)
|
||||
filesToNotDelete.add(downloadFile.completeOrSaveFile)
|
||||
}
|
||||
filesToNotDelete.add(musicDirectory)
|
||||
filesToNotDelete.add(musicDirectory.getPath())
|
||||
return filesToNotDelete
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import android.os.Environment
|
||||
import android.text.TextUtils
|
||||
import android.util.Pair
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.FileWriter
|
||||
@ -28,6 +27,7 @@ import java.util.regex.Pattern
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
object FileUtil {
|
||||
|
||||
@ -43,12 +43,12 @@ object FileUtil {
|
||||
const val SUFFIX_SMALL = ".jpeg-small"
|
||||
private const val UNNAMED = "unnamed"
|
||||
|
||||
fun getSongFile(song: MusicDirectory.Entry): File {
|
||||
fun getSongFile(song: MusicDirectory.Entry): String {
|
||||
val dir = getAlbumDirectory(song)
|
||||
|
||||
// Do not generate new name for offline files. Offline files will have their Path as their Id.
|
||||
if (!TextUtils.isEmpty(song.id)) {
|
||||
if (song.id.startsWith(dir.absolutePath)) return File(song.id)
|
||||
if (song.id.startsWith(dir)) return song.id
|
||||
}
|
||||
|
||||
// Generate a file name for the song
|
||||
@ -70,7 +70,7 @@ object FileUtil {
|
||||
} else {
|
||||
fileName.append(song.suffix)
|
||||
}
|
||||
return File(dir, fileName.toString())
|
||||
return "$dir/$fileName"
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ -104,9 +104,9 @@ object FileUtil {
|
||||
* @param entry The album entry
|
||||
* @return File object. Not guaranteed that it exists
|
||||
*/
|
||||
fun getAlbumArtFile(entry: MusicDirectory.Entry): File {
|
||||
fun getAlbumArtFile(entry: MusicDirectory.Entry): String? {
|
||||
val albumDir = getAlbumDirectory(entry)
|
||||
return getAlbumArtFile(albumDir)
|
||||
return getAlbumArtFileForAlbumDir(albumDir)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,7 +129,7 @@ object FileUtil {
|
||||
*/
|
||||
fun getArtistArtKey(name: String?, large: Boolean): String {
|
||||
val artist = fileSystemSafe(name)
|
||||
val dir = File(String.format(Locale.ROOT, "%s/%s/%s", musicDirectory.path, artist, UNNAMED))
|
||||
val dir = String.format(Locale.ROOT, "%s/%s/%s", musicDirectory.getPath(), artist, UNNAMED)
|
||||
return getAlbumArtKey(dir, large)
|
||||
}
|
||||
|
||||
@ -139,9 +139,9 @@ object FileUtil {
|
||||
* @param large Whether to get the key for the large or the default image
|
||||
* @return String The hash key
|
||||
*/
|
||||
private fun getAlbumArtKey(albumDir: File, large: Boolean): String {
|
||||
private fun getAlbumArtKey(albumDirPath: String, large: Boolean): String {
|
||||
val suffix = if (large) SUFFIX_LARGE else SUFFIX_SMALL
|
||||
return String.format(Locale.ROOT, "%s%s", Util.md5Hex(albumDir.path), suffix)
|
||||
return String.format(Locale.ROOT, "%s%s", Util.md5Hex(albumDirPath), suffix)
|
||||
}
|
||||
|
||||
fun getAvatarFile(username: String?): File? {
|
||||
@ -159,10 +159,9 @@ object FileUtil {
|
||||
* @return File object. Not guaranteed that it exists
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getAlbumArtFile(albumDir: File): File {
|
||||
val albumArtDir = albumArtDirectory
|
||||
fun getAlbumArtFileForAlbumDir(albumDir: String): String? {
|
||||
val key = getAlbumArtKey(albumDir, true)
|
||||
return File(albumArtDir, key)
|
||||
return getAlbumArtFile(key)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -171,11 +170,11 @@ object FileUtil {
|
||||
* @return File object. Not guaranteed that it exists
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getAlbumArtFile(cacheKey: String?): File? {
|
||||
val albumArtDir = albumArtDirectory
|
||||
fun getAlbumArtFile(cacheKey: String?): String? {
|
||||
val albumArtDir = albumArtDirectory.absolutePath
|
||||
return if (cacheKey == null) {
|
||||
null
|
||||
} else File(albumArtDir, cacheKey)
|
||||
} else "$albumArtDir/$cacheKey"
|
||||
}
|
||||
|
||||
val albumArtDirectory: File
|
||||
@ -186,36 +185,30 @@ object FileUtil {
|
||||
return albumArtDir
|
||||
}
|
||||
|
||||
fun getAlbumDirectory(entry: MusicDirectory.Entry): File {
|
||||
val dir: File
|
||||
if (!TextUtils.isEmpty(entry.path)) {
|
||||
val f = File(fileSystemSafeDir(entry.path))
|
||||
dir = File(
|
||||
String.format(
|
||||
fun getAlbumDirectory(entry: MusicDirectory.Entry): String {
|
||||
val dir: String
|
||||
if (!TextUtils.isEmpty(entry.path) && getParentPath(entry.path!!) != null) {
|
||||
val f = fileSystemSafeDir(entry.path)
|
||||
dir = String.format(
|
||||
Locale.ROOT,
|
||||
"%s/%s",
|
||||
musicDirectory.path,
|
||||
if (entry.isDirectory) f.path else f.parent ?: ""
|
||||
musicDirectory.getPath(),
|
||||
if (entry.isDirectory) f else getParentPath(f) ?: ""
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val artist = fileSystemSafe(entry.artist)
|
||||
var album = fileSystemSafe(entry.album)
|
||||
if (UNNAMED == album) {
|
||||
album = fileSystemSafe(entry.title)
|
||||
}
|
||||
dir = File(String.format(Locale.ROOT, "%s/%s/%s", musicDirectory.path, artist, album))
|
||||
dir = String.format(Locale.ROOT, "%s/%s/%s", musicDirectory.getPath(), artist, album)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
fun createDirectoryForParent(file: File) {
|
||||
val dir = file.parentFile
|
||||
if (dir != null && !dir.exists()) {
|
||||
if (!dir.mkdirs()) {
|
||||
Timber.e("Failed to create directory %s", dir)
|
||||
}
|
||||
}
|
||||
fun createDirectoryForParent(path: String) {
|
||||
val dir = getParentPath(path) ?: return
|
||||
StorageFile.createDirsOnPath(dir)
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
@ -245,13 +238,8 @@ object FileUtil {
|
||||
get() = getOrCreateDirectory("music")
|
||||
|
||||
@JvmStatic
|
||||
val musicDirectory: File
|
||||
get() {
|
||||
val path = Settings.cacheLocation
|
||||
val dir = File(path)
|
||||
val hasAccess = ensureDirectoryExistsAndIsReadWritable(dir)
|
||||
return if (hasAccess.second) dir else defaultMusicDirectory
|
||||
}
|
||||
val musicDirectory: StorageFile
|
||||
get() = StorageFile.getMediaRoot()
|
||||
|
||||
@JvmStatic
|
||||
@Suppress("ReturnCount")
|
||||
@ -326,6 +314,16 @@ object FileUtil {
|
||||
* Similar to [File.listFiles], but returns a sorted set.
|
||||
* Never returns `null`, instead a warning is logged, and an empty set is returned.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun listFiles(dir: StorageFile): SortedSet<StorageFile> {
|
||||
val files = dir.listFiles()
|
||||
if (files == null) {
|
||||
Timber.w("Failed to list children for %s", dir.getPath())
|
||||
return TreeSet()
|
||||
}
|
||||
return TreeSet(files.asList())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun listFiles(dir: File): SortedSet<File> {
|
||||
val files = dir.listFiles()
|
||||
@ -336,7 +334,7 @@ object FileUtil {
|
||||
return TreeSet(files.asList())
|
||||
}
|
||||
|
||||
fun listMediaFiles(dir: File): SortedSet<File> {
|
||||
fun listMediaFiles(dir: StorageFile): SortedSet<StorageFile> {
|
||||
val files = listFiles(dir)
|
||||
val iterator = files.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
@ -348,7 +346,7 @@ object FileUtil {
|
||||
return files
|
||||
}
|
||||
|
||||
private fun isMediaFile(file: File): Boolean {
|
||||
private fun isMediaFile(file: StorageFile): Boolean {
|
||||
val extension = getExtension(file.name)
|
||||
return MUSIC_FILE_EXTENSIONS.contains(extension) ||
|
||||
VIDEO_FILE_EXTENSIONS.contains(extension)
|
||||
@ -393,6 +391,15 @@ object FileUtil {
|
||||
return String.format(Locale.ROOT, "%s.partial.%s", getBaseName(name), getExtension(name))
|
||||
}
|
||||
|
||||
fun getNameFromPath(path: String): String {
|
||||
return path.substringAfterLast('/')
|
||||
}
|
||||
|
||||
fun getParentPath(path: String): String? {
|
||||
if (!path.contains('/')) return null
|
||||
return path.substringBeforeLast('/')
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file name of a .complete file of the given file.
|
||||
*
|
||||
@ -453,9 +460,9 @@ object FileUtil {
|
||||
try {
|
||||
fw.write("#EXTM3U\n")
|
||||
for (e in playlist.getChildren()) {
|
||||
var filePath = getSongFile(e).absolutePath
|
||||
var filePath = getSongFile(e)
|
||||
|
||||
if (!File(filePath).exists()) {
|
||||
if (!StorageFile.isPathExists(filePath)) {
|
||||
val ext = getExtension(filePath)
|
||||
val base = getBaseName(filePath)
|
||||
filePath = "$base.complete.$ext"
|
||||
|
@ -0,0 +1,273 @@
|
||||
/*
|
||||
* StorageFile.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import android.content.res.AssetFileDescriptor
|
||||
import android.net.Uri
|
||||
import com.github.k1rakishou.fsaf.FileManager
|
||||
import com.github.k1rakishou.fsaf.document_file.CachingDocumentFile
|
||||
import com.github.k1rakishou.fsaf.file.AbstractFile
|
||||
import com.github.k1rakishou.fsaf.file.RawFile
|
||||
import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Provides filesystem access abstraction which works
|
||||
* both on File based paths and Storage Access Framework Uris
|
||||
*/
|
||||
class StorageFile private constructor(
|
||||
private var parent: StorageFile?,
|
||||
private var abstractFile: AbstractFile,
|
||||
private var fileManager: FileManager
|
||||
): Comparable<StorageFile> {
|
||||
|
||||
override fun compareTo(other: StorageFile): Int {
|
||||
return getPath().compareTo(other.getPath())
|
||||
}
|
||||
|
||||
var name: String = fileManager.getName(abstractFile)
|
||||
|
||||
var isDirectory: Boolean = fileManager.isDirectory(abstractFile)
|
||||
|
||||
var isFile: Boolean = fileManager.isFile(abstractFile)
|
||||
|
||||
fun length(): Long = fileManager.getLength(abstractFile)
|
||||
|
||||
fun lastModified(): Long = fileManager.lastModified(abstractFile)
|
||||
|
||||
fun delete(): Boolean = fileManager.delete(abstractFile)
|
||||
|
||||
fun listFiles(): Array<StorageFile> {
|
||||
val fileList = fileManager.listFiles(abstractFile)
|
||||
return fileList.map { file -> StorageFile(this, file, fileManager) }.toTypedArray()
|
||||
}
|
||||
|
||||
fun getFileOutputStream(): OutputStream {
|
||||
if (isRawFile()) return File(abstractFile.getFullPath()).outputStream()
|
||||
return fileManager.getOutputStream(abstractFile)
|
||||
?: throw IOException("Couldn't retrieve OutputStream")
|
||||
}
|
||||
|
||||
fun getFileOutputStream(append: Boolean): OutputStream {
|
||||
if (isRawFile()) return FileOutputStream(File(abstractFile.getFullPath()), append)
|
||||
val mode = if (append) "wa" else "w"
|
||||
val descriptor = UApp.applicationContext().contentResolver.openAssetFileDescriptor(
|
||||
abstractFile.getFileRoot<CachingDocumentFile>().holder.uri(), mode)
|
||||
return descriptor?.createOutputStream()
|
||||
?: throw IOException("Couldn't retrieve OutputStream")
|
||||
}
|
||||
|
||||
fun getFileInputStream(): InputStream {
|
||||
if (isRawFile()) return FileInputStream(abstractFile.getFullPath())
|
||||
return fileManager.getInputStream(abstractFile)
|
||||
?: throw IOException("Couldn't retrieve InputStream")
|
||||
}
|
||||
|
||||
// TODO there are a few functions which could be getters
|
||||
// They are functions for now to help us distinguish them from similar getters in File. These can be changed after the refactor is complete.
|
||||
fun getPath(): String {
|
||||
if (isRawFile()) return abstractFile.getFullPath()
|
||||
if (getParent() != null) return getParent()!!.getPath() + "/" + name
|
||||
return Uri.parse(abstractFile.getFullPath()).toString()
|
||||
}
|
||||
|
||||
fun getParent(): StorageFile? {
|
||||
if (isRawFile()) {
|
||||
return StorageFile(
|
||||
null,
|
||||
fileManager.fromRawFile(File(abstractFile.getFullPath()).parentFile!!),
|
||||
fileManager
|
||||
)
|
||||
}
|
||||
return parent
|
||||
}
|
||||
|
||||
fun isRawFile(): Boolean {
|
||||
return abstractFile is RawFile
|
||||
}
|
||||
|
||||
fun getRawFilePath(): String? {
|
||||
return if (abstractFile is RawFile) abstractFile.getFullPath()
|
||||
else null
|
||||
}
|
||||
|
||||
fun getDocumentFileDescriptor(openMode: String): AssetFileDescriptor? {
|
||||
return if (abstractFile !is RawFile) {
|
||||
UApp.applicationContext().contentResolver.openAssetFileDescriptor(
|
||||
abstractFile.getFileRoot<CachingDocumentFile>().holder.uri(),
|
||||
openMode
|
||||
)
|
||||
} else null
|
||||
}
|
||||
|
||||
companion object {
|
||||
// TODO it would be nice to check the access rights and reset the cache directory on error
|
||||
private val MusicCacheFileManager: Lazy<FileManager> = lazy {
|
||||
val manager = FileManager(UApp.applicationContext())
|
||||
manager.registerBaseDir<MusicCacheBaseDirectory>(MusicCacheBaseDirectory())
|
||||
manager
|
||||
}
|
||||
|
||||
fun getFromParentAndName(parent: StorageFile, name: String): StorageFile {
|
||||
val file = parent.fileManager.findFile(parent.abstractFile, name)
|
||||
?: parent.fileManager.createFile(parent.abstractFile, name)!!
|
||||
return StorageFile(parent, file, parent.fileManager)
|
||||
}
|
||||
|
||||
fun getMediaRoot(): StorageFile {
|
||||
return StorageFile(
|
||||
null,
|
||||
MusicCacheFileManager.value.newBaseDirectoryFile<MusicCacheBaseDirectory>()!!,
|
||||
MusicCacheFileManager.value
|
||||
)
|
||||
}
|
||||
|
||||
// TODO sometimes getFromPath is called after isPathExists, but the file may be gone because it was deleted in another thread.
|
||||
// Create a function where these two are merged
|
||||
fun getFromPath(path: String): StorageFile {
|
||||
Timber.v("StorageFile getFromPath %s", path)
|
||||
val normalizedPath = normalizePath(path)
|
||||
if (!normalizedPath.isUri()) {
|
||||
return StorageFile(
|
||||
null,
|
||||
MusicCacheFileManager.value.fromPath(normalizedPath),
|
||||
MusicCacheFileManager.value
|
||||
)
|
||||
}
|
||||
|
||||
val segments = getUriSegments(normalizedPath)
|
||||
?: throw IOException("Can't get path because the root has changed")
|
||||
|
||||
var file = StorageFile(null, getMediaRoot().abstractFile, MusicCacheFileManager.value)
|
||||
segments.forEach { segment ->
|
||||
file = StorageFile(
|
||||
file,
|
||||
MusicCacheFileManager.value.findFile(file.abstractFile, segment)
|
||||
?: throw IOException("File not found"),
|
||||
file.fileManager
|
||||
)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
fun getOrCreateFileFromPath(path: String): StorageFile {
|
||||
val normalizedPath = normalizePath(path)
|
||||
if (!normalizedPath.isUri()) {
|
||||
File(normalizedPath).createNewFile()
|
||||
return StorageFile(
|
||||
null,
|
||||
MusicCacheFileManager.value.fromPath(normalizedPath),
|
||||
MusicCacheFileManager.value
|
||||
)
|
||||
}
|
||||
|
||||
val segments = getUriSegments(normalizedPath)
|
||||
?: throw IOException("Can't get path because the root has changed")
|
||||
|
||||
var file = StorageFile(null, getMediaRoot().abstractFile, MusicCacheFileManager.value)
|
||||
segments.forEach { segment ->
|
||||
file = StorageFile(
|
||||
file,
|
||||
MusicCacheFileManager.value.findFile(file.abstractFile, segment)
|
||||
?: MusicCacheFileManager.value.createFile(file.abstractFile, segment)!!,
|
||||
file.fileManager
|
||||
)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
fun isPathExists(path: String): Boolean {
|
||||
val normalizedPath = normalizePath(path)
|
||||
if (!normalizedPath.isUri()) return File(normalizedPath).exists()
|
||||
|
||||
val segments = getUriSegments(normalizedPath) ?: return false
|
||||
|
||||
var file = getMediaRoot().abstractFile
|
||||
segments.forEach { segment ->
|
||||
file = MusicCacheFileManager.value.findFile(file, segment) ?: return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun createDirsOnPath(path: String) {
|
||||
val normalizedPath = normalizePath(path)
|
||||
if (!normalizedPath.isUri()) {
|
||||
File(normalizedPath).mkdirs()
|
||||
return
|
||||
}
|
||||
|
||||
val segments = getUriSegments(normalizedPath)
|
||||
?: throw IOException("Can't get path because the root has changed")
|
||||
|
||||
var file = getMediaRoot().abstractFile
|
||||
segments.forEach { segment ->
|
||||
file = MusicCacheFileManager.value.createDir(file, segment)
|
||||
?: throw IOException("Can't create directory")
|
||||
}
|
||||
}
|
||||
|
||||
fun rename(pathFrom: String, pathTo: String) {
|
||||
val normalizedPathFrom = normalizePath(pathFrom)
|
||||
val normalizedPathTo = normalizePath(pathTo)
|
||||
|
||||
Timber.d("Renaming from %s to %s", normalizedPathFrom, normalizedPathTo)
|
||||
|
||||
val fileFrom = getFromPath(normalizedPathFrom)
|
||||
val parentTo = getFromPath(FileUtil.getParentPath(normalizedPathTo)!!)
|
||||
val fileTo = getFromParentAndName(parentTo, FileUtil.getNameFromPath(normalizedPathTo))
|
||||
|
||||
MusicCacheFileManager.value.copyFileContents(fileFrom.abstractFile, fileTo.abstractFile)
|
||||
fileFrom.delete()
|
||||
}
|
||||
|
||||
private fun getUriSegments(uri: String): List<String>? {
|
||||
val rootPath = getMediaRoot().getPath()
|
||||
if (!uri.startsWith(rootPath)) return null
|
||||
val pathWithoutRoot = uri.substringAfter(rootPath)
|
||||
return pathWithoutRoot.split('/').filter { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun normalizePath(path: String): String {
|
||||
// FSAF replaces spaces in paths with "_", so we must do the same everywhere
|
||||
// TODO paths sometimes contain double "/". These are currently replaced to single one.
|
||||
// The nice solution would be to check and fix why this happens
|
||||
return path.replace(' ', '_').replace(Regex("(?<!:)//"), "/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MusicCacheBaseDirectory : BaseDirectory() {
|
||||
|
||||
override fun getDirFile(): File {
|
||||
return FileUtil.defaultMusicDirectory
|
||||
}
|
||||
|
||||
override fun getDirUri(): Uri? {
|
||||
if (!Settings.cacheLocation.isUri()) return null
|
||||
return Uri.parse(Settings.cacheLocation)
|
||||
}
|
||||
|
||||
override fun currentActiveBaseDirType(): ActiveBaseDirType {
|
||||
return when {
|
||||
Settings.cacheLocation.isUri() -> ActiveBaseDirType.SafBaseDir
|
||||
else -> ActiveBaseDirType.JavaFileBaseDir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isUri(): Boolean {
|
||||
// TODO is there a better way to tell apart a path and an URI?
|
||||
return this.contains(':')
|
||||
}
|
@ -2,9 +2,9 @@ package org.moire.ultrasonic.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import java.io.File
|
||||
import java.io.PrintWriter
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Logs the stack trace of uncaught exceptions to a file on the SD card.
|
||||
|
@ -21,6 +21,7 @@ import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.net.wifi.WifiManager
|
||||
@ -38,9 +39,6 @@ import android.widget.Toast
|
||||
import androidx.annotation.AnyRes
|
||||
import androidx.media.utils.MediaConstants
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.security.MessageDigest
|
||||
@ -51,6 +49,7 @@ import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||
import org.moire.ultrasonic.domain.Bookmark
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
@ -58,6 +57,7 @@ import org.moire.ultrasonic.domain.PlayerState
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.service.DownloadFile
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
private const val LINE_LENGTH = 60
|
||||
private const val DEGRADE_PRECISION_AFTER = 10
|
||||
@ -110,39 +110,10 @@ object Util {
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun atomicCopy(from: File, to: File) {
|
||||
val tmp = File(String.format(Locale.ROOT, "%s.tmp", to.path))
|
||||
val input = FileInputStream(from)
|
||||
val out = FileOutputStream(tmp)
|
||||
try {
|
||||
input.channel.transferTo(0, from.length(), out.channel)
|
||||
out.close()
|
||||
if (!tmp.renameTo(to)) {
|
||||
throw IOException(
|
||||
String.format(Locale.ROOT, "Failed to rename %s to %s", tmp, to)
|
||||
)
|
||||
}
|
||||
Timber.i("Copied %s to %s", from, to)
|
||||
} catch (x: IOException) {
|
||||
close(out)
|
||||
delete(to)
|
||||
throw x
|
||||
} finally {
|
||||
close(input)
|
||||
close(out)
|
||||
delete(tmp)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun renameFile(from: File, to: File) {
|
||||
if (from.renameTo(to)) {
|
||||
Timber.i("Renamed %s to %s", from, to)
|
||||
} else {
|
||||
atomicCopy(from, to)
|
||||
}
|
||||
fun renameFile(from: String, to: String) {
|
||||
StorageFile.rename(from, to)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ -155,6 +126,17 @@ object Util {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun delete(file: String?): Boolean {
|
||||
if (file != null && StorageFile.isPathExists(file)) {
|
||||
if (!StorageFile.getFromPath(file).delete()) {
|
||||
Timber.w("Failed to delete file %s", file)
|
||||
return false
|
||||
}
|
||||
Timber.i("Deleted file %s", file)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun delete(file: File?): Boolean {
|
||||
if (file != null && file.exists()) {
|
||||
if (!file.delete()) {
|
||||
@ -513,7 +495,7 @@ object Util {
|
||||
intent.putExtra("artist", song.artist)
|
||||
intent.putExtra("album", song.album)
|
||||
val albumArtFile = FileUtil.getAlbumArtFile(song)
|
||||
intent.putExtra("coverart", albumArtFile.absolutePath)
|
||||
intent.putExtra("coverart", albumArtFile)
|
||||
} else {
|
||||
intent.putExtra("title", "")
|
||||
intent.putExtra("artist", "")
|
||||
@ -617,8 +599,8 @@ object Util {
|
||||
|
||||
if (Settings.shouldSendBluetoothAlbumArt) {
|
||||
val albumArtFile = FileUtil.getAlbumArtFile(song)
|
||||
intent.putExtra("coverart", albumArtFile.absolutePath)
|
||||
intent.putExtra("cover", albumArtFile.absolutePath)
|
||||
intent.putExtra("coverart", albumArtFile)
|
||||
intent.putExtra("cover", albumArtFile)
|
||||
}
|
||||
|
||||
intent.putExtra("position", playerPosition.toLong())
|
||||
@ -777,10 +759,11 @@ object Util {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun scanMedia(file: File?) {
|
||||
val uri = Uri.fromFile(file)
|
||||
val scanFileIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri)
|
||||
appContext().sendBroadcast(scanFileIntent)
|
||||
fun scanMedia(file: String?) {
|
||||
// TODO this doesn't work for URIs
|
||||
MediaScannerConnection.scanFile(
|
||||
UApp.applicationContext(), arrayOf(file),
|
||||
null, null)
|
||||
}
|
||||
|
||||
fun getResourceFromAttribute(context: Context, resId: Int): Int {
|
||||
|
@ -191,7 +191,7 @@
|
||||
<string name="settings.directory_cache_time_60">1 ora</string>
|
||||
<string name="settings.disc_sort">Ordina Canzoni secondo Disco</string>
|
||||
<string name="settings.disc_sort_summary">Ordina lista canzoni secondo il numero disco e traccia</string>
|
||||
<string name="settings.display_bitrate">Visualizza Bitrate Ed Estensione File</string>
|
||||
<string name="settings.display_bitrate">Visualizza Bitrate Ed Estensione FileAdapter</string>
|
||||
<string name="settings.display_bitrate_summary">Aggiungi nome artista con bitrare ed estensione file</string>
|
||||
<string name="settings.download_transition">Visualizza Download Durante Riproduzione</string>
|
||||
<string name="settings.download_transition_summary">Passa al download quando inizia riproduzione</string>
|
||||
|
@ -214,7 +214,7 @@
|
||||
<string name="settings.directory_cache_time_60">1 hour</string>
|
||||
<string name="settings.disc_sort">Sort Songs By Disc</string>
|
||||
<string name="settings.disc_sort_summary">Sort song list by disc number and track number</string>
|
||||
<string name="settings.display_bitrate">Display Bitrate and File Suffix</string>
|
||||
<string name="settings.display_bitrate">Display Bitrate and FileAdapter Suffix</string>
|
||||
<string name="settings.display_bitrate_summary">Append artist name with bitrate and file suffix</string>
|
||||
<string name="settings.download_transition">Show Downloads on Play</string>
|
||||
<string name="settings.download_transition_summary">Transition to download activity when starting playback</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user