diff --git a/app/build.gradle b/app/build.gradle index 42719d181..396f119b2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -189,7 +189,7 @@ sonar { dependencies { /** Desugaring **/ - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3' /** NewPipe libraries **/ // You can use a local version by uncommenting a few lines in settings.gradle diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java index 48ae54284..1b74c90a5 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredDirectoryHelper.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.provider.DocumentsContract; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -14,21 +15,27 @@ import androidx.documentfile.provider.DocumentFile; import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.util.FilePickerActivityHelper; -import java.io.File; import java.io.IOException; 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.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.Root.COLUMN_DOCUMENT_ID; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; public class StoredDirectoryHelper { + private static final String TAG = StoredDirectoryHelper.class.getSimpleName(); public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; - private File ioTree; + private Path ioTree; private DocumentFile docTree; private Context context; @@ -40,7 +47,7 @@ public class StoredDirectoryHelper { this.tag = tag; if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) { - this.ioTree = new File(URI.create(path.toString())); + ioTree = Paths.get(URI.create(path.toString())); return; } @@ -64,13 +71,17 @@ public class StoredDirectoryHelper { } public StoredFileHelper createUniqueFile(final String name, final String mime) { - final ArrayList matches = new ArrayList<>(); + final List matches = new ArrayList<>(); final String[] filename = splitFilename(name); - final String lcFilename = filename[0].toLowerCase(); + final String lcFileName = filename[0].toLowerCase(); if (docTree == null) { - for (final File file : ioTree.listFiles()) { - addIfStartWith(matches, lcFilename, file.getName()); + try (Stream stream = Files.list(ioTree)) { + 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 { // warning: SAF file listing is very slow @@ -82,37 +93,37 @@ public class StoredDirectoryHelper { final ContentResolver cr = context.getContentResolver(); try (Cursor cursor = cr.query(docTreeChildren, projection, selection, - new String[]{lcFilename}, null)) { + new String[]{lcFileName}, null)) { if (cursor != null) { 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); - } 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 - if (lcName != null) { - return createFile(name, mime, true); + // check if the filename is in use + String lcName = name.toLowerCase(); + 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); 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); } } @@ -141,11 +152,11 @@ public class StoredDirectoryHelper { } public Uri getUri() { - return docTree == null ? Uri.fromFile(ioTree) : docTree.getUri(); + return docTree == null ? Uri.fromFile(ioTree.toFile()) : docTree.getUri(); } 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() { 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()) { @@ -206,8 +219,8 @@ public class StoredDirectoryHelper { public Uri findFile(final String filename) { if (docTree == null) { - final File res = new File(ioTree, filename); - return res.exists() ? Uri.fromFile(res) : null; + final Path res = ioTree.resolve(filename); + return Files.exists(res) ? Uri.fromFile(res.toFile()) : null; } final DocumentFile res = findFileSAFHelper(context, docTree, filename); @@ -215,7 +228,7 @@ public class StoredDirectoryHelper { } 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 @Override public String toString() { - return (docTree == null ? Uri.fromFile(ioTree) : docTree.getUri()).toString(); + return (docTree == null ? Uri.fromFile(ioTree.toFile()) : docTree.getUri()).toString(); } //////////////////// // Utils /////////////////// - private static void addIfStartWith(final ArrayList list, @NonNull final String base, + private static void addIfStartWith(final List list, @NonNull final String base, final String str) { if (isNullOrEmpty(str)) { 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) { 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) { - return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext); + return name + "(" + idx + ")" + ext; } /** diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java index 1f0c91456..5404426c4 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java @@ -23,6 +23,9 @@ import java.io.File; import java.io.IOException; import java.io.Serializable; 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.FileStreamSAF; @@ -36,7 +39,7 @@ public class StoredFileHelper implements Serializable { private transient DocumentFile docFile; private transient DocumentFile docTree; - private transient File ioFile; + private transient Path ioPath; private transient Context context; protected String source; @@ -49,7 +52,8 @@ public class StoredFileHelper implements Serializable { public StoredFileHelper(final Context context, final Uri uri, final String mime) { if (FilePickerActivityHelper.isOwnFileUri(context, uri)) { - ioFile = Utils.getFileForUri(uri); + final File ioFile = Utils.getFileForUri(uri); + ioPath = ioFile.toPath(); source = Uri.fromFile(ioFile).toString(); } else { docFile = DocumentFile.fromSingleUri(context, uri); @@ -100,26 +104,18 @@ public class StoredFileHelper implements Serializable { 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 { - this.ioFile = new File(location, filename); + ioPath = location.resolve(filename); - if (this.ioFile.exists()) { - if (!this.ioFile.isFile() && !this.ioFile.delete()) { - 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"); - } - } + Files.deleteIfExists(ioPath); + Files.createFile(ioPath); - this.source = Uri.fromFile(this.ioFile).toString(); - this.sourceTree = Uri.fromFile(location).toString(); + source = Uri.fromFile(ioPath.toFile()).toString(); + sourceTree = Uri.fromFile(location.toFile()).toString(); - this.srcName = ioFile.getName(); - this.srcType = mime; + srcName = ioPath.getFileName().toString(); + srcType = mime; } public StoredFileHelper(final Context context, @Nullable final Uri parent, @@ -129,12 +125,12 @@ public class StoredFileHelper implements Serializable { if (path.getScheme() == null || path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) { - this.ioFile = new File(URI.create(this.source)); + this.ioPath = Paths.get(URI.create(this.source)); } else { final DocumentFile file = DocumentFile.fromSingleUri(context, path); if (file == null) { - throw new RuntimeException("SAF not available"); + throw new IOException("SAF not available"); } this.context = context; @@ -187,7 +183,7 @@ public class StoredFileHelper implements Serializable { assertValid(); if (docFile == null) { - return new FileStream(ioFile); + return new FileStream(ioPath.toFile()); } else { return new FileStreamSAF(context.getContentResolver(), docFile.getUri()); } @@ -211,7 +207,7 @@ public class StoredFileHelper implements Serializable { public Uri getUri() { assertValid(); - return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri(); + return docFile == null ? Uri.fromFile(ioPath.toFile()) : docFile.getUri(); } public Uri getParentUri() { @@ -233,7 +229,12 @@ public class StoredFileHelper implements Serializable { return true; } 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(); @@ -252,21 +253,30 @@ public class StoredFileHelper implements Serializable { public long length() { 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() { if (source == null) { return false; } - return docFile == null ? ioFile.canWrite() : docFile.canWrite(); + return docFile == null ? Files.isWritable(ioPath) : docFile.canWrite(); } public String getName() { if (source == null) { return srcName; } else if (docFile == null) { - return ioFile.getName(); + return ioPath.getFileName().toString(); } final String name = docFile.getName(); @@ -287,12 +297,11 @@ public class StoredFileHelper implements Serializable { } public boolean existsAsFile() { - if (source == null || (docFile == null && ioFile == null)) { + if (source == null || (docFile == null && ioPath == null)) { if (DEBUG) { Log.d(TAG, "existsAsFile called but something is null: source = [" + (source == null ? "null => storage is invalid" : source) - + "], docFile = [" + (docFile == null ? "null" : docFile) - + "], ioFile = [" + (ioFile == null ? "null" : ioFile) + "]"); + + "], docFile = [" + docFile + "], ioPath = [" + ioPath + "]"); } return false; } @@ -300,7 +309,7 @@ public class StoredFileHelper implements Serializable { // WARNING: DocumentFile.exists() and DocumentFile.isFile() methods are slow // docFile.isVirtual() means it is non-physical? return docFile == null - ? (ioFile.exists() && ioFile.isFile()) + ? Files.isRegularFile(ioPath) : (docFile.exists() && docFile.isFile()); } @@ -310,8 +319,10 @@ public class StoredFileHelper implements Serializable { if (docFile == null) { try { - result = ioFile.createNewFile(); + Files.createFile(ioPath); + result = true; } catch (final IOException e) { + Log.e(TAG, "Exception while creating " + ioPath, e); return false; } } else if (docTree == null) { @@ -332,7 +343,8 @@ public class StoredFileHelper implements Serializable { } if (result) { - source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString(); + source = (docFile == null ? Uri.fromFile(ioPath.toFile()) : docFile.getUri()) + .toString(); srcName = getName(); srcType = getType(); } @@ -352,7 +364,7 @@ public class StoredFileHelper implements Serializable { docTree = null; docFile = null; - ioFile = null; + ioPath = null; context = null; } @@ -383,7 +395,7 @@ public class StoredFileHelper implements Serializable { } if (this.isDirect()) { - return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath()); + return this.ioPath.equals(storage.ioPath); } return DocumentsContract.getDocumentId(this.docFile.getUri())