Migrate DownloadFile to Kotlin

This commit is contained in:
tzugen 2021-04-10 15:59:33 +02:00
parent 3139c94d11
commit cf68038e20
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
3 changed files with 297 additions and 400 deletions

View File

@ -16,505 +16,380 @@
Copyright 2009 (C) Sindre Mehus Copyright 2009 (C) Sindre Mehus
*/ */
package org.moire.ultrasonic.service; package org.moire.ultrasonic.service
import android.content.Context; import android.content.Context
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.WifiLock
import android.os.PowerManager; import android.os.PowerManager
import android.text.TextUtils; import android.os.PowerManager.WakeLock
import timber.log.Timber; import android.text.TextUtils
import androidx.lifecycle.MutableLiveData
import org.jetbrains.annotations.NotNull; import java.io.File
import org.moire.ultrasonic.domain.MusicDirectory; import java.io.FileOutputStream
import org.moire.ultrasonic.util.CacheCleaner; import java.io.IOException
import org.moire.ultrasonic.util.CancellableTask; import java.io.InputStream
import org.moire.ultrasonic.util.FileUtil; import java.io.OutputStream
import org.moire.ultrasonic.util.Util; import java.io.RandomAccessFile
import org.koin.java.KoinJavaComponent.inject
import java.io.File; import org.moire.ultrasonic.domain.MusicDirectory
import java.io.FileOutputStream; import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import java.io.IOException; import org.moire.ultrasonic.util.CacheCleaner
import java.io.InputStream; import org.moire.ultrasonic.util.CancellableTask
import java.io.OutputStream; import org.moire.ultrasonic.util.FileUtil
import java.io.RandomAccessFile; import org.moire.ultrasonic.util.Util
import timber.log.Timber
import kotlin.Lazy;
import kotlin.Pair;
import static android.content.Context.POWER_SERVICE;
import static android.os.PowerManager.ON_AFTER_RELEASE;
import static android.os.PowerManager.SCREEN_DIM_WAKE_LOCK;
import static org.koin.java.KoinJavaComponent.inject;
/** /**
* This class represents a singe Song or Video that can be downloaded.
*
* @author Sindre Mehus * @author Sindre Mehus
* @version $Id$ * @version $Id$
*/ */
public class DownloadFile class DownloadFile(
{ private val context: Context,
private final Context context; val song: MusicDirectory.Entry,
private final MusicDirectory.Entry song; private val save: Boolean
private final File partialFile; ) {
private final File completeFile; val partialFile: File
private final File saveFile; val completeFile: File
private val saveFile: File = FileUtil.getSongFile(context, song)
private val mediaStoreService: MediaStoreService
private var downloadTask: CancellableTask? = null
var isFailed = false
private final MediaStoreService mediaStoreService; private val desiredBitRate: Int = Util.getMaxBitRate(context)
private CancellableTask downloadTask;
private final boolean save;
private boolean failed;
private int bitRate;
private volatile boolean isPlaying;
private volatile boolean saveWhenDone;
private volatile boolean completeWhenDone;
private final Lazy<Downloader> downloader = inject(Downloader.class); @Volatile
private var isPlaying = false
public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) @Volatile
{ private var saveWhenDone = false
super();
this.context = context;
this.song = song;
this.save = save;
saveFile = FileUtil.getSongFile(context, song); @Volatile
bitRate = Util.getMaxBitRate(context); private var completeWhenDone = false
partialFile = new File(saveFile.getParent(), String.format("%s.partial.%s", FileUtil.getBaseName(saveFile.getName()), FileUtil.getExtension(saveFile.getName())));
completeFile = new File(saveFile.getParent(), String.format("%s.complete.%s", FileUtil.getBaseName(saveFile.getName()), FileUtil.getExtension(saveFile.getName())));
mediaStoreService = new MediaStoreService(context);
}
public MusicDirectory.Entry getSong() private val downloader = inject(Downloader::class.java)
{
return song; val progress: MutableLiveData<Int> = MutableLiveData(0)
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. * Returns the effective bit rate.
*/ */
public int getBitRate() fun getBitRate(): Int {
{ return if (song.bitRate == null) desiredBitRate else song.bitRate!!
if (!partialFile.exists())
{
bitRate = Util.getMaxBitRate(context);
} }
if (bitRate > 0) @Synchronized
{ fun download() {
return bitRate; FileUtil.createDirectoryForParent(saveFile)
isFailed = false
downloadTask = DownloadTask()
downloadTask!!.start()
} }
return song.getBitRate() == null ? 160 : song.getBitRate(); @Synchronized
fun cancelDownload() {
if (downloadTask != null) {
downloadTask!!.cancel()
} }
public synchronized void download()
{
FileUtil.createDirectoryForParent(saveFile);
failed = false;
if (!partialFile.exists())
{
bitRate = Util.getMaxBitRate(context);
} }
downloadTask = new DownloadTask(); fun getCompleteFile(): File {
downloadTask.start(); if (saveFile.exists()) {
return saveFile
} }
public synchronized void cancelDownload() return if (completeFile.exists()) {
{ completeFile
if (downloadTask != null) } else saveFile
{
downloadTask.cancel();
}
} }
public File getCompleteFile() val completeOrPartialFile: File
{ get() = if (isCompleteFileAvailable) {
if (saveFile.exists()) getCompleteFile()
{
return saveFile;
}
if (completeFile.exists())
{
return completeFile;
}
return saveFile;
}
public File getCompleteOrPartialFile() {
if (isCompleteFileAvailable()) {
return getCompleteFile();
} else { } else {
return getPartialFile(); partialFile
}
} }
public File getPartialFile() val isSaved: Boolean
{ get() = saveFile.exists()
return partialFile;
@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
} }
public boolean isSaved() fun delete() {
{ cancelDownload()
return saveFile.exists(); Util.delete(partialFile)
Util.delete(completeFile)
Util.delete(saveFile)
mediaStoreService.deleteFromMediaStore(this)
} }
public synchronized boolean isCompleteFileAvailable() fun unpin() {
{ if (saveFile.exists()) {
return saveFile.exists() || completeFile.exists();
}
public synchronized boolean isWorkDone()
{
return saveFile.exists() || (completeFile.exists() && !save) || saveWhenDone || completeWhenDone;
}
public synchronized boolean isDownloading()
{
return downloadTask != null && downloadTask.isRunning();
}
public synchronized boolean isDownloadCancelled()
{
return downloadTask != null && downloadTask.isCancelled();
}
public boolean shouldSave()
{
return save;
}
public boolean isFailed()
{
return failed;
}
public void delete()
{
cancelDownload();
Util.delete(partialFile);
Util.delete(completeFile);
Util.delete(saveFile);
mediaStoreService.deleteFromMediaStore(this);
}
public void unpin()
{
if (saveFile.exists())
{
if (!saveFile.renameTo(completeFile)){ if (!saveFile.renameTo(completeFile)){
Timber.w("Renaming file failed. Original file: %s; Rename to: %s", saveFile.getName(), completeFile.getName()); Timber.w(
"Renaming file failed. Original file: %s; Rename to: %s",
saveFile.name, completeFile.name
)
} }
} }
} }
public boolean cleanup() fun cleanup(): Boolean {
{ var ok = true
boolean ok = true; if (completeFile.exists() || saveFile.exists()) {
ok = Util.delete(partialFile)
if (completeFile.exists() || saveFile.exists())
{
ok = Util.delete(partialFile);
} }
if (saveFile.exists()) if (saveFile.exists()) {
{ ok = ok and Util.delete(completeFile)
ok &= Util.delete(completeFile);
} }
return ok; return ok
} }
// In support of LRU caching. // In support of LRU caching.
public void updateModificationDate() fun updateModificationDate() {
{ updateModificationDate(saveFile)
updateModificationDate(saveFile); updateModificationDate(partialFile)
updateModificationDate(partialFile); updateModificationDate(completeFile)
updateModificationDate(completeFile);
} }
private static void updateModificationDate(File file) fun setPlaying(isPlaying: Boolean) {
{ if (!isPlaying) doPendingRename()
if (file.exists()) this.isPlaying = isPlaying
{
boolean 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
RandomAccessFile raf = new RandomAccessFile(file, "rw");
long length = raf.length();
raf.setLength(length + 1);
raf.setLength(length);
raf.close();
}
catch (Exception e)
{
Timber.w("Failed to set last-modified date on %s", file);
}
}
}
} }
public void setPlaying(boolean isPlaying) // Do a pending rename after the song has stopped playing
{ private fun doPendingRename() {
try try {
{ if (saveWhenDone) {
if (saveWhenDone && !isPlaying) Util.renameFile(completeFile, saveFile)
{ saveWhenDone = false
Util.renameFile(completeFile, saveFile); } else if (completeWhenDone) {
saveWhenDone = false; if (save) {
Util.renameFile(partialFile, saveFile)
mediaStoreService.saveInMediaStore(this@DownloadFile)
} else {
Util.renameFile(partialFile, completeFile)
} }
else if (completeWhenDone && !isPlaying) completeWhenDone = false
{
if (save)
{
Util.renameFile(partialFile, saveFile);
mediaStoreService.saveInMediaStore(DownloadFile.this);
} }
else } catch (ex: IOException) {
{ Timber.w("Failed to rename file %s to %s", completeFile, saveFile)
Util.renameFile(partialFile, completeFile);
} }
completeWhenDone = false;
}
}
catch (IOException ex)
{
Timber.w("Failed to rename file %s to %s", completeFile, saveFile);
} }
this.isPlaying = isPlaying; override fun toString(): String {
return String.format("DownloadFile (%s)", song)
} }
@NotNull private inner class DownloadTask : CancellableTask() {
@Override override fun execute() {
public String toString() var inputStream: InputStream? = null
{ var outputStream: FileOutputStream? = null
return String.format("DownloadFile (%s)", song); var wakeLock: WakeLock? = null
} var wifiLock: WifiLock? = null
try {
wakeLock = acquireWakeLock(wakeLock)
wifiLock = Util.createWifiLock(context, toString())
wifiLock.acquire()
private class DownloadTask extends CancellableTask if (saveFile.exists()) {
{ Timber.i("%s already exists. Skipping.", saveFile)
@Override return
public void execute()
{
InputStream in = null;
FileOutputStream out = null;
PowerManager.WakeLock wakeLock = null;
WifiManager.WifiLock wifiLock = null;
try
{
if (Util.isScreenLitOnDownload(context))
{
PowerManager pm = (PowerManager) context.getSystemService(POWER_SERVICE);
wakeLock = pm.newWakeLock(SCREEN_DIM_WAKE_LOCK | ON_AFTER_RELEASE, toString());
wakeLock.acquire(10*60*1000L /*10 minutes*/);
Timber.i("Acquired wake lock %s", wakeLock);
} }
wifiLock = Util.createWifiLock(context, toString()); if (completeFile.exists()) {
wifiLock.acquire(); if (save) {
if (isPlaying) {
if (saveFile.exists()) saveWhenDone = true
{ } else {
Timber.i("%s already exists. Skipping.", saveFile); Util.renameFile(completeFile, saveFile)
return;
} }
if (completeFile.exists()) } else {
{ Timber.i("%s already exists. Skipping.", completeFile)
if (save)
{
if (isPlaying)
{
saveWhenDone = true;
} }
else return
{
Util.renameFile(completeFile, saveFile);
}
}
else
{
Timber.i("%s already exists. Skipping.", completeFile);
}
return;
} }
MusicService musicService = MusicServiceFactory.getMusicService(context); val musicService = getMusicService(context)
// Some devices seem to throw error on partial file which doesn't exist // Some devices seem to throw error on partial file which doesn't exist
boolean compare; val needsDownloading: Boolean
val duration = song.duration
var fileLength: Long = 0
Integer duration = song.getDuration(); if (!partialFile.exists()) {
long fileLength = 0; fileLength = partialFile.length()
if (!partialFile.exists())
{
fileLength = partialFile.length();
} }
try needsDownloading = (
{ desiredBitRate == 0 || duration == null ||
compare = (bitRate == 0) || (duration == null || duration == 0) || (fileLength == 0); duration == 0 || fileLength == 0L
//(bitRate * song.getDuration() * 1000 / 8) > partialFile.length(); )
}
catch (Exception e)
{
compare = true;
}
if (compare) if (needsDownloading) {
{
// Attempt partial HTTP GET, appending to the file if it exists. // Attempt partial HTTP GET, appending to the file if it exists.
Pair<InputStream, Boolean> response = musicService val (inStream, partial) = musicService
.getDownloadInputStream(song, partialFile.length(), bitRate); .getDownloadInputStream(song, partialFile.length(), desiredBitRate)
if (response.getSecond()) inputStream = inStream
{
Timber.i("Executed partial HTTP GET, skipping %d bytes", partialFile.length()); if (partial) {
Timber.i(
"Executed partial HTTP GET, skipping %d bytes",
partialFile.length()
)
} }
out = new FileOutputStream(partialFile, response.getSecond()); outputStream = FileOutputStream(partialFile, partial)
long n = copy(response.getFirst(), out);
Timber.i("Downloaded %d bytes to %s", n, partialFile);
out.flush();
out.close();
if (isCancelled()) val len = inputStream.copyTo(outputStream) {
{ totalBytesCopied ->
throw new Exception(String.format("Download of '%s' was cancelled", song)); setProgress(totalBytesCopied)
} }
downloadAndSaveCoverArt(musicService); 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)
} }
if (isPlaying) if (isPlaying) {
{ completeWhenDone = true
completeWhenDone = true; } else {
if (save) {
Util.renameFile(partialFile, saveFile)
mediaStoreService.saveInMediaStore(this@DownloadFile)
} else {
Util.renameFile(partialFile, completeFile)
} }
else
{
if (save)
{
Util.renameFile(partialFile, saveFile);
mediaStoreService.saveInMediaStore(DownloadFile.this);
} }
else } catch (x: Exception) {
{ Util.close(outputStream)
Util.renameFile(partialFile, completeFile); Util.delete(completeFile)
Util.delete(saveFile)
if (!isCancelled) {
isFailed = true
Timber.w(x, "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(context).cleanSpace()
downloader.value.checkDownloads()
} }
catch (Exception x)
{
Util.close(out);
Util.delete(completeFile);
Util.delete(saveFile);
if (!isCancelled())
{
failed = true;
Timber.w(x, "Failed to download '%s'.", song);
} }
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)
} }
finally return wakeLock1
{
Util.close(in);
Util.close(out);
if (wakeLock != null)
{
wakeLock.release();
Timber.i("Released wake lock %s", wakeLock);
} }
if (wifiLock != null) override fun toString(): String {
{ return String.format("DownloadTask (%s)", song)
wifiLock.release();
} }
new CacheCleaner(context).cleanSpace(); private fun downloadAndSaveCoverArt(musicService: MusicService) {
try {
if (!TextUtils.isEmpty(song.coverArt)) {
val size = Util.getMinDisplayMetric(context)
musicService.getCoverArt(context, song, size, true, true)
}
} catch (x: Exception) {
Timber.e(x, "Failed to get cover art.")
}
downloader.getValue().checkDownloads(); }
@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
} }
} }
@NotNull private fun setProgress(totalBytesCopied: Long) {
@Override if (song.size != null) {
public String toString() progress.postValue((totalBytesCopied * 100 / song.size!!).toInt())
{ }
return String.format("DownloadTask (%s)", song);
}
private void downloadAndSaveCoverArt(MusicService musicService)
{
try
{
if (!TextUtils.isEmpty(song.getCoverArt())) {
int size = Util.getMinDisplayMetric(context);
musicService.getCoverArt(context, song, size, true, true);
}
}
catch (Exception x)
{
Timber.e(x, "Failed to get cover art.");
}
}
private long copy(final InputStream in, OutputStream out) throws IOException
{
// Start a thread that will close the input stream if the task is
// cancelled, thus causing the copy() method to return.
new Thread()
{
@Override
public void run()
{
while (true)
{
Util.sleepQuietly(3000L);
if (isCancelled())
{
Util.close(in);
return;
} }
if (!isRunning()) companion object {
{ private fun updateModificationDate(file: File) {
return; 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)
} }
} }
} }
}.start();
byte[] buffer = new byte[1024 * 16];
long count = 0;
int n;
long lastLog = System.currentTimeMillis();
while (!isCancelled() && (n = in.read(buffer)) != -1)
{
out.write(buffer, 0, n);
count += n;
long now = System.currentTimeMillis();
if (now - lastLog > 3000L)
{ // Only every so often.
Timber.i("Downloaded %s of %s", Util.formatBytes(count), song);
lastLog = now;
}
}
return count;
} }
} }
} }

