Embedded GigaGet download manager. First try.

This commit is contained in:
David 2016-04-21 20:28:01 -03:00
parent f08b1224c9
commit 4bae12aa55
58 changed files with 3121 additions and 40 deletions

View file

@ -0,0 +1,14 @@
package us.shandian.giga.get;
public interface DownloadManager
{
public static final int BLOCK_SIZE = 512 * 1024;
public int startMission(String url, String name, int threads);
public void resumeMission(int id);
public void pauseMission(int id);
public void deleteMission(int id);
public DownloadMission getMission(int id);
public int getCount();
public String getLocation();
}

View file

@ -0,0 +1,214 @@
package us.shandian.giga.get;
import android.content.Context;
import android.util.Log;
import com.google.gson.Gson;
import java.io.File;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadManagerImpl implements DownloadManager
{
private static final String TAG = DownloadManagerImpl.class.getSimpleName();
private Context mContext;
private String mLocation;
protected ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>();
public DownloadManagerImpl(Context context, String location) {
mContext = context;
mLocation = location;
loadMissions();
}
@Override
public int startMission(String url, String name, int threads) {
DownloadMission mission = new DownloadMission();
mission.url = url;
mission.name = name;
mission.location = mLocation;
mission.timestamp = System.currentTimeMillis();
mission.threadCount = threads;
new Initializer(mContext, mission).start();
return insertMission(mission);
}
@Override
public void resumeMission(int i) {
DownloadMission d = getMission(i);
if (!d.running && d.errCode == -1) {
d.start();
}
}
@Override
public void pauseMission(int i) {
DownloadMission d = getMission(i);
if (d.running) {
d.pause();
}
}
@Override
public void deleteMission(int i) {
getMission(i).delete();
mMissions.remove(i);
}
private void loadMissions() {
File f = new File(mLocation);
if (f.exists() && f.isDirectory()) {
File[] subs = f.listFiles();
for (File sub : subs) {
if (sub.isDirectory()) {
continue;
}
if (sub.getName().endsWith(".giga")) {
String str = Utility.readFromFile(sub.getAbsolutePath());
if (str != null && !str.trim().equals("")) {
if (DEBUG) {
Log.d(TAG, "loading mission " + sub.getName());
Log.d(TAG, str);
}
DownloadMission mis = new Gson().fromJson(str, DownloadMission.class);
if (mis.finished) {
sub.delete();
continue;
}
mis.running = false;
mis.recovered = true;
insertMission(mis);
}
} else if (!sub.getName().startsWith(".") && !new File(sub.getPath() + ".giga").exists()) {
// Add a dummy mission for downloaded files
DownloadMission mis = new DownloadMission();
mis.length = sub.length();
mis.done = mis.length;
mis.finished = true;
mis.running = false;
mis.name = sub.getName();
mis.location = mLocation;
mis.timestamp = sub.lastModified();
insertMission(mis);
}
}
}
}
@Override
public DownloadMission getMission(int i) {
return mMissions.get(i);
}
@Override
public int getCount() {
return mMissions.size();
}
private int insertMission(DownloadMission mission) {
int i = -1;
DownloadMission m = null;
if (mMissions.size() > 0) {
do {
m = mMissions.get(++i);
} while (m.timestamp > mission.timestamp && i < mMissions.size() - 1);
//if (i > 0) i--;
} else {
i = 0;
}
mMissions.add(i, mission);
return i;
}
@Override
public String getLocation() {
return mLocation;
}
private class Initializer extends Thread {
private Context context;
private DownloadMission mission;
public Initializer(Context context, DownloadMission mission) {
this.context = context;
this.mission = mission;
}
@Override
public void run() {
try {
URL url = new URL(mission.url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
mission.length = conn.getContentLength();
if (mission.length <= 0) {
mission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
//mission.notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
return;
}
// Open again
conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Range", "bytes=" + (mission.length - 10) + "-" + mission.length);
if (conn.getResponseCode() != 206) {
// Fallback to single thread if no partial content support
mission.fallback = true;
if (DEBUG) {
Log.d(TAG, "falling back");
}
}
if (DEBUG) {
Log.d(TAG, "response = " + conn.getResponseCode());
}
mission.blocks = mission.length / BLOCK_SIZE;
if (mission.threadCount > mission.blocks) {
mission.threadCount = (int) mission.blocks;
}
if (mission.threadCount <= 0) {
mission.threadCount = 1;
}
if (mission.blocks * BLOCK_SIZE < mission.length) {
mission.blocks++;
}
new File(mission.location).mkdirs();
new File(mission.location + "/" + mission.name).createNewFile();
RandomAccessFile af = new RandomAccessFile(mission.location + "/" + mission.name, "rw");
af.setLength(mission.length);
af.close();
mission.start();
} catch (Exception e) {
// TODO Notify
throw new RuntimeException(e);
}
}
}
}

View file

@ -0,0 +1,229 @@
package us.shandian.giga.get;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.google.gson.Gson;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashMap;
import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadMission
{
private static final String TAG = DownloadMission.class.getSimpleName();
public static interface MissionListener {
HashMap<MissionListener, Handler> handlerStore = new HashMap<>();
public void onProgressUpdate(long done, long total);
public void onFinish();
public void onError(int errCode);
}
public static final int ERROR_SERVER_UNSUPPORTED = 206;
public static final int ERROR_UNKNOWN = 233;
public String name = "";
public String url = "";
public String location = "";
public long blocks = 0;
public long length = 0;
public long done = 0;
public int threadCount = 3;
public int finishCount = 0;
public ArrayList<Long> threadPositions = new ArrayList<Long>();
public HashMap<Long, Boolean> blockState = new HashMap<Long, Boolean>();
public boolean running = false;
public boolean finished = false;
public boolean fallback = false;
public int errCode = -1;
public long timestamp = 0;
public transient boolean recovered = false;
private transient ArrayList<WeakReference<MissionListener>> mListeners = new ArrayList<WeakReference<MissionListener>>();
private transient boolean mWritingToFile = false;
public boolean isBlockPreserved(long block) {
return blockState.containsKey(block) ? blockState.get(block) : false;
}
public void preserveBlock(long block) {
synchronized (blockState) {
blockState.put(block, true);
}
}
public void setPosition(int id, long position) {
threadPositions.set(id, position);
}
public long getPosition(int id) {
return threadPositions.get(id);
}
public synchronized void notifyProgress(long deltaLen) {
if (!running) return;
if (recovered) {
recovered = false;
}
done += deltaLen;
if (done > length) {
done = length;
}
if (done != length) {
writeThisToFile();
}
for (WeakReference<MissionListener> ref: mListeners) {
final MissionListener listener = ref.get();
if (listener != null) {
MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override
public void run() {
listener.onProgressUpdate(done, length);
}
});
}
}
}
public synchronized void notifyFinished() {
if (errCode > 0) return;
finishCount++;
if (finishCount == threadCount) {
onFinish();
}
}
private void onFinish() {
if (errCode > 0) return;
if (DEBUG) {
Log.d(TAG, "onFinish");
}
running = false;
finished = true;
deleteThisFromFile();
for (WeakReference<MissionListener> ref : mListeners) {
final MissionListener listener = ref.get();
if (listener != null) {
MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override
public void run() {
listener.onFinish();
}
});
}
}
}
public synchronized void notifyError(int err) {
errCode = err;
writeThisToFile();
for (WeakReference<MissionListener> ref : mListeners) {
final MissionListener listener = ref.get();
MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override
public void run() {
listener.onError(errCode);
}
});
}
}
public synchronized void addListener(MissionListener listener) {
Handler handler = new Handler(Looper.getMainLooper());
MissionListener.handlerStore.put(listener, handler);
mListeners.add(new WeakReference<MissionListener>(listener));
}
public synchronized void removeListener(MissionListener listener) {
for (Iterator<WeakReference<MissionListener>> iterator = mListeners.iterator();
iterator.hasNext(); ) {
WeakReference<MissionListener> weakRef = iterator.next();
if (listener!=null && listener == weakRef.get())
{
iterator.remove();
}
}
}
public void start() {
if (!running && !finished) {
running = true;
if (!fallback) {
for (int i = 0; i < threadCount; i++) {
if (threadPositions.size() <= i && !recovered) {
threadPositions.add((long) i);
}
new Thread(new DownloadRunnable(this, i)).start();
}
} else {
// In fallback mode, resuming is not supported.
threadCount = 1;
done = 0;
blocks = 0;
new Thread(new DownloadRunnableFallback(this)).start();
}
}
}
public void pause() {
if (running) {
running = false;
recovered = true;
// TODO: Notify & Write state to info file
// if (err)
}
}
public void delete() {
deleteThisFromFile();
new File(location + "/" + name).delete();
}
public void writeThisToFile() {
if (!mWritingToFile) {
mWritingToFile = true;
new Thread() {
@Override
public void run() {
doWriteThisToFile();
mWritingToFile = false;
}
}.start();
}
}
private void doWriteThisToFile() {
synchronized (blockState) {
Utility.writeToFile(location + "/" + name + ".giga", new Gson().toJson(this));
}
}
private void deleteThisFromFile() {
new File(location + "/" + name + ".giga").delete();
}
}

