Audinaut-subsonic-app-android/app/src/main/java/net/nullsum/audinaut/service/DownloadFile.java

580 lines
19 KiB
Java
Raw Normal View History

2016-12-18 18:41:30 +01: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
*/
2017-01-09 08:01:12 +01:00
package net.nullsum.audinaut.service;
2016-12-18 18:41:30 +01:00
import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.util.Log;
2017-01-09 08:01:12 +01:00
import net.nullsum.audinaut.domain.MusicDirectory;
2017-02-27 05:36:43 +01:00
import net.nullsum.audinaut.util.BufferFile;
import net.nullsum.audinaut.util.CacheCleaner;
2017-01-09 08:01:12 +01:00
import net.nullsum.audinaut.util.Constants;
import net.nullsum.audinaut.util.FileUtil;
2017-02-27 05:36:43 +01:00
import net.nullsum.audinaut.util.SilentBackgroundTask;
2017-01-09 08:01:12 +01:00
import net.nullsum.audinaut.util.Util;
2016-12-18 18:41:30 +01:00
2018-03-25 03:28:28 +02:00
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
2017-03-12 19:40:13 +01:00
import okhttp3.Response;
2016-12-18 18:41:30 +01:00
/**
* @author Sindre Mehus
* @version $Id$
*/
public class DownloadFile implements BufferFile {
private static final String TAG = DownloadFile.class.getSimpleName();
private static final int MAX_FAILURES = 5;
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;
2018-03-25 03:28:28 +02:00
private final boolean save;
private final Long contentLength = null;
2016-12-18 18:41:30 +01:00
private DownloadTask downloadTask;
2017-03-12 07:58:17 +01:00
private boolean failedDownload = false;
2016-12-18 18:41:30 +01:00
private int failed = 0;
private int bitRate;
2017-03-12 07:58:17 +01:00
private boolean isPlaying = false;
private boolean saveWhenDone = false;
private boolean completeWhenDone = false;
private boolean rateLimit = false;
2016-12-18 18:41:30 +01:00
public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) {
this.context = context;
this.song = song;
this.save = save;
saveFile = FileUtil.getSongFile(context, song);
bitRate = getActualBitrate();
partialFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
".partial." + FileUtil.getExtension(saveFile.getName()));
completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
".complete." + FileUtil.getExtension(saveFile.getName()));
mediaStoreService = new MediaStoreService(context);
}
public MusicDirectory.Entry getSong() {
return song;
}
2018-03-25 03:28:28 +02:00
2017-03-12 07:58:17 +01:00
public boolean isSong() {
return song.isSong();
}
2016-12-18 18:41:30 +01:00
/**
* Returns the effective bit rate.
*/
public int getBitRate() {
2018-03-25 03:28:28 +02:00
if (!partialFile.exists()) {
2017-03-12 07:58:17 +01:00
bitRate = getActualBitrate();
}
2016-12-18 18:41:30 +01:00
if (bitRate > 0) {
return bitRate;
}
return song.getBitRate() == null ? 160 : song.getBitRate();
}
2018-03-25 03:28:28 +02:00
2017-03-12 07:58:17 +01:00
private int getActualBitrate() {
int br = Util.getMaxBitrate(context);
2018-03-25 03:28:28 +02:00
if (br == 0 && song.getTranscodedSuffix() != null && "mp3".equals(song.getTranscodedSuffix().toLowerCase())) {
if (song.getBitRate() != null) {
2017-03-12 07:58:17 +01:00
br = Math.min(320, song.getBitRate());
} else {
br = 320;
}
2018-03-25 03:28:28 +02:00
} else if (song.getSuffix() != null && (song.getTranscodedSuffix() == null || song.getSuffix().equals(song.getTranscodedSuffix()))) {
2017-03-12 07:58:17 +01:00
// If just downsampling, don't try to upsample (ie: 128 kpbs -> 192 kpbs)
2018-03-25 03:28:28 +02:00
if (song.getBitRate() != null && (br == 0 || br > song.getBitRate())) {
2017-03-12 07:58:17 +01:00
br = song.getBitRate();
}
}
return br;
}
public Long getContentLength() {
return contentLength;
}
@Override
public long getEstimatedSize() {
2018-03-25 03:28:28 +02:00
if (contentLength != null) {
2017-03-12 07:58:17 +01:00
return contentLength;
}
File file = getCompleteFile();
2018-03-25 03:28:28 +02:00
if (file.exists()) {
2017-03-12 07:58:17 +01:00
return file.length();
2018-03-25 03:28:28 +02:00
} else if (song.getDuration() == null) {
2017-03-12 07:58:17 +01:00
return 0;
} else {
int br = (getBitRate() * 1000) / 8;
int duration = song.getDuration();
return br * duration;
}
}
2016-12-18 18:41:30 +01:00
public synchronized void download() {
2017-03-12 07:58:17 +01:00
rateLimit = false;
2016-12-18 18:41:30 +01:00
preDownload();
downloadTask.execute();
}
2018-03-25 03:28:28 +02:00
2016-12-18 18:41:30 +01:00
private void preDownload() {
2017-03-12 07:58:17 +01:00
FileUtil.createDirectoryForParent(saveFile);
2016-12-18 18:41:30 +01:00
failedDownload = false;
2018-03-25 03:28:28 +02:00
if (!partialFile.exists()) {
2017-03-12 07:58:17 +01:00
bitRate = getActualBitrate();
}
downloadTask = new DownloadTask(context);
2016-12-18 18:41:30 +01:00
}
public synchronized void cancelDownload() {
if (downloadTask != null) {
downloadTask.cancel();
}
}
2017-03-12 07:58:17 +01:00
@Override
public File getFile() {
if (saveFile.exists()) {
return saveFile;
} else if (completeFile.exists()) {
return completeFile;
} else {
return partialFile;
}
}
2016-12-18 18:41:30 +01:00
public File getCompleteFile() {
if (saveFile.exists()) {
return saveFile;
}
if (completeFile.exists()) {
return completeFile;
}
return saveFile;
}
public File getPartialFile() {
return partialFile;
}
public boolean isSaved() {
return saveFile.exists();
}
public synchronized boolean isCompleteFileAvailable() {
return saveFile.exists() || completeFile.exists();
}
2017-03-12 07:58:17 +01:00
@Override
2016-12-18 18:41:30 +01:00
public synchronized boolean isWorkDone() {
return saveFile.exists() || (completeFile.exists() && !save) || saveWhenDone || completeWhenDone;
}
2017-03-12 07:58:17 +01:00
@Override
public void onStart() {
setPlaying(true);
}
2016-12-18 18:41:30 +01:00
2017-03-12 07:58:17 +01:00
@Override
public void onStop() {
setPlaying(false);
}
2016-12-18 18:41:30 +01:00
2017-03-12 07:58:17 +01:00
@Override
public synchronized void onResume() {
2018-03-25 03:28:28 +02:00
if (!isWorkDone() && !isFailedMax() && !isDownloading() && !isDownloadCancelled()) {
2017-03-12 07:58:17 +01:00
download();
}
}
2016-12-18 18:41:30 +01:00
2017-03-12 07:58:17 +01:00
public synchronized boolean isDownloading() {
2016-12-18 18:41:30 +01:00
return downloadTask != null && downloadTask.isRunning();
}
public synchronized boolean isDownloadCancelled() {
return downloadTask != null && downloadTask.isCancelled();
}
public boolean shouldSave() {
return save;
}
public boolean isFailed() {
return failedDownload;
}
2018-03-25 03:28:28 +02:00
2016-12-18 18:41:30 +01:00
public boolean isFailedMax() {
2017-03-12 07:58:17 +01:00
return failed > MAX_FAILURES;
2016-12-18 18:41:30 +01:00
}
public void delete() {
cancelDownload();
2017-03-12 07:58:17 +01:00
2016-12-18 18:41:30 +01:00
// Remove from mediaStore BEFORE deleting file since it calls getCompleteFile
2017-03-12 07:58:17 +01:00
deleteFromStore();
// Delete all possible versions of the file
File parent = partialFile.getParentFile();
2016-12-18 18:41:30 +01:00
Util.delete(partialFile);
Util.delete(completeFile);
Util.delete(saveFile);
2017-03-12 07:58:17 +01:00
FileUtil.deleteEmptyDir(parent);
2016-12-18 18:41:30 +01:00
}
public void unpin() {
if (saveFile.exists()) {
2017-03-12 07:58:17 +01:00
// Delete old store entry before renaming to pinned file
2016-12-18 18:41:30 +01:00
saveFile.renameTo(completeFile);
2017-03-12 07:58:17 +01:00
renameInStore(saveFile, completeFile);
2016-12-18 18:41:30 +01:00
}
}
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 void updateModificationDate(File file) {
if (file.exists()) {
boolean ok = file.setLastModified(System.currentTimeMillis());
if (!ok) {
Log.w(TAG, "Failed to set last-modified date on " + file);
}
}
}
2017-03-12 07:58:17 +01:00
2018-03-25 03:28:28 +02:00
public void renamePartial() {
Util.renameFile(partialFile, completeFile);
saveToStore();
}
2017-03-12 07:58:17 +01:00
public void setPlaying(boolean isPlaying) {
2018-03-25 03:28:28 +02:00
if (saveWhenDone && !isPlaying) {
Util.renameFile(completeFile, saveFile);
renameInStore(completeFile, saveFile);
saveWhenDone = false;
} else if (completeWhenDone && !isPlaying) {
if (save) {
Util.renameFile(partialFile, saveFile);
saveToStore();
} else {
Util.renameFile(partialFile, completeFile);
saveToStore();
2017-03-12 07:58:17 +01:00
}
2018-03-25 03:28:28 +02:00
completeWhenDone = false;
2017-03-12 07:58:17 +01:00
}
this.isPlaying = isPlaying;
}
private void deleteFromStore() {
try {
mediaStoreService.deleteFromMediaStore(this);
2018-03-25 03:28:28 +02:00
} catch (Exception e) {
2017-03-12 07:58:17 +01:00
Log.w(TAG, "Failed to remove from store", e);
}
}
2018-03-25 03:28:28 +02:00
2017-03-12 07:58:17 +01:00
private void saveToStore() {
2018-03-25 03:28:28 +02:00
if (!Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_HIDE_MEDIA, false)) {
2017-03-12 07:58:17 +01:00
try {
mediaStoreService.saveInMediaStore(this);
2018-03-25 03:28:28 +02:00
} catch (Exception e) {
2017-03-12 07:58:17 +01:00
Log.w(TAG, "Failed to save in media store", e);
}
}
}
2018-03-25 03:28:28 +02:00
2017-03-12 07:58:17 +01:00
private void renameInStore(File start, File end) {
try {
mediaStoreService.renameInMediaStore(start, end);
2018-03-25 03:28:28 +02:00
} catch (Exception e) {
2017-03-12 07:58:17 +01:00
Log.w(TAG, "Failed to rename in store", e);
}
}
2016-12-18 18:41:30 +01:00
@Override
public String toString() {
return "DownloadFile (" + song + ")";
}
2017-03-12 07:58:17 +01:00
// Don't do this. Causes infinite loop if two instances of same song
/*@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
2016-12-18 18:41:30 +01:00
2017-03-12 07:58:17 +01:00
DownloadFile downloadFile = (DownloadFile) o;
return Util.equals(this.getSong(), downloadFile.getSong());
}*/
2016-12-18 18:41:30 +01:00
private class DownloadTask extends SilentBackgroundTask<Void> {
2017-03-12 07:58:17 +01:00
private MusicService musicService;
2016-12-18 18:41:30 +01:00
2017-03-12 07:58:17 +01:00
public DownloadTask(Context context) {
super(context);
}
2016-12-18 18:41:30 +01:00
@Override
public Void doInBackground() throws InterruptedException {
InputStream in = null;
FileOutputStream out = null;
PowerManager.WakeLock wakeLock = null;
2017-03-12 07:58:17 +01:00
WifiManager.WifiLock wifiLock = null;
2016-12-18 18:41:30 +01:00
try {
if (Util.isScreenLitOnDownload(context)) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, toString());
wakeLock.acquire();
}
2017-03-12 07:58:17 +01:00
wifiLock = Util.createWifiLock(context, toString());
wifiLock.acquire();
2016-12-18 18:41:30 +01:00
if (saveFile.exists()) {
Log.i(TAG, saveFile + " already exists. Skipping.");
checkDownloads();
return null;
}
if (completeFile.exists()) {
if (save) {
2018-03-25 03:28:28 +02:00
if (isPlaying) {
2017-03-12 07:58:17 +01:00
saveWhenDone = true;
} else {
Util.renameFile(completeFile, saveFile);
renameInStore(completeFile, saveFile);
}
2016-12-18 18:41:30 +01:00
} else {
Log.i(TAG, completeFile + " already exists. Skipping.");
}
checkDownloads();
return null;
}
2018-03-25 03:28:28 +02:00
if (musicService == null) {
2017-03-12 07:58:17 +01:00
musicService = MusicServiceFactory.getMusicService(context);
}
// Some devices seem to throw error on partial file which doesn't exist
boolean compare;
try {
compare = (bitRate == 0) || (song.getDuration() == 0) || (partialFile.length() == 0) || (bitRate * song.getDuration() * 1000 / 8) > partialFile.length();
2018-03-25 03:28:28 +02:00
} catch (Exception e) {
2017-03-12 07:58:17 +01:00
compare = true;
}
2018-03-25 03:28:28 +02:00
if (compare) {
2017-03-12 07:58:17 +01:00
// Attempt partial HTTP GET, appending to the file if it exists.
2017-03-12 19:40:13 +01:00
Response response = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this);
2018-03-25 03:28:28 +02:00
if (response.header("Content-Length") != null) {
2017-03-12 19:40:13 +01:00
Log.i(TAG, "Content Length: " + contentLength);
2017-03-12 07:58:17 +01:00
}
2017-03-12 19:40:13 +01:00
boolean partial = response.code() == 206;
2017-03-12 07:58:17 +01:00
if (partial) {
Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes");
}
2017-03-12 19:40:13 +01:00
in = response.body().byteStream();
2017-03-12 07:58:17 +01:00
out = new FileOutputStream(partialFile, partial);
long n = copy(in, out);
Log.i(TAG, "Downloaded " + n + " bytes to " + partialFile);
out.flush();
out.close();
if (isCancelled()) {
throw new Exception("Download of '" + song + "' was cancelled");
2018-03-25 03:28:28 +02:00
} else if (partialFile.length() == 0) {
2017-03-12 07:58:17 +01:00
throw new Exception("Download of '" + song + "' failed. File is 0 bytes long.");
}
downloadAndSaveCoverArt(musicService);
}
2018-03-25 03:28:28 +02:00
if (isPlaying) {
2017-03-12 07:58:17 +01:00
completeWhenDone = true;
} else {
2018-03-25 03:28:28 +02:00
if (save) {
2017-03-12 07:58:17 +01:00
Util.renameFile(partialFile, saveFile);
} else {
Util.renameFile(partialFile, completeFile);
}
DownloadFile.this.saveToStore();
}
2016-12-18 18:41:30 +01:00
2018-03-25 03:28:28 +02:00
} catch (InterruptedException x) {
2017-03-12 07:58:17 +01:00
throw x;
2018-03-25 03:28:28 +02:00
} catch (FileNotFoundException x) {
2017-03-12 07:58:17 +01:00
Util.delete(completeFile);
Util.delete(saveFile);
2018-03-25 03:28:28 +02:00
if (!isCancelled()) {
2017-03-12 07:58:17 +01:00
failed = MAX_FAILURES + 1;
failedDownload = true;
Log.w(TAG, "Failed to download '" + song + "'.", x);
}
2018-03-25 03:28:28 +02:00
} catch (IOException x) {
2017-03-12 07:58:17 +01:00
Util.delete(completeFile);
Util.delete(saveFile);
2018-03-25 03:28:28 +02:00
if (!isCancelled()) {
2017-03-12 07:58:17 +01:00
failedDownload = true;
Log.w(TAG, "Failed to download '" + song + "'.", x);
}
} catch (Exception x) {
2016-12-18 18:41:30 +01:00
Util.delete(completeFile);
Util.delete(saveFile);
if (!isCancelled()) {
2017-03-12 07:58:17 +01:00
failed++;
2016-12-18 18:41:30 +01:00
failedDownload = true;
Log.w(TAG, "Failed to download '" + song + "'.", x);
}
} finally {
Util.close(in);
Util.close(out);
if (wakeLock != null) {
wakeLock.release();
Log.i(TAG, "Released wake lock " + wakeLock);
}
2017-03-12 07:58:17 +01:00
if (wifiLock != null) {
wifiLock.release();
}
}
// Only run these if not interrupted, ie: cancelled
DownloadService downloadService = DownloadService.getInstance();
2018-03-25 03:28:28 +02:00
if (downloadService != null && !isCancelled()) {
2017-03-12 07:58:17 +01:00
new CacheCleaner(context, downloadService).cleanSpace();
checkDownloads();
}
return null;
2016-12-18 18:41:30 +01:00
}
2017-03-12 07:58:17 +01:00
2016-12-18 18:41:30 +01:00
private void checkDownloads() {
2017-03-12 07:58:17 +01:00
DownloadService downloadService = DownloadService.getInstance();
2018-03-25 03:28:28 +02:00
if (downloadService != null) {
2017-03-12 07:58:17 +01:00
downloadService.checkDownloads();
}
2016-12-18 18:41:30 +01:00
}
@Override
public String toString() {
return "DownloadTask (" + song + ")";
}
2018-03-25 03:28:28 +02:00
private void downloadAndSaveCoverArt(MusicService musicService) {
2016-12-18 18:41:30 +01:00
try {
if (song.getCoverArt() != null) {
2017-03-12 07:58:17 +01:00
// Check if album art already exists, don't want to needlessly load into memory
File albumArtFile = FileUtil.getAlbumArtFile(context, song);
2018-03-25 03:28:28 +02:00
if (!albumArtFile.exists()) {
2017-03-12 07:58:17 +01:00
musicService.getCoverArt(context, song, 0, null, null);
}
2016-12-18 18:41:30 +01:00
}
} catch (Exception x) {
Log.e(TAG, "Failed to get cover art.", x);
}
}
private long copy(final InputStream in, OutputStream out) throws IOException, InterruptedException {
// Start a thread that will close the input stream if the task is
// cancelled, thus causing the copy() method to return.
new Thread("DownloadFile_copy") {
@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();
2017-03-12 07:58:17 +01:00
long lastCount = 0;
2016-12-18 18:41:30 +01:00
2017-03-12 07:58:17 +01:00
boolean activeLimit = rateLimit;
2016-12-18 18:41:30 +01:00
while (!isCancelled() && (n = in.read(buffer)) != -1) {
out.write(buffer, 0, n);
count += n;
2017-03-12 07:58:17 +01:00
lastCount += n;
2016-12-18 18:41:30 +01:00
long now = System.currentTimeMillis();
if (now - lastLog > 3000L) { // Only every so often.
Log.i(TAG, "Downloaded " + Util.formatBytes(count) + " of " + song);
lastLog = now;
2017-03-12 07:58:17 +01:00
lastCount = 0;
// Re-establish every few seconds whether screen is on or not
2018-03-25 03:28:28 +02:00
if (rateLimit) {
2017-03-12 07:58:17 +01:00
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
2018-03-25 03:28:28 +02:00
activeLimit = pm.isScreenOn();
2017-03-12 07:58:17 +01:00
}
2016-12-18 18:41:30 +01:00
}
2017-03-12 07:58:17 +01:00
2016-12-18 18:41:30 +01:00
// If screen is on and rateLimit is true, stop downloading from exhausting bandwidth
2018-03-25 03:28:28 +02:00
if (activeLimit) {
2017-03-12 07:58:17 +01:00
Thread.sleep(10L);
2016-12-18 18:41:30 +01:00
}
}
return count;
}
}
}