View File

@ -549,6 +549,28 @@ public class FileUtil
return index == -1 ? name : name.substring(0, index); return index == -1 ? name : name.substring(0, index);
} }
/**
* Returns the file name of a .partial file of the given file.
*
* @param name The filename in question.
* @return The .partial file name
*/
public static String getPartialFile(String name)
{
return String.format("%s.partial.%s", FileUtil.getBaseName(name), FileUtil.getExtension(name));
}
/**
* Returns the file name of a .complete file of the given file.
*
* @param name The filename in question.
* @return The .complete file name
*/
public static String getCompleteFile(String name)
{
return String.format("%s.complete.%s", FileUtil.getBaseName(name), FileUtil.getExtension(name));
}
public static <T extends Serializable> boolean serialize(Context context, T obj, String fileName) public static <T extends Serializable> boolean serialize(Context context, T obj, String fileName)
{ {
File file = new File(context.getCacheDir(), fileName); File file = new File(context.getCacheDir(), fileName);

View File

@ -755,7 +755,7 @@ class LocalMediaPlayer(
} }
// Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to. // Calculate roughly how many bytes BUFFER_LENGTH_SECONDS corresponds to.
val bitRate = downloadFile.bitRate val bitRate = downloadFile.getBitRate()
val byteCount = max(100000, bitRate * 1024L / 8L * bufferLength) val byteCount = max(100000, bitRate * 1024L / 8L * bufferLength)
// Find out how large the file should grow before resuming playback. // Find out how large the file should grow before resuming playback.