Merge pull request #10248 from Isira-Seneviratne/NIO_downloads

Improve the download helpers using the Java 7 NIO API.
This commit is contained in:
Isira Seneviratne 2023-09-17 21:09:02 +05:30 committed by GitHub
commit b1ab261890
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 98 additions and 67 deletions

View File

@ -189,7 +189,7 @@ sonar {
dependencies { dependencies {
/** Desugaring **/ /** Desugaring **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3'
/** NewPipe libraries **/ /** NewPipe libraries **/
// You can use a local version by uncommenting a few lines in settings.gradle // You can use a local version by uncommenting a few lines in settings.gradle

View File

@ -6,6 +6,7 @@ import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.provider.DocumentsContract; import android.provider.DocumentsContract;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -14,21 +15,27 @@ import androidx.documentfile.provider.DocumentFile;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME; import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID; import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
public class StoredDirectoryHelper { public class StoredDirectoryHelper {
private static final String TAG = StoredDirectoryHelper.class.getSimpleName();
public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION; | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
private File ioTree; private Path ioTree;
private DocumentFile docTree; private DocumentFile docTree;
private Context context; private Context context;
@ -40,7 +47,7 @@ public class StoredDirectoryHelper {
this.tag = tag; this.tag = tag;
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) { if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) {
this.ioTree = new File(URI.create(path.toString())); ioTree = Paths.get(URI.create(path.toString()));
return; return;
} }
@ -64,13 +71,17 @@ public class StoredDirectoryHelper {
} }
public StoredFileHelper createUniqueFile(final String name, final String mime) { public StoredFileHelper createUniqueFile(final String name, final String mime) {
final ArrayList<String> matches = new ArrayList<>(); final List<String> matches = new ArrayList<>();
final String[] filename = splitFilename(name); final String[] filename = splitFilename(name);
final String lcFilename = filename[0].toLowerCase(); final String lcFileName = filename[0].toLowerCase();
if (docTree == null) { if (docTree == null) {
for (final File file : ioTree.listFiles()) { try (Stream<Path> stream = Files.list(ioTree)) {
addIfStartWith(matches, lcFilename, file.getName()); matches.addAll(stream.map(path -> path.getFileName().toString().toLowerCase())
.filter(fileName -> fileName.startsWith(lcFileName))
.collect(Collectors.toList()));
} catch (final IOException e) {
Log.e(TAG, "Exception while traversing " + ioTree, e);
} }
} else { } else {
// warning: SAF file listing is very slow // warning: SAF file listing is very slow
@ -82,37 +93,37 @@ public class StoredDirectoryHelper {
final ContentResolver cr = context.getContentResolver(); final ContentResolver cr = context.getContentResolver();
try (Cursor cursor = cr.query(docTreeChildren, projection, selection, try (Cursor cursor = cr.query(docTreeChildren, projection, selection,
new String[]{lcFilename}, null)) { new String[]{lcFileName}, null)) {
if (cursor != null) { if (cursor != null) {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
addIfStartWith(matches, lcFilename, cursor.getString(0)); addIfStartWith(matches, lcFileName, cursor.getString(0));
} }
} }
} }
} }
if (matches.size() < 1) { if (matches.isEmpty()) {
return createFile(name, mime, true); return createFile(name, mime, true);
} else { }
// check if the filename is in use
String lcName = name.toLowerCase();
for (final String testName : matches) {
if (testName.equals(lcName)) {
lcName = null;
break;
}
}
// check if not in use // check if the filename is in use
if (lcName != null) { String lcName = name.toLowerCase();
return createFile(name, mime, true); for (final String testName : matches) {
if (testName.equals(lcName)) {
lcName = null;
break;
} }
} }
// create file if filename not in use
if (lcName != null) {
return createFile(name, mime, true);
}
Collections.sort(matches, String::compareTo); Collections.sort(matches, String::compareTo);
for (int i = 1; i < 1000; i++) { for (int i = 1; i < 1000; i++) {
if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0) { if (Collections.binarySearch(matches, makeFileName(lcFileName, i, filename[1])) < 0) {
return createFile(makeFileName(filename[0], i, filename[1]), mime, true); return createFile(makeFileName(filename[0], i, filename[1]), mime, true);
} }
} }
@ -141,11 +152,11 @@ public class StoredDirectoryHelper {
} }
public Uri getUri() { public Uri getUri() {
return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri(); return docTree == null ? Uri.fromFile(ioTree.toFile()) : docTree.getUri();
} }
public boolean exists() { public boolean exists() {
return docTree == null ? ioTree.exists() : docTree.exists(); return docTree == null ? Files.exists(ioTree) : docTree.exists();
} }
/** /**
@ -169,7 +180,9 @@ public class StoredDirectoryHelper {
*/ */
public boolean mkdirs() { public boolean mkdirs() {
if (docTree == null) { if (docTree == null) {
return ioTree.exists() || ioTree.mkdirs(); // TODO: Use Files.createDirectories() when AGP 8.1 is available:
// https://issuetracker.google.com/issues/282544786
return Files.exists(ioTree) || ioTree.toFile().mkdirs();
} }
if (docTree.exists()) { if (docTree.exists()) {
@ -206,8 +219,8 @@ public class StoredDirectoryHelper {
public Uri findFile(final String filename) { public Uri findFile(final String filename) {
if (docTree == null) { if (docTree == null) {
final File res = new File(ioTree, filename); final Path res = ioTree.resolve(filename);
return res.exists() ? Uri.fromFile(res) : null; return Files.exists(res) ? Uri.fromFile(res.toFile()) : null;
} }
final DocumentFile res = findFileSAFHelper(context, docTree, filename); final DocumentFile res = findFileSAFHelper(context, docTree, filename);
@ -215,7 +228,7 @@ public class StoredDirectoryHelper {
} }
public boolean canWrite() { public boolean canWrite() {
return docTree == null ? ioTree.canWrite() : docTree.canWrite(); return docTree == null ? Files.isWritable(ioTree) : docTree.canWrite();
} }
/** /**
@ -230,14 +243,14 @@ public class StoredDirectoryHelper {
@NonNull @NonNull
@Override @Override
public String toString() { public String toString() {
return (docTree == null ? Uri.fromFile(ioTree) : docTree.getUri()).toString(); return (docTree == null ? Uri.fromFile(ioTree.toFile()) : docTree.getUri()).toString();
} }
//////////////////// ////////////////////
// Utils // Utils
/////////////////// ///////////////////
private static void addIfStartWith(final ArrayList<String> list, @NonNull final String base, private static void addIfStartWith(final List<String> list, @NonNull final String base,
final String str) { final String str) {
if (isNullOrEmpty(str)) { if (isNullOrEmpty(str)) {
return; return;
@ -248,6 +261,12 @@ public class StoredDirectoryHelper {
} }
} }
/**
* Splits the filename into the name and extension.
*
* @param filename The filename to split
* @return A String array with the name at index 0 and extension at index 1
*/
private static String[] splitFilename(@NonNull final String filename) { private static String[] splitFilename(@NonNull final String filename) {
final int dotIndex = filename.lastIndexOf('.'); final int dotIndex = filename.lastIndexOf('.');
@ -259,7 +278,7 @@ public class StoredDirectoryHelper {
} }
private static String makeFileName(final String name, final int idx, final String ext) { private static String makeFileName(final String name, final int idx, final String ext) {
return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext); return name + "(" + idx + ")" + ext;
} }
/** /**

View File

@ -23,6 +23,9 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.net.URI; import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import us.shandian.giga.io.FileStream; import us.shandian.giga.io.FileStream;
import us.shandian.giga.io.FileStreamSAF; import us.shandian.giga.io.FileStreamSAF;
@ -36,7 +39,7 @@ public class StoredFileHelper implements Serializable {
private transient DocumentFile docFile; private transient DocumentFile docFile;
private transient DocumentFile docTree; private transient DocumentFile docTree;
private transient File ioFile; private transient Path ioPath;
private transient Context context; private transient Context context;
protected String source; protected String source;
@ -49,7 +52,8 @@ public class StoredFileHelper implements Serializable {
public StoredFileHelper(final Context context, final Uri uri, final String mime) { public StoredFileHelper(final Context context, final Uri uri, final String mime) {
if (FilePickerActivityHelper.isOwnFileUri(context, uri)) { if (FilePickerActivityHelper.isOwnFileUri(context, uri)) {
ioFile = Utils.getFileForUri(uri); final File ioFile = Utils.getFileForUri(uri);
ioPath = ioFile.toPath();
source = Uri.fromFile(ioFile).toString(); source = Uri.fromFile(ioFile).toString();
} else { } else {
docFile = DocumentFile.fromSingleUri(context, uri); docFile = DocumentFile.fromSingleUri(context, uri);
@ -100,26 +104,18 @@ public class StoredFileHelper implements Serializable {
this.srcType = this.docFile.getType(); this.srcType = this.docFile.getType();
} }
StoredFileHelper(final File location, final String filename, final String mime) StoredFileHelper(final Path location, final String filename, final String mime)
throws IOException { throws IOException {
this.ioFile = new File(location, filename); ioPath = location.resolve(filename);
if (this.ioFile.exists()) { Files.deleteIfExists(ioPath);
if (!this.ioFile.isFile() && !this.ioFile.delete()) { Files.createFile(ioPath);
throw new IOException("The filename is already in use by non-file entity "
+ "and cannot overwrite it");
}
} else {
if (!this.ioFile.createNewFile()) {
throw new IOException("Cannot create the file");
}
}
this.source = Uri.fromFile(this.ioFile).toString(); source = Uri.fromFile(ioPath.toFile()).toString();
this.sourceTree = Uri.fromFile(location).toString(); sourceTree = Uri.fromFile(location.toFile()).toString();
this.srcName = ioFile.getName(); srcName = ioPath.getFileName().toString();
this.srcType = mime; srcType = mime;
} }
public StoredFileHelper(final Context context, @Nullable final Uri parent, public StoredFileHelper(final Context context, @Nullable final Uri parent,
@ -129,12 +125,12 @@ public class StoredFileHelper implements Serializable {
if (path.getScheme() == null if (path.getScheme() == null
|| path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) { || path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
this.ioFile = new File(URI.create(this.source)); this.ioPath = Paths.get(URI.create(this.source));
} else { } else {
final DocumentFile file = DocumentFile.fromSingleUri(context, path); final DocumentFile file = DocumentFile.fromSingleUri(context, path);
if (file == null) { if (file == null) {
throw new RuntimeException("SAF not available"); throw new IOException("SAF not available");
} }
this.context = context; this.context = context;
@ -187,7 +183,7 @@ public class StoredFileHelper implements Serializable {
assertValid(); assertValid();
if (docFile == null) { if (docFile == null) {
return new FileStream(ioFile); return new FileStream(ioPath.toFile());
} else { } else {
return new FileStreamSAF(context.getContentResolver(), docFile.getUri()); return new FileStreamSAF(context.getContentResolver(), docFile.getUri());
} }
@ -211,7 +207,7 @@ public class StoredFileHelper implements Serializable {
public Uri getUri() { public Uri getUri() {
assertValid(); assertValid();
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri(); return docFile == null ? Uri.fromFile(ioPath.toFile()) : docFile.getUri();
} }
public Uri getParentUri() { public Uri getParentUri() {
@ -233,7 +229,12 @@ public class StoredFileHelper implements Serializable {
return true; return true;
} }
if (docFile == null) { if (docFile == null) {
return ioFile.delete(); try {
return Files.deleteIfExists(ioPath);
} catch (final IOException e) {
Log.e(TAG, "Exception while deleting " + ioPath, e);
return false;
}
} }
final boolean res = docFile.delete(); final boolean res = docFile.delete();
@ -252,21 +253,30 @@ public class StoredFileHelper implements Serializable {
public long length() { public long length() {
assertValid(); assertValid();
return docFile == null ? ioFile.length() : docFile.length(); if (docFile == null) {
try {
return Files.size(ioPath);
} catch (final IOException e) {
Log.e(TAG, "Exception while getting the size of " + ioPath, e);
return 0;
}
} else {
return docFile.length();
}
} }
public boolean canWrite() { public boolean canWrite() {
if (source == null) { if (source == null) {
return false; return false;
} }
return docFile == null ? ioFile.canWrite() : docFile.canWrite(); return docFile == null ? Files.isWritable(ioPath) : docFile.canWrite();
} }
public String getName() { public String getName() {
if (source == null) { if (source == null) {
return srcName; return srcName;
} else if (docFile == null) { } else if (docFile == null) {
return ioFile.getName(); return ioPath.getFileName().toString();
} }
final String name = docFile.getName(); final String name = docFile.getName();
@ -287,12 +297,11 @@ public class StoredFileHelper implements Serializable {
} }
public boolean existsAsFile() { public boolean existsAsFile() {
if (source == null || (docFile == null && ioFile == null)) { if (source == null || (docFile == null && ioPath == null)) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "existsAsFile called but something is null: source = [" Log.d(TAG, "existsAsFile called but something is null: source = ["
+ (source == null ? "null => storage is invalid" : source) + (source == null ? "null => storage is invalid" : source)
+ "], docFile = [" + (docFile == null ? "null" : docFile) + "], docFile = [" + docFile + "], ioPath = [" + ioPath + "]");
+ "], ioFile = [" + (ioFile == null ? "null" : ioFile) + "]");
} }
return false; return false;
} }
@ -300,7 +309,7 @@ public class StoredFileHelper implements Serializable {
// WARNING: DocumentFile.exists() and DocumentFile.isFile() methods are slow // WARNING: DocumentFile.exists() and DocumentFile.isFile() methods are slow
// docFile.isVirtual() means it is non-physical? // docFile.isVirtual() means it is non-physical?
return docFile == null return docFile == null
? (ioFile.exists() && ioFile.isFile()) ? Files.isRegularFile(ioPath)
: (docFile.exists() && docFile.isFile()); : (docFile.exists() && docFile.isFile());
} }
@ -310,8 +319,10 @@ public class StoredFileHelper implements Serializable {
if (docFile == null) { if (docFile == null) {
try { try {
result = ioFile.createNewFile(); Files.createFile(ioPath);
result = true;
} catch (final IOException e) { } catch (final IOException e) {
Log.e(TAG, "Exception while creating " + ioPath, e);
return false; return false;
} }
} else if (docTree == null) { } else if (docTree == null) {
@ -332,7 +343,8 @@ public class StoredFileHelper implements Serializable {
} }
if (result) { if (result) {
source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString(); source = (docFile == null ? Uri.fromFile(ioPath.toFile()) : docFile.getUri())
.toString();
srcName = getName(); srcName = getName();
srcType = getType(); srcType = getType();
} }
@ -352,7 +364,7 @@ public class StoredFileHelper implements Serializable {
docTree = null; docTree = null;
docFile = null; docFile = null;
ioFile = null; ioPath = null;
context = null; context = null;
} }
@ -383,7 +395,7 @@ public class StoredFileHelper implements Serializable {
} }
if (this.isDirect()) { if (this.isDirect()) {
return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath()); return this.ioPath.equals(storage.ioPath);
} }
return DocumentsContract.getDocumentId(this.docFile.getUri()) return DocumentsContract.getDocumentId(this.docFile.getUri())