View file

@ -0,0 +1,174 @@
package us.shandian.giga.get;
import android.os.Handler;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadRunnable implements Runnable
{
private static final String TAG = DownloadRunnable.class.getSimpleName();
private DownloadMission mMission;
private int mId;
public DownloadRunnable(DownloadMission mission, int id) {
mMission = mission;
mId = id;
}
@Override
public void run() {
boolean retry = mMission.recovered;
long position = mMission.getPosition(mId);
if (DEBUG) {
Log.d(TAG, mId + ":default pos " + position);
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
}
while (mMission.errCode == -1 && mMission.running && position < mMission.blocks) {
if (Thread.currentThread().isInterrupted()) {
mMission.pause();
return;
}
if (DEBUG && retry) {
Log.d(TAG, mId + ":retry is true. Resuming at " + position);
}
// Wait for an unblocked position
while (!retry && position < mMission.blocks && mMission.isBlockPreserved(position)) {
if (DEBUG) {
Log.d(TAG, mId + ":position " + position + " preserved, passing");
}
position++;
}
retry = false;
if (position >= mMission.blocks) {
break;
}
if (DEBUG) {
Log.d(TAG, mId + ":preserving position " + position);
}
mMission.preserveBlock(position);
mMission.setPosition(mId, position);
long start = position * DownloadManager.BLOCK_SIZE;
long end = start + DownloadManager.BLOCK_SIZE - 1;
if (end >= mMission.length) {
end = mMission.length - 1;
}
HttpURLConnection conn = null;
int total = 0;
try {
URL url = new URL(mMission.url);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
if (DEBUG) {
Log.d(TAG, mId + ":" + conn.getRequestProperty("Range"));
Log.d(TAG, mId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode());
}
// A server may be ignoring the range requet
if (conn.getResponseCode() != 206) {
mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
if (DEBUG) {
Log.e(TAG, mId + ":Unsupported " + conn.getResponseCode());
}
break;
}
RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw");
f.seek(start);
BufferedInputStream ipt = new BufferedInputStream(conn.getInputStream());
byte[] buf = new byte[512];
while (start < end && mMission.running) {
int len = ipt.read(buf, 0, 512);
if (len == -1) {
break;
} else {
start += len;
total += len;
f.write(buf, 0, len);
notifyProgress(len);
}
}
if (DEBUG && mMission.running) {
Log.d(TAG, mId + ":position " + position + " finished, total length " + total);
}
f.close();
ipt.close();
// TODO We should save progress for each thread
} catch (Exception e) {
// TODO Retry count limit & notify error
retry = true;
notifyProgress(-total);
if (DEBUG) {
Log.d(TAG, mId + ":position " + position + " retrying");
}
}
}
if (DEBUG) {
Log.d(TAG, "thread " + mId + " exited main loop");
}
if (mMission.errCode == -1 && mMission.running) {
if (DEBUG) {
Log.d(TAG, "no error has happened, notifying");
}
notifyFinished();
}
if (DEBUG && !mMission.running) {
Log.d(TAG, "The mission has been paused. Passing.");
}
}
private void notifyProgress(final long len) {
synchronized (mMission) {
mMission.notifyProgress(len);
}
}
private void notifyError(final int err) {
synchronized (mMission) {
mMission.notifyError(err);
mMission.pause();
}
}
private void notifyFinished() {
synchronized (mMission) {
mMission.notifyFinished();
}
}
}

