package com.crossbow.volley;
import android.content.Context;
import com.crossbow.volley.toolbox.BasicFileStack;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* The file queue handles the creation of a queue and the dispatching of the file read/writes to
* separate threads. This is run independently of the normal {@link com.android.volley.RequestQueue RequestQueue}
*
* The default thread count is 3.
*/
public class FileQueue {
private PriorityBlockingQueue<FileRequest<?>> requestQueue = new PriorityBlockingQueue<>();
private FileStack fileStack;
private List<FileRequest> inFlightRequests = new LinkedList<>();
private AtomicInteger sequenceNumber = new AtomicInteger();
private static final int THREAD_COUNT = 3;
private FileDispatcher[] fileDispatchers = new FileDispatcher[THREAD_COUNT];
private FileDelivery fileDelivery;
private boolean running = false;
private Map<String, LinkedList<FileRequest<?>>> blockedRequests = new HashMap<>();
private Set<String> inUseFiles = new HashSet<>();
/**
* Creates a new file queue
* @param fileDelivery the File deliver to use
* @param fileStack the file stack to use for reading a writing
* @param threadCount the size of the thread pool. Defaults to 3.
*/
public FileQueue(FileDelivery fileDelivery, FileStack fileStack, int threadCount) {
this.fileDelivery = fileDelivery;
this.fileStack = fileStack;
fileDispatchers = new FileDispatcher[threadCount];
}
/**
* Creates a new file queue with the default thread pool size of 3
*/
public FileQueue(FileDelivery fileDelivery, FileStack fileStack) {
this(fileDelivery, fileStack, THREAD_COUNT);
}
/**
* Creates a new file queue with the default File delivery, Reader and thread pool size.
*/
public FileQueue(Context context) {
this(new BasicFileDelivery(), new BasicFileStack(context), THREAD_COUNT);
}
/**
* Adds a request to the queue. {@link #start()} has to be called inorder for requests to be processed
* @param fileRequest
*/
public void add(FileRequest fileRequest) {
if(fileRequest == null) {
return;
}
//Set the sequence number
int sequence = sequenceNumber.incrementAndGet();
fileRequest.sequence(sequence);
fileRequest.setFileQueue(this);
String filePath = fileRequest.getFilePath();
//add to the inFlight requests
synchronized (inFlightRequests) {
inFlightRequests.add(fileRequest);
}
synchronized (inUseFiles) {
if(inUseFiles.contains(filePath)) {
if(blockedRequests.containsKey(filePath)) {
List<FileRequest<?>> list = blockedRequests.get(filePath);
list.add(fileRequest);
}
else {
LinkedList<FileRequest<?>> list = new LinkedList<>();
list.add(fileRequest);
blockedRequests.put(filePath, list);
}
fileRequest.mark("file-in-use-blocking");
}
else {
inUseFiles.add(filePath);
requestQueue.add(fileRequest);
fileRequest.mark("add-to-queue");
}
}
}
//Visible for testing
PriorityBlockingQueue<FileRequest<?>> getRequestQueue() {
return requestQueue;
}
/**
* Start the request queue processing requests
*/
public void start() {
if(running) {
return;
}
running = true;
for (int i = 0; i < fileDispatchers.length; i++) {
fileDispatchers[i] = new FileDispatcher(requestQueue, fileStack, fileDelivery);
fileDispatchers[i].start();
}
}
/**
* Stops the queue from processing any new requests
*/
public void stop() {
running = false;
for (int i = 0; i < fileDispatchers.length; i++) {
fileDispatchers[i] = new FileDispatcher(requestQueue, fileStack, fileDelivery);
fileDispatchers[i].quit();
}
}
/**
* Cancels any requests that match the current request filter
* @param filter FileRequestFilter to use to cancel the requests
*/
public void cancelAll(FileRequestFilter filter) {
synchronized (inFlightRequests) {
for(FileRequest<?> request : inFlightRequests) {
if(filter.apply(request)) request.cancel();
}
}
}
/**
* Cancels every request in the queue
*/
public void cancelAll() {
synchronized (inFlightRequests) {
for(FileRequest<?> request : inFlightRequests) {
request.cancel();
}
}
}
/**
* Cancel all the requests that have a matching tag. The tag is set using {@link FileRequest#setTag(Object) FileRequest.setTag}
* @param tag to match against
*/
public void cancelAll(Object tag) {
if(tag == null) {
throw new IllegalArgumentException("Cant cancel for a null tag");
}
synchronized (inFlightRequests) {
for(FileRequest<?> request : inFlightRequests) {
if(tag.equals(request.getTag())) {
request.cancel();
}
}
}
}
protected void finishRequest(FileRequest fileRequest) {
synchronized (inFlightRequests) {
inFlightRequests.remove(fileRequest);
}
synchronized (inUseFiles) {
String filePath = fileRequest.getFilePath();
inUseFiles.remove(filePath);
//get any blocked requests nad add to the queue
if(blockedRequests.containsKey(filePath)) {
LinkedList<FileRequest<?>> blocked = blockedRequests.get(filePath);
FileRequest<?> request = blocked.pop();
if(!request.isCanceled()) {
requestQueue.add(request);
inUseFiles.add(filePath);
fileRequest.mark("request-unblocked");
}
else {
fileRequest.mark("request-unblocked-canceled");
}
if (blocked.isEmpty()) {
blockedRequests.remove(filePath);
}
}
}
}
}