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:
kapodamy 2019-04-09 18:38:34 -03:00
parent 7ca7952790
commit 64626a7709
28 changed files with 946 additions and 589 deletions

View file

@ -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) {

View file

@ -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() {

View file

@ -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;
}
}

View file

@ -12,5 +12,7 @@ public class FinishedMission extends Mission {
length = mission.length;// ¿or mission.done?
timestamp = mission.timestamp;
kind = mission.kind;
storage = mission.storage;
}
}

View file

@ -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());
}
}

View file

@ -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);