View file

@ -0,0 +1,74 @@
package us.shandian.giga.get;
import java.io.BufferedInputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
// Single-threaded fallback mode
public class DownloadRunnableFallback implements Runnable
{
private DownloadMission mMission;
//private int mId;
public DownloadRunnableFallback(DownloadMission mission) {
//mId = id;
mMission = mission;
}
@Override
public void run() {
try {
URL url = new URL(mMission.url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
if (conn.getResponseCode() != 200 && conn.getResponseCode() != 206) {
notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
} else {
RandomAccessFile f = new RandomAccessFile(mMission.location + "/" + mMission.name, "rw");
f.seek(0);
BufferedInputStream ipt = new BufferedInputStream(conn.getInputStream());
byte[] buf = new byte[512];
int len = 0;
while ((len = ipt.read(buf, 0, 512)) != -1 && mMission.running) {
f.write(buf, 0, len);
notifyProgress(len);
if (Thread.currentThread().interrupted()) {
break;
}
}
f.close();
ipt.close();
}
} catch (Exception e) {
notifyError(DownloadMission.ERROR_UNKNOWN);
}
if (mMission.errCode == -1 && mMission.running) {
notifyFinished();
}
}
private void notifyProgress(final long len) {
synchronized (mMission) {
mMission.notifyProgress(len);
}
}
private void notifyError(final int err) {
synchronized (mMission) {
mMission.notifyError(err);
mMission.pause();
}
}
private void notifyFinished() {
synchronized (mMission) {
mMission.notifyFinished();
}
}
}

View file

@ -0,0 +1,88 @@
package us.shandian.giga.get;
import android.content.Context;
import java.util.Map.Entry;
import java.util.HashMap;
public class FilteredDownloadManagerWrapper implements DownloadManager
{
private boolean mDownloaded = false; // T=Filter downloaded files; F=Filter downloading files
private DownloadManager mManager;
private HashMap<Integer, Integer> mElementsMap = new HashMap<Integer, Integer>();
public FilteredDownloadManagerWrapper(DownloadManager manager, boolean filterDownloaded) {
mManager = manager;
mDownloaded = filterDownloaded;
refreshMap();
}
private void refreshMap() {
mElementsMap.clear();
int size = 0;
for (int i = 0; i < mManager.getCount(); i++) {
if (mManager.getMission(i).finished == mDownloaded) {
mElementsMap.put(size++, i);
}
}
}
private int toRealPosition(int pos) {
if (mElementsMap.containsKey(pos)) {
return mElementsMap.get(pos);
} else {
return -1;
}
}
private int toFakePosition(int pos) {
for (Entry<Integer, Integer> entry : mElementsMap.entrySet()) {
if (entry.getValue() == pos) {
return entry.getKey();
}
}
return -1;
}
@Override
public int startMission(String url, String name, int threads) {
int ret = mManager.startMission(url, name, threads);
refreshMap();
return toFakePosition(ret);
}
@Override
public void resumeMission(int id) {
mManager.resumeMission(toRealPosition(id));
}
@Override
public void pauseMission(int id) {
mManager.pauseMission(toRealPosition(id));
}
@Override
public void deleteMission(int id) {
mManager.deleteMission(toRealPosition(id));
}
@Override
public DownloadMission getMission(int id) {
return mManager.getMission(toRealPosition(id));
}
@Override
public int getCount() {
return mElementsMap.size();
}
@Override
public String getLocation() {
return mManager.getLocation();
}
}