Implement Storage Access Framework
* re-work finished mission database * re-work DownloadMission and bump it Serializable version * keep the classic Java IO API * SAF Tree API support on Android Lollipop or higher * add wrapper for SAF stream opening * implement Closeable in SharpStream to replace the dispose() method * do required changes for this API: ** remove any file creation logic from DownloadInitializer ** make PostProcessing Serializable and reduce the number of iterations ** update all strings.xml files ** storage helpers: StoredDirectoryHelper & StoredFileHelper ** best effort to handle any kind of SAF errors/exceptions
This commit is contained in:
parent
8d8059229f
commit
7ca7952790
62 changed files with 2439 additions and 1180 deletions
|
|
@ -3,10 +3,10 @@ package us.shandian.giga.get;
|
|||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
|
|
@ -111,34 +111,10 @@ public class DownloadInitializer extends Thread {
|
|||
if (!mMission.running || Thread.interrupted()) return;
|
||||
}
|
||||
|
||||
File file;
|
||||
if (mMission.current == 0) {
|
||||
file = new File(mMission.location);
|
||||
if (!Utility.mkdir(file, true)) {
|
||||
mMission.notifyError(DownloadMission.ERROR_PATH_CREATION, null);
|
||||
return;
|
||||
}
|
||||
|
||||
file = new File(file, mMission.name);
|
||||
|
||||
// if the name is used by another process, delete it
|
||||
if (file.exists() && !file.isFile() && !file.delete()) {
|
||||
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
mMission.notifyError(DownloadMission.ERROR_FILE_CREATION, null);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
file = new File(mMission.location, mMission.name);
|
||||
}
|
||||
|
||||
RandomAccessFile af = new RandomAccessFile(file, "rw");
|
||||
af.setLength(mMission.offsets[mMission.current] + mMission.length);
|
||||
af.seek(mMission.offsets[mMission.current]);
|
||||
af.close();
|
||||
SharpStream fs = mMission.storage.getStream();
|
||||
fs.setLength(mMission.offsets[mMission.current] + mMission.length);
|
||||
fs.seek(mMission.offsets[mMission.current]);
|
||||
fs.close();
|
||||
|
||||
if (!mMission.running || Thread.interrupted()) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package us.shandian.giga.get;
|
|||
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
|
@ -17,6 +18,7 @@ import java.util.List;
|
|||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
import us.shandian.giga.postprocessing.Postprocessing;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
|
@ -24,7 +26,7 @@ import us.shandian.giga.util.Utility;
|
|||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
||||
public class DownloadMission extends Mission {
|
||||
private static final long serialVersionUID = 3L;// last bump: 8 november 2018
|
||||
private static final long serialVersionUID = 4L;// last bump: 27 march 2019
|
||||
|
||||
static final int BUFFER_SIZE = 64 * 1024;
|
||||
final static int BLOCK_SIZE = 512 * 1024;
|
||||
|
|
@ -43,6 +45,7 @@ public class DownloadMission extends Mission {
|
|||
public static final int ERROR_POSTPROCESSING_STOPPED = 1008;
|
||||
public static final int ERROR_POSTPROCESSING_HOLD = 1009;
|
||||
public static final int ERROR_INSUFFICIENT_STORAGE = 1010;
|
||||
public static final int ERROR_PROGRESS_LOST = 1011;
|
||||
public static final int ERROR_HTTP_NO_CONTENT = 204;
|
||||
public static final int ERROR_HTTP_UNSUPPORTED_RANGE = 206;
|
||||
|
||||
|
|
@ -71,16 +74,6 @@ public class DownloadMission extends Mission {
|
|||
*/
|
||||
public long[] offsets;
|
||||
|
||||
/**
|
||||
* The post-processing algorithm arguments
|
||||
*/
|
||||
public String[] postprocessingArgs;
|
||||
|
||||
/**
|
||||
* The post-processing algorithm name
|
||||
*/
|
||||
public String postprocessingName;
|
||||
|
||||
/**
|
||||
* Indicates if the post-processing state:
|
||||
* 0: ready
|
||||
|
|
@ -88,12 +81,12 @@ public class DownloadMission extends Mission {
|
|||
* 2: completed
|
||||
* 3: hold
|
||||
*/
|
||||
public volatile int postprocessingState;
|
||||
public volatile int psState;
|
||||
|
||||
/**
|
||||
* Indicate if the post-processing algorithm works on the same file
|
||||
* the post-processing algorithm instance
|
||||
*/
|
||||
public boolean postprocessingThis;
|
||||
public transient Postprocessing psAlgorithm;
|
||||
|
||||
/**
|
||||
* The current resource to download, see {@code urls[current]} and {@code offsets[current]}
|
||||
|
|
@ -138,36 +131,23 @@ public class DownloadMission extends Mission {
|
|||
public transient volatile Thread[] threads = new Thread[0];
|
||||
private transient Thread init = null;
|
||||
|
||||
|
||||
protected DownloadMission() {
|
||||
|
||||
}
|
||||
|
||||
public DownloadMission(String url, String name, String location, char kind) {
|
||||
this(new String[]{url}, name, location, kind, null, null);
|
||||
}
|
||||
|
||||
public DownloadMission(String[] urls, String name, String location, char kind, String postprocessingName, String[] postprocessingArgs) {
|
||||
if (name == null) throw new NullPointerException("name is null");
|
||||
if (name.isEmpty()) throw new IllegalArgumentException("name is empty");
|
||||
public DownloadMission(String[] urls, StoredFileHelper storage, char kind, Postprocessing psInstance) {
|
||||
if (urls == null) throw new NullPointerException("urls is null");
|
||||
if (urls.length < 1) throw new IllegalArgumentException("urls is empty");
|
||||
if (location == null) throw new NullPointerException("location is null");
|
||||
if (location.isEmpty()) throw new IllegalArgumentException("location is empty");
|
||||
this.urls = urls;
|
||||
this.name = name;
|
||||
this.location = location;
|
||||
this.kind = kind;
|
||||
this.offsets = new long[urls.length];
|
||||
this.enqueued = true;
|
||||
this.maxRetry = 3;
|
||||
this.storage = storage;
|
||||
|
||||
if (postprocessingName != null) {
|
||||
Postprocessing algorithm = Postprocessing.getAlgorithm(postprocessingName, null);
|
||||
this.postprocessingThis = algorithm.worksOnSameFile;
|
||||
this.offsets[0] = algorithm.recommendedReserve;
|
||||
this.postprocessingName = postprocessingName;
|
||||
this.postprocessingArgs = postprocessingArgs;
|
||||
if (psInstance != null) {
|
||||
this.psAlgorithm = psInstance;
|
||||
this.offsets[0] = psInstance.recommendedReserve;
|
||||
} else {
|
||||
if (DEBUG && urls.length > 1) {
|
||||
Log.w(TAG, "mission created with multiple urls ¿missing post-processing algorithm?");
|
||||
|
|
@ -359,22 +339,12 @@ public class DownloadMission extends Mission {
|
|||
Log.e(TAG, "notifyError() code = " + code, err);
|
||||
|
||||
if (err instanceof IOException) {
|
||||
if (err.getMessage().contains("Permission denied")) {
|
||||
if (storage.canWrite() || err.getMessage().contains("Permission denied")) {
|
||||
code = ERROR_PERMISSION_DENIED;
|
||||
err = null;
|
||||
} else if (err.getMessage().contains("write failed: ENOSPC")) {
|
||||
} else if (err.getMessage().contains("ENOSPC")) {
|
||||
code = ERROR_INSUFFICIENT_STORAGE;
|
||||
err = null;
|
||||
} else {
|
||||
try {
|
||||
File storage = new File(location);
|
||||
if (storage.canWrite() && storage.getUsableSpace() < (getLength() - done)) {
|
||||
code = ERROR_INSUFFICIENT_STORAGE;
|
||||
err = null;
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
// is a permission error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -433,11 +403,11 @@ public class DownloadMission extends Mission {
|
|||
action = "Failed";
|
||||
}
|
||||
|
||||
Log.d(TAG, action + " postprocessing on " + location + File.separator + name);
|
||||
Log.d(TAG, action + " postprocessing on " + storage.getName());
|
||||
|
||||
synchronized (blockState) {
|
||||
// don't return without fully write the current state
|
||||
postprocessingState = state;
|
||||
psState = state;
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
}
|
||||
}
|
||||
|
|
@ -456,7 +426,7 @@ public class DownloadMission extends Mission {
|
|||
running = true;
|
||||
errCode = ERROR_NOTHING;
|
||||
|
||||
if (current >= urls.length && postprocessingName != null) {
|
||||
if (current >= urls.length && psAlgorithm != null) {
|
||||
runAsync(1, () -> {
|
||||
if (doPostprocessing()) {
|
||||
running = false;
|
||||
|
|
@ -593,7 +563,7 @@ public class DownloadMission extends Mission {
|
|||
* @return true, otherwise, false
|
||||
*/
|
||||
public boolean isFinished() {
|
||||
return current >= urls.length && (postprocessingName == null || postprocessingState == 2);
|
||||
return current >= urls.length && (psAlgorithm == null || psState == 2);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -602,7 +572,13 @@ public class DownloadMission extends Mission {
|
|||
* @return {@code true} if this mission is unrecoverable
|
||||
*/
|
||||
public boolean isPsFailed() {
|
||||
return postprocessingName != null && errCode == DownloadMission.ERROR_POSTPROCESSING && postprocessingThis;
|
||||
switch (errCode) {
|
||||
case ERROR_POSTPROCESSING:
|
||||
case ERROR_POSTPROCESSING_STOPPED:
|
||||
return psAlgorithm.worksOnSameFile;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -611,7 +587,7 @@ public class DownloadMission extends Mission {
|
|||
* @return true, otherwise, false
|
||||
*/
|
||||
public boolean isPsRunning() {
|
||||
return postprocessingName != null && (postprocessingState == 1 || postprocessingState == 3);
|
||||
return psAlgorithm != null && (psState == 1 || psState == 3);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -625,7 +601,7 @@ public class DownloadMission extends Mission {
|
|||
|
||||
public long getLength() {
|
||||
long calculated;
|
||||
if (postprocessingState == 1 || postprocessingState == 3) {
|
||||
if (psState == 1 || psState == 3) {
|
||||
calculated = length;
|
||||
} else {
|
||||
calculated = offsets[current < offsets.length ? current : (offsets.length - 1)] + length;
|
||||
|
|
@ -652,38 +628,60 @@ public class DownloadMission extends Mission {
|
|||
* @param recover {@code true} to retry, otherwise, {@code false} to cancel
|
||||
*/
|
||||
public void psContinue(boolean recover) {
|
||||
postprocessingState = 1;
|
||||
psState = 1;
|
||||
errCode = recover ? ERROR_NOTHING : ERROR_POSTPROCESSING;
|
||||
threads[0].interrupt();
|
||||
}
|
||||
|
||||
/**
|
||||
* changes the StoredFileHelper for another and saves the changes to the metadata file
|
||||
*
|
||||
* @param newStorage the new StoredFileHelper instance to use
|
||||
*/
|
||||
public void changeStorage(@NonNull StoredFileHelper newStorage) {
|
||||
storage = newStorage;
|
||||
// commit changes on the metadata file
|
||||
runAsync(-2, this::writeThisToFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whatever the backed storage is invalid
|
||||
*
|
||||
* @return {@code true}, if storage is invalid and cannot be used
|
||||
*/
|
||||
public boolean hasInvalidStorage() {
|
||||
return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whatever is possible to start the mission
|
||||
*
|
||||
* @return {@code true} is this mission is "sane", otherwise, {@code false}
|
||||
*/
|
||||
public boolean canDownload() {
|
||||
return !(isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) && !isFinished() && !hasInvalidStorage();
|
||||
}
|
||||
|
||||
private boolean doPostprocessing() {
|
||||
if (postprocessingName == null || postprocessingState == 2) return true;
|
||||
if (psAlgorithm == null || psState == 2) return true;
|
||||
|
||||
notifyPostProcessing(1);
|
||||
notifyProgress(0);
|
||||
|
||||
if (DEBUG)
|
||||
Thread.currentThread().setName("[" + TAG + "] post-processing = " + postprocessingName + " filename = " + name);
|
||||
Thread.currentThread().setName("[" + TAG + "] ps = " +
|
||||
psAlgorithm.getClass().getSimpleName() +
|
||||
" filename = " + storage.getName()
|
||||
);
|
||||
|
||||
threads = new Thread[]{Thread.currentThread()};
|
||||
|
||||
Exception exception = null;
|
||||
|
||||
try {
|
||||
Postprocessing
|
||||
.getAlgorithm(postprocessingName, this)
|
||||
.run();
|
||||
psAlgorithm.run(this);
|
||||
} catch (Exception err) {
|
||||
StringBuilder args = new StringBuilder(" ");
|
||||
if (postprocessingArgs != null) {
|
||||
for (String arg : postprocessingArgs) {
|
||||
args.append(", ");
|
||||
args.append(arg);
|
||||
}
|
||||
args.delete(0, 1);
|
||||
}
|
||||
Log.e(TAG, String.format("Post-processing failed. algorithm = %s args = [%s]", postprocessingName, args), err);
|
||||
Log.e(TAG, "Post-processing failed. " + psAlgorithm.toString(), err);
|
||||
|
||||
if (errCode == ERROR_NOTHING) errCode = ERROR_POSTPROCESSING;
|
||||
|
||||
|
|
@ -733,7 +731,7 @@ public class DownloadMission extends Mission {
|
|||
// >=1: any download thread
|
||||
|
||||
if (DEBUG) {
|
||||
who.setName(String.format("%s[%s] %s", TAG, id, name));
|
||||
who.setName(String.format("%s[%s] %s", TAG, id, storage.getName()));
|
||||
}
|
||||
|
||||
who.start();
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ package us.shandian.giga.get;
|
|||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
|
|
@ -40,12 +41,12 @@ public class DownloadRunnable extends Thread {
|
|||
Log.d(TAG, mId + ":recovered: " + mMission.recovered);
|
||||
}
|
||||
|
||||
RandomAccessFile f;
|
||||
SharpStream f;
|
||||
InputStream is = null;
|
||||
|
||||
try {
|
||||
f = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
|
||||
} catch (FileNotFoundException e) {
|
||||
f = mMission.storage.getStream();
|
||||
} catch (IOException e) {
|
||||
mMission.notifyError(e);// this never should happen
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import android.annotation.SuppressLint;
|
|||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
|
||||
|
||||
import us.shandian.giga.util.Utility;
|
||||
|
||||
import static org.schabi.newpipe.BuildConfig.DEBUG;
|
||||
|
|
@ -22,11 +22,10 @@ public class DownloadRunnableFallback extends Thread {
|
|||
private static final String TAG = "DownloadRunnableFallback";
|
||||
|
||||
private final DownloadMission mMission;
|
||||
private final int mId = 1;
|
||||
|
||||
private int mRetryCount = 0;
|
||||
private InputStream mIs;
|
||||
private RandomAccessFile mF;
|
||||
private SharpStream mF;
|
||||
private HttpURLConnection mConn;
|
||||
|
||||
DownloadRunnableFallback(@NonNull DownloadMission mission) {
|
||||
|
|
@ -43,11 +42,7 @@ public class DownloadRunnableFallback extends Thread {
|
|||
// nothing to do
|
||||
}
|
||||
|
||||
try {
|
||||
if (mF != null) mF.close();
|
||||
} catch (IOException e) {
|
||||
// ¿ejected media storage? ¿file deleted? ¿storage ran out of space?
|
||||
}
|
||||
if (mF != null) mF.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -67,6 +62,7 @@ public class DownloadRunnableFallback extends Thread {
|
|||
try {
|
||||
long rangeStart = (mMission.unknownLength || start < 1) ? -1 : start;
|
||||
|
||||
int mId = 1;
|
||||
mConn = mMission.openConnection(mId, rangeStart, -1);
|
||||
mMission.establishConnection(mId, mConn);
|
||||
|
||||
|
|
@ -81,7 +77,7 @@ public class DownloadRunnableFallback extends Thread {
|
|||
if (!mMission.unknownLength)
|
||||
mMission.unknownLength = Utility.getContentLength(mConn) == -1;
|
||||
|
||||
mF = new RandomAccessFile(mMission.getDownloadedFile(), "rw");
|
||||
mF = mMission.storage.getStream();
|
||||
mF.seek(mMission.offsets[mMission.current] + start);
|
||||
|
||||
mIs = mConn.getInputStream();
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package us.shandian.giga.get;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class FinishedMission extends Mission {
|
||||
|
||||
public FinishedMission() {
|
||||
}
|
||||
|
||||
public FinishedMission(DownloadMission mission) {
|
||||
public FinishedMission(@NonNull DownloadMission mission) {
|
||||
source = mission.source;
|
||||
length = mission.length;// ¿or mission.done?
|
||||
timestamp = mission.timestamp;
|
||||
name = mission.name;
|
||||
location = mission.location;
|
||||
kind = mission.kind;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package us.shandian.giga.get;
|
||||
|
||||
import java.io.File;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
|
||||
public abstract class Mission implements Serializable {
|
||||
private static final long serialVersionUID = 0L;// last bump: 5 october 2018
|
||||
private static final long serialVersionUID = 1L;// last bump: 27 march 2019
|
||||
|
||||
/**
|
||||
* Source url of the resource
|
||||
|
|
@ -23,28 +26,23 @@ public abstract class Mission implements Serializable {
|
|||
*/
|
||||
public long timestamp;
|
||||
|
||||
/**
|
||||
* The filename
|
||||
*/
|
||||
public String name;
|
||||
|
||||
/**
|
||||
* The directory to store the download
|
||||
*/
|
||||
public String location;
|
||||
|
||||
/**
|
||||
* pre-defined content type
|
||||
*/
|
||||
public char kind;
|
||||
|
||||
/**
|
||||
* The downloaded file
|
||||
*/
|
||||
public StoredFileHelper storage;
|
||||
|
||||
/**
|
||||
* get the target file on the storage
|
||||
*
|
||||
* @return File object
|
||||
*/
|
||||
public File getDownloadedFile() {
|
||||
return new File(location, name);
|
||||
public Uri getDownloadedFileUri() {
|
||||
return storage.getUri();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -53,8 +51,8 @@ public abstract class Mission implements Serializable {
|
|||
* @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false}
|
||||
*/
|
||||
public boolean delete() {
|
||||
deleted = true;
|
||||
return getDownloadedFile().delete();
|
||||
if (storage != null) return storage.delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -62,10 +60,11 @@ public abstract class Mission implements Serializable {
|
|||
*/
|
||||
public transient boolean deleted = false;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(timestamp);
|
||||
return "[" + calendar.getTime().toString() + "] " + location + File.separator + name;
|
||||
return "[" + calendar.getTime().toString() + "] " + getDownloadedFileUri().getPath();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
package us.shandian.giga.get.sqlite;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.FinishedMission;
|
||||
import us.shandian.giga.get.Mission;
|
||||
|
||||
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_LOCATION;
|
||||
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.KEY_NAME;
|
||||
import static us.shandian.giga.get.sqlite.DownloadMissionHelper.MISSIONS_TABLE_NAME;
|
||||
|
||||
public class DownloadDataSource {
|
||||
|
||||
private static final String TAG = "DownloadDataSource";
|
||||
private final DownloadMissionHelper downloadMissionHelper;
|
||||
|
||||
public DownloadDataSource(Context context) {
|
||||
downloadMissionHelper = new DownloadMissionHelper(context);
|
||||
}
|
||||
|
||||
public ArrayList<FinishedMission> loadFinishedMissions() {
|
||||
SQLiteDatabase database = downloadMissionHelper.getReadableDatabase();
|
||||
Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null,
|
||||
null, null, null, DownloadMissionHelper.KEY_TIMESTAMP);
|
||||
|
||||
int count = cursor.getCount();
|
||||
if (count == 0) return new ArrayList<>(1);
|
||||
|
||||
ArrayList<FinishedMission> result = new ArrayList<>(count);
|
||||
while (cursor.moveToNext()) {
|
||||
result.add(DownloadMissionHelper.getMissionFromCursor(cursor));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void addMission(DownloadMission downloadMission) {
|
||||
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
|
||||
ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission);
|
||||
database.insert(MISSIONS_TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
public void deleteMission(Mission downloadMission) {
|
||||
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
|
||||
database.delete(MISSIONS_TABLE_NAME,
|
||||
KEY_LOCATION + " = ? AND " +
|
||||
KEY_NAME + " = ?",
|
||||
new String[]{downloadMission.location, downloadMission.name});
|
||||
}
|
||||
|
||||
public void updateMission(DownloadMission downloadMission) {
|
||||
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||
SQLiteDatabase database = downloadMissionHelper.getWritableDatabase();
|
||||
ContentValues values = DownloadMissionHelper.getValuesOfMission(downloadMission);
|
||||
String whereClause = KEY_LOCATION + " = ? AND " +
|
||||
KEY_NAME + " = ?";
|
||||
int rowsAffected = database.update(MISSIONS_TABLE_NAME, values,
|
||||
whereClause, new String[]{downloadMission.location, downloadMission.name});
|
||||
if (rowsAffected != 1) {
|
||||
Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
package us.shandian.giga.get.sqlite;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.FinishedMission;
|
||||
|
||||
/**
|
||||
* SQLiteHelper to store finished {@link us.shandian.giga.get.DownloadMission}'s
|
||||
*/
|
||||
public class DownloadMissionHelper extends SQLiteOpenHelper {
|
||||
private final String TAG = "DownloadMissionHelper";
|
||||
|
||||
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
|
||||
private static final String DATABASE_NAME = "downloads.db";
|
||||
|
||||
private static final int DATABASE_VERSION = 3;
|
||||
|
||||
/**
|
||||
* The table name of download missions
|
||||
*/
|
||||
static final String MISSIONS_TABLE_NAME = "download_missions";
|
||||
|
||||
/**
|
||||
* The key to the directory location of the mission
|
||||
*/
|
||||
static final String KEY_LOCATION = "location";
|
||||
/**
|
||||
* The key to the urls of a mission
|
||||
*/
|
||||
static final String KEY_SOURCE_URL = "url";
|
||||
/**
|
||||
* The key to the name of a mission
|
||||
*/
|
||||
static final String KEY_NAME = "name";
|
||||
|
||||
/**
|
||||
* The key to the done.
|
||||
*/
|
||||
static final String KEY_DONE = "bytes_downloaded";
|
||||
|
||||
static final String KEY_TIMESTAMP = "timestamp";
|
||||
|
||||
static final String KEY_KIND = "kind";
|
||||
|
||||
/**
|
||||
* The statement to create the table
|
||||
*/
|
||||
private static final String MISSIONS_CREATE_TABLE =
|
||||
"CREATE TABLE " + MISSIONS_TABLE_NAME + " (" +
|
||||
KEY_LOCATION + " TEXT NOT NULL, " +
|
||||
KEY_NAME + " TEXT NOT NULL, " +
|
||||
KEY_SOURCE_URL + " TEXT NOT NULL, " +
|
||||
KEY_DONE + " INTEGER NOT NULL, " +
|
||||
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
|
||||
KEY_KIND + " TEXT NOT NULL, " +
|
||||
" UNIQUE(" + KEY_LOCATION + ", " + KEY_NAME + "));";
|
||||
|
||||
public DownloadMissionHelper(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(MISSIONS_CREATE_TABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion == 2) {
|
||||
db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME + " ADD COLUMN " + KEY_KIND + " TEXT;");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all values of the download mission as ContentValues.
|
||||
*
|
||||
* @param downloadMission the download mission
|
||||
* @return the content values
|
||||
*/
|
||||
public static ContentValues getValuesOfMission(DownloadMission downloadMission) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KEY_SOURCE_URL, downloadMission.source);
|
||||
values.put(KEY_LOCATION, downloadMission.location);
|
||||
values.put(KEY_NAME, downloadMission.name);
|
||||
values.put(KEY_DONE, downloadMission.done);
|
||||
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
|
||||
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
|
||||
return values;
|
||||
}
|
||||
|
||||
public static FinishedMission getMissionFromCursor(Cursor cursor) {
|
||||
if (cursor == null) throw new NullPointerException("cursor is null");
|
||||
|
||||
String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND));
|
||||
if (kind == null || kind.isEmpty()) kind = "?";
|
||||
|
||||
FinishedMission mission = new FinishedMission();
|
||||
mission.name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME));
|
||||
mission.location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION));
|
||||
mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE_URL));;
|
||||
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
|
||||
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
|
||||
mission.kind = kind.charAt(0);
|
||||
|
||||
return mission;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
package us.shandian.giga.get.sqlite;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
import us.shandian.giga.get.FinishedMission;
|
||||
import us.shandian.giga.get.Mission;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
|
||||
/**
|
||||
* SQLite helper to store finished {@link us.shandian.giga.get.FinishedMission}'s
|
||||
*/
|
||||
public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||
|
||||
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
|
||||
private static final String DATABASE_NAME = "downloads.db";
|
||||
|
||||
private static final int DATABASE_VERSION = 4;
|
||||
|
||||
/**
|
||||
* The table name of download missions (old)
|
||||
*/
|
||||
private static final String MISSIONS_TABLE_NAME_v2 = "download_missions";
|
||||
|
||||
/**
|
||||
* The table name of download missions
|
||||
*/
|
||||
private static final String FINISHED_MISSIONS_TABLE_NAME = "finished_missions";
|
||||
|
||||
/**
|
||||
* The key to the urls of a mission
|
||||
*/
|
||||
private static final String KEY_SOURCE = "url";
|
||||
|
||||
|
||||
/**
|
||||
* The key to the done.
|
||||
*/
|
||||
private static final String KEY_DONE = "bytes_downloaded";
|
||||
|
||||
private static final String KEY_TIMESTAMP = "timestamp";
|
||||
|
||||
private static final String KEY_KIND = "kind";
|
||||
|
||||
private static final String KEY_PATH = "path";
|
||||
|
||||
/**
|
||||
* The statement to create the table
|
||||
*/
|
||||
private static final String MISSIONS_CREATE_TABLE =
|
||||
"CREATE TABLE " + FINISHED_MISSIONS_TABLE_NAME + " (" +
|
||||
KEY_PATH + " TEXT NOT NULL, " +
|
||||
KEY_SOURCE + " TEXT NOT NULL, " +
|
||||
KEY_DONE + " INTEGER NOT NULL, " +
|
||||
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
|
||||
KEY_KIND + " TEXT NOT NULL, " +
|
||||
" UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));";
|
||||
|
||||
|
||||
private Context context;
|
||||
|
||||
public FinishedMissionStore(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(MISSIONS_CREATE_TABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion == 2) {
|
||||
db.execSQL("ALTER TABLE " + MISSIONS_TABLE_NAME_v2 + " ADD COLUMN " + KEY_KIND + " TEXT;");
|
||||
oldVersion++;
|
||||
}
|
||||
|
||||
if (oldVersion == 3) {
|
||||
final String KEY_LOCATION = "location";
|
||||
final String KEY_NAME = "name";
|
||||
|
||||
db.execSQL(MISSIONS_CREATE_TABLE);
|
||||
|
||||
Cursor cursor = db.query(MISSIONS_TABLE_NAME_v2, null, null,
|
||||
null, null, null, KEY_TIMESTAMP);
|
||||
|
||||
int count = cursor.getCount();
|
||||
if (count > 0) {
|
||||
db.beginTransaction();
|
||||
while (cursor.moveToNext()) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KEY_SOURCE, cursor.getString(cursor.getColumnIndex(KEY_SOURCE)));
|
||||
values.put(KEY_DONE, cursor.getString(cursor.getColumnIndex(KEY_DONE)));
|
||||
values.put(KEY_TIMESTAMP, cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP)));
|
||||
values.put(KEY_KIND, cursor.getString(cursor.getColumnIndex(KEY_KIND)));
|
||||
values.put(KEY_PATH, Uri.fromFile(
|
||||
new File(
|
||||
cursor.getString(cursor.getColumnIndex(KEY_LOCATION)),
|
||||
cursor.getString(cursor.getColumnIndex(KEY_NAME))
|
||||
)
|
||||
).toString());
|
||||
|
||||
db.insert(FINISHED_MISSIONS_TABLE_NAME, null, values);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
cursor.close();
|
||||
db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all values of the download mission as ContentValues.
|
||||
*
|
||||
* @param downloadMission the download mission
|
||||
* @return the content values
|
||||
*/
|
||||
private ContentValues getValuesOfMission(@NonNull Mission downloadMission) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KEY_SOURCE, downloadMission.source);
|
||||
values.put(KEY_PATH, downloadMission.storage.getUri().toString());
|
||||
values.put(KEY_DONE, downloadMission.length);
|
||||
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
|
||||
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
|
||||
return values;
|
||||
}
|
||||
|
||||
private FinishedMission getMissionFromCursor(Cursor cursor) {
|
||||
if (cursor == null) throw new NullPointerException("cursor is null");
|
||||
|
||||
String kind = cursor.getString(cursor.getColumnIndex(KEY_KIND));
|
||||
if (kind == null || kind.isEmpty()) kind = "?";
|
||||
|
||||
String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH));
|
||||
|
||||
FinishedMission mission = new FinishedMission();
|
||||
|
||||
mission.source = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE));
|
||||
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
|
||||
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
|
||||
mission.kind = kind.charAt(0);
|
||||
|
||||
try {
|
||||
mission.storage = new StoredFileHelper(context, Uri.parse(path), "");
|
||||
} catch (Exception e) {
|
||||
Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e);
|
||||
mission.storage = new StoredFileHelper(path, "", "");
|
||||
}
|
||||
|
||||
return mission;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////
|
||||
// Data source methods
|
||||
///////////////////////////////////
|
||||
|
||||
public ArrayList<FinishedMission> loadFinishedMissions() {
|
||||
SQLiteDatabase database = getReadableDatabase();
|
||||
Cursor cursor = database.query(FINISHED_MISSIONS_TABLE_NAME, null, null,
|
||||
null, null, null, KEY_TIMESTAMP + " DESC");
|
||||
|
||||
int count = cursor.getCount();
|
||||
if (count == 0) return new ArrayList<>(1);
|
||||
|
||||
ArrayList<FinishedMission> result = new ArrayList<>(count);
|
||||
while (cursor.moveToNext()) {
|
||||
result.add(getMissionFromCursor(cursor));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void addFinishedMission(DownloadMission downloadMission) {
|
||||
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||
SQLiteDatabase database = getWritableDatabase();
|
||||
ContentValues values = getValuesOfMission(downloadMission);
|
||||
database.insert(FINISHED_MISSIONS_TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
public void deleteMission(Mission mission) {
|
||||
if (mission == null) throw new NullPointerException("mission is null");
|
||||
String path = mission.getDownloadedFileUri().toString();
|
||||
|
||||
SQLiteDatabase database = getWritableDatabase();
|
||||
|
||||
if (mission instanceof FinishedMission)
|
||||
database.delete(FINISHED_MISSIONS_TABLE_NAME, KEY_TIMESTAMP + " = ?, " + KEY_PATH + " = ?", new String[]{path});
|
||||
else
|
||||
throw new UnsupportedOperationException("DownloadMission");
|
||||
}
|
||||
|
||||
public void updateMission(Mission mission) {
|
||||
if (mission == null) throw new NullPointerException("mission is null");
|
||||
SQLiteDatabase database = getWritableDatabase();
|
||||
ContentValues values = getValuesOfMission(mission);
|
||||
String path = mission.getDownloadedFileUri().toString();
|
||||
|
||||
int rowsAffected;
|
||||
|
||||
if (mission instanceof FinishedMission)
|
||||
rowsAffected = database.update(FINISHED_MISSIONS_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{path});
|
||||
else
|
||||
throw new UnsupportedOperationException("DownloadMission");
|
||||
|
||||
if (rowsAffected != 1) {
|
||||
Log.e("FinishedMissionStore", "Expected 1 row to be affected by update but got " + rowsAffected);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue