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