/* * 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 }