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

390 lines
13 KiB
Kotlin
Raw Normal View History

2013-04-06 21:47:24 +02:00
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
2021-04-10 15:59:33 +02:00
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.java.KoinJavaComponent.inject
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
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
2013-04-06 21:47:24 +02:00
/**
2021-04-10 15:59:33 +02:00
* This class represents a singe Song or Video that can be downloaded.
*
2013-04-06 21:47:24 +02:00
* @author Sindre Mehus
* @version $Id$
*/
2021-04-10 15:59:33 +02:00
class DownloadFile(
private val context: Context,
val song: MusicDirectory.Entry,
private val save: Boolean
) {
val partialFile: File
val completeFile: File
private val saveFile: File = FileUtil.getSongFile(context, song)
private val mediaStoreService: MediaStoreService
private var downloadTask: CancellableTask? = null
var isFailed = false
2021-04-10 15:59:33 +02:00
private val desiredBitRate: Int = Util.getMaxBitRate(context)
2021-04-10 15:59:33 +02:00
@Volatile
private var isPlaying = false
2021-04-10 15:59:33 +02:00
@Volatile
private var saveWhenDone = false
2021-04-10 15:59:33 +02:00
@Volatile
private var completeWhenDone = false
2021-04-10 15:59:33 +02:00
private val downloader = inject(Downloader::class.java)
2021-04-10 15:59:33 +02:00
val progress: MutableLiveData<Int> = MutableLiveData(0)
2021-04-10 15:59:33 +02:00
init {
partialFile = File(saveFile.parent, FileUtil.getPartialFile(saveFile.name))
completeFile = File(saveFile.parent, FileUtil.getCompleteFile(saveFile.name))
mediaStoreService = MediaStoreService(context)
}
/**
* Returns the effective bit rate.
*/
2021-04-10 15:59:33 +02:00
fun getBitRate(): Int {
return if (song.bitRate == null) desiredBitRate else song.bitRate!!
}
2021-04-10 15:59:33 +02:00
@Synchronized
fun download() {
FileUtil.createDirectoryForParent(saveFile)
isFailed = false
downloadTask = DownloadTask()
downloadTask!!.start()
}
2021-04-10 15:59:33 +02:00
@Synchronized
fun cancelDownload() {
if (downloadTask != null) {
downloadTask!!.cancel()
}
}
2021-03-24 15:04:25 +01:00
2021-04-17 12:25:21 +02:00
val completeOrSaveFile: File
get() = if (saveFile.exists()) {
saveFile
} else {
2021-04-10 15:59:33 +02:00
completeFile
2021-04-17 12:25:21 +02:00
}
2021-04-10 15:59:33 +02:00
val completeOrPartialFile: File
get() = if (isCompleteFileAvailable) {
2021-04-17 12:25:21 +02:00
completeOrSaveFile
} else {
2021-04-10 15:59:33 +02:00
partialFile
}
2021-04-10 15:59:33 +02:00
val isSaved: Boolean
get() = saveFile.exists()
2021-04-10 15:59:33 +02:00
@get:Synchronized
val isCompleteFileAvailable: Boolean
get() = saveFile.exists() || completeFile.exists()
2021-04-10 15:59:33 +02:00
@get:Synchronized
val isWorkDone: Boolean
get() = saveFile.exists() || completeFile.exists() && !save ||
saveWhenDone || completeWhenDone
2021-04-10 15:59:33 +02:00
@get:Synchronized
val isDownloading: Boolean
get() = downloadTask != null && downloadTask!!.isRunning
@get:Synchronized
val isDownloadCancelled: Boolean
get() = downloadTask != null && downloadTask!!.isCancelled
2021-04-10 15:59:33 +02:00
fun shouldSave(): Boolean {
return save
}
2021-04-10 15:59:33 +02:00
fun delete() {
cancelDownload()
Util.delete(partialFile)
Util.delete(completeFile)
Util.delete(saveFile)
mediaStoreService.deleteFromMediaStore(this)
}
2021-04-10 15:59:33 +02:00
fun unpin() {
if (saveFile.exists()) {
if (!saveFile.renameTo(completeFile)) {
2021-04-10 15:59:33 +02:00
Timber.w(
"Renaming file failed. Original file: %s; Rename to: %s",
saveFile.name, completeFile.name
)
}
}
}
2021-04-10 15:59:33 +02:00
fun cleanup(): Boolean {
var ok = true
if (completeFile.exists() || saveFile.exists()) {
ok = Util.delete(partialFile)
}
2021-04-10 15:59:33 +02:00
if (saveFile.exists()) {
ok = ok and Util.delete(completeFile)
}
2021-04-10 15:59:33 +02:00
return ok
}
// In support of LRU caching.
2021-04-10 15:59:33 +02:00
fun updateModificationDate() {
updateModificationDate(saveFile)
updateModificationDate(partialFile)
updateModificationDate(completeFile)
}
2021-04-10 15:59:33 +02:00
fun setPlaying(isPlaying: Boolean) {
if (!isPlaying) doPendingRename()
this.isPlaying = isPlaying
}
2021-04-10 15:59:33 +02:00
// 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)
mediaStoreService.saveInMediaStore(this@DownloadFile)
} else {
Util.renameFile(partialFile, completeFile)
}
2021-04-10 15:59:33 +02:00
completeWhenDone = false
}
2021-04-10 15:59:33 +02:00
} catch (ex: IOException) {
Timber.w("Failed to rename file %s to %s", completeFile, saveFile)
}
}
2013-04-06 21:47:24 +02:00
2021-04-10 15:59:33 +02:00
override fun toString(): String {
return String.format("DownloadFile (%s)", song)
}
2013-04-06 21:47:24 +02:00
2021-04-10 15:59:33 +02:00
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(context, toString())
wifiLock.acquire()
if (saveFile.exists()) {
Timber.i("%s already exists. Skipping.", saveFile)
return
}
2021-04-10 15:59:33 +02:00
if (completeFile.exists()) {
if (save) {
if (isPlaying) {
saveWhenDone = true
} else {
Util.renameFile(completeFile, saveFile)
}
2021-04-10 15:59:33 +02:00
} else {
Timber.i("%s already exists. Skipping.", completeFile)
}
2021-04-10 15:59:33 +02:00
return
}
2013-04-06 21:47:24 +02:00
2021-04-10 15:59:33 +02:00
val musicService = getMusicService(context)
2013-04-06 21:47:24 +02:00
// Some devices seem to throw error on partial file which doesn't exist
2021-04-10 15:59:33 +02:00
val needsDownloading: Boolean
val duration = song.duration
var fileLength: Long = 0
2021-04-10 15:59:33 +02:00
if (!partialFile.exists()) {
fileLength = partialFile.length()
}
2021-04-10 15:59:33 +02:00
needsDownloading = (
desiredBitRate == 0 || duration == null ||
duration == 0 || fileLength == 0L
)
2021-04-10 15:59:33 +02:00
if (needsDownloading) {
// Attempt partial HTTP GET, appending to the file if it exists.
2021-04-10 15:59:33 +02:00
val (inStream, partial) = musicService
.getDownloadInputStream(song, partialFile.length(), desiredBitRate)
inputStream = inStream
2021-04-10 15:59:33 +02:00
if (partial) {
Timber.i(
"Executed partial HTTP GET, skipping %d bytes",
partialFile.length()
)
}
2021-04-10 15:59:33 +02:00
outputStream = FileOutputStream(partialFile, partial)
val len = inputStream.copyTo(outputStream) { totalBytesCopied ->
2021-04-10 15:59:33 +02:00
setProgress(totalBytesCopied)
}
2021-04-10 15:59:33 +02:00
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(musicService)
}
2021-04-10 15:59:33 +02:00
if (isPlaying) {
completeWhenDone = true
} else {
if (save) {
Util.renameFile(partialFile, saveFile)
mediaStoreService.saveInMediaStore(this@DownloadFile)
} else {
Util.renameFile(partialFile, completeFile)
}
}
2021-04-10 15:59:33 +02:00
} catch (x: Exception) {
Util.close(outputStream)
Util.delete(completeFile)
Util.delete(saveFile)
if (!isCancelled) {
isFailed = true
Timber.w(x, "Failed to download '%s'.", song)
}
2021-04-10 15:59:33 +02:00
} finally {
Util.close(inputStream)
Util.close(outputStream)
if (wakeLock != null) {
wakeLock.release()
Timber.i("Released wake lock %s", wakeLock)
}
2021-04-10 15:59:33 +02:00
wifiLock?.release()
CacheCleaner(context).cleanSpace()
downloader.value.checkDownloads()
}
}
2021-04-10 15:59:33 +02:00
private fun acquireWakeLock(wakeLock: WakeLock?): WakeLock? {
var wakeLock1 = wakeLock
if (Util.isScreenLitOnDownload(context)) {
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)
}
2021-04-10 15:59:33 +02:00
return wakeLock1
}
2021-04-10 15:59:33 +02:00
override fun toString(): String {
return String.format("DownloadTask (%s)", song)
}
2021-04-10 15:59:33 +02:00
private fun downloadAndSaveCoverArt(musicService: MusicService) {
try {
if (!TextUtils.isEmpty(song.coverArt)) {
val size = Util.getMinDisplayMetric(context)
musicService.getCoverArt(context, song, size, true, true)
}
2021-04-10 15:59:33 +02:00
} catch (x: Exception) {
Timber.e(x, "Failed to get cover art.")
}
}
2021-04-10 15:59:33 +02:00
@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)
}
2021-04-10 15:59:33 +02:00
return bytesCopied
}
}
2021-04-10 15:59:33 +02:00
private fun setProgress(totalBytesCopied: Long) {
if (song.size != null) {
progress.postValue((totalBytesCopied * 100 / song.size!!).toInt())
}
}
2021-04-10 15:59:33 +02:00
companion object {
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
val raf = RandomAccessFile(file, "rw")
val length = raf.length()
raf.setLength(length + 1)
raf.setLength(length)
raf.close()
} catch (e: Exception) {
Timber.w("Failed to set last-modified date on %s", file)
}
}
}
}
}
2021-04-10 15:59:33 +02:00
}