diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml
index 53d2ef0e..8347a5e7 100644
--- a/ultrasonic/lint-baseline.xml
+++ b/ultrasonic/lint-baseline.xml
@@ -320,28 +320,6 @@
column="25"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception
+ public Pair getDownloadInputStream(MusicDirectory.Entry song, long offset, int maxBitrate) throws Exception
{
- return musicService.getDownloadInputStream(context, song, offset, maxBitrate, task);
+ return musicService.getDownloadInputStream(song, offset, maxBitrate);
}
@Override
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java
deleted file mode 100644
index eb71800f..00000000
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/DownloadFile.java
+++ /dev/null
@@ -1,521 +0,0 @@
-/*
- 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 .
-
- Copyright 2009 (C) Sindre Mehus
- */
-package org.moire.ultrasonic.service;
-
-import android.content.Context;
-import android.net.wifi.WifiManager;
-import android.os.PowerManager;
-import android.text.TextUtils;
-import timber.log.Timber;
-
-import org.jetbrains.annotations.NotNull;
-import org.moire.ultrasonic.domain.MusicDirectory;
-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 java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.RandomAccessFile;
-
-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;
-
-/**
- * @author Sindre Mehus
- * @version $Id$
- */
-public class DownloadFile
-{
- private final Context context;
- private final MusicDirectory.Entry song;
- private final File partialFile;
- private final File completeFile;
- private final File saveFile;
-
- private final MediaStoreService mediaStoreService;
- 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 = inject(Downloader.class);
-
- public DownloadFile(Context context, MusicDirectory.Entry song, boolean save)
- {
- super();
- this.context = context;
- this.song = song;
- this.save = save;
-
- saveFile = FileUtil.getSongFile(context, song);
- bitRate = Util.getMaxBitRate(context);
- 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()
- {
- return song;
- }
-
- /**
- * Returns the effective bit rate.
- */
- public int getBitRate()
- {
- if (!partialFile.exists())
- {
- bitRate = Util.getMaxBitRate(context);
- }
-
- if (bitRate > 0)
- {
- return bitRate;
- }
-
- return song.getBitRate() == null ? 160 : song.getBitRate();
- }
-
- public synchronized void download()
- {
- FileUtil.createDirectoryForParent(saveFile);
- failed = false;
-
- if (!partialFile.exists())
- {
- bitRate = Util.getMaxBitRate(context);
- }
-
- downloadTask = new DownloadTask();
- downloadTask.start();
- }
-
- public synchronized void cancelDownload()
- {
- if (downloadTask != null)
- {
- downloadTask.cancel();
- }
- }
-
- public File getCompleteFile()
- {
- if (saveFile.exists())
- {
- return saveFile;
- }
-
- if (completeFile.exists())
- {
- return completeFile;
- }
-
- return saveFile;
- }
-
- public File getCompleteOrPartialFile() {
- if (isCompleteFileAvailable()) {
- return getCompleteFile();
- } else {
- return getPartialFile();
- }
- }
-
- public File getPartialFile()
- {
- return partialFile;
- }
-
- public boolean isSaved()
- {
- return saveFile.exists();
- }
-
- public synchronized boolean isCompleteFileAvailable()
- {
- 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)){
- Timber.w("Renaming file failed. Original file: %s; Rename to: %s", saveFile.getName(), completeFile.getName());
- }
- }
- }
-
- public boolean cleanup()
- {
- boolean ok = true;
-
- if (completeFile.exists() || saveFile.exists())
- {
- ok = Util.delete(partialFile);
- }
-
- if (saveFile.exists())
- {
- ok &= Util.delete(completeFile);
- }
-
- return ok;
- }
-
- // In support of LRU caching.
- public void updateModificationDate()
- {
- updateModificationDate(saveFile);
- updateModificationDate(partialFile);
- updateModificationDate(completeFile);
- }
-
- private static void updateModificationDate(File file)
- {
- if (file.exists())
- {
- 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)
- {
- try
- {
- if (saveWhenDone && !isPlaying)
- {
- Util.renameFile(completeFile, saveFile);
- saveWhenDone = false;
- }
- else if (completeWhenDone && !isPlaying)
- {
- if (save)
- {
- Util.renameFile(partialFile, saveFile);
- mediaStoreService.saveInMediaStore(DownloadFile.this);
- }
- else
- {
- Util.renameFile(partialFile, completeFile);
- }
-
- completeWhenDone = false;
- }
- }
- catch (IOException ex)
- {
- Timber.w("Failed to rename file %s to %s", completeFile, saveFile);
- }
-
- this.isPlaying = isPlaying;
- }
-
- @NotNull
- @Override
- public String toString()
- {
- return String.format("DownloadFile (%s)", song);
- }
-
- private class DownloadTask extends CancellableTask
- {
- @Override
- 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());
- 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;
- }
-
- MusicService musicService = MusicServiceFactory.getMusicService(context);
-
- // Some devices seem to throw error on partial file which doesn't exist
- boolean compare;
-
- Integer duration = song.getDuration();
- long fileLength = 0;
-
- if (!partialFile.exists())
- {
- fileLength = partialFile.length();
- }
-
- try
- {
- compare = (bitRate == 0) || (duration == null || duration == 0) || (fileLength == 0);
- //(bitRate * song.getDuration() * 1000 / 8) > partialFile.length();
- }
- catch (Exception e)
- {
- compare = true;
- }
-
- if (compare)
- {
- // Attempt partial HTTP GET, appending to the file if it exists.
- Pair response = musicService
- .getDownloadInputStream(context, song, partialFile.length(), bitRate,
- DownloadTask.this);
-
- if (response.getSecond())
- {
- Timber.i("Executed partial HTTP GET, skipping %d bytes", partialFile.length());
- }
-
- out = new FileOutputStream(partialFile, response.getSecond());
- long n = copy(response.getFirst(), out);
- Timber.i("Downloaded %d bytes to %s", n, partialFile);
- out.flush();
- out.close();
-
- if (isCancelled())
- {
- throw new Exception(String.format("Download of '%s' was cancelled", song));
- }
-
- downloadAndSaveCoverArt(musicService);
- }
-
- if (isPlaying)
- {
- completeWhenDone = true;
- }
- else
- {
- if (save)
- {
- Util.renameFile(partialFile, saveFile);
- mediaStoreService.saveInMediaStore(DownloadFile.this);
- }
- else
- {
- Util.renameFile(partialFile, completeFile);
- }
- }
- }
- catch (Exception x)
- {
- Util.close(out);
- Util.delete(completeFile);
- Util.delete(saveFile);
-
- if (!isCancelled())
- {
- failed = true;
- Timber.w(x, "Failed to download '%s'.", song);
- }
-
- }
- finally
- {
- Util.close(in);
- Util.close(out);
- if (wakeLock != null)
- {
- wakeLock.release();
- Timber.i("Released wake lock %s", wakeLock);
- }
- if (wifiLock != null)
- {
- wifiLock.release();
- }
-
- new CacheCleaner(context).cleanSpace();
-
- downloader.getValue().checkDownloads();
- }
- }
-
- @NotNull
- @Override
- public String toString()
- {
- 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())
- {
- return;
- }
- }
- }
- }.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;
- }
- }
-}
\ No newline at end of file
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicService.java
index 8ef64e81..86782318 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicService.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicService.java
@@ -108,7 +108,7 @@ public interface MusicService
* Return response {@link InputStream} and a {@link Boolean} that indicates if this response is
* partial.
*/
- Pair getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) throws Exception;
+ Pair getDownloadInputStream(MusicDirectory.Entry song, long offset, int maxBitrate) throws Exception;
// TODO: Refactor and remove this call (see RestMusicService implementation)
String getVideoUrl(Context context, String id, boolean useFlash) throws Exception;
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java
index 80688551..a78d09b2 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java
@@ -893,7 +893,7 @@ public class OfflineMusicService implements MusicService
}
@Override
- public Pair getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) {
+ public Pair getDownloadInputStream(MusicDirectory.Entry song, long offset, int maxBitrate) {
Timber.w("OfflineMusicService.getDownloadInputStream was called but it isn't available");
return null;
}
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java
index f9d71d20..890788f8 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java
@@ -229,7 +229,7 @@ public class CacheCleaner
for (DownloadFile downloadFile : downloader.getValue().getDownloads())
{
filesToNotDelete.add(downloadFile.getPartialFile());
- filesToNotDelete.add(downloadFile.getCompleteFile());
+ filesToNotDelete.add(downloadFile.getCompleteOrSaveFile());
}
filesToNotDelete.add(FileUtil.getMusicDirectory(context));
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java
index 9a52eff5..61c0b05b 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java
@@ -549,6 +549,28 @@ public class FileUtil
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 boolean serialize(Context context, T obj, String fileName)
{
File file = new File(context.getCacheDir(), fileName);
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java
index 0e9870c6..8dfb1cd5 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java
@@ -194,7 +194,7 @@ public class StreamProxy implements Runnable
while (isRunning && !client.isClosed())
{
// See if there's more to send
- File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteFile() : downloadFile.getPartialFile();
+ File file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteOrSaveFile() : downloadFile.getPartialFile();
int cbSentThisBatch = 0;
if (file.exists())
diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java
index ca1c5e0e..bd4bb8bc 100644
--- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java
+++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/Util.java
@@ -343,6 +343,23 @@ public class Util
toast.show();
}
+
+ /**
+ * Formats an Int to a percentage string
+ * For instance:
+ *
+ * format(99)
returns "99 %".
+ *
+ *
+ * @param percent The percent as a range from 0 - 100
+ * @return The formatted string.
+ */
+ public static synchronized String formatPercentage(int percent)
+ {
+ return Math.min(Math.max(percent,0),100) + " %";
+ }
+
+
/**
* Converts a byte-count to a formatted string suitable for display to the user.
* For instance:
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt
new file mode 100644
index 00000000..08b081c6
--- /dev/null
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt
@@ -0,0 +1,377 @@
+/*
+ * 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.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
+
+/**
+ * This class represents a singe Song or Video that can be downloaded.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+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
+
+ private val desiredBitRate: Int = Util.getMaxBitRate(context)
+
+ @Volatile
+ private var isPlaying = false
+
+ @Volatile
+ private var saveWhenDone = false
+
+ @Volatile
+ private var completeWhenDone = false
+
+ private val downloader = inject(Downloader::class.java)
+
+ val progress: MutableLiveData = 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.
+ */
+ 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 delete() {
+ cancelDownload()
+ Util.delete(partialFile)
+ Util.delete(completeFile)
+ Util.delete(saveFile)
+ mediaStoreService.deleteFromMediaStore(this)
+ }
+
+ 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)
+ mediaStoreService.saveInMediaStore(this@DownloadFile)
+ } else {
+ Util.renameFile(partialFile, completeFile)
+ }
+ completeWhenDone = false
+ }
+ } catch (ex: IOException) {
+ Timber.w("Failed to rename file %s to %s", completeFile, saveFile)
+ }
+ }
+
+ override fun toString(): String {
+ return String.format("DownloadFile (%s)", song)
+ }
+
+ 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
+ }
+
+ 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(context)
+
+ // 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)
+
+ 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(musicService)
+ }
+
+ if (isPlaying) {
+ completeWhenDone = true
+ } else {
+ if (save) {
+ Util.renameFile(partialFile, saveFile)
+ mediaStoreService.saveInMediaStore(this@DownloadFile)
+ } else {
+ Util.renameFile(partialFile, completeFile)
+ }
+ }
+ } catch (x: Exception) {
+ Util.close(outputStream)
+ 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()
+ }
+ }
+
+ 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)
+ }
+ return wakeLock1
+ }
+
+ override fun toString(): String {
+ return String.format("DownloadTask (%s)", song)
+ }
+
+ 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.")
+ }
+ }
+
+ @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("Failed to set last-modified date on %s", file)
+ }
+ }
+ }
+ }
+}
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt
index 87df94f7..5dc20678 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt
@@ -1,3 +1,10 @@
+/*
+ * LocalMediaPlayer.kt
+ * Copyright (C) 2009-2021 Ultrasonic developers
+ *
+ * Distributed under terms of the GNU GPLv3 license.
+ */
+
package org.moire.ultrasonic.service
import android.app.PendingIntent
@@ -755,7 +762,7 @@ class LocalMediaPlayer(
}
// 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)
// Find out how large the file should grow before resuming playback.
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
index 8f380d9e..e724065b 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt
@@ -59,7 +59,6 @@ import org.moire.ultrasonic.domain.toDomainEntitiesList
import org.moire.ultrasonic.domain.toDomainEntity
import org.moire.ultrasonic.domain.toDomainEntityList
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
-import org.moire.ultrasonic.util.CancellableTask
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util
import timber.log.Timber
@@ -611,11 +610,9 @@ open class RESTMusicService(
@Throws(Exception::class)
override fun getDownloadInputStream(
- context: Context,
song: MusicDirectory.Entry,
offset: Long,
- maxBitrate: Int,
- task: CancellableTask
+ maxBitrate: Int
): Pair {
val songOffset = if (offset < 0) 0 else offset
diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt
index 48441f58..4ef7a6c8 100644
--- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt
+++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt
@@ -225,59 +225,7 @@ class SongView(context: Context) : UpdateView(context), Checkable {
downloadFile = mediaPlayerControllerLazy.value.getDownloadFileForSong(entry)
- val partialFile = downloadFile!!.partialFile
-
- if (downloadFile!!.isWorkDone) {
- val newLeftImageType =
- if (downloadFile!!.isSaved) ImageType.Pin else ImageType.Downloaded
-
- if (leftImageType != newLeftImageType) {
- leftImage = if (downloadFile!!.isSaved) pinImage else downloadedImage
- leftImageType = newLeftImageType
- }
- } else {
- leftImageType = ImageType.None
- leftImage = null
- }
-
- val rightImageType: ImageType
- val rightImage: Drawable?
-
- if (
- downloadFile!!.isDownloading &&
- !downloadFile!!.isDownloadCancelled &&
- partialFile.exists()
- ) {
- viewHolder?.status?.text = Util.formatLocalizedBytes(
- partialFile.length(), this.context
- )
-
- rightImageType = ImageType.Downloading
- rightImage = downloadingImage
- } else {
- rightImageType = ImageType.None
- rightImage = null
-
- val statusText = viewHolder?.status?.text
- if (!statusText.isNullOrEmpty()) viewHolder?.status?.text = null
- }
- if (previousLeftImageType != leftImageType || previousRightImageType != rightImageType) {
- previousLeftImageType = leftImageType
- previousRightImageType = rightImageType
-
- if (viewHolder?.status != null) {
- viewHolder?.status?.setCompoundDrawablesWithIntrinsicBounds(
- leftImage, null, rightImage, null
- )
-
- if (rightImage === downloadingImage) {
- val frameAnimation = rightImage as AnimationDrawable?
-
- frameAnimation!!.setVisible(true, true)
- frameAnimation.start()
- }
- }
- }
+ updateDownloadStatus(downloadFile!!)
if (entry?.starred != true) {
if (viewHolder?.star?.drawable !== starHollowDrawable) {
@@ -325,6 +273,56 @@ class SongView(context: Context) : UpdateView(context), Checkable {
}
}
+ private fun updateDownloadStatus(downloadFile: DownloadFile) {
+
+ if (downloadFile.isWorkDone) {
+ val newLeftImageType =
+ if (downloadFile.isSaved) ImageType.Pin else ImageType.Downloaded
+
+ if (leftImageType != newLeftImageType) {
+ leftImage = if (downloadFile.isSaved) pinImage else downloadedImage
+ leftImageType = newLeftImageType
+ }
+ } else {
+ leftImageType = ImageType.None
+ leftImage = null
+ }
+
+ val rightImageType: ImageType
+ val rightImage: Drawable?
+
+ if (downloadFile.isDownloading && !downloadFile.isDownloadCancelled) {
+ viewHolder?.status?.text = Util.formatPercentage(downloadFile.progress.value!!)
+
+ rightImageType = ImageType.Downloading
+ rightImage = downloadingImage
+ } else {
+ rightImageType = ImageType.None
+ rightImage = null
+
+ val statusText = viewHolder?.status?.text
+ if (!statusText.isNullOrEmpty()) viewHolder?.status?.text = null
+ }
+
+ if (previousLeftImageType != leftImageType || previousRightImageType != rightImageType) {
+ previousLeftImageType = leftImageType
+ previousRightImageType = rightImageType
+
+ if (viewHolder?.status != null) {
+ viewHolder?.status?.setCompoundDrawablesWithIntrinsicBounds(
+ leftImage, null, rightImage, null
+ )
+
+ if (rightImage === downloadingImage) {
+ val frameAnimation = rightImage as AnimationDrawable?
+
+ frameAnimation!!.setVisible(true, true)
+ frameAnimation.start()
+ }
+ }
+ }
+ }
+
override fun setChecked(b: Boolean) {
viewHolder?.check?.isChecked = b
}
diff --git a/ultrasonic/src/main/res/layout-land/download.xml b/ultrasonic/src/main/res/layout-land/current_playing.xml
similarity index 93%
rename from ultrasonic/src/main/res/layout-land/download.xml
rename to ultrasonic/src/main/res/layout-land/current_playing.xml
index e1219d57..2235bb65 100644
--- a/ultrasonic/src/main/res/layout-land/download.xml
+++ b/ultrasonic/src/main/res/layout-land/current_playing.xml
@@ -5,13 +5,13 @@
a:orientation="horizontal">
-
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout-port/download.xml b/ultrasonic/src/main/res/layout/current_playing.xml
similarity index 93%
rename from ultrasonic/src/main/res/layout-port/download.xml
rename to ultrasonic/src/main/res/layout/current_playing.xml
index 807b4ebb..43c5d843 100644
--- a/ultrasonic/src/main/res/layout-port/download.xml
+++ b/ultrasonic/src/main/res/layout/current_playing.xml
@@ -5,13 +5,13 @@
a:orientation="vertical" >
-
+
diff --git a/ultrasonic/src/main/res/layout/download_playlist.xml b/ultrasonic/src/main/res/layout/current_playlist.xml
similarity index 89%
rename from ultrasonic/src/main/res/layout/download_playlist.xml
rename to ultrasonic/src/main/res/layout/current_playlist.xml
index 39e709b5..1b933376 100644
--- a/ultrasonic/src/main/res/layout/download_playlist.xml
+++ b/ultrasonic/src/main/res/layout/current_playlist.xml
@@ -1,33 +1,33 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout/media_buttons.xml b/ultrasonic/src/main/res/layout/media_buttons.xml
index c1f299dd..92f02e91 100644
--- a/ultrasonic/src/main/res/layout/media_buttons.xml
+++ b/ultrasonic/src/main/res/layout/media_buttons.xml
@@ -8,7 +8,7 @@
a:layout_marginRight="12dp" >
+ a:src="?attr/media_shuffle"
+ a:contentDescription="@string/buttons.shuffle" />
+ a:src="?attr/media_previous"
+ a:contentDescription="@string/buttons.previous" />
+ tools:visibility="gone"
+ a:contentDescription="@string/buttons.play" />
+ a:src="?attr/media_pause"
+ a:contentDescription="@string/buttons.pause" />
+ tools:visibility="gone"
+ a:contentDescription="@string/buttons.stop" />
+ a:src="?attr/media_next"
+ a:contentDescription="@string/buttons.next"/>
+ a:src="?attr/media_repeat_off"
+ a:contentDescription="@string/buttons.repeat" />
\ No newline at end of file
diff --git a/ultrasonic/src/main/res/layout/player_media_info.xml b/ultrasonic/src/main/res/layout/player_media_info.xml
index 2c967948..6704b6b3 100644
--- a/ultrasonic/src/main/res/layout/player_media_info.xml
+++ b/ultrasonic/src/main/res/layout/player_media_info.xml
@@ -15,7 +15,7 @@
a:orientation="vertical">
@@ -18,7 +18,7 @@
a:layout_height="match_parent">
Chat
Ultrasonic Main
Now Playing
+ Play
+ Pause
+ Repeat
+ Shuffle
+ Stop
+ Next
+ Previous
Podcast
No podcasts channels registered
Podcast