ultrasonic-app-subsonic-and.../ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt

395 lines
12 KiB
Kotlin

/*
* DownloadFile.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.service
import android.content.Context
import android.net.wifi.WifiManager.WifiLock
import android.os.PowerManager
import android.os.PowerManager.WakeLock
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.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory
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.FileUtil
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* This class represents a singe Song or Video that can be downloaded.
*
* @author Sindre Mehus
* @version $Id$
*/
class DownloadFile(
val song: MusicDirectory.Entry,
private val save: Boolean
) : KoinComponent {
val partialFile: File
val completeFile: File
private val saveFile: File = FileUtil.getSongFile(song)
private var downloadTask: CancellableTask? = null
var isFailed = false
private var retryCount = MAX_RETRIES
private val desiredBitRate: Int = Util.getMaxBitRate()
@Volatile
private var isPlaying = false
@Volatile
private var saveWhenDone = false
@Volatile
private var completeWhenDone = false
private val downloader: Downloader by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject()
val progress: MutableLiveData<Int> = MutableLiveData(0)
init {
partialFile = File(saveFile.parent, FileUtil.getPartialFile(saveFile.name))
completeFile = File(saveFile.parent, FileUtil.getCompleteFile(saveFile.name))
}
/**
* Returns the effective bit rate.
*/
fun getBitRate(): Int {
return if (song.bitRate == null) desiredBitRate else song.bitRate!!
}
@Synchronized
fun download() {
FileUtil.createDirectoryForParent(saveFile)
isFailed = false
downloadTask = DownloadTask()
downloadTask!!.start()
}
@Synchronized
fun cancelDownload() {
if (downloadTask != null) {
downloadTask!!.cancel()
}
}
val completeOrSaveFile: File
get() = if (saveFile.exists()) {
saveFile
} else {
completeFile
}
val completeOrPartialFile: File
get() = if (isCompleteFileAvailable) {
completeOrSaveFile
} else {
partialFile
}
val isSaved: Boolean
get() = saveFile.exists()
@get:Synchronized
val isCompleteFileAvailable: Boolean
get() = saveFile.exists() || completeFile.exists()
@get:Synchronized
val isWorkDone: Boolean
get() = saveFile.exists() || completeFile.exists() && !save ||
saveWhenDone || completeWhenDone
@get:Synchronized
val isDownloading: Boolean
get() = downloadTask != null && downloadTask!!.isRunning
@get:Synchronized
val isDownloadCancelled: Boolean
get() = downloadTask != null && downloadTask!!.isCancelled
fun shouldSave(): Boolean {
return save
}
fun shouldRetry(): Boolean {
return (retryCount > 0)
}
fun delete() {
cancelDownload()
Util.delete(partialFile)
Util.delete(completeFile)
Util.delete(saveFile)
Util.scanMedia(saveFile)
}
fun unpin() {
if (saveFile.exists()) {
if (!saveFile.renameTo(completeFile)) {
Timber.w(
"Renaming file failed. Original file: %s; Rename to: %s",
saveFile.name, completeFile.name
)
}
}
}
fun cleanup(): Boolean {
var ok = true
if (completeFile.exists() || saveFile.exists()) {
ok = Util.delete(partialFile)
}
if (saveFile.exists()) {
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
}
// Do a pending rename after the song has stopped playing
private fun doPendingRename() {
try {
if (saveWhenDone) {
Util.renameFile(completeFile, saveFile)
saveWhenDone = false
} else if (completeWhenDone) {
if (save) {
Util.renameFile(partialFile, saveFile)
Util.scanMedia(saveFile)
} else {
Util.renameFile(partialFile, completeFile)
}
completeWhenDone = false
}
} catch (e: IOException) {
Timber.w(e, "Failed to rename file %s to %s", completeFile, saveFile)
}
}
override fun toString(): String {
return String.format("DownloadFile (%s)", song)
}
@Suppress("TooGenericExceptionCaught")
private inner class DownloadTask : CancellableTask() {
override fun execute() {
var inputStream: InputStream? = null
var outputStream: FileOutputStream? = null
var wakeLock: WakeLock? = null
var wifiLock: WifiLock? = null
try {
wakeLock = acquireWakeLock(wakeLock)
wifiLock = Util.createWifiLock(toString())
wifiLock.acquire()
if (saveFile.exists()) {
Timber.i("%s already exists. Skipping.", saveFile)
return
}
if (completeFile.exists()) {
if (save) {
if (isPlaying) {
saveWhenDone = true
} else {
Util.renameFile(completeFile, saveFile)
}
} else {
Timber.i("%s already exists. Skipping.", completeFile)
}
return
}
val musicService = getMusicService()
// Some devices seem to throw error on partial file which doesn't exist
val needsDownloading: Boolean
val duration = song.duration
var fileLength: Long = 0
if (!partialFile.exists()) {
fileLength = partialFile.length()
}
needsDownloading = (
desiredBitRate == 0 || duration == null ||
duration == 0 || fileLength == 0L
)
if (needsDownloading) {
// Attempt partial HTTP GET, appending to the file if it exists.
val (inStream, partial) = musicService.getDownloadInputStream(
song, partialFile.length(), desiredBitRate, save
)
inputStream = inStream
if (partial) {
Timber.i(
"Executed partial HTTP GET, skipping %d bytes",
partialFile.length()
)
}
outputStream = FileOutputStream(partialFile, partial)
val len = inputStream.copyTo(outputStream) { totalBytesCopied ->
setProgress(totalBytesCopied)
}
Timber.i("Downloaded %d bytes to %s", len, partialFile)
inputStream.close()
outputStream.flush()
outputStream.close()
if (isCancelled) {
throw Exception(String.format("Download of '%s' was cancelled", song))
}
downloadAndSaveCoverArt()
}
if (isPlaying) {
completeWhenDone = true
} else {
if (save) {
Util.renameFile(partialFile, saveFile)
Util.scanMedia(saveFile)
} else {
Util.renameFile(partialFile, completeFile)
}
}
} catch (e: Exception) {
Util.close(outputStream)
Util.delete(completeFile)
Util.delete(saveFile)
if (!isCancelled) {
isFailed = true
if (retryCount > 0) {
--retryCount
}
Timber.w(e, "Failed to download '%s'.", song)
}
} finally {
Util.close(inputStream)
Util.close(outputStream)
if (wakeLock != null) {
wakeLock.release()
Timber.i("Released wake lock %s", wakeLock)
}
wifiLock?.release()
CacheCleaner().cleanSpace()
downloader.checkDownloads()
}
}
private fun acquireWakeLock(wakeLock: WakeLock?): WakeLock? {
var wakeLock1 = wakeLock
if (Util.isScreenLitOnDownload()) {
val context = UApp.applicationContext()
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
val flags = PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ON_AFTER_RELEASE
wakeLock1 = pm.newWakeLock(flags, toString())
wakeLock1.acquire(10 * 60 * 1000L /*10 minutes*/)
Timber.i("Acquired wake lock %s", wakeLock1)
}
return wakeLock1
}
override fun toString(): String {
return String.format("DownloadTask (%s)", song)
}
private fun downloadAndSaveCoverArt() {
try {
if (!TextUtils.isEmpty(song.coverArt)) {
// Download the largest size that we can display in the UI
imageLoaderProvider.getImageLoader().cacheCoverArt(song)
}
} catch (all: Exception) {
Timber.e(all, "Failed to get cover art.")
}
}
@Throws(IOException::class)
fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long) -> Any): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = read(buffer)
while (!isCancelled && bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
onCopy(bytesCopied)
bytes = read(buffer)
}
return bytesCopied
}
}
private fun setProgress(totalBytesCopied: Long) {
if (song.size != null) {
progress.postValue((totalBytesCopied * 100 / song.size!!).toInt())
}
}
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)
}
}
}
}
companion object {
const val MAX_RETRIES = 5
}
}