/*
* Copyright (C) 2015 Actor LLC. <https://actor.im>
*/
package im.actor.core.modules.file;
import im.actor.core.entity.FileReference;
import im.actor.core.modules.ModuleContext;
import im.actor.core.modules.ModuleActor;
import im.actor.runtime.HTTP;
import im.actor.runtime.Log;
import im.actor.runtime.Storage;
import im.actor.runtime.actors.ActorRef;
import im.actor.runtime.actors.ActorCancellable;
import im.actor.runtime.files.FileSystemReference;
import im.actor.runtime.files.OutputFile;
import im.actor.runtime.http.HTTPError;
public class DownloadTask extends ModuleActor {
private static final int SIM_BLOCKS_COUNT = 4;
private static final int NOTIFY_THROTTLE = 1000;
private static final int DEFAULT_RETRY = 15;
private final String TAG;
private final boolean LOG;
private FileReference fileReference;
private ActorRef manager;
private FileSystemReference destReference;
private OutputFile outputFile;
private boolean isCompleted;
private long lastNotifyDate;
private float currentProgress;
private ActorCancellable notifyCancellable;
private String fileUrl;
private int blockSize = 128 * 1024;
private int blocksCount;
private int nextBlock = 0;
private int currentDownloads = 0;
private int downloaded = 0;
public DownloadTask(FileReference fileReference, ActorRef manager, ModuleContext context) {
super(context);
this.TAG = "DownloadTask{" + fileReference.getFileId() + "}";
this.LOG = context.getConfiguration().isEnableFilesLogging();
this.fileReference = fileReference;
this.manager = manager;
}
@Override
public void preStart() {
if (LOG) {
Log.d(TAG, "Creating file...");
}
destReference = Storage.createTempFile();
if (destReference == null) {
reportError();
if (LOG) {
Log.d(TAG, "Unable to create reference");
}
return;
}
destReference.openWrite(fileReference.getFileSize()).then(r -> {
outputFile = r;
requestUrl();
}).failure(e -> {
reportError();
if (LOG) {
Log.d(TAG, "Unable to write wile");
}
});
}
@Override
public void onReceive(Object message) {
if (message instanceof Retry) {
Retry retry = (Retry) message;
retryPart(retry.getBlockIndex(), retry.getFileOffset(), retry.getAttempt());
} else {
super.onReceive(message);
}
}
private void requestUrl() {
if (LOG) {
Log.d(TAG, "Loading url...");
}
context().getFilesModule().getFileUrlInt().askForUrl(fileReference.getFileId(), fileReference.getAccessHash()).then(url -> {
fileUrl = url;
if (LOG) {
Log.d(TAG, "Loaded file url: " + fileUrl);
}
startDownload();
}).failure(e -> {
if (LOG) {
Log.d(TAG, "Unable to load file url");
}
reportError();
});
}
private void startDownload() {
blocksCount = fileReference.getFileSize() / blockSize;
if (fileReference.getFileSize() % blockSize != 0) {
blocksCount++;
}
if (LOG) {
Log.d(TAG, "Starting downloading " + blocksCount + " blocks");
}
checkQueue();
}
private void completeDownload() {
if (isCompleted) {
return;
}
if (LOG) {
Log.d(TAG, "Closing file...");
}
if (!outputFile.close()) {
reportError();
return;
}
FileSystemReference reference = Storage.commitTempFile(destReference, fileReference.getFileId(),
fileReference.getFileName());
if (reference == null) {
reportError();
return;
}
if (LOG) {
Log.d(TAG, "Complete download {" + reference.getDescriptor() + "}");
}
reportComplete(reference);
}
private void checkQueue() {
if (isCompleted) {
return;
}
if (LOG) {
Log.d(TAG, "checkQueue " + currentDownloads + "/" + nextBlock);
}
if (currentDownloads == 0 && nextBlock >= blocksCount) {
completeDownload();
} else if (currentDownloads < SIM_BLOCKS_COUNT && nextBlock < blocksCount) {
currentDownloads++;
int blockIndex = nextBlock++;
int offset = blockIndex * blockSize;
if (LOG) {
Log.d(TAG, "Starting part #" + blockIndex + " download");
}
downloadPart(blockIndex, offset, 0);
checkQueue();
} else {
if (LOG) {
Log.d(TAG, "Task queue is full");
}
}
}
private void retryPart(int blockIndex, int fileOffset, int attempt) {
if (isCompleted) {
return;
}
if (LOG) {
Log.d(TAG, "Trying again part #" + blockIndex + " download");
}
downloadPart(blockIndex, fileOffset, attempt);
}
private void downloadPart(final int blockIndex, final int fileOffset, final int attempt) {
HTTP.getMethod(fileUrl, fileOffset, blockSize, fileReference.getFileSize()).then(r -> {
downloaded++;
if (LOG) {
Log.d(TAG, "Download part #" + blockIndex + " completed");
}
if (!outputFile.write(fileOffset, r.getContent(), 0, r.getContent().length)) {
reportError();
return;
}
currentDownloads--;
reportProgress(downloaded / (float) blocksCount);
checkQueue();
}).failure(e -> {
if ((e instanceof HTTPError)
&& ((((HTTPError) e).getErrorCode() >= 500
&& ((HTTPError) e).getErrorCode() < 600)
|| ((HTTPError) e).getErrorCode() == 0)) {
// Server on unknown error
int retryInSecs = DEFAULT_RETRY;
if (LOG) {
Log.w(TAG, "Download part #" + blockIndex + " failure #" + ((HTTPError) e).getErrorCode() + " trying again in " + retryInSecs + " sec, attempt #" + (attempt + 1));
}
self().send(new Retry(blockIndex, fileOffset, attempt + 1));
} else {
if (LOG) {
Log.d(TAG, "Download part #" + blockIndex + " failure");
}
reportError();
}
});
}
private void reportError() {
if (isCompleted) {
return;
}
isCompleted = true;
manager.send(new DownloadManager.OnDownloadedError(fileReference.getFileId()));
}
private void reportProgress(float progress) {
if (isCompleted) {
return;
}
if (progress > currentProgress) {
currentProgress = progress;
}
long delta = im.actor.runtime.Runtime.getActorTime() - lastNotifyDate;
if (notifyCancellable != null) {
notifyCancellable.cancel();
notifyCancellable = null;
}
if (delta > NOTIFY_THROTTLE) {
lastNotifyDate = im.actor.runtime.Runtime.getActorTime();
performReportProgress();
} else {
notifyCancellable = schedule((Runnable) () -> performReportProgress(), delta);
}
}
private void performReportProgress() {
if (isCompleted) {
return;
}
manager.send(new DownloadManager.OnDownloadProgress(fileReference.getFileId(), currentProgress));
}
private void reportComplete(FileSystemReference reference) {
if (isCompleted) {
return;
}
isCompleted = true;
manager.send(new DownloadManager.OnDownloaded(fileReference.getFileId(), reference));
}
private class Retry {
private int blockIndex;
private int fileOffset;
private int attempt;
public Retry(int blockIndex, int fileOffset, int attempt) {
this.blockIndex = blockIndex;
this.fileOffset = fileOffset;
this.attempt = attempt;
}
public int getBlockIndex() {
return blockIndex;
}
public int getFileOffset() {
return fileOffset;
}
public int getAttempt() {
return attempt;
}
}
}