more SAF implementation
* full support for Directory API (Android Lollipop or later) * best effort to handle any kind errors (missing file, revoked permissions, etc) and recover the download * implemented directory choosing * fix download database version upgrading * misc. cleanup * do not release permission on the old save path (if the user change the download directory) under SAF api
This commit is contained in:
parent
7ca7952790
commit
64626a7709
28 changed files with 946 additions and 589 deletions
|
|
@ -28,7 +28,7 @@ public class DownloadInitializer extends Thread {
|
|||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mMission.current > 0) mMission.resetState();
|
||||
if (mMission.current > 0) mMission.resetState(false,true, DownloadMission.ERROR_NOTHING);
|
||||
|
||||
int retryCount = 0;
|
||||
while (true) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ 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;
|
||||
|
|
@ -86,7 +85,7 @@ public class DownloadMission extends Mission {
|
|||
/**
|
||||
* the post-processing algorithm instance
|
||||
*/
|
||||
public transient Postprocessing psAlgorithm;
|
||||
public Postprocessing psAlgorithm;
|
||||
|
||||
/**
|
||||
* The current resource to download, see {@code urls[current]} and {@code offsets[current]}
|
||||
|
|
@ -483,7 +482,7 @@ public class DownloadMission extends Mission {
|
|||
if (init != null && Thread.currentThread() != init && init.isAlive()) {
|
||||
init.interrupt();
|
||||
synchronized (blockState) {
|
||||
resetState();
|
||||
resetState(false, true, ERROR_NOTHING);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -525,10 +524,18 @@ public class DownloadMission extends Mission {
|
|||
return res;
|
||||
}
|
||||
|
||||
void resetState() {
|
||||
|
||||
/**
|
||||
* Resets the mission state
|
||||
*
|
||||
* @param rollback {@code true} true to forget all progress, otherwise, {@code false}
|
||||
* @param persistChanges {@code true} to commit changes to the metadata file, otherwise, {@code false}
|
||||
*/
|
||||
public void resetState(boolean rollback, boolean persistChanges, int errorCode) {
|
||||
done = 0;
|
||||
blocks = -1;
|
||||
errCode = ERROR_NOTHING;
|
||||
errCode = errorCode;
|
||||
errObject = null;
|
||||
fallback = false;
|
||||
unknownLength = false;
|
||||
finishCount = 0;
|
||||
|
|
@ -537,7 +544,10 @@ public class DownloadMission extends Mission {
|
|||
blockState.clear();
|
||||
threads = new Thread[0];
|
||||
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
if (rollback) current = 0;
|
||||
|
||||
if (persistChanges)
|
||||
Utility.writeToFile(metadata, DownloadMission.this);
|
||||
}
|
||||
|
||||
private void initializer() {
|
||||
|
|
@ -633,33 +643,22 @@ public class DownloadMission extends Mission {
|
|||
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();
|
||||
return errCode == ERROR_PROGRESS_LOST || storage == null || storage.isInvalid() || !storage.existsAsFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whatever is possible to start the mission
|
||||
*
|
||||
* @return {@code true} is this mission is "sane", otherwise, {@code false}
|
||||
* @return {@code true} is this mission its "healthy", otherwise, {@code false}
|
||||
*/
|
||||
public boolean canDownload() {
|
||||
return !(isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) && !isFinished() && !hasInvalidStorage();
|
||||
public boolean isCorrupt() {
|
||||
return (isPsFailed() || errCode == ERROR_POSTPROCESSING_HOLD) || isFinished() || hasInvalidStorage();
|
||||
}
|
||||
|
||||
private boolean doPostprocessing() {
|
||||
|
|
|
|||
|
|
@ -137,6 +137,10 @@ public class DownloadRunnable extends Thread {
|
|||
mMission.setThreadBytePosition(mId, total);// download paused, save progress for this block
|
||||
|
||||
} catch (Exception e) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, mId + ": position=" + blockPosition + " total=" + total + " stopped due exception", e);
|
||||
}
|
||||
|
||||
mMission.setThreadBytePosition(mId, total);
|
||||
|
||||
if (!mMission.running || e instanceof ClosedByInterruptException) break;
|
||||
|
|
@ -146,10 +150,6 @@ public class DownloadRunnable extends Thread {
|
|||
break;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, mId + ":position " + blockPosition + " retrying due exception", e);
|
||||
}
|
||||
|
||||
retry = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,5 +12,7 @@ public class FinishedMission extends Mission {
|
|||
length = mission.length;// ¿or mission.done?
|
||||
timestamp = mission.timestamp;
|
||||
kind = mission.kind;
|
||||
storage = mission.storage;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package us.shandian.giga.get;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
|
@ -36,15 +35,6 @@ public abstract class Mission implements Serializable {
|
|||
*/
|
||||
public StoredFileHelper storage;
|
||||
|
||||
/**
|
||||
* get the target file on the storage
|
||||
*
|
||||
* @return File object
|
||||
*/
|
||||
public Uri getDownloadedFileUri() {
|
||||
return storage.getUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the downloaded file
|
||||
*
|
||||
|
|
@ -52,7 +42,7 @@ public abstract class Mission implements Serializable {
|
|||
*/
|
||||
public boolean delete() {
|
||||
if (storage != null) return storage.delete();
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,6 +55,6 @@ public abstract class Mission implements Serializable {
|
|||
public String toString() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(timestamp);
|
||||
return "[" + calendar.getTime().toString() + "] " + getDownloadedFileUri().getPath();
|
||||
return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
/**
|
||||
* The table name of download missions
|
||||
*/
|
||||
private static final String FINISHED_MISSIONS_TABLE_NAME = "finished_missions";
|
||||
private static final String FINISHED_TABLE_NAME = "finished_missions";
|
||||
|
||||
/**
|
||||
* The key to the urls of a mission
|
||||
|
|
@ -58,7 +58,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
* The statement to create the table
|
||||
*/
|
||||
private static final String MISSIONS_CREATE_TABLE =
|
||||
"CREATE TABLE " + FINISHED_MISSIONS_TABLE_NAME + " (" +
|
||||
"CREATE TABLE " + FINISHED_TABLE_NAME + " (" +
|
||||
KEY_PATH + " TEXT NOT NULL, " +
|
||||
KEY_SOURCE + " TEXT NOT NULL, " +
|
||||
KEY_DONE + " INTEGER NOT NULL, " +
|
||||
|
|
@ -111,7 +111,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
)
|
||||
).toString());
|
||||
|
||||
db.insert(FINISHED_MISSIONS_TABLE_NAME, null, values);
|
||||
db.insert(FINISHED_TABLE_NAME, null, values);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
|
|
@ -154,10 +154,10 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
mission.kind = kind.charAt(0);
|
||||
|
||||
try {
|
||||
mission.storage = new StoredFileHelper(context, Uri.parse(path), "");
|
||||
mission.storage = new StoredFileHelper(context,null, Uri.parse(path), "");
|
||||
} catch (Exception e) {
|
||||
Log.e("FinishedMissionStore", "failed to load the storage path of: " + path, e);
|
||||
mission.storage = new StoredFileHelper(path, "", "");
|
||||
mission.storage = new StoredFileHelper(null, path, "", "");
|
||||
}
|
||||
|
||||
return mission;
|
||||
|
|
@ -170,7 +170,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
|
||||
public ArrayList<FinishedMission> loadFinishedMissions() {
|
||||
SQLiteDatabase database = getReadableDatabase();
|
||||
Cursor cursor = database.query(FINISHED_MISSIONS_TABLE_NAME, null, null,
|
||||
Cursor cursor = database.query(FINISHED_TABLE_NAME, null, null,
|
||||
null, null, null, KEY_TIMESTAMP + " DESC");
|
||||
|
||||
int count = cursor.getCount();
|
||||
|
|
@ -188,33 +188,47 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||
if (downloadMission == null) throw new NullPointerException("downloadMission is null");
|
||||
SQLiteDatabase database = getWritableDatabase();
|
||||
ContentValues values = getValuesOfMission(downloadMission);
|
||||
database.insert(FINISHED_MISSIONS_TABLE_NAME, null, values);
|
||||
database.insert(FINISHED_TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
public void deleteMission(Mission mission) {
|
||||
if (mission == null) throw new NullPointerException("mission is null");
|
||||
String path = mission.getDownloadedFileUri().toString();
|
||||
String ts = String.valueOf(mission.timestamp);
|
||||
|
||||
SQLiteDatabase database = getWritableDatabase();
|
||||
|
||||
if (mission instanceof FinishedMission)
|
||||
database.delete(FINISHED_MISSIONS_TABLE_NAME, KEY_TIMESTAMP + " = ?, " + KEY_PATH + " = ?", new String[]{path});
|
||||
else
|
||||
if (mission instanceof FinishedMission) {
|
||||
if (mission.storage.isInvalid()) {
|
||||
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ?", new String[]{ts});
|
||||
} else {
|
||||
database.delete(FINISHED_TABLE_NAME, KEY_TIMESTAMP + " = ? AND " + KEY_PATH + " = ?", new String[]{
|
||||
ts, mission.storage.getUri().toString()
|
||||
});
|
||||
}
|
||||
} 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();
|
||||
String ts = String.valueOf(mission.timestamp);
|
||||
|
||||
int rowsAffected;
|
||||
|
||||
if (mission instanceof FinishedMission)
|
||||
rowsAffected = database.update(FINISHED_MISSIONS_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{path});
|
||||
else
|
||||
if (mission instanceof FinishedMission) {
|
||||
if (mission.storage.isInvalid()) {
|
||||
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_TIMESTAMP + " = ?", new String[]{ts});
|
||||
} else {
|
||||
rowsAffected = database.update(FINISHED_TABLE_NAME, values, KEY_PATH + " = ?", new String[]{
|
||||
mission.storage.getUri().toString()
|
||||
});
|
||||
}
|
||||
} 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