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

580 lines
19 KiB
Java

/*
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
*/
package net.nullsum.audinaut.service;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.util.Log;
import net.nullsum.audinaut.domain.MusicDirectory;
import net.nullsum.audinaut.util.BufferFile;
import net.nullsum.audinaut.util.CacheCleaner;
import net.nullsum.audinaut.util.Constants;
import net.nullsum.audinaut.util.FileUtil;
import net.nullsum.audinaut.util.SilentBackgroundTask;
import net.nullsum.audinaut.util.Util;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import okhttp3.Response;
/**
* @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;
private final boolean save;
private final Long contentLength = null;
private DownloadTask downloadTask;
private boolean failedDownload = false;
private int failed = 0;
private int bitRate;
private boolean isPlaying = false;
private boolean saveWhenDone = false;
private boolean completeWhenDone = false;
private boolean rateLimit = false;
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;
}
public boolean isSong() {
return song.isSong();
}
/**
* Returns the effective bit rate.
*/
public int getBitRate() {
if (!partialFile.exists()) {
bitRate = getActualBitrate();
}
if (bitRate > 0) {
return bitRate;
}
return song.getBitRate() == null ? 160 : song.getBitRate();
}
private int getActualBitrate() {
int br = Util.getMaxBitrate(context);
if (br == 0 && song.getTranscodedSuffix() != null && "mp3".equals(song.getTranscodedSuffix().toLowerCase())) {
if (song.getBitRate() != null) {
br = Math.min(320, song.getBitRate());
} else {
br = 320;
}
} else if (song.getSuffix() != null && (song.getTranscodedSuffix() == null || song.getSuffix().equals(song.getTranscodedSuffix()))) {
// If just downsampling, don't try to upsample (ie: 128 kpbs -> 192 kpbs)
if (song.getBitRate() != null && (br == 0 || br > song.getBitRate())) {
br = song.getBitRate();
}
}
return br;
}
public Long getContentLength() {
return contentLength;
}
@Override
public long getEstimatedSize() {
if (contentLength != null) {
return contentLength;
}
File file = getCompleteFile();
if (file.exists()) {
return file.length();
} else if (song.getDuration() == null) {
return 0;
} else {
int br = (getBitRate() * 1000) / 8;
int duration = song.getDuration();
return br * duration;
}
}
public synchronized void download() {
rateLimit = false;
preDownload();
downloadTask.execute();
}
private void preDownload() {
FileUtil.createDirectoryForParent(saveFile);
failedDownload = false;
if (!partialFile.exists()) {
bitRate = getActualBitrate();
}
downloadTask = new DownloadTask(context);
}
public synchronized void cancelDownload() {
if (downloadTask != null) {
downloadTask.cancel();
}
}
@Override
public File getFile() {
if (saveFile.exists()) {
return saveFile;
} else if (completeFile.exists()) {
return completeFile;
} else {
return partialFile;
}
}
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();
}
@Override
public synchronized boolean isWorkDone() {
return saveFile.exists() || (completeFile.exists() && !save) || saveWhenDone || completeWhenDone;
}
@Override
public void onStart() {
setPlaying(true);
}
@Override
public void onStop() {
setPlaying(false);
}
@Override
public synchronized void onResume() {
if (!isWorkDone() && !isFailedMax() && !isDownloading() && !isDownloadCancelled()) {
download();
}
}
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 failedDownload;
}
public boolean isFailedMax() {
return failed > MAX_FAILURES;
}
public void delete() {
cancelDownload();
// Remove from mediaStore BEFORE deleting file since it calls getCompleteFile
deleteFromStore();
// Delete all possible versions of the file
File parent = partialFile.getParentFile();
Util.delete(partialFile);
Util.delete(completeFile);
Util.delete(saveFile);
FileUtil.deleteEmptyDir(parent);
}
public void unpin() {
if (saveFile.exists()) {
// Delete old store entry before renaming to pinned file
saveFile.renameTo(completeFile);
renameInStore(saveFile, completeFile);
}
}
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);
}
}
}
public void renamePartial() {
Util.renameFile(partialFile, completeFile);
saveToStore();
}
public void setPlaying(boolean isPlaying) {
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();
}
completeWhenDone = false;
}
this.isPlaying = isPlaying;
}
private void deleteFromStore() {
try {
mediaStoreService.deleteFromMediaStore(this);
} catch (Exception e) {
Log.w(TAG, "Failed to remove from store", e);
}
}
private void saveToStore() {
if (!Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_HIDE_MEDIA, false)) {
try {
mediaStoreService.saveInMediaStore(this);
} catch (Exception e) {
Log.w(TAG, "Failed to save in media store", e);
}
}
}
private void renameInStore(File start, File end) {
try {
mediaStoreService.renameInMediaStore(start, end);
} catch (Exception e) {
Log.w(TAG, "Failed to rename in store", e);
}
}
@Override
public String toString() {
return "DownloadFile (" + song + ")";
}
// 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;
}
DownloadFile downloadFile = (DownloadFile) o;
return Util.equals(this.getSong(), downloadFile.getSong());
}*/
private class DownloadTask extends SilentBackgroundTask<Void> {
private MusicService musicService;
public DownloadTask(Context context) {
super(context);
}
@Override
public Void doInBackground() throws InterruptedException {
InputStream in = null;
FileOutputStream out = null;
PowerManager.WakeLock wakeLock = null;
WifiManager.WifiLock wifiLock = null;
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();
}
wifiLock = Util.createWifiLock(context, toString());
wifiLock.acquire();
if (saveFile.exists()) {
Log.i(TAG, saveFile + " already exists. Skipping.");
checkDownloads();
return null;
}
if (completeFile.exists()) {
if (save) {
if (isPlaying) {
saveWhenDone = true;
} else {
Util.renameFile(completeFile, saveFile);
renameInStore(completeFile, saveFile);
}
} else {
Log.i(TAG, completeFile + " already exists. Skipping.");
}
checkDownloads();
return null;
}
if (musicService == null) {
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();
} catch (Exception e) {
compare = true;
}
if (compare) {
// Attempt partial HTTP GET, appending to the file if it exists.
Response response = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this);
if (response.header("Content-Length") != null) {
Log.i(TAG, "Content Length: " + contentLength);
}
boolean partial = response.code() == 206;
if (partial) {
Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes");
}
in = response.body().byteStream();
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");
} else if (partialFile.length() == 0) {
throw new Exception("Download of '" + song + "' failed. File is 0 bytes long.");
}
downloadAndSaveCoverArt(musicService);
}
if (isPlaying) {
completeWhenDone = true;
} else {
if (save) {
Util.renameFile(partialFile, saveFile);
} else {
Util.renameFile(partialFile, completeFile);
}
DownloadFile.this.saveToStore();
}
} catch (InterruptedException x) {
throw x;
} catch (FileNotFoundException x) {
Util.delete(completeFile);
Util.delete(saveFile);
if (!isCancelled()) {
failed = MAX_FAILURES + 1;
failedDownload = true;
Log.w(TAG, "Failed to download '" + song + "'.", x);
}
} catch (IOException x) {
Util.delete(completeFile);
Util.delete(saveFile);
if (!isCancelled()) {
failedDownload = true;
Log.w(TAG, "Failed to download '" + song + "'.", x);
}
} catch (Exception x) {
Util.delete(completeFile);
Util.delete(saveFile);
if (!isCancelled()) {
failed++;
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);
}
if (wifiLock != null) {
wifiLock.release();
}
}
// Only run these if not interrupted, ie: cancelled
DownloadService downloadService = DownloadService.getInstance();
if (downloadService != null && !isCancelled()) {
new CacheCleaner(context, downloadService).cleanSpace();
checkDownloads();
}
return null;
}
private void checkDownloads() {
DownloadService downloadService = DownloadService.getInstance();
if (downloadService != null) {
downloadService.checkDownloads();
}
}
@Override
public String toString() {
return "DownloadTask (" + song + ")";
}
private void downloadAndSaveCoverArt(MusicService musicService) {
try {
if (song.getCoverArt() != null) {
// Check if album art already exists, don't want to needlessly load into memory
File albumArtFile = FileUtil.getAlbumArtFile(context, song);
if (!albumArtFile.exists()) {
musicService.getCoverArt(context, song, 0, null, null);
}
}
} 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();
long lastCount = 0;
boolean activeLimit = rateLimit;
while (!isCancelled() && (n = in.read(buffer)) != -1) {
out.write(buffer, 0, n);
count += n;
lastCount += n;
long now = System.currentTimeMillis();
if (now - lastLog > 3000L) { // Only every so often.
Log.i(TAG, "Downloaded " + Util.formatBytes(count) + " of " + song);
lastLog = now;
lastCount = 0;
// Re-establish every few seconds whether screen is on or not
if (rateLimit) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
activeLimit = pm.isScreenOn();
}
}
// If screen is on and rateLimit is true, stop downloading from exhausting bandwidth
if (activeLimit) {
Thread.sleep(10L);
}
}
return count;
}
}
}