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
|
|
@ -12,7 +12,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
|
||||
private final static int QUEUE_BUFFER_SIZE = 8 * 1024;// 8 KiB
|
||||
private final static int NOTIFY_BYTES_INTERVAL = 64 * 1024;// 64 KiB
|
||||
private final static int THRESHOLD_AUX_LENGTH = 3 * 1024 * 1024;// 3 MiB
|
||||
private final static int THRESHOLD_AUX_LENGTH = 15 * 1024 * 1024;// 15 MiB
|
||||
|
||||
private OffsetChecker callback;
|
||||
|
||||
|
|
@ -44,41 +44,84 @@ public class CircularFileWriter extends SharpStream {
|
|||
reportPosition = NOTIFY_BYTES_INTERVAL;
|
||||
}
|
||||
|
||||
private void flushAuxiliar() throws IOException {
|
||||
private void flushAuxiliar(long amount) throws IOException {
|
||||
if (aux.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean underflow = out.getOffset() >= out.length;
|
||||
|
||||
out.flush();
|
||||
aux.flush();
|
||||
|
||||
boolean underflow = aux.offset < aux.length || out.offset < out.length;
|
||||
|
||||
aux.target.seek(0);
|
||||
out.target.seek(out.length);
|
||||
|
||||
long length = aux.length;
|
||||
out.length += aux.length;
|
||||
|
||||
long length = amount;
|
||||
while (length > 0) {
|
||||
int read = (int) Math.min(length, Integer.MAX_VALUE);
|
||||
read = aux.target.read(aux.queue, 0, Math.min(read, aux.queue.length));
|
||||
|
||||
if (read < 1) {
|
||||
amount -= length;
|
||||
break;
|
||||
}
|
||||
|
||||
out.writeProof(aux.queue, read);
|
||||
length -= read;
|
||||
}
|
||||
|
||||
if (underflow) {
|
||||
out.offset += aux.offset;
|
||||
out.target.seek(out.offset);
|
||||
if (out.offset >= out.length) {
|
||||
// calculate the aux underflow pointer
|
||||
if (aux.offset < amount) {
|
||||
out.offset += aux.offset;
|
||||
aux.offset = 0;
|
||||
out.target.seek(out.offset);
|
||||
} else {
|
||||
aux.offset -= amount;
|
||||
out.offset = out.length + amount;
|
||||
}
|
||||
} else {
|
||||
aux.offset = 0;
|
||||
}
|
||||
} else {
|
||||
out.offset = out.length;
|
||||
out.offset += amount;
|
||||
aux.offset -= amount;
|
||||
}
|
||||
|
||||
out.length += amount;
|
||||
|
||||
if (out.length > maxLengthKnown) {
|
||||
maxLengthKnown = out.length;
|
||||
}
|
||||
|
||||
if (amount < aux.length) {
|
||||
// move the excess data to the beginning of the file
|
||||
long readOffset = amount;
|
||||
long writeOffset = 0;
|
||||
byte[] buffer = new byte[128 * 1024]; // 128 KiB
|
||||
|
||||
aux.length -= amount;
|
||||
length = aux.length;
|
||||
while (length > 0) {
|
||||
int read = (int) Math.min(length, Integer.MAX_VALUE);
|
||||
read = aux.target.read(buffer, 0, Math.min(read, buffer.length));
|
||||
|
||||
aux.target.seek(writeOffset);
|
||||
aux.writeProof(buffer, read);
|
||||
|
||||
writeOffset += read;
|
||||
readOffset += read;
|
||||
length -= read;
|
||||
|
||||
aux.target.seek(readOffset);
|
||||
}
|
||||
|
||||
aux.target.setLength(aux.length);
|
||||
return;
|
||||
}
|
||||
|
||||
if (aux.length > THRESHOLD_AUX_LENGTH) {
|
||||
aux.target.setLength(THRESHOLD_AUX_LENGTH);// or setLength(0);
|
||||
}
|
||||
|
|
@ -94,7 +137,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public long finalizeFile() throws IOException {
|
||||
flushAuxiliar();
|
||||
flushAuxiliar(aux.length);
|
||||
|
||||
out.flush();
|
||||
|
||||
|
|
@ -148,7 +191,7 @@ public class CircularFileWriter extends SharpStream {
|
|||
if (end == -1) {
|
||||
available = Integer.MAX_VALUE;
|
||||
} else if (end < offsetOut) {
|
||||
throw new IOException("The reported offset is invalid: " + String.valueOf(offsetOut));
|
||||
throw new IOException("The reported offset is invalid: " + end + "<" + offsetOut);
|
||||
} else {
|
||||
available = end - offsetOut;
|
||||
}
|
||||
|
|
@ -167,16 +210,10 @@ public class CircularFileWriter extends SharpStream {
|
|||
length = aux.length + len;
|
||||
}
|
||||
|
||||
if (length > available || length < THRESHOLD_AUX_LENGTH) {
|
||||
aux.write(b, off, len);
|
||||
} else {
|
||||
if (underflow) {
|
||||
aux.write(b, off, len);
|
||||
flushAuxiliar();
|
||||
} else {
|
||||
flushAuxiliar();
|
||||
out.write(b, off, len);// write directly on the output
|
||||
}
|
||||
aux.write(b, off, len);
|
||||
|
||||
if (length >= THRESHOLD_AUX_LENGTH && length <= available) {
|
||||
flushAuxiliar(available);
|
||||
}
|
||||
} else {
|
||||
if (underflow) {
|
||||
|
|
@ -234,8 +271,13 @@ public class CircularFileWriter extends SharpStream {
|
|||
@Override
|
||||
public void seek(long offset) throws IOException {
|
||||
long total = out.length + aux.length;
|
||||
|
||||
if (offset == total) {
|
||||
return;// nothing to do
|
||||
// do not ignore the seek offset if a underflow exists
|
||||
long relativeOffset = out.getOffset() + aux.getOffset();
|
||||
if (relativeOffset == total) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// flush everything, avoid any underflow
|
||||
|
|
@ -409,6 +451,9 @@ public class CircularFileWriter extends SharpStream {
|
|||
}
|
||||
|
||||
protected void seek(long absoluteOffset) throws IOException {
|
||||
if (absoluteOffset == offset) {
|
||||
return;// nothing to do
|
||||
}
|
||||
offset = absoluteOffset;
|
||||
target.seek(absoluteOffset);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,4 +137,9 @@ public class FileStreamSAF extends SharpStream {
|
|||
public void seek(long offset) throws IOException {
|
||||
channel.position(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long length() throws IOException {
|
||||
return channel.size();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import android.annotation.TargetApi;
|
|||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresApi;
|
||||
|
|
@ -13,8 +15,13 @@ import android.support.v4.provider.DocumentFile;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
|
||||
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
|
||||
|
||||
|
||||
public class StoredDirectoryHelper {
|
||||
public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
|
|
@ -22,14 +29,27 @@ public class StoredDirectoryHelper {
|
|||
private File ioTree;
|
||||
private DocumentFile docTree;
|
||||
|
||||
private ContentResolver contentResolver;
|
||||
private Context context;
|
||||
|
||||
private String tag;
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException {
|
||||
this.contentResolver = context.getContentResolver();
|
||||
this.tag = tag;
|
||||
|
||||
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) {
|
||||
this.ioTree = new File(URI.create(path.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
this.context = context;
|
||||
|
||||
try {
|
||||
this.context.getContentResolver().takePersistableUriPermission(path, PERMISSION_FLAGS);
|
||||
} catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
this.docTree = DocumentFile.fromTreeUri(context, path);
|
||||
|
||||
if (this.docTree == null)
|
||||
|
|
@ -37,23 +57,75 @@ public class StoredDirectoryHelper {
|
|||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public StoredDirectoryHelper(@NonNull String location, String tag) {
|
||||
public StoredDirectoryHelper(@NonNull URI location, String tag) {
|
||||
ioTree = new File(location);
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StoredFileHelper createFile(String filename, String mime) {
|
||||
return createFile(filename, mime, false);
|
||||
}
|
||||
|
||||
public StoredFileHelper createUniqueFile(String name, String mime) {
|
||||
ArrayList<String> matches = new ArrayList<>();
|
||||
String[] filename = splitFilename(name);
|
||||
String lcFilename = filename[0].toLowerCase();
|
||||
|
||||
if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
for (File file : ioTree.listFiles())
|
||||
addIfStartWith(matches, lcFilename, file.getName());
|
||||
} else {
|
||||
// warning: SAF file listing is very slow
|
||||
Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||
docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri())
|
||||
);
|
||||
|
||||
String[] projection = new String[]{COLUMN_DISPLAY_NAME};
|
||||
String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%";
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
|
||||
try (Cursor cursor = cr.query(docTreeChildren, projection, selection, new String[]{lcFilename}, null)) {
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext())
|
||||
addIfStartWith(matches, lcFilename, cursor.getString(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.size() < 1) {
|
||||
return createFile(name, mime, true);
|
||||
} else {
|
||||
// check if the filename is in use
|
||||
String lcName = name.toLowerCase();
|
||||
for (String testName : matches) {
|
||||
if (testName.equals(lcName)) {
|
||||
lcName = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// check if not in use
|
||||
if (lcName != null) return createFile(name, mime, true);
|
||||
}
|
||||
|
||||
Collections.sort(matches, String::compareTo);
|
||||
|
||||
for (int i = 1; i < 1000; i++) {
|
||||
if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0)
|
||||
return createFile(makeFileName(filename[0], i, filename[1]), mime, true);
|
||||
}
|
||||
|
||||
return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime, false);
|
||||
}
|
||||
|
||||
private StoredFileHelper createFile(String filename, String mime, boolean safe) {
|
||||
StoredFileHelper storage;
|
||||
|
||||
try {
|
||||
if (docTree == null) {
|
||||
storage = new StoredFileHelper(ioTree, filename, tag);
|
||||
storage.sourceTree = Uri.fromFile(ioTree).toString();
|
||||
} else {
|
||||
storage = new StoredFileHelper(docTree, contentResolver, filename, mime, tag);
|
||||
storage.sourceTree = docTree.getUri().toString();
|
||||
}
|
||||
if (docTree == null)
|
||||
storage = new StoredFileHelper(ioTree, filename, mime);
|
||||
else
|
||||
storage = new StoredFileHelper(context, docTree, filename, mime, safe);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -63,67 +135,6 @@ public class StoredDirectoryHelper {
|
|||
return storage;
|
||||
}
|
||||
|
||||
public StoredFileHelper createUniqueFile(String filename, String mime) {
|
||||
ArrayList<String> existingNames = new ArrayList<>(50);
|
||||
|
||||
String ext;
|
||||
|
||||
int dotIndex = filename.lastIndexOf('.');
|
||||
if (dotIndex < 0 || (dotIndex == filename.length() - 1)) {
|
||||
ext = "";
|
||||
} else {
|
||||
ext = filename.substring(dotIndex);
|
||||
filename = filename.substring(0, dotIndex - 1);
|
||||
}
|
||||
|
||||
String name;
|
||||
if (docTree == null) {
|
||||
for (File file : ioTree.listFiles()) {
|
||||
name = file.getName().toLowerCase();
|
||||
if (name.startsWith(filename)) existingNames.add(name);
|
||||
}
|
||||
} else {
|
||||
for (DocumentFile file : docTree.listFiles()) {
|
||||
name = file.getName();
|
||||
if (name == null) continue;
|
||||
name = name.toLowerCase();
|
||||
if (name.startsWith(filename)) existingNames.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
boolean free = true;
|
||||
String lwFilename = filename.toLowerCase();
|
||||
for (String testName : existingNames) {
|
||||
if (testName.equals(lwFilename)) {
|
||||
free = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (free) return createFile(filename, mime);
|
||||
|
||||
String[] sortedNames = existingNames.toArray(new String[0]);
|
||||
Arrays.sort(sortedNames);
|
||||
|
||||
String newName;
|
||||
int downloadIndex = 0;
|
||||
do {
|
||||
newName = filename + " (" + downloadIndex + ")" + ext;
|
||||
++downloadIndex;
|
||||
if (downloadIndex == 1000) { // Probably an error on our side
|
||||
newName = System.currentTimeMillis() + ext;
|
||||
break;
|
||||
}
|
||||
} while (Arrays.binarySearch(sortedNames, newName) >= 0);
|
||||
|
||||
|
||||
return createFile(newName, mime);
|
||||
}
|
||||
|
||||
public boolean isDirect() {
|
||||
return docTree == null;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri();
|
||||
}
|
||||
|
|
@ -136,34 +147,18 @@ public class StoredDirectoryHelper {
|
|||
return tag;
|
||||
}
|
||||
|
||||
public void acquirePermissions() throws IOException {
|
||||
if (docTree == null) return;
|
||||
|
||||
try {
|
||||
contentResolver.takePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS);
|
||||
} catch (Throwable e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void revokePermissions() throws IOException {
|
||||
if (docTree == null) return;
|
||||
|
||||
try {
|
||||
contentResolver.releasePersistableUriPermission(docTree.getUri(), PERMISSION_FLAGS);
|
||||
} catch (Throwable e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Uri findFile(String filename) {
|
||||
if (docTree == null)
|
||||
return Uri.fromFile(new File(ioTree, filename));
|
||||
if (docTree == null) {
|
||||
File res = new File(ioTree, filename);
|
||||
return res.exists() ? Uri.fromFile(res) : null;
|
||||
}
|
||||
|
||||
// findFile() method is very slow
|
||||
DocumentFile file = docTree.findFile(filename);
|
||||
DocumentFile res = findFileSAFHelper(context, docTree, filename);
|
||||
return res == null ? null : res.getUri();
|
||||
}
|
||||
|
||||
return file == null ? null : file.getUri();
|
||||
public boolean canWrite() {
|
||||
return docTree == null ? ioTree.canWrite() : docTree.canWrite();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
@ -172,4 +167,76 @@ public class StoredDirectoryHelper {
|
|||
return docTree == null ? Uri.fromFile(ioTree).toString() : docTree.getUri().toString();
|
||||
}
|
||||
|
||||
|
||||
////////////////////
|
||||
// Utils
|
||||
///////////////////
|
||||
|
||||
private static void addIfStartWith(ArrayList<String> list, @NonNull String base, String str) {
|
||||
if (str == null || str.isEmpty()) return;
|
||||
str = str.toLowerCase();
|
||||
if (str.startsWith(base)) list.add(str);
|
||||
}
|
||||
|
||||
private static String[] splitFilename(@NonNull String filename) {
|
||||
int dotIndex = filename.lastIndexOf('.');
|
||||
|
||||
if (dotIndex < 0 || (dotIndex == filename.length() - 1))
|
||||
return new String[]{filename, ""};
|
||||
|
||||
return new String[]{filename.substring(0, dotIndex), filename.substring(dotIndex)};
|
||||
}
|
||||
|
||||
private static String makeFileName(String name, int idx, String ext) {
|
||||
return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast (but not enough) file/directory finder under the storage access framework
|
||||
*
|
||||
* @param context The context
|
||||
* @param tree Directory where search
|
||||
* @param filename Target filename
|
||||
* @return A {@link android.support.v4.provider.DocumentFile} contain the reference, otherwise, null
|
||||
*/
|
||||
static DocumentFile findFileSAFHelper(@Nullable Context context, DocumentFile tree, String filename) {
|
||||
if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return tree.findFile(filename);// warning: this is very slow
|
||||
}
|
||||
|
||||
if (!tree.canRead()) return null;// missing read permission
|
||||
|
||||
final int name = 0;
|
||||
final int documentId = 1;
|
||||
|
||||
// LOWER() SQL function is not supported
|
||||
String selection = COLUMN_DISPLAY_NAME + " = ?";
|
||||
//String selection = COLUMN_DISPLAY_NAME + " LIKE ?%";
|
||||
|
||||
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||
tree.getUri(), DocumentsContract.getDocumentId(tree.getUri())
|
||||
);
|
||||
String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID};
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
|
||||
filename = filename.toLowerCase();
|
||||
|
||||
try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, new String[]{filename}, null)) {
|
||||
if (cursor == null) return null;
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
if (cursor.isNull(name) || !cursor.getString(name).toLowerCase().startsWith(filename))
|
||||
continue;
|
||||
|
||||
return DocumentFile.fromSingleUri(
|
||||
context, DocumentsContract.buildDocumentUriUsingTree(
|
||||
tree.getUri(), cursor.getString(documentId)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import android.net.Uri;
|
|||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.provider.DocumentFile;
|
||||
|
||||
|
|
@ -25,79 +26,52 @@ public class StoredFileHelper implements Serializable {
|
|||
private transient DocumentFile docFile;
|
||||
private transient DocumentFile docTree;
|
||||
private transient File ioFile;
|
||||
private transient ContentResolver contentResolver;
|
||||
private transient Context context;
|
||||
|
||||
protected String source;
|
||||
String sourceTree;
|
||||
private String sourceTree;
|
||||
|
||||
protected String tag;
|
||||
|
||||
private String srcName;
|
||||
private String srcType;
|
||||
|
||||
public StoredFileHelper(String filename, String mime, String tag) {
|
||||
public StoredFileHelper(@Nullable Uri parent, String filename, String mime, String tag) {
|
||||
this.source = null;// this instance will be "invalid" see invalidate()/isInvalid() methods
|
||||
|
||||
this.srcName = filename;
|
||||
this.srcType = mime == null ? DEFAULT_MIME : mime;
|
||||
if (parent != null) this.sourceTree = parent.toString();
|
||||
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
StoredFileHelper(DocumentFile tree, ContentResolver contentResolver, String filename, String mime, String tag) throws IOException {
|
||||
StoredFileHelper(@Nullable Context context, DocumentFile tree, String filename, String mime, boolean safe) throws IOException {
|
||||
this.docTree = tree;
|
||||
this.contentResolver = contentResolver;
|
||||
this.context = context;
|
||||
|
||||
// this is very slow, because SAF does not allow overwrite
|
||||
DocumentFile res = this.docTree.findFile(filename);
|
||||
DocumentFile res;
|
||||
|
||||
if (res != null && res.exists() && res.isDirectory()) {
|
||||
if (!res.delete())
|
||||
throw new IOException("Directory with the same name found but cannot delete");
|
||||
res = null;
|
||||
}
|
||||
|
||||
if (res == null) {
|
||||
res = this.docTree.createFile(mime == null ? DEFAULT_MIME : mime, filename);
|
||||
if (safe) {
|
||||
// no conflicts (the filename is not in use)
|
||||
res = this.docTree.createFile(mime, filename);
|
||||
if (res == null) throw new IOException("Cannot create the file");
|
||||
} else {
|
||||
res = createSAF(context, mime, filename);
|
||||
}
|
||||
|
||||
this.docFile = res;
|
||||
this.source = res.getUri().toString();
|
||||
this.srcName = getName();
|
||||
this.srcType = getType();
|
||||
|
||||
this.source = docFile.getUri().toString();
|
||||
this.sourceTree = docTree.getUri().toString();
|
||||
|
||||
this.srcName = this.docFile.getName();
|
||||
this.srcType = this.docFile.getType();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public StoredFileHelper(Context context, @NonNull Uri path, String tag) throws IOException {
|
||||
this.source = path.toString();
|
||||
this.tag = tag;
|
||||
|
||||
if (path.getScheme() == null || path.getScheme().equalsIgnoreCase("file")) {
|
||||
this.ioFile = new File(URI.create(this.source));
|
||||
} else {
|
||||
DocumentFile file = DocumentFile.fromSingleUri(context, path);
|
||||
if (file == null)
|
||||
throw new UnsupportedOperationException("Cannot get the file via SAF");
|
||||
|
||||
this.contentResolver = context.getContentResolver();
|
||||
this.docFile = file;
|
||||
|
||||
try {
|
||||
this.contentResolver.takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
} catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
this.srcName = getName();
|
||||
this.srcType = getType();
|
||||
}
|
||||
|
||||
public StoredFileHelper(File location, String filename, String tag) throws IOException {
|
||||
StoredFileHelper(File location, String filename, String mime) throws IOException {
|
||||
this.ioFile = new File(location, filename);
|
||||
this.tag = tag;
|
||||
|
||||
if (this.ioFile.exists()) {
|
||||
if (!this.ioFile.isFile() && !this.ioFile.delete())
|
||||
|
|
@ -108,22 +82,58 @@ public class StoredFileHelper implements Serializable {
|
|||
}
|
||||
|
||||
this.source = Uri.fromFile(this.ioFile).toString();
|
||||
this.sourceTree = Uri.fromFile(location).toString();
|
||||
|
||||
this.srcName = ioFile.getName();
|
||||
this.srcType = mime;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public StoredFileHelper(Context context, @Nullable Uri parent, @NonNull Uri path, String tag) throws IOException {
|
||||
this.tag = tag;
|
||||
this.source = path.toString();
|
||||
|
||||
if (path.getScheme() == null || path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
|
||||
this.ioFile = new File(URI.create(this.source));
|
||||
} else {
|
||||
DocumentFile file = DocumentFile.fromSingleUri(context, path);
|
||||
|
||||
if (file == null) throw new RuntimeException("SAF not available");
|
||||
|
||||
this.context = context;
|
||||
|
||||
if (file.getName() == null) {
|
||||
this.source = null;
|
||||
return;
|
||||
} else {
|
||||
this.docFile = file;
|
||||
takePermissionSAF();
|
||||
}
|
||||
}
|
||||
|
||||
if (parent != null) {
|
||||
if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme()))
|
||||
this.docTree = DocumentFile.fromTreeUri(context, parent);
|
||||
|
||||
this.sourceTree = parent.toString();
|
||||
}
|
||||
|
||||
this.srcName = getName();
|
||||
this.srcType = getType();
|
||||
}
|
||||
|
||||
|
||||
public static StoredFileHelper deserialize(@NonNull StoredFileHelper storage, Context context) throws IOException {
|
||||
Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree);
|
||||
|
||||
if (storage.isInvalid())
|
||||
return new StoredFileHelper(storage.srcName, storage.srcType, storage.tag);
|
||||
return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag);
|
||||
|
||||
StoredFileHelper instance = new StoredFileHelper(context, Uri.parse(storage.source), storage.tag);
|
||||
StoredFileHelper instance = new StoredFileHelper(context, treeUri, Uri.parse(storage.source), storage.tag);
|
||||
|
||||
if (storage.sourceTree != null) {
|
||||
instance.docTree = DocumentFile.fromTreeUri(context, Uri.parse(instance.sourceTree));
|
||||
|
||||
if (instance.docTree == null)
|
||||
throw new IOException("Cannot deserialize the tree, ¿revoked permissions?");
|
||||
}
|
||||
// under SAF, if the target document is deleted, conserve the filename and mime
|
||||
if (instance.srcName == null) instance.srcName = storage.srcName;
|
||||
if (instance.srcType == null) instance.srcType = storage.srcType;
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
|
@ -143,13 +153,14 @@ public class StoredFileHelper implements Serializable {
|
|||
who.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
|
||||
public SharpStream getStream() throws IOException {
|
||||
invalid();
|
||||
|
||||
if (docFile == null)
|
||||
return new FileStream(ioFile);
|
||||
else
|
||||
return new FileStreamSAF(contentResolver, docFile.getUri());
|
||||
return new FileStreamSAF(context.getContentResolver(), docFile.getUri());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -173,6 +184,12 @@ public class StoredFileHelper implements Serializable {
|
|||
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri();
|
||||
}
|
||||
|
||||
public Uri getParentUri() {
|
||||
invalid();
|
||||
|
||||
return sourceTree == null ? null : Uri.parse(sourceTree);
|
||||
}
|
||||
|
||||
public void truncate() throws IOException {
|
||||
invalid();
|
||||
|
||||
|
|
@ -182,17 +199,17 @@ public class StoredFileHelper implements Serializable {
|
|||
}
|
||||
|
||||
public boolean delete() {
|
||||
invalid();
|
||||
|
||||
if (source == null) return true;
|
||||
if (docFile == null) return ioFile.delete();
|
||||
|
||||
|
||||
boolean res = docFile.delete();
|
||||
|
||||
try {
|
||||
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
|
||||
contentResolver.releasePersistableUriPermission(docFile.getUri(), flags);
|
||||
context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags);
|
||||
} catch (Exception ex) {
|
||||
// ¿what happen?
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
return res;
|
||||
|
|
@ -209,18 +226,22 @@ public class StoredFileHelper implements Serializable {
|
|||
return docFile == null ? ioFile.canWrite() : docFile.canWrite();
|
||||
}
|
||||
|
||||
public File getIOFile() {
|
||||
return ioFile;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
if (source == null) return srcName;
|
||||
return docFile == null ? ioFile.getName() : docFile.getName();
|
||||
if (source == null)
|
||||
return srcName;
|
||||
else if (docFile == null)
|
||||
return ioFile.getName();
|
||||
|
||||
String name = docFile.getName();
|
||||
return name == null ? srcName : name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
if (source == null) return srcType;
|
||||
return docFile == null ? DEFAULT_MIME : docFile.getType();// not obligatory for Java IO
|
||||
if (source == null || docFile == null)
|
||||
return srcType;
|
||||
|
||||
String type = docFile.getType();
|
||||
return type == null ? srcType : type;
|
||||
}
|
||||
|
||||
public String getTag() {
|
||||
|
|
@ -231,29 +252,41 @@ public class StoredFileHelper implements Serializable {
|
|||
if (source == null) return false;
|
||||
|
||||
boolean exists = docFile == null ? ioFile.exists() : docFile.exists();
|
||||
boolean asFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical?
|
||||
boolean isFile = docFile == null ? ioFile.isFile() : docFile.isFile();// ¿docFile.isVirtual() means is no-physical?
|
||||
|
||||
return exists && asFile;
|
||||
return exists && isFile;
|
||||
}
|
||||
|
||||
public boolean create() {
|
||||
invalid();
|
||||
boolean result;
|
||||
|
||||
if (docFile == null) {
|
||||
try {
|
||||
return ioFile.createNewFile();
|
||||
result = ioFile.createNewFile();
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
} else if (docTree == null) {
|
||||
result = false;
|
||||
} else {
|
||||
if (!docTree.canRead() || !docTree.canWrite()) return false;
|
||||
try {
|
||||
docFile = createSAF(context, srcType, srcName);
|
||||
if (docFile == null || docFile.getName() == null) return false;
|
||||
result = true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (docTree == null || docFile.getName() == null) return false;
|
||||
if (result) {
|
||||
source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString();
|
||||
srcName = getName();
|
||||
srcType = getType();
|
||||
}
|
||||
|
||||
DocumentFile res = docTree.createFile(docFile.getName(), docFile.getType() == null ? DEFAULT_MIME : docFile.getType());
|
||||
if (res == null) return false;
|
||||
|
||||
docFile = res;
|
||||
return true;
|
||||
return result;
|
||||
}
|
||||
|
||||
public void invalidate() {
|
||||
|
|
@ -264,20 +297,25 @@ public class StoredFileHelper implements Serializable {
|
|||
|
||||
source = null;
|
||||
|
||||
sourceTree = null;
|
||||
docTree = null;
|
||||
docFile = null;
|
||||
ioFile = null;
|
||||
contentResolver = null;
|
||||
}
|
||||
|
||||
private void invalid() {
|
||||
if (source == null)
|
||||
throw new IllegalStateException("In invalid state");
|
||||
context = null;
|
||||
}
|
||||
|
||||
public boolean equals(StoredFileHelper storage) {
|
||||
if (this.isInvalid() != storage.isInvalid()) return false;
|
||||
if (this == storage) return true;
|
||||
|
||||
// note: do not compare tags, files can have the same parent folder
|
||||
//if (stringMismatch(this.tag, storage.tag)) return false;
|
||||
|
||||
if (stringMismatch(getLowerCase(this.sourceTree), getLowerCase(this.sourceTree)))
|
||||
return false;
|
||||
|
||||
if (this.isInvalid() || storage.isInvalid()) {
|
||||
return this.srcName.equalsIgnoreCase(storage.srcName) && this.srcType.equalsIgnoreCase(storage.srcType);
|
||||
}
|
||||
|
||||
if (this.isDirect() != storage.isDirect()) return false;
|
||||
|
||||
if (this.isDirect())
|
||||
|
|
@ -298,4 +336,46 @@ public class StoredFileHelper implements Serializable {
|
|||
else
|
||||
return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree) + " tag=" + tag;
|
||||
}
|
||||
|
||||
|
||||
private void invalid() {
|
||||
if (source == null)
|
||||
throw new IllegalStateException("In invalid state");
|
||||
}
|
||||
|
||||
private void takePermissionSAF() throws IOException {
|
||||
try {
|
||||
context.getContentResolver().takePersistableUriPermission(docFile.getUri(), StoredDirectoryHelper.PERMISSION_FLAGS);
|
||||
} catch (Exception e) {
|
||||
if (docFile.getName() == null) throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private DocumentFile createSAF(@Nullable Context context, String mime, String filename) throws IOException {
|
||||
DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(context, docTree, filename);
|
||||
|
||||
if (res != null && res.exists() && res.isDirectory()) {
|
||||
if (!res.delete())
|
||||
throw new IOException("Directory with the same name found but cannot delete");
|
||||
res = null;
|
||||
}
|
||||
|
||||
if (res == null) {
|
||||
res = this.docTree.createFile(srcType == null ? DEFAULT_MIME : mime, filename);
|
||||
if (res == null) throw new IOException("Cannot create the file");
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private String getLowerCase(String str) {
|
||||
return str == null ? null : str.toLowerCase();
|
||||
}
|
||||
|
||||
private boolean stringMismatch(String str1, String str2) {
|
||||
if (str1 == null && str2 == null) return false;
|
||||
if ((str1 == null) != (str2 == null)) return true;
|
||||
|
||||
return !str1.equals(str2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue