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:
kapodamy 2019-04-05 14:45:39 -03:00
parent 9e34fee58c
commit f6b32823ba
62 changed files with 2439 additions and 1180 deletions

View file

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