/*
* Copyright (C) 2015 Actor LLC. <https://actor.im>
*/
package im.actor.core.modules.file;
import java.util.ArrayList;
import java.util.HashMap;
import im.actor.core.entity.FileReference;
import im.actor.core.modules.ModuleContext;
import im.actor.core.modules.file.entity.Downloaded;
import im.actor.core.modules.ModuleActor;
import im.actor.core.util.RandomUtils;
import im.actor.core.viewmodel.FileCallback;
import im.actor.core.viewmodel.FileEventCallback;
import im.actor.runtime.*;
import im.actor.runtime.actors.ActorRef;
import im.actor.runtime.actors.Props;
import im.actor.runtime.actors.messages.PoisonPill;
import im.actor.runtime.files.FileSystemReference;
import im.actor.runtime.storage.KeyValueEngine;
import im.actor.runtime.threading.WeakReferenceCompat;
public class DownloadManager extends ModuleActor {
private static final String TAG = "DownloadManager";
private static final int SIM_MAX_DOWNLOADS = 2;
private boolean LOG;
private KeyValueEngine<Downloaded> downloaded;
private ArrayList<QueueItem> queue = new ArrayList<>();
private HashMap<Long, ArrayList<FileCallback>> callbacks = new HashMap<>();
private ArrayList<WeakCallbackHolder> globalCallbacks = new ArrayList<>();
public DownloadManager(ModuleContext context) {
super(context);
}
@Override
public void preStart() {
super.preStart();
downloaded = context().getFilesModule().getDownloadedEngine();
LOG = config().isEnableFilesLogging();
}
// Tasks
public void requestState(long fileId, final FileCallback callback) {
if (LOG) {
Log.d(TAG, "Requesting state file #" + fileId);
}
Downloaded downloaded1 = downloaded.getValue(fileId);
if (downloaded1 != null) {
FileSystemReference reference = Storage.fileFromDescriptor(downloaded1.getDescriptor());
boolean isExist = reference.isExist();
int fileSize = reference.getSize();
if (isExist && fileSize == downloaded1.getFileSize()) {
if (LOG) {
Log.d(TAG, "- Downloaded");
}
final FileSystemReference fileSystemReference = Storage.fileFromDescriptor(downloaded1.getDescriptor());
im.actor.runtime.Runtime.dispatch(() -> callback.onDownloaded(fileSystemReference));
return;
} else {
if (LOG) {
Log.d(TAG, "- File is corrupted");
if (!isExist) {
Log.d(TAG, "- File not found");
}
if (fileSize != downloaded1.getFileSize()) {
Log.d(TAG, "- Incorrect file size. Expected: " + downloaded1.getFileSize() + ", got: " + fileSize);
}
}
downloaded.removeItem(downloaded1.getFileId());
}
}
final QueueItem queueItem = findItem(fileId);
if (queueItem == null) {
im.actor.runtime.Runtime.dispatch(() -> callback.onNotDownloaded());
} else {
if (queueItem.isStarted) {
final float progress = queueItem.progress;
im.actor.runtime.Runtime.dispatch(() -> callback.onDownloading(progress));
} else if (queueItem.isStopped) {
im.actor.runtime.Runtime.dispatch(() -> callback.onNotDownloaded());
} else {
im.actor.runtime.Runtime.dispatch(() -> callback.onDownloading(0));
}
}
}
public void bindDownload(final FileReference fileReference, boolean autoStart, final FileCallback callback) {
if (LOG) {
Log.d(TAG, "Binding file #" + fileReference.getFileId());
}
Downloaded downloaded1 = downloaded.getValue(fileReference.getFileId());
if (downloaded1 != null) {
FileSystemReference reference = Storage.fileFromDescriptor(downloaded1.getDescriptor());
boolean isExist = reference.isExist();
int fileSize = reference.getSize();
if (isExist && fileSize == downloaded1.getFileSize()) {
if (LOG) {
Log.d(TAG, "- Downloaded");
}
final FileSystemReference fileSystemReference = Storage.fileFromDescriptor(downloaded1.getDescriptor());
im.actor.runtime.Runtime.dispatch(() -> callback.onDownloaded(fileSystemReference));
return;
} else {
if (LOG) {
Log.d(TAG, "- File is corrupted");
if (!isExist) {
Log.d(TAG, "- File not found");
}
if (fileSize != downloaded1.getFileSize()) {
Log.d(TAG, "- Incorrect file size. Expected: " + downloaded1.getFileSize() + ", got: " + fileSize);
}
}
downloaded.removeItem(downloaded1.getFileId());
}
}
QueueItem queueItem = findItem(fileReference.getFileId());
if (queueItem == null) {
if (LOG) {
Log.d(TAG, "- Adding to queue");
}
queueItem = new QueueItem(fileReference);
if (autoStart) {
queueItem.isStopped = false;
im.actor.runtime.Runtime.dispatch(() -> callback.onDownloading(0));
} else {
queueItem.isStopped = true;
im.actor.runtime.Runtime.dispatch(() -> callback.onNotDownloaded());
}
queue.add(0, queueItem);
} else {
Log.d(TAG, "- Promoting in queue");
promote(fileReference.getFileId());
if (queueItem.isStopped) {
im.actor.runtime.Runtime.dispatch(() -> callback.onNotDownloaded());
} else {
if (queueItem.isStarted) {
final float progress = queueItem.progress;
im.actor.runtime.Runtime.dispatch(() -> callback.onDownloading(progress));
} else {
im.actor.runtime.Runtime.dispatch(() -> callback.onDownloading(0));
}
}
}
getSubscribers(fileReference.getFileId()).add(callback);
checkQueue();
}
public void startDownload(FileReference fileReference) {
if (LOG) {
Log.d(TAG, "Starting download #" + fileReference.getFileId());
}
Downloaded downloaded1 = downloaded.getValue(fileReference.getFileId());
if (downloaded1 != null) {
// Already downloaded
return;
}
QueueItem queueItem = findItem(fileReference.getFileId());
if (queueItem == null) {
if (LOG) {
Log.d(TAG, "- Adding to queue");
}
queueItem = new QueueItem(fileReference);
queueItem.isStopped = false;
queue.add(0, queueItem);
} else {
if (LOG) {
Log.d(TAG, "- Promoting in queue");
}
if (queueItem.isStopped) {
queueItem.isStopped = false;
for (final FileCallback callback : getSubscribers(fileReference.getFileId())) {
im.actor.runtime.Runtime.dispatch(() -> callback.onDownloading(0));
}
}
promote(fileReference.getFileId());
}
checkQueue();
}
public void cancelDownload(long fileId) {
if (LOG) {
Log.d(TAG, "Stopping download #" + fileId);
}
QueueItem queueItem = findItem(fileId);
if (queueItem == null) {
if (LOG) {
Log.d(TAG, "- Not present in queue");
}
} else {
if (queueItem.isStarted) {
if (LOG) {
Log.d(TAG, "- Stopping actor");
}
queueItem.taskRef.send(PoisonPill.INSTANCE);
queueItem.taskRef = null;
queueItem.isStarted = false;
}
if (LOG) {
Log.d(TAG, "- Marking as stopped");
}
queueItem.isStopped = true;
for (final FileCallback callback : getSubscribers(fileId)) {
im.actor.runtime.Runtime.dispatch(() -> callback.onNotDownloaded());
}
}
checkQueue();
}
public void unbindDownload(long fileId, boolean autoCancel, FileCallback callback) {
if (LOG) {
Log.d(TAG, "Unbind file #" + fileId);
}
QueueItem queueItem = findItem(fileId);
if (queueItem == null) {
if (LOG) {
Log.d(TAG, "- Not present in queue");
}
} else {
if (autoCancel) {
if (queueItem.isStarted) {
if (LOG) {
Log.d(TAG, "- Stopping actor");
}
queueItem.taskRef.send(PoisonPill.INSTANCE);
queueItem.taskRef = null;
queueItem.isStarted = false;
}
if (!queueItem.isStopped) {
if (LOG) {
Log.d(TAG, "- Marking as stopped");
}
queueItem.isStopped = true;
for (final FileCallback c : getSubscribers(fileId)) {
if (c != callback) {
im.actor.runtime.Runtime.dispatch(() -> c.onNotDownloaded());
}
}
}
queue.remove(queueItem);
} else {
if (LOG) {
Log.d(TAG, "- Removing callback");
}
getSubscribers(fileId).remove(callback);
}
}
checkQueue();
}
// Callback
private void subscribe(FileEventCallback callback) {
globalCallbacks.add(new WeakCallbackHolder(callback));
cleanWeakSubscribers();
}
private void unsubscribe(FileEventCallback callback) {
for (WeakCallbackHolder callbackHolder : globalCallbacks) {
if (callbackHolder.getCallbackWeakReference() == callback) {
globalCallbacks.remove(callbackHolder);
break;
}
}
cleanWeakSubscribers();
}
private void cleanWeakSubscribers() {
ArrayList<WeakCallbackHolder> toRemove = new ArrayList<>();
for (WeakCallbackHolder callbackHolder : globalCallbacks) {
if (callbackHolder.getCallbackWeakReference() == null) {
toRemove.add(callbackHolder);
}
}
globalCallbacks.removeAll(toRemove);
}
// Queue processing
private void checkQueue() {
if (LOG) {
Log.d(TAG, "- Checking queue");
}
int activeDownloads = 0;
for (QueueItem queueItem : queue) {
if (queueItem.isStarted) {
activeDownloads++;
}
}
if (activeDownloads >= SIM_MAX_DOWNLOADS) {
if (LOG) {
Log.d(TAG, "- Already have max number of simultaneous downloads");
}
return;
}
QueueItem pendingQueue = null;
for (QueueItem queueItem : queue) {
if (!queueItem.isStarted && !queueItem.isStopped) {
pendingQueue = queueItem;
break;
}
}
if (pendingQueue == null) {
if (LOG) {
Log.d(TAG, "- No work for downloading");
}
return;
}
if (LOG) {
Log.d(TAG, "- Starting download file #" + pendingQueue.fileReference.getFileId());
}
pendingQueue.isStarted = true;
final QueueItem finalPendingQueue = pendingQueue;
pendingQueue.taskRef = system().actorOf(Props.create(() -> new DownloadTask(finalPendingQueue.fileReference, self(), context())).changeDispatcher("heavy"), "actor/download/task_" + RandomUtils.nextRid());
}
public void onDownloadProgress(long fileId, final float progress) {
if (LOG) {
Log.d(TAG, "onDownloadProgress file #" + fileId + " " + progress);
}
QueueItem queueItem = findItem(fileId);
if (queueItem == null) {
return;
}
if (!queueItem.isStarted) {
return;
}
queueItem.progress = progress;
for (final FileCallback fileCallback : getSubscribers(fileId)) {
im.actor.runtime.Runtime.dispatch(() -> fileCallback.onDownloading(progress));
}
}
public void onDownloaded(final long fileId, final FileSystemReference reference) {
if (LOG) {
Log.d(TAG, "onDownloaded file #" + fileId);
}
QueueItem queueItem = findItem(fileId);
if (queueItem == null) {
return;
}
if (!queueItem.isStarted) {
return;
}
downloaded.addOrUpdateItem(new Downloaded(queueItem.fileReference.getFileId(),
queueItem.fileReference.getFileSize(), reference.getDescriptor()));
queue.remove(queueItem);
queueItem.taskRef.send(PoisonPill.INSTANCE);
for (final WeakCallbackHolder weakReference : globalCallbacks) {
final FileEventCallback callback = weakReference.getCallbackWeakReference().get();
if (callback != null) {
im.actor.runtime.Runtime.dispatch(() -> callback.onDownloaded(fileId));
}
}
for (final FileCallback fileCallback : getSubscribers(fileId)) {
im.actor.runtime.Runtime.dispatch(() -> fileCallback.onDownloaded(reference));
}
checkQueue();
}
public void onDownloadError(long fileId) {
if (LOG) {
Log.d(TAG, "onDownloadError file #" + fileId);
}
QueueItem queueItem = findItem(fileId);
if (queueItem == null) {
return;
}
if (!queueItem.isStarted) {
return;
}
queueItem.taskRef.send(PoisonPill.INSTANCE);
queueItem.isStopped = true;
queueItem.isStarted = false;
for (final FileCallback fileCallback : getSubscribers(fileId)) {
im.actor.runtime.Runtime.dispatch(() -> fileCallback.onNotDownloaded());
}
checkQueue();
}
private QueueItem findItem(long id) {
for (QueueItem q : queue) {
if (q.fileReference.getFileId() == id) {
return q;
}
}
return null;
}
private void promote(long id) {
for (QueueItem q : queue) {
if (q.fileReference.getFileId() == id) {
if (!q.isStarted) {
queue.remove(q);
queue.add(0, q);
}
return;
}
}
}
private ArrayList<FileCallback> getSubscribers(long fileId) {
ArrayList<FileCallback> res = callbacks.get(fileId);
if (res == null) {
res = new ArrayList<>();
callbacks.put(fileId, res);
}
return res;
}
private class QueueItem {
private FileReference fileReference;
private boolean isStopped;
private boolean isStarted;
private float progress;
private ActorRef taskRef;
private QueueItem(FileReference fileReference) {
this.fileReference = fileReference;
}
}
//region Messages
@Override
public void onReceive(Object message) {
if (message instanceof BindDownload) {
BindDownload requestDownload = (BindDownload) message;
bindDownload(requestDownload.getFileReference(),
requestDownload.isAutostart(),
requestDownload.getCallback());
} else if (message instanceof CancelDownload) {
CancelDownload cancelDownload = (CancelDownload) message;
cancelDownload(cancelDownload.getFileId());
} else if (message instanceof UnbindDownload) {
UnbindDownload unbindDownload = (UnbindDownload) message;
unbindDownload(unbindDownload.getFileId(), unbindDownload.isAutocancel(), unbindDownload.getCallback());
} else if (message instanceof StartDownload) {
StartDownload startDownload = (StartDownload) message;
startDownload(startDownload.getFileReference());
} else if (message instanceof OnDownloadProgress) {
OnDownloadProgress downloadProgress = (OnDownloadProgress) message;
onDownloadProgress(downloadProgress.getFileId(), downloadProgress.getProgress());
} else if (message instanceof OnDownloaded) {
OnDownloaded onDownloaded = (OnDownloaded) message;
onDownloaded(onDownloaded.getFileId(), onDownloaded.getReference());
} else if (message instanceof OnDownloadedError) {
OnDownloadedError error = (OnDownloadedError) message;
onDownloadError(error.getFileId());
} else if (message instanceof RequestState) {
RequestState requestState = (RequestState) message;
requestState(requestState.getFileId(), requestState.getCallback());
} else if (message instanceof SubscribeToDownloads) {
subscribe(((SubscribeToDownloads) message).getCallback());
} else if (message instanceof UnsubscribeToDownloads) {
unsubscribe(((UnsubscribeToDownloads) message).getCallback());
} else {
super.onReceive(message);
}
}
public static class RequestState {
private long fileId;
private FileCallback callback;
public RequestState(long fileId, FileCallback callback) {
this.fileId = fileId;
this.callback = callback;
}
public long getFileId() {
return fileId;
}
public FileCallback getCallback() {
return callback;
}
}
public static class BindDownload {
private FileReference fileReference;
private boolean isAutostart;
private FileCallback callback;
public BindDownload(FileReference fileReference, boolean isAutostart, FileCallback callback) {
this.fileReference = fileReference;
this.isAutostart = isAutostart;
this.callback = callback;
}
public FileReference getFileReference() {
return fileReference;
}
public boolean isAutostart() {
return isAutostart;
}
public FileCallback getCallback() {
return callback;
}
}
public static class StartDownload {
private FileReference fileReference;
public StartDownload(FileReference fileReference) {
this.fileReference = fileReference;
}
public FileReference getFileReference() {
return fileReference;
}
}
public static class CancelDownload {
private long fileId;
public CancelDownload(long fileId) {
this.fileId = fileId;
}
public long getFileId() {
return fileId;
}
}
public static class UnbindDownload {
private long fileId;
private boolean isAutocancel;
private FileCallback callback;
public UnbindDownload(long fileId, boolean isAutocancel, FileCallback callback) {
this.fileId = fileId;
this.isAutocancel = isAutocancel;
this.callback = callback;
}
public long getFileId() {
return fileId;
}
public FileCallback getCallback() {
return callback;
}
public boolean isAutocancel() {
return isAutocancel;
}
}
public static class OnDownloadProgress {
private long fileId;
private float progress;
public OnDownloadProgress(long fileId, float progress) {
this.fileId = fileId;
this.progress = progress;
}
public long getFileId() {
return fileId;
}
public float getProgress() {
return progress;
}
}
public static class OnDownloaded {
private long fileId;
private FileSystemReference reference;
public OnDownloaded(long fileId, FileSystemReference reference) {
this.fileId = fileId;
this.reference = reference;
}
public long getFileId() {
return fileId;
}
public FileSystemReference getReference() {
return reference;
}
}
public static class OnDownloadedError {
private long fileId;
public OnDownloadedError(long fileId) {
this.fileId = fileId;
}
public long getFileId() {
return fileId;
}
}
public static class SubscribeToDownloads {
private FileEventCallback callback;
public SubscribeToDownloads(FileEventCallback callback) {
this.callback = callback;
}
public FileEventCallback getCallback() {
return callback;
}
}
public static class UnsubscribeToDownloads {
private FileEventCallback callback;
public UnsubscribeToDownloads(FileEventCallback callback) {
this.callback = callback;
}
public FileEventCallback getCallback() {
return callback;
}
}
private class WeakCallbackHolder {
private WeakReferenceCompat<FileEventCallback> callbackWeakReference;
public WeakCallbackHolder(FileEventCallback callbackWeakReference) {
this.callbackWeakReference = im.actor.runtime.Runtime.createWeakReference(callbackWeakReference);
}
public WeakReferenceCompat<FileEventCallback> getCallbackWeakReference() {
return callbackWeakReference;
}
}
//endregion
}