From 702ade2a256398030ac12a1e61d69a068c2c8fce Mon Sep 17 00:00:00 2001 From: Muntashir Al-Islam Date: Sun, 12 Nov 2023 09:20:11 +0600 Subject: [PATCH] [Refactor] Move Path classes to libcore:io module Signed-off-by: Muntashir Al-Islam --- .../muntashirakon/AppManager/apk/ApkFile.java | 2 +- .../AppManager/apk/ApkUtils.java | 2 +- .../apk/installer/PackageInstallerCompat.java | 5 +- .../apk/splitapk/SplitApkExporter.java | 2 +- .../AppManager/backup/BackupOp.java | 2 +- .../backup/adb/AndroidBackupPacker.java | 2 +- .../backup/adb/AndroidBackupUnpacker.java | 2 +- .../backup/convert/OABConverter.java | 4 +- .../backup/convert/SBConverter.java | 8 +- .../backup/convert/TBConverter.java | 6 +- .../AppManager/crypto/AESCrypto.java | 8 +- .../AppManager/fm/FmViewModel.java | 2 +- .../AppManager/logcat/LogViewerViewModel.java | 2 +- .../AppManager/scanner/vt/VirusTotal.java | 2 +- .../AppManager/self/filecache/FileCache.java | 4 +- .../self/imagecache/ImageFileCache.java | 2 +- .../ImportExportKeyStoreDialogFragment.java | 4 +- .../AppManager/ssaid/SettingsStateV26.java | 2 +- .../AppManager/ssaid/SettingsStateV31.java | 2 +- .../AppManager/utils/FileUtils.java | 28 +- .../AppManager/utils/TarUtils.java | 4 +- ...ttributes.java => PathAttributesImpl.java} | 49 +- ...tentInfo.java => PathContentInfoImpl.java} | 96 +-- .../io/{Path.java => PathImpl.java} | 724 ++++-------------- .../io/github/muntashirakon/io/Paths.java | 22 +- .../io/fs/VirtualFileSystem.java | 2 +- .../muntashirakon/io/fs/ZipFileSystem.java | 4 +- .../AppManager/utils/TarUtilsTest.java | 2 +- .../io/SplitInputStreamTest.java | 2 +- .../io/SplitOutputStreamTest.java | 2 +- .../tar/TarArchiveInputStreamTest.java | 2 +- .../tar/TarArchiveOutputStreamTest.java | 4 +- .../bzip2/BZip2CompressorInputStreamTest.java | 4 +- .../BZip2CompressorOutputStreamTest.java | 4 +- .../gzip/GzipCompressorInputStreamTest.java | 4 +- .../gzip/GzipCompressorOutputStreamTest.java | 4 +- .../io/CharSequenceInputStream.java | 3 +- .../io/github/muntashirakon/io/IoUtils.java | 51 +- .../java/io/github/muntashirakon/io/Path.java | 619 +++++++++++++++ .../muntashirakon/io/PathAttributes.java | 36 + .../muntashirakon/io/PathContentInfo.java | 74 ++ .../github/muntashirakon/io/PathReader.java | 17 - .../github/muntashirakon/io/PathWriter.java | 16 - .../muntashirakon/io/SplitInputStream.java | 6 +- .../muntashirakon/io/SplitOutputStream.java | 0 45 files changed, 1036 insertions(+), 806 deletions(-) rename app/src/main/java/io/github/muntashirakon/io/{PathAttributes.java => PathAttributesImpl.java} (68%) rename app/src/main/java/io/github/muntashirakon/io/{PathContentInfo.java => PathContentInfoImpl.java} (61%) rename app/src/main/java/io/github/muntashirakon/io/{Path.java => PathImpl.java} (65%) rename {app => libcore/io}/src/main/java/io/github/muntashirakon/io/CharSequenceInputStream.java (99%) rename {app => libcore/io}/src/main/java/io/github/muntashirakon/io/IoUtils.java (72%) create mode 100644 libcore/io/src/main/java/io/github/muntashirakon/io/Path.java create mode 100644 libcore/io/src/main/java/io/github/muntashirakon/io/PathAttributes.java create mode 100644 libcore/io/src/main/java/io/github/muntashirakon/io/PathContentInfo.java rename {app => libcore/io}/src/main/java/io/github/muntashirakon/io/PathReader.java (54%) rename {app => libcore/io}/src/main/java/io/github/muntashirakon/io/PathWriter.java (55%) rename {app => libcore/io}/src/main/java/io/github/muntashirakon/io/SplitInputStream.java (97%) rename {app => libcore/io}/src/main/java/io/github/muntashirakon/io/SplitOutputStream.java (100%) diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkFile.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkFile.java index 5372094f203..b855279619c 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkFile.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkFile.java @@ -487,7 +487,7 @@ public void extractObb(Path writableObbDir) throws IOException { // Extract obb file to the destination directory try (InputStream zipInputStream = mZipFile.getInputStream(obbEntry); OutputStream outputStream = obbDir.openOutputStream()) { - IoUtils.copy(zipInputStream, outputStream, -1, null); + IoUtils.copy(zipInputStream, outputStream); } } } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkUtils.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkUtils.java index de5e37820d3..ee4661f7b7b 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkUtils.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkUtils.java @@ -106,7 +106,7 @@ public static void backupApk(@NonNull Context ctx, @NonNull String packageName, } else { // Regular apk apkFile = backupPath.createNewFile(outputName + EXT_APK, null); - IoUtils.copy(Paths.get(info.publicSourceDir), apkFile, null); + IoUtils.copy(Paths.get(info.publicSourceDir), apkFile); } } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerCompat.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerCompat.java index b86183965db..317061d8a54 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerCompat.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerCompat.java @@ -65,6 +65,7 @@ import io.github.muntashirakon.AppManager.users.Users; import io.github.muntashirakon.AppManager.utils.BroadcastUtils; import io.github.muntashirakon.AppManager.utils.ContextUtils; +import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.AppManager.utils.MiuiUtils; import io.github.muntashirakon.AppManager.utils.ThreadUtils; import io.github.muntashirakon.AppManager.utils.UIUtils; @@ -595,7 +596,7 @@ public boolean install(@NonNull ApkFile apkFile, @NonNull List selectedS long entrySize = entry.getFileSize(options.isSignApkFiles()); try (InputStream apkInputStream = entry.getInputStream(options.isSignApkFiles()); OutputStream apkOutputStream = mSession.openWrite(entry.getFileName(), 0, entrySize)) { - IoUtils.copy(apkInputStream, apkOutputStream, totalSize, progressHandler); + FileUtils.copy(apkInputStream, apkOutputStream, totalSize, progressHandler); mSession.fsync(apkOutputStream); Log.d(TAG, "Install: copied entry %s", entry.name); } catch (IOException e) { @@ -655,7 +656,7 @@ public boolean install(@NonNull Path[] apkFiles, @NonNull String packageName, @N for (Path apkFile : apkFiles) { try (InputStream apkInputStream = apkFile.openInputStream(); OutputStream apkOutputStream = mSession.openWrite(apkFile.getName(), 0, apkFile.length())) { - IoUtils.copy(apkInputStream, apkOutputStream, totalSize, progressHandler); + FileUtils.copy(apkInputStream, apkOutputStream, totalSize, progressHandler); mSession.fsync(apkOutputStream); } catch (IOException e) { callFinish(STATUS_FAILURE_SESSION_WRITE); diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/SplitApkExporter.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/SplitApkExporter.java index 8e1a863e0a4..7e2c5ad81c2 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/SplitApkExporter.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/SplitApkExporter.java @@ -93,7 +93,7 @@ static void addFile(@NonNull ZipOutputStream zipOutputStream, @NonNull Path file zipEntry.setTime(timestamp); zipOutputStream.putNextEntry(zipEntry); try (InputStream apkInputStream = filePath.openInputStream()) { - IoUtils.copy(apkInputStream, zipOutputStream, -1, null); + IoUtils.copy(apkInputStream, zipOutputStream); } zipOutputStream.closeEntry(); } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupOp.java b/app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupOp.java index f25b5e39646..43fcbd6fc35 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupOp.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupOp.java @@ -332,7 +332,7 @@ private void backupKeyStore() throws BackupException { // Called only when the try { String newFileName = Utils.replaceOnce(keyStoreFileName, String.valueOf(mApplicationInfo.uid), String.valueOf(KEYSTORE_PLACEHOLDER)); - IoUtils.copy(keyStorePath.findFile(keyStoreFileName), cachePath.findOrCreateFile(newFileName, null), null); + IoUtils.copy(keyStorePath.findFile(keyStoreFileName), cachePath.findOrCreateFile(newFileName, null)); cachedKeyStoreFileNames.add(newFileName); keyStoreFilters.add(Pattern.quote(newFileName)); } catch (Throwable e) { diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/AndroidBackupPacker.java b/app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/AndroidBackupPacker.java index e5cb88fd9a7..01c211cad32 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/AndroidBackupPacker.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/AndroidBackupPacker.java @@ -22,7 +22,7 @@ public static void fromTar(@NonNull Path tarSource, @NonNull Path abDest, @Nulla OutputStream os = abDest.openOutputStream(); try (InputStream is = tarSource.openInputStream(); OutputStream realOs = header.write(os)) { - IoUtils.copy(is, realOs, -1, null); + IoUtils.copy(is, realOs); } catch (IOException e) { throw e; } catch (Exception e) { diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/AndroidBackupUnpacker.java b/app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/AndroidBackupUnpacker.java index 9f4e2444cef..9843b6dd9d7 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/AndroidBackupUnpacker.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/backup/adb/AndroidBackupUnpacker.java @@ -20,7 +20,7 @@ public static void toTar(@NonNull Path abSource, @NonNull Path tarDest, @Nullabl InputStream is = abSource.openInputStream(); try (OutputStream os = tarDest.openOutputStream(); InputStream realIs = header.read(is)) { - IoUtils.copy(realIs, os, -1, null); + IoUtils.copy(realIs, os); } catch (Exception e) { ExUtils.rethrowAsIOException(e); } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/OABConverter.java b/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/OABConverter.java index 6f54ee7871d..99f2eed649a 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/OABConverter.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/OABConverter.java @@ -375,7 +375,7 @@ private void backupData() throws BackupException { // We need to use a temporary file tmpFile = FileCache.getGlobalFileCache().createCachedFile(files[0].getExtension()); try (OutputStream fos = new FileOutputStream(tmpFile)) { - IoUtils.copy(zis, fos, -1, null); + IoUtils.copy(zis, fos); } } String fileName = zipEntry.getName().replaceFirst(Pattern.quote(mPackageName + "/"), ""); @@ -389,7 +389,7 @@ private void backupData() throws BackupException { if (tmpFile != null) { // Copy from the temporary file try (FileInputStream fis = new FileInputStream(tmpFile)) { - IoUtils.copy(fis, tos, -1, null); + IoUtils.copy(fis, tos); } finally { FileCache.getGlobalFileCache().delete(tmpFile); } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/SBConverter.java b/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/SBConverter.java index a47b3e1ffe2..9c7c8c22004 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/SBConverter.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/SBConverter.java @@ -269,7 +269,7 @@ private void backupData() throws BackupException { // We need to use a temporary file tmpFile = FileCache.getGlobalFileCache().createCachedFile(dataFile.getExtension()); try (OutputStream fos = new FileOutputStream(tmpFile)) { - IoUtils.copy(zis, fos, -1, null); + IoUtils.copy(zis, fos); } } String fileName = zipEntry.getName().replaceFirst(Pattern.quote(mPackageName + "/"), ""); @@ -283,7 +283,7 @@ private void backupData() throws BackupException { if (tmpFile != null) { // Copy from the temporary file try (FileInputStream fis = new FileInputStream(tmpFile)) { - IoUtils.copy(fis, tos, -1, null); + IoUtils.copy(fis, tos); } finally { FileCache.getGlobalFileCache().delete(tmpFile); } @@ -308,7 +308,7 @@ private void generateMetadata() throws BackupException { mCachedApk = FileUtils.getTempPath(mPackageName, "base.apk"); try (InputStream pis = getApkFile().openInputStream()) { try (OutputStream fos = mCachedApk.openOutputStream()) { - IoUtils.copy(pis, fos, -1, null); + IoUtils.copy(pis, fos); } mFilesToBeDeleted.add(getApkFile()); } catch (IOException e) { @@ -417,7 +417,7 @@ private String[] cacheAndGetSplitConfigs() throws IOException, RemoteException { splits.add(splitName); Path file = mCachedApk.requireParent().findOrCreateFile(splitName, null); try (OutputStream fos = file.openOutputStream()) { - IoUtils.copy(zis, fos, -1, null); + IoUtils.copy(zis, fos); } catch (IOException e) { file.delete(); throw e; diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/TBConverter.java b/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/TBConverter.java index e70cd9fcaad..2fad36291a7 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/TBConverter.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/backup/convert/TBConverter.java @@ -228,7 +228,7 @@ private void backupApkFile() throws BackupException { } try (OutputStream fos = baseApkFile.openOutputStream()) { // The whole file is the APK - IoUtils.copy(is, fos, -1, null); + IoUtils.copy(is, fos); } finally { is.close(); } @@ -356,11 +356,11 @@ private void backupData() throws BackupException { if (!inTarEntry.isDirectory() && !inTarEntry.isSymbolicLink()) { if (isExternal) { if (extTos != null) { - IoUtils.copy(tis, extTos, -1, null); + IoUtils.copy(tis, extTos); } } else { if (intTos != null) { - IoUtils.copy(tis, intTos, -1, null); + IoUtils.copy(tis, intTos); } } } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/crypto/AESCrypto.java b/app/src/main/java/io/github/muntashirakon/AppManager/crypto/AESCrypto.java index 1ab4685e187..d11be8b72e1 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/crypto/AESCrypto.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/crypto/AESCrypto.java @@ -134,7 +134,7 @@ public void encrypt(@NonNull InputStream unencryptedStream, @NonNull OutputStrea cipher.init(true, getParams()); // Convert unencrypted stream to encrypted stream try (OutputStream cipherOS = new CipherOutputStream(encryptedStream, cipher)) { - IoUtils.copy(unencryptedStream, cipherOS, -1, null); + IoUtils.copy(unencryptedStream, cipherOS); } } @@ -152,7 +152,7 @@ public void decrypt(@NonNull InputStream encryptedStream, @NonNull OutputStream cipher.init(false, getParams()); // Convert encrypted stream to unencrypted stream try (InputStream cipherIS = new CipherInputStream(encryptedStream, cipher)) { - IoUtils.copy(cipherIS, unencryptedStream, -1, null); + IoUtils.copy(cipherIS, unencryptedStream); } } @@ -186,11 +186,11 @@ private void handleFiles(boolean forEncryption, @NonNull Path[] files) throws IO OutputStream os = outputPath.openOutputStream()) { if (forEncryption) { try (OutputStream cipherOS = new CipherOutputStream(os, cipher)) { - IoUtils.copy(is, cipherOS, -1, null); + IoUtils.copy(is, cipherOS); } } else { // Cipher.DECRYPT_MODE try (InputStream cipherIS = new CipherInputStream(is, cipher)) { - IoUtils.copy(cipherIS, os, -1, null); + IoUtils.copy(cipherIS, os); } } } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/fm/FmViewModel.java b/app/src/main/java/io/github/muntashirakon/AppManager/fm/FmViewModel.java index 4c13a78d0b0..ee7445cdd77 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/fm/FmViewModel.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/fm/FmViewModel.java @@ -343,7 +343,7 @@ private void loadFiles(@NonNull Uri uri, @Nullable String scrollToFilename) { } Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(mCurrentUri, documentId); Path child = Paths.getTreeDocument(path, documentUri); - PathAttributes attributes = PathAttributes.fromSafTreeCursor(documentUri, c); + PathAttributes attributes = Paths.getAttributesFromSafTreeCursor(documentUri, c); FmItem fmItem = new FmItem(child, attributes); mFmItems.add(fmItem); if (fmItem.isDirectory) { diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/logcat/LogViewerViewModel.java b/app/src/main/java/io/github/muntashirakon/AppManager/logcat/LogViewerViewModel.java index 6f8e52c2c1c..4dcc14fdb03 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/logcat/LogViewerViewModel.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/logcat/LogViewerViewModel.java @@ -314,7 +314,7 @@ public void saveLogs(@NonNull Path path, @NonNull SendLogDetails sendLogDetails) } try (OutputStream output = path.openOutputStream()) { try (InputStream input = sendLogDetails.getAttachment().openInputStream()) { - IoUtils.copy(input, output, -1, null); + IoUtils.copy(input, output); } mLogSavedLiveData.postValue(path); } catch (IOException e) { diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/scanner/vt/VirusTotal.java b/app/src/main/java/io/github/muntashirakon/AppManager/scanner/vt/VirusTotal.java index 06c33883e15..8ac03f0797f 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/scanner/vt/VirusTotal.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/scanner/vt/VirusTotal.java @@ -205,7 +205,7 @@ public static void addMultipartFormData(@NonNull OutputStream os, @NonNull Strin .getBytes(StandardCharsets.UTF_8)); os.write(("Content-Type: application/octet-stream\r\n").getBytes(StandardCharsets.UTF_8)); os.write(("Content-Transfer-Encoding: chunked\r\n\r\n").getBytes(StandardCharsets.UTF_8)); - IoUtils.copy(is, os, -1, null); + IoUtils.copy(is, os); } @WorkerThread diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/self/filecache/FileCache.java b/app/src/main/java/io/github/muntashirakon/AppManager/self/filecache/FileCache.java index 5c4630bb85e..d9da72513a6 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/self/filecache/FileCache.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/self/filecache/FileCache.java @@ -85,7 +85,7 @@ public File getCachedFile(@NonNull Path source) throws IOException { } else if (source.lastModified() > 0 && source.lastModified() < tempFile.lastModified()) { return tempFile; } - IoUtils.copy(source, Paths.get(tempFile), null); + IoUtils.copy(source, Paths.get(tempFile)); return tempFile; } @@ -94,7 +94,7 @@ public File getCachedFile(@NonNull InputStream is, @Nullable String extension) t File tempFile = File.createTempFile("file_", "." + (extension != null ? extension : "tmp"), mCacheDir); mFileCache.add(tempFile); try (OutputStream os = new FileOutputStream(tempFile)) { - IoUtils.copy(is, os, -1, null); + IoUtils.copy(is, os); } return tempFile; } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/self/imagecache/ImageFileCache.java b/app/src/main/java/io/github/muntashirakon/AppManager/self/imagecache/ImageFileCache.java index 585f92d62b7..405c2ef9d3f 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/self/imagecache/ImageFileCache.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/self/imagecache/ImageFileCache.java @@ -38,7 +38,7 @@ public ImageFileCache() { public void putImage(@NonNull String name, @NonNull InputStream inputStream) throws IOException { File iconFile = getImageFile(name); try (OutputStream os = new FileOutputStream(iconFile)) { - IoUtils.copy(inputStream, os, -1, null); + IoUtils.copy(inputStream, os); } } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/settings/crypto/ImportExportKeyStoreDialogFragment.java b/app/src/main/java/io/github/muntashirakon/AppManager/settings/crypto/ImportExportKeyStoreDialogFragment.java index 16ee2924ca2..a0256a0abd4 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/settings/crypto/ImportExportKeyStoreDialogFragment.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/settings/crypto/ImportExportKeyStoreDialogFragment.java @@ -46,7 +46,7 @@ public class ImportExportKeyStoreDialogFragment extends DialogFragment { try (InputStream is = new FileInputStream(AM_KEYSTORE_FILE); OutputStream os = mActivity.getContentResolver().openOutputStream(uri)) { if (os == null) throw new IOException("Unable to open URI"); - IoUtils.copy(is, os, -1, null); + IoUtils.copy(is, os); mActivity.runOnUiThread(() -> { UIUtils.displayShortToast(R.string.done); dismiss(); @@ -77,7 +77,7 @@ public class ImportExportKeyStoreDialogFragment extends DialogFragment { try (InputStream is = mActivity.getContentResolver().openInputStream(uri); OutputStream os = new FileOutputStream(AM_KEYSTORE_FILE)) { if (is == null) throw new IOException("Unable to open URI"); - IoUtils.copy(is, os, -1, null); + IoUtils.copy(is, os); if (KeyStoreManager.hasKeyStorePassword()) { CountDownLatch waitForKs = new CountDownLatch(1); KeyStoreManager.inputKeyStorePassword(mActivity, waitForKs::countDown); diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/ssaid/SettingsStateV26.java b/app/src/main/java/io/github/muntashirakon/AppManager/ssaid/SettingsStateV26.java index bc92224e36a..bc526b9b4a9 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/ssaid/SettingsStateV26.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/ssaid/SettingsStateV26.java @@ -877,7 +877,7 @@ private void readStateSyncLocked() throws IllegalStateException { if (parseStateFromXmlStreamLocked(in)) { // Parsed state from fallback file. Restore original file with fallback file try { - IoUtils.copy(statePersistFallbackFile, mStatePersistFile, null); + IoUtils.copy(statePersistFallbackFile, mStatePersistFile); } catch (IOException ignored) { // Failed to copy, but it's okay because we already parsed states from fallback file } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/ssaid/SettingsStateV31.java b/app/src/main/java/io/github/muntashirakon/AppManager/ssaid/SettingsStateV31.java index 71d63a577c2..9f57827656d 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/ssaid/SettingsStateV31.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/ssaid/SettingsStateV31.java @@ -889,7 +889,7 @@ private void readStateSyncLocked() throws IllegalStateException { if (parseStateFromXmlStreamLocked(in)) { // Parsed state from fallback file. Restore original file with fallback file try { - IoUtils.copy(statePersistFallbackFile, mStatePersistFile, null); + IoUtils.copy(statePersistFallbackFile, mStatePersistFile); } catch (IOException ignored) { // Failed to copy, but it's okay because we already parsed states from fallback file } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/utils/FileUtils.java b/app/src/main/java/io/github/muntashirakon/AppManager/utils/FileUtils.java index 3d68f29b83f..96bc56844a7 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/utils/FileUtils.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/utils/FileUtils.java @@ -32,6 +32,7 @@ import java.util.zip.ZipEntry; import io.github.muntashirakon.AppManager.logs.Log; +import io.github.muntashirakon.AppManager.progress.ProgressHandler; import io.github.muntashirakon.AppManager.self.filecache.FileCache; import io.github.muntashirakon.io.FileSystemManager; import io.github.muntashirakon.io.IoUtils; @@ -113,12 +114,37 @@ public static boolean isAssetDirectory(@NonNull Context context, @NonNull String return files != null && files.length > 0; } + @AnyThread + public static long copy(@NonNull Path from, @NonNull Path to, @Nullable ProgressHandler progressHandler) + throws IOException { + try (InputStream in = from.openInputStream(); + OutputStream out = to.openOutputStream()) { + return copy(in, out, from.length(), progressHandler); + } + } + + /** + * Copy the contents of one stream to another. + * + * @param totalSize Total size of the stream. Only used for handling progress. Set {@code -1} if unknown. + */ + @AnyThread + public static long copy(@NonNull InputStream in, @NonNull OutputStream out, long totalSize, + @Nullable ProgressHandler progressHandler) throws IOException { + float lastProgress = progressHandler != null ? progressHandler.getLastProgress() : 0; + return IoUtils.copy(in, out, ThreadUtils.getBackgroundThreadExecutor(), progress -> { + if (progressHandler != null) { + progressHandler.postUpdate(100, lastProgress + (progress * 100f / totalSize)); + } + }); + } + @WorkerThread public static void copyFromAsset(@NonNull Context context, @NonNull String fileName, @NonNull Path dest) throws IOException { try (InputStream is = context.getAssets().open(fileName); OutputStream os = dest.openOutputStream()) { - IoUtils.copy(is, os, -1, null); + IoUtils.copy(is, os); } } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/utils/TarUtils.java b/app/src/main/java/io/github/muntashirakon/AppManager/utils/TarUtils.java index b98d1b77538..471222df2e9 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/utils/TarUtils.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/utils/TarUtils.java @@ -112,7 +112,7 @@ public static List create(@NonNull @TarType String type, @NonNull Path sou tos.putArchiveEntry(tarEntry); if (!file.isDirectory()) { try (InputStream is = file.openInputStream()) { - IoUtils.copy(is, tos, -1, null); + IoUtils.copy(is, tos); } } } @@ -223,7 +223,7 @@ public static void extract(@NonNull @TarType String type, @NonNull Path[] source } if (!entry.isDirectory()) { try (OutputStream os = file.openOutputStream()) { - IoUtils.copy(tis, os, -1, null); + IoUtils.copy(tis, os); } } } diff --git a/app/src/main/java/io/github/muntashirakon/io/PathAttributes.java b/app/src/main/java/io/github/muntashirakon/io/PathAttributesImpl.java similarity index 68% rename from app/src/main/java/io/github/muntashirakon/io/PathAttributes.java rename to app/src/main/java/io/github/muntashirakon/io/PathAttributesImpl.java index 0a01cc7d4ee..68f9445e52f 100644 --- a/app/src/main/java/io/github/muntashirakon/io/PathAttributes.java +++ b/app/src/main/java/io/github/muntashirakon/io/PathAttributesImpl.java @@ -21,24 +21,24 @@ import io.github.muntashirakon.AppManager.utils.ExUtils; -public class PathAttributes { +class PathAttributesImpl extends PathAttributes { @NonNull - public static PathAttributes fromFile(@NonNull ExtendedRawDocumentFile file) { + public static PathAttributesImpl fromFile(@NonNull ExtendedRawDocumentFile file) { ExtendedFile f = file.getFile(); int mode = ExUtils.requireNonNullElse(f::getMode, 0); - return new PathAttributes(f.getName(), file.getType(), f.lastModified(), f.lastAccess(), f.creationTime(), + return new PathAttributesImpl(f.getName(), file.getType(), f.lastModified(), f.lastAccess(), f.creationTime(), OsConstants.S_ISREG(mode), OsConstants.S_ISDIR(mode), OsConstants.S_ISLNK(mode), f.length()); } @NonNull - public static PathAttributes fromVirtual(@NonNull VirtualDocumentFile file) { + public static PathAttributesImpl fromVirtual(@NonNull VirtualDocumentFile file) { int mode = file.getMode(); - return new PathAttributes(file.getName(), file.getType(), file.lastModified(), file.lastAccess(), file.creationTime(), + return new PathAttributesImpl(file.getName(), file.getType(), file.lastModified(), file.lastAccess(), file.creationTime(), OsConstants.S_ISREG(mode), OsConstants.S_ISDIR(mode), OsConstants.S_ISLNK(mode), file.length()); } @NonNull - public static PathAttributes fromSaf(@NonNull Context context, @NonNull DocumentFile safDocumentFile) + public static PathAttributesImpl fromSaf(@NonNull Context context, @NonNull DocumentFile safDocumentFile) throws IOException { Uri documentUri = safDocumentFile.getUri(); ContentResolver resolver = context.getContentResolver(); @@ -71,7 +71,7 @@ public static PathAttributes fromSaf(@NonNull Context context, @NonNull Document if (name == null) { name = DocumentFileUtils.resolveAltNameForSaf(safDocumentFile); } - return new PathAttributes(name, type, lastModified, 0, 0, !isDirectory, isDirectory, + return new PathAttributesImpl(name, type, lastModified, 0, 0, !isDirectory, isDirectory, false, size); } catch (IOException e) { throw e; @@ -81,7 +81,7 @@ public static PathAttributes fromSaf(@NonNull Context context, @NonNull Document } @NonNull - public static PathAttributes fromSafTreeCursor(@NonNull Uri treeUri, @NonNull Cursor c) { + public static PathAttributesImpl fromSafTreeCursor(@NonNull Uri treeUri, @NonNull Cursor c) { if (!DocumentsContractCompat.isTreeUri(treeUri)) { throw new IllegalArgumentException("Not a tree document."); } @@ -110,35 +110,14 @@ public static PathAttributes fromSafTreeCursor(@NonNull Uri treeUri, @NonNull Cu if (name == null) { name = DocumentFileUtils.resolveAltNameForTreeUri(treeUri); } - return new PathAttributes(name, type, lastModified, 0, 0, !isDirectory, isDirectory, + return new PathAttributesImpl(name, type, lastModified, 0, 0, !isDirectory, isDirectory, false, size); } - @NonNull - public final String name; - @Nullable - public final String mimeType; - public final long lastModified; - public final long lastAccess; - public final long creationTime; - public final boolean isRegularFile; - public final boolean isDirectory; - public final boolean isSymbolicLink; - public final boolean isOtherFile; - public final long size; - - private PathAttributes(@NonNull String displayName, @Nullable String mimeType, long lastModified, long lastAccess, - long creationTime, boolean isRegularFile, boolean isDirectory, boolean isSymbolicLink, - long size) { - this.name = displayName; - this.mimeType = mimeType; - this.lastModified = lastModified; - this.lastAccess = lastAccess; - this.creationTime = creationTime; - this.isRegularFile = isRegularFile; - this.isDirectory = isDirectory; - this.isSymbolicLink = isSymbolicLink; - this.isOtherFile = !isRegularFile && !isDirectory && !isSymbolicLink; - this.size = size; + private PathAttributesImpl(@NonNull String displayName, @Nullable String mimeType, long lastModified, long lastAccess, + long creationTime, boolean isRegularFile, boolean isDirectory, boolean isSymbolicLink, + long size) { + super(displayName, mimeType, lastModified, lastAccess, creationTime, isRegularFile, isDirectory, isSymbolicLink, + size); } } diff --git a/app/src/main/java/io/github/muntashirakon/io/PathContentInfo.java b/app/src/main/java/io/github/muntashirakon/io/PathContentInfoImpl.java similarity index 61% rename from app/src/main/java/io/github/muntashirakon/io/PathContentInfo.java rename to app/src/main/java/io/github/muntashirakon/io/PathContentInfoImpl.java index 57e0bd40d24..2bbf5550798 100644 --- a/app/src/main/java/io/github/muntashirakon/io/PathContentInfo.java +++ b/app/src/main/java/io/github/muntashirakon/io/PathContentInfoImpl.java @@ -16,8 +16,8 @@ import io.github.muntashirakon.AppManager.fm.ContentType2; import io.github.muntashirakon.AppManager.logs.Log; -public class PathContentInfo { - public static final String TAG = PathContentInfo.class.getSimpleName(); +class PathContentInfoImpl extends PathContentInfo { + public static final String TAG = PathContentInfoImpl.class.getSimpleName(); // Associations not present in ContentInfoUtil, they're derived from simple-name private static final HashMap sSimpleNameMimeAssociations = new HashMap() {{ put("SQLite", "application/vnd.sqlite3"); @@ -30,7 +30,7 @@ public class PathContentInfo { private static ContentInfoUtil sContentInfoUtil; @NonNull - public static PathContentInfo fromExtension(@NonNull Path path) { + public static PathContentInfoImpl fromExtension(@NonNull Path path) { if (path.isDirectory()) { return DIRECTORY; } @@ -47,7 +47,7 @@ public static PathContentInfo fromExtension(@NonNull Path path) { } @NonNull - public static PathContentInfo fromPath(@NonNull Path path) { + public static PathContentInfoImpl fromPath(@NonNull Path path) { if (path.isDirectory()) { return DIRECTORY; } @@ -64,11 +64,11 @@ public static PathContentInfo fromPath(@NonNull Path path) { // instead which is currently a WIP. if (extInfo != null) { return withPartialOverride(fromPathContentInfo( - new PathContentInfo(extInfo.getName(), contentInfo.getMessage(), extInfo.getMimeType(), + new PathContentInfoImpl(extInfo.getName(), contentInfo.getMessage(), extInfo.getMimeType(), extInfo.getFileExtensions(), contentInfo.isPartial())), extType2); } if (extType2 != null) { - return fromPathContentInfo(new PathContentInfo(extType2.getSimpleName(), contentInfo.getMessage(), + return fromPathContentInfo(new PathContentInfoImpl(extType2.getSimpleName(), contentInfo.getMessage(), extType2.getMimeType(), extType2.getFileExtensions(), contentInfo.isPartial())); } return fromContentInfo(contentInfo); @@ -85,12 +85,12 @@ public static PathContentInfo fromPath(@NonNull Path path) { return fromContentType2(ContentType2.OTHER); } - private static PathContentInfo withPartialOverride(@NonNull PathContentInfo contentInfo, @Nullable ContentType2 contentType2) { + private static PathContentInfoImpl withPartialOverride(@NonNull PathContentInfoImpl contentInfo, @Nullable ContentType2 contentType2) { if (contentType2 != null) { boolean partial = contentInfo.isPartial() || Boolean.TRUE.equals(sPartialOverrides.get(contentInfo.getMimeType())); if (partial) { // Override MIME type, name and extension - return new PathContentInfo(contentType2.getSimpleName(), contentInfo.getMessage(), + return new PathContentInfoImpl(contentType2.getSimpleName(), contentInfo.getMessage(), contentType2.getMimeType(), contentType2.getFileExtensions(), false); } } @@ -98,7 +98,7 @@ private static PathContentInfo withPartialOverride(@NonNull PathContentInfo cont } @NonNull - private static PathContentInfo fromContentInfo(@NonNull ContentInfo contentInfo) { + private static PathContentInfoImpl fromContentInfo(@NonNull ContentInfo contentInfo) { String mime = sSimpleNameMimeAssociations.get(contentInfo.getName()); if (mime != null) { ContentType2 contentType2 = ContentType2.fromMimeType(mime); @@ -110,15 +110,15 @@ private static PathContentInfo fromContentInfo(@NonNull ContentInfo contentInfo) if (contentType2.getFileExtensions() != null) { extensions.addAll(Arrays.asList(contentType2.getFileExtensions())); } - return new PathContentInfo(contentInfo.getName(), contentInfo.getMessage(), mime, + return new PathContentInfoImpl(contentInfo.getName(), contentInfo.getMessage(), mime, extensions.isEmpty() ? null : extensions.toArray(new String[0]), contentInfo.isPartial()); } - return new PathContentInfo(contentInfo.getName(), contentInfo.getMessage(), contentInfo.getMimeType(), + return new PathContentInfoImpl(contentInfo.getName(), contentInfo.getMessage(), contentInfo.getMimeType(), contentInfo.getFileExtensions(), contentInfo.isPartial()); } @NonNull - private static PathContentInfo fromPathContentInfo(@NonNull PathContentInfo contentInfo) { + private static PathContentInfoImpl fromPathContentInfo(@NonNull PathContentInfoImpl contentInfo) { String mime = sSimpleNameMimeAssociations.get(contentInfo.getName()); if (mime != null) { ContentType2 contentType2 = ContentType2.fromMimeType(mime); @@ -130,82 +130,24 @@ private static PathContentInfo fromPathContentInfo(@NonNull PathContentInfo cont if (contentType2.getFileExtensions() != null) { extensions.addAll(Arrays.asList(contentType2.getFileExtensions())); } - return new PathContentInfo(contentInfo.getName(), contentInfo.getMessage(), mime, + return new PathContentInfoImpl(contentInfo.getName(), contentInfo.getMessage(), mime, extensions.isEmpty() ? null : extensions.toArray(new String[0]), contentInfo.isPartial()); } return contentInfo; } @NonNull - private static PathContentInfo fromContentType2(@NonNull ContentType2 contentType2) { - return new PathContentInfo(contentType2.getSimpleName(), null, contentType2.getMimeType(), + private static PathContentInfoImpl fromContentType2(@NonNull ContentType2 contentType2) { + return new PathContentInfoImpl(contentType2.getSimpleName(), null, contentType2.getMimeType(), contentType2.getFileExtensions(), false); } - public static final PathContentInfo DIRECTORY = new PathContentInfo("Directory", null, + private static final PathContentInfoImpl DIRECTORY = new PathContentInfoImpl("Directory", null, "resource/folder", null, false); - @NonNull - private final String mName; - @Nullable - private final String mMessage; - @Nullable - private final String mMimeType; - @Nullable - private final String[] mFileExtensions; - private final boolean mPartial; - - public PathContentInfo(@NonNull String name, @Nullable String message, @Nullable String mimeType, - @Nullable String[] fileExtensions, boolean partial) { - mName = name; - mMessage = message; - mMimeType = mimeType; - mFileExtensions = fileExtensions; - mPartial = partial; - } - - /** - * Returns the short name of the content either from the content-type or extracted from the message. If the - * content-type is known then this is a specific name string. Otherwise, this is usually the first word of the - * message generated by the magic file. - */ - @NonNull - public String getName() { - return mName; - } - - /** - * Returns the mime-type or null if none. - */ - @Nullable - public String getMimeType() { - return mMimeType; - } - - /** - * Returns the full message as generated by the magic matching code or null if none. This should be similar to the - * output from the Unix file(1) command. - */ - @Nullable - public String getMessage() { - return mMessage; - } - - /** - * Returns an array of associated file-extensions or null if none. - */ - @Nullable - public String[] getFileExtensions() { - return mFileExtensions; - } - /** - * Whether this was a partial match. For some types, there is a main matching pattern and then more - * specific patterns which detect additional features of the type. A partial match means that none of the more - * specific patterns fully matched the content. It's probably still of the type but just not a variant that the - * entries from the magic file(s) know about. - */ - public boolean isPartial() { - return mPartial; + private PathContentInfoImpl(@NonNull String name, @Nullable String message, @Nullable String mimeType, + @Nullable String[] fileExtensions, boolean partial) { + super(name, message, mimeType, fileExtensions, partial); } } diff --git a/app/src/main/java/io/github/muntashirakon/io/Path.java b/app/src/main/java/io/github/muntashirakon/io/PathImpl.java similarity index 65% rename from app/src/main/java/io/github/muntashirakon/io/Path.java rename to app/src/main/java/io/github/muntashirakon/io/PathImpl.java index 94e996f5314..ab9e038e323 100644 --- a/app/src/main/java/io/github/muntashirakon/io/Path.java +++ b/app/src/main/java/io/github/muntashirakon/io/PathImpl.java @@ -32,8 +32,6 @@ import androidx.documentfile.provider.MediaDocumentFile; import androidx.documentfile.provider.VirtualDocumentFile; -import org.jetbrains.annotations.Contract; - import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -41,27 +39,23 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Objects; -import aosp.libcore.util.EmptyArray; import io.github.muntashirakon.AppManager.compat.StorageManagerCompat; import io.github.muntashirakon.AppManager.ipc.LocalServices; import io.github.muntashirakon.AppManager.self.SelfPermissions; import io.github.muntashirakon.AppManager.utils.ContextUtils; -import io.github.muntashirakon.AppManager.utils.ExUtils; import io.github.muntashirakon.AppManager.utils.FileUtils; import io.github.muntashirakon.io.fs.VirtualFileSystem; /** * Provide an interface to {@link File} and {@link DocumentFile} with basic functionalities. */ -public class Path implements Comparable { - public static final String TAG = Path.class.getSimpleName(); +class PathImpl extends Path { + public static final String TAG = PathImpl.class.getSimpleName(); private static final List EXCLUSIVE_ACCESS_GRANTED = new ArrayList<>(); private static final List EXCLUSIVE_ACCESS_PATHS = new ArrayList<>(); @@ -159,38 +153,31 @@ private static DocumentFile getRequiredRawDocument(@NonNull String path) { // An invalid MIME so that it doesn't match any extension private static final String DEFAULT_MIME = "application/x-invalid-mime-type"; - @NonNull - private final Context mContext; - @NonNull - private DocumentFile mDocumentFile; - - /* package */ Path(@NonNull Context context, @NonNull String fileLocation) { - mContext = context; - mDocumentFile = getRequiredRawDocument(fileLocation); + /* package */ PathImpl(@NonNull Context context, @NonNull String fileLocation) { + super(context, getRequiredRawDocument(fileLocation)); } - /* package */ Path(@NonNull Context context, @NonNull VirtualFileSystem fs) { - mContext = context; - mDocumentFile = new VirtualDocumentFile(getParentFile(context, fs), fs); + /* package */ PathImpl(@NonNull Context context, @NonNull VirtualFileSystem fs) { + super(context, new VirtualDocumentFile(getParentFile(context, fs), fs)); } - /* package */ Path(@NonNull Context context, @NonNull String fileLocation, boolean privileged) throws RemoteException { - mContext = context; + /* package */ PathImpl(@NonNull Context context, @NonNull String fileLocation, boolean privileged) throws RemoteException { + super(context, null); if (privileged) { FileSystemManager fs = LocalServices.getFileSystemManager(); - mDocumentFile = new ExtendedRawDocumentFile(fs.getFile(fileLocation)); + documentFile = new ExtendedRawDocumentFile(fs.getFile(fileLocation)); } else { ExtendedFile file = FileSystemManager.getLocal().getFile(fileLocation); - mDocumentFile = new ExtendedRawDocumentFile(LocalFileOverlay.getOverlayFile(file)); + documentFile = new ExtendedRawDocumentFile(LocalFileOverlay.getOverlayFile(file)); } } - /* package */ Path(@NonNull Context context, @NonNull Uri uri) { - mContext = context; + /* package */ PathImpl(@NonNull Context context, @NonNull Uri uri) { + super(context, null); // At first check if the Uri is in VFS since it gets higher priority. Path fsRoot = VirtualFileSystem.getFsRoot(uri); if (fsRoot != null) { - mDocumentFile = fsRoot.mDocumentFile; + documentFile = fsRoot.documentFile; return; } if (uri.getScheme() == null) { @@ -218,11 +205,11 @@ private static DocumentFile getRequiredRawDocument(@NonNull String path) { String path = Paths.sanitize(parsedUri.second, true); if (TextUtils.isEmpty(path) || path.equals(File.separator)) { // Root requested - documentFile = rootPath.mDocumentFile; + documentFile = rootPath.documentFile; } else { // Find file is acceptable here since the file always exists String[] pathComponents = path.split(File.separator); - DocumentFile finalDocumentFile = rootPath.mDocumentFile; + DocumentFile finalDocumentFile = rootPath.documentFile; for (String pathComponent : pathComponents) { finalDocumentFile = Objects.requireNonNull(finalDocumentFile.findFile(pathComponent)); } @@ -236,20 +223,20 @@ private static DocumentFile getRequiredRawDocument(@NonNull String path) { throw new IllegalArgumentException("Unsupported uri " + uri); } // Setting mDocumentFile at the end ensures that it is never null - mDocumentFile = documentFile; + this.documentFile = documentFile; } /** * NOTE: This construct is only applicable for tree Uri */ - /* package */ Path(@Nullable Path parent, @NonNull Context context, @NonNull Uri documentUri) { - mContext = context; - DocumentFile parentDocumentFile = parent != null ? parent.mDocumentFile : null; - mDocumentFile = DocumentFileUtils.newTreeDocumentFile(parentDocumentFile, context, documentUri); + /* package */ PathImpl(@Nullable Path parent, @NonNull Context context, @NonNull Uri documentUri) { + super(context, null); + DocumentFile parentDocumentFile = parent != null ? parent.documentFile : null; + documentFile = DocumentFileUtils.newTreeDocumentFile(parentDocumentFile, context, documentUri); } - private Path(@NonNull Context context, @NonNull DocumentFile documentFile) { - mContext = context; + private PathImpl(@NonNull Context context, @NonNull DocumentFile documentFile) { + super(context, null); if (documentFile instanceof ExtendedRawDocumentFile) { ExtendedFile file = ((ExtendedRawDocumentFile) documentFile).getFile(); if (file instanceof LocalFile) { @@ -259,43 +246,19 @@ private Path(@NonNull Context context, @NonNull DocumentFile documentFile) { } } } - mDocumentFile = documentFile; + this.documentFile = documentFile; } - /** - * Return the last segment of this path. - */ @NonNull public String getName() { // Last path segment is required. - String name = mDocumentFile.getName(); + String name = documentFile.getName(); if (name != null) { return name; } - return DocumentFileUtils.resolveAltNameForSaf(mDocumentFile); + return DocumentFileUtils.resolveAltNameForSaf(documentFile); } - @Nullable - public String getExtension() { - String name = getName(); - int lastIndexOfDot = name.lastIndexOf('.'); - if (lastIndexOfDot == -1 || lastIndexOfDot + 1 == name.length()) { - return null; - } - return name.substring(lastIndexOfDot + 1).toLowerCase(Locale.ROOT); - } - - /** - * Return a URI for the underlying document represented by this file. This - * can be used with other platform APIs to manipulate or share the - * underlying content. {@link DocumentFile#isDocumentUri(Context, Uri)} can - * be used to test if the returned Uri is backed by an - * {@link android.provider.DocumentsProvider}. - */ - @NonNull - public Uri getUri() { - return mDocumentFile.getUri(); - } /** * Return the underlying {@link ExtendedFile} if the path is backed by a real file, @@ -303,8 +266,8 @@ public Uri getUri() { */ @Nullable public ExtendedFile getFile() { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { - return ((ExtendedRawDocumentFile) mDocumentFile).getFile(); + if (documentFile instanceof ExtendedRawDocumentFile) { + return ((ExtendedRawDocumentFile) documentFile).getFile(); } return null; } @@ -314,8 +277,8 @@ public ExtendedFile getFile() { */ @Nullable public String getFilePath() { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { - return mDocumentFile.getUri().getPath(); + if (documentFile instanceof ExtendedRawDocumentFile) { + return documentFile.getUri().getPath(); } return null; } @@ -326,7 +289,7 @@ public String getFilePath() { */ @Nullable public String getRealFilePath() throws IOException { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { return Objects.requireNonNull(getFile()).getCanonicalPath(); } return null; @@ -338,20 +301,17 @@ public String getRealFilePath() throws IOException { */ @Nullable public Path getRealPath() throws IOException { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { return Paths.get(Objects.requireNonNull(getFile()).getCanonicalFile()); } return null; } - /** - * Return the MIME type of the path - */ @NonNull public String getType() { - String type = getRealDocumentFile(mDocumentFile).getType(); + String type = getRealDocumentFile(documentFile).getType(); if (type == null) { - type = PathContentInfo.fromExtension(this).getMimeType(); + type = PathContentInfoImpl.fromExtension(this).getMimeType(); } if (type == null) { type = "application/octet-stream"; @@ -359,36 +319,19 @@ public String getType() { return type; } - /** - * Return the content info of the path. - *

- * This is an expensive operation and should be done in a non-UI thread. - */ @NonNull public PathContentInfo getPathContentInfo() { - return PathContentInfo.fromPath(this); + return PathContentInfoImpl.fromPath(this); } - /** - * Returns the length of this path in bytes. Returns 0 if the path does not - * exist, or if the length is unknown. The result for a directory is not - * defined. - */ @CheckResult public long length() { - return getRealDocumentFile(mDocumentFile).length(); + return getRealDocumentFile(documentFile).length(); } - /** - * Recreate this path if required. - *

- * This only recreates files and not directories in order to avoid potential mass destructive operation. - * - * @return {@code true} iff the path has been recreated. - */ @CheckResult public boolean recreate() { - DocumentFile documentFile = getRealDocumentFile(mDocumentFile); + DocumentFile documentFile = getRealDocumentFile(this.documentFile); if (documentFile.isDirectory()) { // Directory does not need to be created again. return true; @@ -413,40 +356,15 @@ public boolean recreate() { } } - /** - * Create a new file as a direct child of this directory. If the file - * already exists, and it is not a directory, it will try to delete it - * and create a new one. - * - * @param displayName Display name for the file with or without extension. - * The name must not contain any file separator. - * @param mimeType Mime type for the new file. Underlying provider may - * choose to add extension based on the mime type. If - * displayName contains an extension, set it to null. - * @return The newly created file. - * @throws IOException If the target is a mount point, a directory, or the current file is not a - * directory, or failed for any other reason. - * @throws IllegalArgumentException If the display name contains file separator. - */ @NonNull public Path createNewFile(@NonNull String displayName, @Nullable String mimeType) throws IOException { displayName = Paths.sanitize(displayName, true); if (displayName == null) { throw new IOException("Empty display name."); } - return createFileAsDirectChild(mContext, mDocumentFile, displayName, mimeType); + return createFileAsDirectChild(context, documentFile, displayName, mimeType); } - /** - * Create a new directory as a direct child of this directory. - * - * @param displayName Display name for the directory. The name must not - * contain any file separator. - * @return The newly created directory. - * @throws IOException If the target is a mount point or the current file is not a directory, - * or failed for any other reason. - * @throws IllegalArgumentException If the display name contains file separator. - */ @NonNull public Path createNewDirectory(@NonNull String displayName) throws IOException { displayName = Paths.sanitize(displayName, true); @@ -456,32 +374,16 @@ public Path createNewDirectory(@NonNull String displayName) throws IOException { if (displayName.indexOf(File.separatorChar) != -1) { throw new IllegalArgumentException("Display name contains file separator."); } - DocumentFile documentFile = getRealDocumentFile(mDocumentFile); + DocumentFile documentFile = getRealDocumentFile(this.documentFile); if (!documentFile.isDirectory()) { throw new IOException("Current file is not a directory."); } checkVfs(Paths.appendPathSegment(documentFile.getUri(), displayName)); DocumentFile file = documentFile.createDirectory(displayName); if (file == null) throw new IOException("Could not create directory named " + displayName); - return new Path(mContext, file); + return new PathImpl(context, file); } - /** - * Create a new file at some arbitrary level under this directory, - * non-existing paths are created if necessary. If the file already exists, - * and it isn't a directory, it will try to delete it and create a new one. - * If mount points encountered while iterating through the paths, it will - * try to create a new file under the last mount point. - * - * @param displayName Display name for the file with or without extension - * and/or file separator. - * @param mimeType Mime type for the new file. Underlying provider may - * choose to add extension based on the mime type. If - * displayName contains an extension, set it to null. - * @return The newly created file. - * @throws IOException If the target is a mount point, a directory or failed for any other reason. - * @throws IllegalArgumentException If the display name is malformed. - */ @NonNull public Path createNewArbitraryFile(@NonNull String displayName, @Nullable String mimeType) throws IOException { displayName = Paths.sanitize(displayName, true); @@ -497,20 +399,10 @@ public Path createNewArbitraryFile(@NonNull String displayName, @Nullable String throw new IOException("Could not create directories in the parent directory."); } } - DocumentFile file = createArbitraryDirectories(mDocumentFile, names, names.length - 1); - return createFileAsDirectChild(mContext, file, names[names.length - 1], mimeType); + DocumentFile file = createArbitraryDirectories(documentFile, names, names.length - 1); + return createFileAsDirectChild(context, file, names[names.length - 1], mimeType); } - - /** - * Create all the non-existing directories under this directory. If mount - * points encountered while iterating through the paths, it will try to - * create a new directory under the last mount point. - * - * @param displayName Relative path to the target directory. - * @return The newly created directory. - * @throws IOException If the target is a mount point, or failed for any other reason. - */ @NonNull public Path createDirectoriesIfRequired(@NonNull String displayName) throws IOException { displayName = Paths.sanitize(displayName, true); @@ -526,19 +418,10 @@ public Path createDirectoriesIfRequired(@NonNull String displayName) throws IOEx throw new IOException("Could not create directories in the parent directory."); } } - DocumentFile file = createArbitraryDirectories(mDocumentFile, dirNames, dirNames.length); - return new Path(mContext, file); + DocumentFile file = createArbitraryDirectories(documentFile, dirNames, dirNames.length); + return new PathImpl(context, file); } - /** - * Create all the non-existing directories under this directory. If mount - * points encountered while iterating through the paths, it will try to - * create a new directory under the last mount point. - * - * @param displayName Relative path to the target directory. - * @return The newly created directory. - * @throws IOException If the target exists, or it is a mount point, or failed for any other reason. - */ @NonNull public Path createDirectories(@NonNull String displayName) throws IOException { displayName = Paths.sanitize(displayName, true); @@ -554,11 +437,11 @@ public Path createDirectories(@NonNull String displayName) throws IOException { throw new IOException("Could not create directories in the parent directory."); } } - DocumentFile file = createArbitraryDirectories(mDocumentFile, dirNames, dirNames.length - 1); + DocumentFile file = createArbitraryDirectories(documentFile, dirNames, dirNames.length - 1); // Special case for the last segment String lastSegment = dirNames[dirNames.length - 1]; Path fsRoot = VirtualFileSystem.getFsRoot(Paths.appendPathSegment(file.getUri(), lastSegment)); - DocumentFile t = fsRoot != null ? fsRoot.mDocumentFile : file.findFile(lastSegment); + DocumentFile t = fsRoot != null ? fsRoot.documentFile : file.findFile(lastSegment); if (t != null) { throw new IOException(t.getUri() + " already exists."); } @@ -566,89 +449,28 @@ public Path createDirectories(@NonNull String displayName) throws IOException { if (t == null) { throw new IOException("Directory" + file.getUri() + File.separator + lastSegment + " could not be created."); } - return new Path(mContext, t); - } - - /** - * Delete this file. If this is a directory, it is deleted recursively. - * - * @return {@code true} if the file was deleted, {@code false} if the file - * is a mount point or any other error occurred. - */ - public boolean delete() { - if (isMountPoint()) { - return false; - } - return mDocumentFile.delete(); + return new PathImpl(context, t); } - /** - * Return the parent file of this document. If this is a mount point, - * the parent is the parent of the mount point. For tree-documents, - * the consistency of the parent file isn't guaranteed as the underlying - * directory tree might be altered by another application. - */ @Nullable public Path getParent() { - DocumentFile file = getRealDocumentFile(mDocumentFile).getParentFile(); - return file == null ? null : new Path(mContext, file); + DocumentFile file = getRealDocumentFile(documentFile).getParentFile(); + return file == null ? null : new PathImpl(context, file); } - /** - * Return the parent file of this document. If this is a mount point, - * the parent is the parent of the mount point. For tree-documents, - * the consistency of the parent file isn't guaranteed as the underlying - * directory tree might be altered by another application. - */ - @NonNull - public Path requireParent() { - return Objects.requireNonNull(getParent()); - } - - /** - * Whether this file has a file denoted by this abstract name. The file - * isn't necessarily have to be a direct child of this file. - * - * @param displayName Display name for the file with extension and/or - * file separator if applicable. - * @return {@code true} if the file denoted by this abstract name exists. - */ public boolean hasFile(@NonNull String displayName) { - return findFileInternal(mDocumentFile, displayName) != null; + return findFileInternal(documentFile, displayName) != null; } - /** - * Return the file denoted by this abstract name in this file. File name - * can be either case-sensitive or case-insensitive depending on the file - * provider. - * - * @param displayName Display name for the file with extension and/or - * file separator if applicable. - * @return The first file that matches the name. - * @throws FileNotFoundException If the file was not found. - */ @NonNull public Path findFile(@NonNull String displayName) throws FileNotFoundException { - DocumentFile nextPath = findFileInternal(mDocumentFile, displayName); + DocumentFile nextPath = findFileInternal(documentFile, displayName); if (nextPath == null) { throw new FileNotFoundException("Cannot find " + this + File.separatorChar + displayName); } - return new Path(mContext, nextPath); + return new PathImpl(context, nextPath); } - /** - * Return a file that is a direct child of this directory, creating if necessary. - * - * @param displayName Display name for the file with or without extension. - * The name must not contain any file separator. - * @param mimeType Mime type for the new file. Underlying provider may - * choose to add extension based on the mime type. If - * displayName contains an extension, set it to null. - * @return The existing or newly created file. - * @throws IOException If the target is a mount point, a directory, or the current file is not a - * directory, or failed for any other reason. - * @throws IllegalArgumentException If the display name contains file separator. - */ @NonNull public Path findOrCreateFile(@NonNull String displayName, @Nullable String mimeType) throws IOException { displayName = Paths.sanitize(displayName, true); @@ -658,7 +480,7 @@ public Path findOrCreateFile(@NonNull String displayName, @Nullable String mimeT if (displayName.indexOf(File.separatorChar) != -1) { throw new IllegalArgumentException("Display name contains file separator."); } - DocumentFile documentFile = getRealDocumentFile(mDocumentFile); + DocumentFile documentFile = getRealDocumentFile(this.documentFile); if (!documentFile.isDirectory()) { throw new IOException("Current file is not a directory."); } @@ -673,27 +495,15 @@ public Path findOrCreateFile(@NonNull String displayName, @Nullable String mimeT if (file.isDirectory()) { throw new IOException("Directory cannot be converted to file"); } - return new Path(mContext, file); + return new PathImpl(context, file); } file = documentFile.createFile(mimeType, displayName); if (file == null) { throw new IOException("Could not create " + documentFile.getUri() + File.separatorChar + nameWithExtension + " with type " + mimeType); } - return new Path(mContext, file); + return new PathImpl(context, file); } - /** - * Return a directory that is a direct child of this directory, creating - * if necessary. - * - * @param displayName Display name for the directory. The name must not - * contain any file separator. - * @return The existing or newly created directory or mount point. - * @throws IOException If the target directory could not be created, or the existing or the - * current file is not a directory. - * @throws IllegalArgumentException If the display name contains file - * separator. - */ @NonNull public Path findOrCreateDirectory(@NonNull String displayName) throws IOException { displayName = Paths.sanitize(displayName, true); @@ -703,7 +513,7 @@ public Path findOrCreateDirectory(@NonNull String displayName) throws IOExceptio if (displayName.indexOf(File.separatorChar) != -1) { throw new IllegalArgumentException("Display name contains file separator."); } - DocumentFile documentFile = getRealDocumentFile(mDocumentFile); + DocumentFile documentFile = getRealDocumentFile(this.documentFile); if (!documentFile.isDirectory()) { throw new IOException("Current file is not a directory."); } @@ -714,96 +524,54 @@ public Path findOrCreateDirectory(@NonNull String displayName) throws IOExceptio if (!file.isDirectory()) { throw new IOException("Existing file is not a directory"); } - return new Path(mContext, file); + return new PathImpl(context, file); } file = documentFile.createDirectory(displayName); if (file == null) throw new IOException("Could not create directory named " + displayName); - return new Path(mContext, file); + return new PathImpl(context, file); } @NonNull public PathAttributes getAttributes() throws IOException { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { - return PathAttributes.fromFile((ExtendedRawDocumentFile) mDocumentFile); + if (documentFile instanceof ExtendedRawDocumentFile) { + return PathAttributesImpl.fromFile((ExtendedRawDocumentFile) documentFile); } - if (mDocumentFile instanceof VirtualDocumentFile) { - return PathAttributes.fromVirtual((VirtualDocumentFile) mDocumentFile); + if (documentFile instanceof VirtualDocumentFile) { + return PathAttributesImpl.fromVirtual((VirtualDocumentFile) documentFile); } - return PathAttributes.fromSaf(mContext, mDocumentFile); + return PathAttributesImpl.fromSaf(context, documentFile); } - /** - * Whether this file can be found. This is useful only for the paths - * accessed using Java File API. In other cases, the file has to exist - * before it can be accessed. However, in SAF, the file can be deleted - * by another application in which case the URI becomes non-existent. - * - * @return {@code true} if the file exists. - */ @CheckResult public boolean exists() { - return getRealDocumentFile(mDocumentFile).exists(); + return getRealDocumentFile(documentFile).exists(); } - /** - * Whether this file is a directory. A mount point is also considered as a - * directory. - *

- * Note that the return value {@code false} does not necessarily mean that - * the path is a file. - * - * @return {@code true} if the file is a directory or a mount point. - */ @CheckResult public boolean isDirectory() { - return getRealDocumentFile(mDocumentFile).isDirectory(); + return getRealDocumentFile(documentFile).isDirectory(); } - /** - * Whether this file is a file. - *

- * Note that the return value {@code false} does not necessarily mean that - * the path is a directory. - * - * @return {@code true} if the file is a file. - */ @CheckResult public boolean isFile() { - return getRealDocumentFile(mDocumentFile).isFile(); + return getRealDocumentFile(documentFile).isFile(); } - /** - * Whether the file is a virtual file i.e. it has no physical existence. - * - * @return {@code true} if this is a virtual file. - */ @CheckResult public boolean isVirtual() { - return getRealDocumentFile(mDocumentFile).isVirtual(); + return getRealDocumentFile(documentFile).isVirtual(); } - /** - * Whether the file is a symbolic link, only applicable for Java File API. - * - * @return {@code true} iff the file is accessed using Java File API and - * is a symbolic link. - */ + @CheckResult public boolean isSymbolicLink() { - if (getRealDocumentFile(mDocumentFile) instanceof ExtendedRawDocumentFile) { + if (getRealDocumentFile(documentFile) instanceof ExtendedRawDocumentFile) { return Objects.requireNonNull(getFile()).isSymlink(); } return false; } - /** - * Creates a new symbolic link named by this abstract pathname to a target file if and only if the pathname is a - * physical file and is not yet exist. - * - * @param target the target of the symbolic link. - * @return {@code true} if target did not exist and the link was successfully created, and {@code false} otherwise. - */ public boolean createNewSymbolicLink(String target) { - if (getRealDocumentFile(mDocumentFile) instanceof ExtendedRawDocumentFile) { + if (getRealDocumentFile(documentFile) instanceof ExtendedRawDocumentFile) { try { return Objects.requireNonNull(getFile()).createNewSymlink(target); } catch (IOException e) { @@ -813,52 +581,37 @@ public boolean createNewSymbolicLink(String target) { return false; } - /** - * Whether the file can be read. - * - * @return {@code true} if it can be read. - */ public boolean canRead() { - return getRealDocumentFile(mDocumentFile).canRead(); + return getRealDocumentFile(documentFile).canRead(); } - /** - * Whether the file can be written. - * - * @return {@code true} if it can be written. - */ public boolean canWrite() { - return getRealDocumentFile(mDocumentFile).canWrite(); + return getRealDocumentFile(documentFile).canWrite(); } - /** - * Whether the file can be executed. - * - * @return {@code true} if it can be executed. - */ public boolean canExecute() { - if (getRealDocumentFile(mDocumentFile) instanceof ExtendedRawDocumentFile) { + if (getRealDocumentFile(documentFile) instanceof ExtendedRawDocumentFile) { return Objects.requireNonNull(getFile()).canExecute(); } return false; } public int getMode() { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { try { return Objects.requireNonNull(getFile()).getMode(); } catch (ErrnoException e) { return 0; } } - if (mDocumentFile instanceof VirtualDocumentFile) { - return ((VirtualDocumentFile) mDocumentFile).getMode(); + if (documentFile instanceof VirtualDocumentFile) { + return ((VirtualDocumentFile) documentFile).getMode(); } return 0; } public boolean setMode(int mode) { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { try { Objects.requireNonNull(getFile()).setMode(mode); return true; @@ -866,51 +619,51 @@ public boolean setMode(int mode) { return false; } } - if (mDocumentFile instanceof VirtualDocumentFile) { - return ((VirtualDocumentFile) mDocumentFile).setMode(mode); + if (documentFile instanceof VirtualDocumentFile) { + return ((VirtualDocumentFile) documentFile).setMode(mode); } return false; } @Nullable public UidGidPair getUidGid() { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { try { return Objects.requireNonNull(getFile()).getUidGid(); } catch (ErrnoException e) { return null; } } - if (mDocumentFile instanceof VirtualDocumentFile) { - return ((VirtualDocumentFile) mDocumentFile).getUidGid(); + if (documentFile instanceof VirtualDocumentFile) { + return ((VirtualDocumentFile) documentFile).getUidGid(); } return null; } public boolean setUidGid(UidGidPair uidGidPair) { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { try { return Objects.requireNonNull(getFile()).setUidGid(uidGidPair.uid, uidGidPair.gid); } catch (ErrnoException e) { return false; } } - if (mDocumentFile instanceof VirtualDocumentFile) { - return ((VirtualDocumentFile) mDocumentFile).setUidGid(uidGidPair); + if (documentFile instanceof VirtualDocumentFile) { + return ((VirtualDocumentFile) documentFile).setUidGid(uidGidPair); } return false; } @Nullable public String getSelinuxContext() { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { return Objects.requireNonNull(getFile()).getSelinuxContext(); } return null; } public boolean setSelinuxContext(@Nullable String context) { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { if (context == null) { return Objects.requireNonNull(getFile()).restoreSelinuxContext(); } @@ -919,17 +672,12 @@ public boolean setSelinuxContext(@Nullable String context) { return false; } - /** - * Whether the file is a mount point, thereby, is being overridden by another file system. - * - * @return {@code true} if this is a mount point. - */ public boolean isMountPoint() { return VirtualFileSystem.isMountPoint(getUri()); } public boolean mkdir() { - DocumentFile documentFile = getRealDocumentFile(mDocumentFile); + DocumentFile documentFile = getRealDocumentFile(this.documentFile); if (documentFile.exists()) { return false; } @@ -940,7 +688,7 @@ public boolean mkdir() { if (parent != null) { DocumentFile thisFile = parent.createDirectory(getName()); if (thisFile != null) { - mDocumentFile = thisFile; + this.documentFile = thisFile; return true; } } @@ -949,7 +697,7 @@ public boolean mkdir() { } public boolean mkdirs() { - DocumentFile documentFile = getRealDocumentFile(mDocumentFile); + DocumentFile documentFile = getRealDocumentFile(this.documentFile); if (documentFile.exists()) { return false; } @@ -961,30 +709,13 @@ public boolean mkdirs() { if (parent != null) { DocumentFile thisFile = parent.createDirectory(getName()); if (thisFile != null) { - mDocumentFile = thisFile; + this.documentFile = thisFile; return true; } } return false; } - /** - * Renames this file to {@code displayName}, both containing in the same directory. - *

- * Note that this method does not throw {@code IOException} on - * failure. Callers must check the return value. - *

- * Some providers may need to create a new document to reflect the rename, - * potentially with a different MIME type, so {@link #getUri()} and - * {@link #getType()} may change to reflect the rename. - *

- * When renaming a directory, children previously enumerated through - * {@link #listFiles()} may no longer be valid. - * - * @param displayName the new display name. - * @return {@code true} on success. It returns {@code false} if the displayName is invalid or if it already exists. - * @throws UnsupportedOperationException when working with a single document - */ public boolean renameTo(@NonNull String displayName) { displayName = Paths.sanitize(displayName, true); if (displayName == null) { @@ -995,7 +726,7 @@ public boolean renameTo(@NonNull String displayName) { // Display name must belong to the same directory. return false; } - DocumentFile parent = mDocumentFile.getParentFile(); + DocumentFile parent = documentFile.getParentFile(); if (parent == null) { return false; } @@ -1009,46 +740,20 @@ public boolean renameTo(@NonNull String displayName) { // Mount point exists return false; } - Uri oldMountPoint = mDocumentFile.getUri(); - if (mDocumentFile.renameTo(displayName)) { + Uri oldMountPoint = documentFile.getUri(); + if (documentFile.renameTo(displayName)) { if (VirtualFileSystem.getFileSystem(oldMountPoint) != null) { // Change mount point - VirtualFileSystem.alterMountPoint(oldMountPoint, mDocumentFile.getUri()); + VirtualFileSystem.alterMountPoint(oldMountPoint, documentFile.getUri()); } return true; } return false; } - /** - * Same as {@link #moveTo(Path, boolean)} with override enabled - */ - public boolean moveTo(@NonNull Path dest) { - return moveTo(dest, true); - } - - /** - * Move the given path based on the following criteria: - *

    - *
  1. If both paths are physical (i.e. uses File API), use normal move behaviour - *
  2. If one of the paths is virtual or the above fails, use special copy and delete operation - *
- *

- * Move behavior is as follows: - *

    - *
  • If both are directories, move {@code this} inside {@code path} - *
  • If both are files, move {@code this} to {@code path} overriding it - *
  • If {@code this} is a file and {@code path} is a directory, move the file inside the directory - *
  • If {@code path} does not exist, it is created based on {@code this}. - *
- * - * @param path Target file/directory which may or may not exist - * @param override Whether to override the files in the destination - * @return {@code true} on success and {@code false} on failure - */ public boolean moveTo(@NonNull Path path, boolean override) { - DocumentFile source = getRealDocumentFile(mDocumentFile); - DocumentFile dest = getRealDocumentFile(path.mDocumentFile); + DocumentFile source = getRealDocumentFile(documentFile); + DocumentFile dest = getRealDocumentFile(path.documentFile); if (!source.exists()) { // Source itself does not exist. return false; @@ -1092,7 +797,7 @@ public boolean moveTo(@NonNull Path path, boolean override) { return false; } if (srcFile.renameTo(dstFile)) { - mDocumentFile = getRequiredRawDocument(dstFile.getAbsolutePath()); + documentFile = getRequiredRawDocument(dstFile.getAbsolutePath()); if (VirtualFileSystem.getFileSystem(Uri.fromFile(srcFile)) != null) { // Move mount point VirtualFileSystem.alterMountPoint(Uri.fromFile(srcFile), Uri.fromFile(dstFile)); @@ -1120,9 +825,9 @@ public boolean moveTo(@NonNull Path path, boolean override) { } try { // Copy all the directory items to the new path and delete source - copyDirectory(mContext, source, newPath); + copyDirectory(context, source, newPath); source.delete(); - mDocumentFile = newPath; + documentFile = newPath; return true; } catch (IOException e) { return false; @@ -1141,9 +846,9 @@ public boolean moveTo(@NonNull Path path, boolean override) { } try { // Copy all the directory items to the new path and delete source - copyDirectory(mContext, source, newPath); + copyDirectory(context, source, newPath); source.delete(); - mDocumentFile = newPath; + documentFile = newPath; return true; } catch (IOException e) { return false; @@ -1177,9 +882,9 @@ public boolean moveTo(@NonNull Path path, boolean override) { } try { // Copy the contents of the source file and delete it - copyFile(mContext, source, newPath); + copyFile(context, source, newPath); source.delete(); - mDocumentFile = newPath; + documentFile = newPath; return true; } catch (IOException e) { return false; @@ -1196,8 +901,8 @@ public Path copyTo(@NonNull Path path) { @Nullable public Path copyTo(@NonNull Path path, boolean override) { - DocumentFile source = getRealDocumentFile(mDocumentFile); - DocumentFile dest = getRealDocumentFile(path.mDocumentFile); + DocumentFile source = getRealDocumentFile(documentFile); + DocumentFile dest = getRealDocumentFile(path.documentFile); if (!source.exists()) { // Source itself does not exist. Log.d(TAG, "Source does not exist."); @@ -1242,8 +947,8 @@ public Path copyTo(@NonNull Path path, boolean override) { } try { // Copy all the directory items to the new path - copyDirectory(mContext, source, newPath, override); - return new Path(mContext, newPath); + copyDirectory(context, source, newPath, override); + return new PathImpl(context, newPath); } catch (IOException e) { Log.d(TAG, "Could not copy files.", e); return null; @@ -1264,8 +969,8 @@ public Path copyTo(@NonNull Path path, boolean override) { } try { // Copy all the directory items to the new path - copyDirectory(mContext, source, newPath, override); - return new Path(mContext, newPath); + copyDirectory(context, source, newPath, override); + return new PathImpl(context, newPath); } catch (IOException e) { Log.d(TAG, "Could not copy files.", e); return null; @@ -1316,8 +1021,8 @@ public Path copyTo(@NonNull Path path, boolean override) { } try { // Copy the contents of the source file - copyFile(mContext, source, newPath); - return new Path(mContext, newPath); + copyFile(context, source, newPath); + return new PathImpl(context, newPath); } catch (IOException e) { Log.d(TAG, "Could not copy files.", e); return null; @@ -1329,25 +1034,25 @@ public Path copyTo(@NonNull Path path, boolean override) { private static void copyFile(@NonNull Context context, @NonNull DocumentFile src, @NonNull DocumentFile dst) throws IOException { - copyFile(new Path(context, src), new Path(context, dst)); + copyFile(new PathImpl(context, src), new PathImpl(context, dst)); } private static void copyFile(@NonNull Path src, @NonNull Path dst) throws IOException { if (src.isMountPoint() || dst.isMountPoint()) { throw new IOException("Either source or destination are a mount point."); } - IoUtils.copy(src, dst, null); + IoUtils.copy(src, dst); } // Copy directory content private static void copyDirectory(@NonNull Context context, @NonNull DocumentFile src, @NonNull DocumentFile dst, boolean override) throws IOException { - copyDirectory(new Path(context, src), new Path(context, dst), override); + copyDirectory(new PathImpl(context, src), new PathImpl(context, dst), override); } private static void copyDirectory(@NonNull Context context, @NonNull DocumentFile src, @NonNull DocumentFile dst) throws IOException { - copyDirectory(new Path(context, src), new Path(context, dst), true); + copyDirectory(new PathImpl(context, src), new PathImpl(context, dst), true); } private static void copyDirectory(@NonNull Path src, @NonNull Path dst, boolean override) throws IOException { @@ -1372,31 +1077,31 @@ private static void copyDirectory(@NonNull Path src, @NonNull Path dst, boolean } public long lastModified() { - return mDocumentFile.lastModified(); + return documentFile.lastModified(); } public boolean setLastModified(long time) { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { return Objects.requireNonNull(getFile()).setLastModified(time); } - if (mDocumentFile instanceof VirtualDocumentFile) { - return ((VirtualDocumentFile) mDocumentFile).setLastModified(time); + if (documentFile instanceof VirtualDocumentFile) { + return ((VirtualDocumentFile) documentFile).setLastModified(time); } return false; } public long lastAccess() { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { return Objects.requireNonNull(getFile()).lastAccess(); } - if (mDocumentFile instanceof VirtualDocumentFile) { - return ((VirtualDocumentFile) mDocumentFile).lastAccess(); + if (documentFile instanceof VirtualDocumentFile) { + return ((VirtualDocumentFile) documentFile).lastAccess(); } return 0; } public boolean setLastAccess(long millis) { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { return Objects.requireNonNull(getFile()).setLastAccess(millis); } // // TODO: 28/6/23 @@ -1407,11 +1112,11 @@ public boolean setLastAccess(long millis) { } public long creationTime() { - if (mDocumentFile instanceof ExtendedRawDocumentFile) { + if (documentFile instanceof ExtendedRawDocumentFile) { return Objects.requireNonNull(getFile()).creationTime(); } - if (mDocumentFile instanceof VirtualDocumentFile) { - return ((VirtualDocumentFile) mDocumentFile).creationTime(); + if (documentFile instanceof VirtualDocumentFile) { + return ((VirtualDocumentFile) documentFile).creationTime(); } return 0; } @@ -1419,7 +1124,7 @@ public long creationTime() { @NonNull public Path[] listFiles() { // Get all file systems mounted at this Uri - DocumentFile documentFile = getRealDocumentFile(mDocumentFile); + DocumentFile documentFile = getRealDocumentFile(this.documentFile); VirtualFileSystem[] fileSystems = VirtualFileSystem.getFileSystemsAtUri(documentFile.getUri()); HashMap nameMountPointMap = new HashMap<>(fileSystems.length); for (VirtualFileSystem fs : fileSystems) { @@ -1432,7 +1137,7 @@ public Path[] listFiles() { // No need to go further Path[] paths = new Path[ss.length]; for (int i = 0; i < ss.length; ++i) { - paths[i] = new Path(mContext, ss[i]); + paths[i] = new PathImpl(context, ss[i]); } return paths; } @@ -1443,64 +1148,15 @@ public Path[] listFiles() { // Mount point exists, remove it from map nameMountPointMap.remove(s.getName()); } - paths.add(new Path(mContext, s)); + paths.add(new PathImpl(context, s)); } // Add all the other mount points for (Uri mountPoint : nameMountPointMap.values()) { - paths.add(new Path(mContext, mountPoint)); + paths.add(new PathImpl(context, mountPoint)); } return paths.toArray(new Path[0]); } - @NonNull - public Path[] listFiles(@Nullable FileFilter filter) { - Path[] ss = listFiles(); - ArrayList files = new ArrayList<>(); - for (Path s : ss) { - if ((filter == null) || filter.accept(s)) { - files.add(s); - } - } - return files.toArray(new Path[0]); - } - - @NonNull - public Path[] listFiles(@Nullable FilenameFilter filter) { - Path[] ss = listFiles(); - ArrayList files = new ArrayList<>(); - for (Path s : ss) { - s.getName(); - if (filter == null || filter.accept(this, s.getName())) { - files.add(s); - } - } - return files.toArray(new Path[0]); - } - - @NonNull - public String[] listFileNames() { - Path[] ss = listFiles(); - ArrayList files = new ArrayList<>(); - for (Path s : ss) { - s.getName(); - files.add(s.getName()); - } - return files.toArray(new String[0]); - } - - @NonNull - public String[] listFileNames(@Nullable FileFilter filter) { - Path[] ss = listFiles(); - ArrayList files = new ArrayList<>(); - for (Path s : ss) { - s.getName(); - if (filter == null || filter.accept(s)) { - files.add(s.getName()); - } - } - return files.toArray(new String[0]); - } - @NonNull public String[] listFileNames(@Nullable FilenameFilter filter) { Path[] ss = listFiles(); @@ -1517,7 +1173,7 @@ public String[] listFileNames(@Nullable FilenameFilter filter) { @NonNull public ParcelFileDescriptor openFileDescriptor(@NonNull String mode, @NonNull HandlerThread callbackThread) throws FileNotFoundException { - DocumentFile documentFile = getRealDocumentFile(mDocumentFile); + DocumentFile documentFile = getRealDocumentFile(this.documentFile); if (documentFile instanceof ExtendedRawDocumentFile) { ExtendedFile file = Objects.requireNonNull(getFile()); if (file instanceof RemoteFile) { @@ -1537,18 +1193,14 @@ public ParcelFileDescriptor openFileDescriptor(@NonNull String mode, @NonNull Ha throw (FileNotFoundException) new FileNotFoundException(e.getMessage()).initCause(e); } } - return FileUtils.getFdFromUri(mContext, documentFile.getUri(), mode); - } - - public OutputStream openOutputStream() throws IOException { - return openOutputStream(false); + return FileUtils.getFdFromUri(context, documentFile.getUri(), mode); } @NonNull public OutputStream openOutputStream(boolean append) throws IOException { - DocumentFile documentFile = resolveFileOrNull(mDocumentFile); + DocumentFile documentFile = resolveFileOrNull(this.documentFile); if (documentFile == null) { - throw new IOException(mDocumentFile.getUri() + " is a directory"); + throw new IOException(this.documentFile.getUri() + " is a directory"); } if (documentFile instanceof ExtendedRawDocumentFile) { try { @@ -1560,7 +1212,7 @@ public OutputStream openOutputStream(boolean append) throws IOException { return ((VirtualDocumentFile) documentFile).openOutputStream(append); } String mode = "w" + (append ? "a" : "t"); - OutputStream os = mContext.getContentResolver().openOutputStream(documentFile.getUri(), mode); + OutputStream os = context.getContentResolver().openOutputStream(documentFile.getUri(), mode); if (os == null) { throw new IOException("Could not resolve Uri: " + documentFile.getUri()); } @@ -1569,9 +1221,9 @@ public OutputStream openOutputStream(boolean append) throws IOException { @NonNull public InputStream openInputStream() throws IOException { - DocumentFile documentFile = resolveFileOrNull(mDocumentFile); + DocumentFile documentFile = resolveFileOrNull(this.documentFile); if (documentFile == null) { - throw new IOException(mDocumentFile.getUri() + " is a directory"); + throw new IOException(this.documentFile.getUri() + " is a directory"); } if (documentFile instanceof ExtendedRawDocumentFile) { try { @@ -1582,7 +1234,7 @@ public InputStream openInputStream() throws IOException { } else if (documentFile instanceof VirtualDocumentFile) { return ((VirtualDocumentFile) documentFile).openInputStream(); } - InputStream is = mContext.getContentResolver().openInputStream(documentFile.getUri()); + InputStream is = context.getContentResolver().openInputStream(documentFile.getUri()); if (is == null) { throw new IOException("Could not resolve Uri: " + documentFile.getUri()); } @@ -1590,9 +1242,9 @@ public InputStream openInputStream() throws IOException { } public FileChannel openFileChannel(int mode) throws IOException { - DocumentFile documentFile = resolveFileOrNull(mDocumentFile); + DocumentFile documentFile = resolveFileOrNull(this.documentFile); if (documentFile == null) { - throw new IOException(mDocumentFile.getUri() + " is a directory"); + throw new IOException(this.documentFile.getUri() + " is a directory"); } if (documentFile instanceof ExtendedRawDocumentFile) { ExtendedFile file = Objects.requireNonNull(getFile()); @@ -1610,92 +1262,6 @@ public FileChannel openFileChannel(int mode) throws IOException { throw new IOException("Target is not backed by a real file"); } - @NonNull - public byte[] getContentAsBinary() { - return getContentAsBinary(EmptyArray.BYTE); - } - - @Nullable - @Contract("!null -> !null") - public byte[] getContentAsBinary(byte[] emptyValue) { - try (InputStream inputStream = openInputStream()) { - return IoUtils.readFully(inputStream, -1, true); - } catch (IOException e) { - if (!(e.getCause() instanceof ErrnoException)) { - // This isn't just another EACCESS exception - e.printStackTrace(); - } - } - return emptyValue; - } - - @NonNull - public String getContentAsString() { - return getContentAsString(""); - } - - @Nullable - @Contract("!null -> !null") - public String getContentAsString(@Nullable String emptyValue) { - String contents = ExUtils.exceptionAsNull(() -> { - try (InputStream inputStream = openInputStream()) { - return new String(IoUtils.readFully(inputStream, -1, true), Charset.defaultCharset()); - } - }); - return contents != null ? contents : emptyValue; - } - - @NonNull - @Override - public String toString() { - return getUri().toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Path)) return false; - Path path = (Path) o; - return mDocumentFile.getUri().equals(path.mDocumentFile.getUri()); - } - - @Override - public int hashCode() { - return mDocumentFile.getUri().hashCode(); - } - - @Override - public int compareTo(@NonNull Path o) { - return mDocumentFile.getUri().compareTo(o.mDocumentFile.getUri()); - } - - @FunctionalInterface - public interface FilenameFilter { - /** - * Tests if a specified file should be included in a file list. - * - * @param dir the directory in which the file was found. - * @param name the name of the file. - * @return true if and only if the name should be - * included in the file list; false otherwise. - */ - boolean accept(Path dir, String name); - } - - @FunctionalInterface - public interface FileFilter { - - /** - * Tests whether or not the specified abstract pathname should be - * included in a pathname list. - * - * @param pathname The abstract pathname to be tested - * @return true if and only if pathname - * should be included - */ - boolean accept(Path pathname); - } - @NonNull private static Path createFileAsDirectChild(@NonNull Context context, @NonNull DocumentFile documentFile, @@ -1726,7 +1292,7 @@ private static Path createFileAsDirectChild(@NonNull Context context, if (file == null) { throw new IOException("Could not create " + documentFile.getUri() + File.separatorChar + nameWithExtension + " with type " + mimeType); } - return new Path(context, file); + return new PathImpl(context, file); } @Nullable @@ -1743,7 +1309,7 @@ private static DocumentFile findFileInternal(@NonNull DocumentFile documentFile, Uri newUri = Paths.appendPathSegment(documentFile.getUri(), part); Path fsRoot = VirtualFileSystem.getFsRoot(newUri); // Mount point has the higher priority - documentFile = fsRoot != null ? fsRoot.mDocumentFile : documentFile.findFile(part); + documentFile = fsRoot != null ? fsRoot.documentFile : documentFile.findFile(part); if (documentFile == null) return null; } return documentFile; @@ -1757,7 +1323,7 @@ private static DocumentFile getParentFile(@NonNull Context context, @NonNull Vir } // FIXME: 9/9/23 This doesn't actually work for content URIs Uri parentUri = Paths.removeLastPathSegment(mountPoint); - return new Path(context, parentUri).mDocumentFile; + return new PathImpl(context, parentUri).documentFile; } @NonNull @@ -1767,7 +1333,7 @@ private static DocumentFile createArbitraryDirectories(@NonNull DocumentFile doc DocumentFile file = getRealDocumentFile(documentFile); for (int i = 0; i < length; ++i) { Path fsRoot = VirtualFileSystem.getFsRoot(Paths.appendPathSegment(file.getUri(), names[i])); - DocumentFile t = fsRoot != null ? fsRoot.mDocumentFile : file.findFile(names[i]); + DocumentFile t = fsRoot != null ? fsRoot.documentFile : file.findFile(names[i]); if (t == null) { t = file.createDirectory(names[i]); } else if (!t.isDirectory()) { @@ -1785,7 +1351,7 @@ private static DocumentFile createArbitraryDirectories(@NonNull DocumentFile doc private static DocumentFile getRealDocumentFile(@NonNull DocumentFile documentFile) { Path fsRoot = VirtualFileSystem.getFsRoot(documentFile.getUri()); if (fsRoot != null) { - return fsRoot.mDocumentFile; + return fsRoot.documentFile; } return documentFile; } diff --git a/app/src/main/java/io/github/muntashirakon/io/Paths.java b/app/src/main/java/io/github/muntashirakon/io/Paths.java index 8c9de05360e..1ebd65828a4 100644 --- a/app/src/main/java/io/github/muntashirakon/io/Paths.java +++ b/app/src/main/java/io/github/muntashirakon/io/Paths.java @@ -3,6 +3,7 @@ package io.github.muntashirakon.io; import android.content.ContentResolver; +import android.database.Cursor; import android.net.Uri; import android.os.RemoteException; import android.os.UserHandleHidden; @@ -91,7 +92,7 @@ public static Path getPrimaryPath(@Nullable String path) { public static Path getUnprivileged(@NonNull File pathName) { Path path = null; try { - path = new Path(ContextUtils.getContext(), pathName.getAbsolutePath(), false); + path = new PathImpl(ContextUtils.getContext(), pathName.getAbsolutePath(), false); } catch (RemoteException ignore) { // This exception is never called in unprivileged mode. } @@ -103,7 +104,7 @@ public static Path getUnprivileged(@NonNull File pathName) { public static Path getUnprivileged(@NonNull String pathName) { Path path = null; try { - path = new Path(ContextUtils.getContext(), pathName, false); + path = new PathImpl(ContextUtils.getContext(), pathName, false); } catch (RemoteException ignore) { // This exception is never called in unprivileged mode. } @@ -113,23 +114,23 @@ public static Path getUnprivileged(@NonNull String pathName) { @NonNull public static Path get(@NonNull String pathName) { - return new Path(ContextUtils.getContext(), Objects.requireNonNull(pathName)); + return new PathImpl(ContextUtils.getContext(), Objects.requireNonNull(pathName)); } @NonNull public static Path get(@NonNull File pathName) { - return new Path(ContextUtils.getContext(), pathName.getAbsolutePath()); + return new PathImpl(ContextUtils.getContext(), pathName.getAbsolutePath()); } @NonNull public static Path get(@NonNull Uri pathUri) { - return new Path(ContextUtils.getContext(), pathUri); + return new PathImpl(ContextUtils.getContext(), pathUri); } @NonNull public static Path getStrict(@NonNull Uri pathUri) throws FileNotFoundException { try { - return new Path(ContextUtils.getContext(), pathUri); + return new PathImpl(ContextUtils.getContext(), pathUri); } catch (IllegalArgumentException e) { throw (FileNotFoundException) (new FileNotFoundException(e.getMessage())).initCause(e); } @@ -137,12 +138,12 @@ public static Path getStrict(@NonNull Uri pathUri) throws FileNotFoundException @NonNull public static Path get(@NonNull VirtualFileSystem fs) { - return new Path(ContextUtils.getContext(), fs); + return new PathImpl(ContextUtils.getContext(), fs); } @NonNull public static Path getTreeDocument(@Nullable Path parent, @NonNull Uri documentUri) { - return new Path(parent, ContextUtils.getContext(), documentUri); + return new PathImpl(parent, ContextUtils.getContext(), documentUri); } @NonNull @@ -185,6 +186,11 @@ public static boolean exists(@Nullable File path) { return path != null && path.exists(); } + @NonNull + public static PathAttributes getAttributesFromSafTreeCursor(@NonNull Uri treeUri, @NonNull Cursor c) { + return PathAttributesImpl.fromSafTreeCursor(treeUri, c); + } + /** * Replace /storage/emulated with /data/media if the directory is inaccessible */ diff --git a/app/src/main/java/io/github/muntashirakon/io/fs/VirtualFileSystem.java b/app/src/main/java/io/github/muntashirakon/io/fs/VirtualFileSystem.java index e235537c2a4..032a60d56f6 100644 --- a/app/src/main/java/io/github/muntashirakon/io/fs/VirtualFileSystem.java +++ b/app/src/main/java/io/github/muntashirakon/io/fs/VirtualFileSystem.java @@ -1077,7 +1077,7 @@ private File getCachedFile(@NonNull Node node, boolean write) throws IOExcept // The file exists physically. It has to be cached first. try (InputStream is = getInputStream(node); FileOutputStream os = new FileOutputStream(cachedFile)) { - IoUtils.copy(is, os, -1, null); + IoUtils.copy(is, os); } } fileCacheItem = new FileCacheItem(cachedFile); diff --git a/app/src/main/java/io/github/muntashirakon/io/fs/ZipFileSystem.java b/app/src/main/java/io/github/muntashirakon/io/fs/ZipFileSystem.java index 09913b26f29..4ca07b533e1 100644 --- a/app/src/main/java/io/github/muntashirakon/io/fs/ZipFileSystem.java +++ b/app/src/main/java/io/github/muntashirakon/io/fs/ZipFileSystem.java @@ -160,7 +160,7 @@ private File getUpdatedZipFile(@NonNull Map> actionList) th File cachedFile = ((VirtualZipEntry) zipEntry).getCachedFile(); if (cachedFile != null) { try (InputStream is = new FileInputStream(cachedFile)) { - IoUtils.copy(is, zos, -1, null); + IoUtils.copy(is, zos); } } // else cached file was not created because the file was only created and never written to zos.closeEntry(); @@ -174,7 +174,7 @@ private File getUpdatedZipFile(@NonNull Map> actionList) th } // Entry is a file try (InputStream is = mZipFile.getInputStream(zipEntry)) { - IoUtils.copy(is, zos, -1, null); + IoUtils.copy(is, zos); } zos.closeEntry(); } diff --git a/app/src/test/java/io/github/muntashirakon/AppManager/utils/TarUtilsTest.java b/app/src/test/java/io/github/muntashirakon/AppManager/utils/TarUtilsTest.java index cd17253c590..ba581a80b81 100644 --- a/app/src/test/java/io/github/muntashirakon/AppManager/utils/TarUtilsTest.java +++ b/app/src/test/java/io/github/muntashirakon/AppManager/utils/TarUtilsTest.java @@ -61,7 +61,7 @@ public void setUp() throws Throwable { tmpFiles.add(testRoot.findOrCreateDirectory("prefixed").findOrCreateFile("prefixed_include.txt", null)); // Copy files to tmpRoot for (int i = 0; i < resFiles.size(); ++i) { - IoUtils.copy(resFiles.get(i), tmpFiles.get(i), null); + IoUtils.copy(resFiles.get(i), tmpFiles.get(i)); } tarGzFilesForExtractTest = TarUtils.create(TarUtils.TAR_GZIP, testRoot, tmpRoot, "am_ex.tar.gz", null, null, null, false).toArray(new Path[0]); diff --git a/app/src/test/java/io/github/muntashirakon/io/SplitInputStreamTest.java b/app/src/test/java/io/github/muntashirakon/io/SplitInputStreamTest.java index 12a189c461f..28ea512e71f 100644 --- a/app/src/test/java/io/github/muntashirakon/io/SplitInputStreamTest.java +++ b/app/src/test/java/io/github/muntashirakon/io/SplitInputStreamTest.java @@ -51,7 +51,7 @@ public void read() throws IOException { junkFiles.add(file); try (SplitInputStream splitInputStream = new SplitInputStream(fileList); OutputStream outputStream = new FileOutputStream(file)) { - IoUtils.copy(splitInputStream, outputStream, -1, null); + IoUtils.copy(splitInputStream, outputStream); } assert classLoader != null; String expectedHash = DigestUtils.getHexDigest(DigestUtils.SHA_256, new File(classLoader.getResource("AppManager_v2.5.22.apks").getFile())); diff --git a/app/src/test/java/io/github/muntashirakon/io/SplitOutputStreamTest.java b/app/src/test/java/io/github/muntashirakon/io/SplitOutputStreamTest.java index 2d0b2f0cf55..94170afc728 100644 --- a/app/src/test/java/io/github/muntashirakon/io/SplitOutputStreamTest.java +++ b/app/src/test/java/io/github/muntashirakon/io/SplitOutputStreamTest.java @@ -49,7 +49,7 @@ public void tearDown() throws Exception { @Test public void write() throws IOException { - IoUtils.copy(inputStream, splitOutputStream, -1, null); + IoUtils.copy(inputStream, splitOutputStream); List expectedHashes = getExpectedHashes(); List actualHashes = getActualHashes(); assertEquals(expectedHashes, actualHashes); diff --git a/app/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStreamTest.java b/app/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStreamTest.java index 65c2ab18652..fa03d39b1c9 100644 --- a/app/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStreamTest.java +++ b/app/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStreamTest.java @@ -53,7 +53,7 @@ public void TestUnTar() throws IOException { File file = new File("/tmp", entry.getName()); // copy TarArchiveInputStream to newPath try (OutputStream os = Paths.get(file).openOutputStream()) { - IoUtils.copy(tis, os, -1, null); + IoUtils.copy(tis, os); } } } diff --git a/app/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStreamTest.java b/app/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStreamTest.java index 0e097a17bc6..eb1e44cc3fb 100644 --- a/app/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStreamTest.java +++ b/app/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStreamTest.java @@ -58,7 +58,7 @@ public void testTarSplit() throws IOException { TarArchiveEntry tarEntry = new TarArchiveEntry(file, file.getName()); tot.putArchiveEntry(tarEntry); try (InputStream is = file.openInputStream()) { - IoUtils.copy(is, tot, -1, null); + IoUtils.copy(is, tot); } tot.closeArchiveEntry(); } @@ -105,7 +105,7 @@ public void testTar() throws IOException { TarArchiveEntry tarEntry = new TarArchiveEntry(file, file.getName()); tot.putArchiveEntry(tarEntry); try (InputStream is = file.openInputStream()) { - IoUtils.copy(is, tot, -1, null); + IoUtils.copy(is, tot); } tot.closeArchiveEntry(); } diff --git a/app/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStreamTest.java b/app/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStreamTest.java index 107dab1db67..5df0582ffae 100644 --- a/app/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStreamTest.java +++ b/app/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStreamTest.java @@ -51,7 +51,7 @@ public void testUnTarBZip2() throws IOException { File file = new File("/tmp", entry.getName()); // copy TarArchiveInputStream to newPath try (OutputStream os = Paths.get(file).openOutputStream()) { - IoUtils.copy(tis, os, -1, null); + IoUtils.copy(tis, os); } } } @@ -94,7 +94,7 @@ public void testSplitUnTarBZip2() throws IOException { File file = new File("/tmp", entry.getName()); // copy TarArchiveInputStream to newPath try (OutputStream os = Paths.get(file).openOutputStream()) { - IoUtils.copy(tis, os, -1, null); + IoUtils.copy(tis, os); } } } diff --git a/app/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorOutputStreamTest.java b/app/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorOutputStreamTest.java index 7293d94a98e..0016ba2b208 100644 --- a/app/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorOutputStreamTest.java +++ b/app/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorOutputStreamTest.java @@ -61,7 +61,7 @@ public void testTarGzip() throws IOException { TarArchiveEntry tarEntry = new TarArchiveEntry(file, file.getName()); tos.putArchiveEntry(tarEntry); try (InputStream is = file.openInputStream()) { - IoUtils.copy(is, tos, -1, null); + IoUtils.copy(is, tos); } tos.closeArchiveEntry(); } @@ -105,7 +105,7 @@ public void testSplitTarBZip2() throws IOException { TarArchiveEntry tarEntry = new TarArchiveEntry(file, file.getName()); tos.putArchiveEntry(tarEntry); try (InputStream is = file.openInputStream()) { - IoUtils.copy(is, tos, -1, null); + IoUtils.copy(is, tos); } tos.closeArchiveEntry(); } diff --git a/app/src/test/java/org/apache/commons/compress/compressors/gzip/GzipCompressorInputStreamTest.java b/app/src/test/java/org/apache/commons/compress/compressors/gzip/GzipCompressorInputStreamTest.java index a024be6b832..864d81e52e9 100644 --- a/app/src/test/java/org/apache/commons/compress/compressors/gzip/GzipCompressorInputStreamTest.java +++ b/app/src/test/java/org/apache/commons/compress/compressors/gzip/GzipCompressorInputStreamTest.java @@ -51,7 +51,7 @@ public void testUnTarGzip() throws IOException { File file = new File("/tmp", entry.getName()); // copy TarArchiveInputStream to newPath try (OutputStream os = Paths.get(file).openOutputStream()) { - IoUtils.copy(tis, os, -1, null); + IoUtils.copy(tis, os); } } } @@ -95,7 +95,7 @@ public void testSplitUnTarGzip() throws IOException { File file = new File("/tmp", entry.getName()); // copy TarArchiveInputStream to newPath try (OutputStream os = Paths.get(file).openOutputStream()) { - IoUtils.copy(tis, os, -1, null); + IoUtils.copy(tis, os); } } } diff --git a/app/src/test/java/org/apache/commons/compress/compressors/gzip/GzipCompressorOutputStreamTest.java b/app/src/test/java/org/apache/commons/compress/compressors/gzip/GzipCompressorOutputStreamTest.java index daa832fc0e1..d1d1344da28 100644 --- a/app/src/test/java/org/apache/commons/compress/compressors/gzip/GzipCompressorOutputStreamTest.java +++ b/app/src/test/java/org/apache/commons/compress/compressors/gzip/GzipCompressorOutputStreamTest.java @@ -61,7 +61,7 @@ public void testTarGzip() throws IOException { TarArchiveEntry tarEntry = new TarArchiveEntry(file, file.getName()); tos.putArchiveEntry(tarEntry); try (InputStream is = file.openInputStream()) { - IoUtils.copy(is, tos, -1, null); + IoUtils.copy(is, tos); } tos.closeArchiveEntry(); } @@ -105,7 +105,7 @@ public void testSplitTarGzip() throws IOException { TarArchiveEntry tarEntry = new TarArchiveEntry(file, file.getName()); tos.putArchiveEntry(tarEntry); try (InputStream is = file.openInputStream()) { - IoUtils.copy(is, tos, -1, null); + IoUtils.copy(is, tos); } tos.closeArchiveEntry(); } diff --git a/app/src/main/java/io/github/muntashirakon/io/CharSequenceInputStream.java b/libcore/io/src/main/java/io/github/muntashirakon/io/CharSequenceInputStream.java similarity index 99% rename from app/src/main/java/io/github/muntashirakon/io/CharSequenceInputStream.java rename to libcore/io/src/main/java/io/github/muntashirakon/io/CharSequenceInputStream.java index 11ebbcd65c8..8d3f8196fbd 100644 --- a/app/src/main/java/io/github/muntashirakon/io/CharSequenceInputStream.java +++ b/libcore/io/src/main/java/io/github/muntashirakon/io/CharSequenceInputStream.java @@ -16,7 +16,6 @@ * Implements an {@link InputStream} to read from String, StringBuffer, StringBuilder or CharBuffer. *

* Note: Supports {@link #mark(int)} and {@link #reset()}. - *

*/ // Copyright 2012 Apache Software Foundation public class CharSequenceInputStream extends InputStream { @@ -257,4 +256,4 @@ private static int checkMinBufferSize(final CharsetEncoder charsetEncoder, final private static float minBufferSize(final CharsetEncoder charsetEncoder) { return charsetEncoder.maxBytesPerChar() * 2; } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/muntashirakon/io/IoUtils.java b/libcore/io/src/main/java/io/github/muntashirakon/io/IoUtils.java similarity index 72% rename from app/src/main/java/io/github/muntashirakon/io/IoUtils.java rename to libcore/io/src/main/java/io/github/muntashirakon/io/IoUtils.java index 0bdd17acfa8..2430bb098b8 100644 --- a/app/src/main/java/io/github/muntashirakon/io/IoUtils.java +++ b/libcore/io/src/main/java/io/github/muntashirakon/io/IoUtils.java @@ -16,9 +16,7 @@ import java.io.OutputStream; import java.nio.charset.Charset; import java.util.Arrays; - -import io.github.muntashirakon.AppManager.progress.ProgressHandler; -import io.github.muntashirakon.AppManager.utils.ThreadUtils; +import java.util.concurrent.Executor; public final class IoUtils { public static final String TAG = IoUtils.class.getSimpleName(); @@ -74,35 +72,42 @@ public static String getInputStreamContent(@NonNull InputStream inputStream) thr } @AnyThread - public static long copy(@NonNull Path from, @NonNull Path to, @Nullable ProgressHandler progressHandler) + public static long copy(@NonNull Path from, @NonNull Path to) throws IOException { try (InputStream in = from.openInputStream(); OutputStream out = to.openOutputStream()) { - return copy(in, out, from.length(), progressHandler); + return copy(in, out); } } /** * Copy the contents of one stream to another. + */ + @AnyThread + public static long copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException { + return copy(in, out, null, null); + } - * @param totalSize Total size of the stream. Only used for handling progress. Set {@code -1} if unknown. + /** + * Copy the contents of one stream to another. + * + * @param executor that listener events should be delivered via. + * @param progressListener to be periodically notified as the copy progresses. */ @AnyThread - public static long copy(@NonNull InputStream in, @NonNull OutputStream out, long totalSize, - @Nullable ProgressHandler progressHandler) throws IOException { + public static long copy(@NonNull InputStream in, @NonNull OutputStream out, @Nullable Executor executor, + @Nullable ProgressListener progressListener) throws IOException { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - float lastProgress = progressHandler != null ? progressHandler.getLastProgress() : 0; - return FileUtils.copy(in, out, null, ThreadUtils.getBackgroundThreadExecutor(), progress -> { - if (progressHandler != null) { - progressHandler.postUpdate(100, lastProgress + (progress * 100f / totalSize)); + return FileUtils.copy(in, out, null, executor, progress -> { + if (progressListener != null) { + progressListener.onProgress(progress); } }); } else { - return copyLarge(in, out, totalSize, progressHandler); + return copyLarge(in, out, executor, progressListener); } } - @AnyThread public static void closeQuietly(@Nullable AutoCloseable closeable) { if (closeable == null) return; @@ -114,24 +119,32 @@ public static void closeQuietly(@Nullable AutoCloseable closeable) { } @AnyThread - private static long copyLarge(@NonNull InputStream in, @NonNull OutputStream out, long totalSize, - @Nullable ProgressHandler progressHandler) throws IOException { + private static long copyLarge(@NonNull InputStream in, @NonNull OutputStream out, @Nullable Executor executor, + @Nullable ProgressListener progressListener) + throws IOException { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; long count = 0; long checkpoint = 0; int n; - float lastProgress = progressHandler != null ? progressHandler.getLastProgress() : 0; while ((n = in.read(buffer)) > 0) { out.write(buffer, 0, n); count += n; checkpoint += n; if (checkpoint >= (1 << 19)) { // 512 kB - if (progressHandler != null) { - progressHandler.postUpdate(100, lastProgress + (count * 100f / totalSize)); + if (executor != null && progressListener != null) { + long countSnapshot = count; + executor.execute(() -> progressListener.onProgress(countSnapshot)); } checkpoint = 0; } } return count; } + + /** + * Listener that is called periodically as progress is made. + */ + public interface ProgressListener { + void onProgress(long progress); + } } diff --git a/libcore/io/src/main/java/io/github/muntashirakon/io/Path.java b/libcore/io/src/main/java/io/github/muntashirakon/io/Path.java new file mode 100644 index 00000000000..cb8e4939b5a --- /dev/null +++ b/libcore/io/src/main/java/io/github/muntashirakon/io/Path.java @@ -0,0 +1,619 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package io.github.muntashirakon.io; + +import android.content.Context; +import android.net.Uri; +import android.os.HandlerThread; +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; + +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; + +import org.jetbrains.annotations.Contract; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Locale; +import java.util.Objects; + +/** + * Provide an interface to {@link File} and {@link DocumentFile} with basic functionalities. + */ +public abstract class Path implements Comparable { + @NonNull + protected final Context context; + @NonNull + protected DocumentFile documentFile; + + protected Path(@NonNull Context context, @NonNull DocumentFile documentFile) { + this.context = context; + this.documentFile = documentFile; + } + + /** + * Return the last segment of this path. + */ + @NonNull + public abstract String getName(); + + @Nullable + public String getExtension() { + String name = getName(); + int lastIndexOfDot = name.lastIndexOf('.'); + if (lastIndexOfDot == -1 || lastIndexOfDot + 1 == name.length()) { + return null; + } + return name.substring(lastIndexOfDot + 1).toLowerCase(Locale.ROOT); + } + + /** + * Return a URI for the underlying document represented by this file. This + * can be used with other platform APIs to manipulate or share the + * underlying content. {@link DocumentFile#isDocumentUri(Context, Uri)} can + * be used to test if the returned Uri is backed by an + * {@link android.provider.DocumentsProvider}. + */ + @NonNull + public Uri getUri() { + return documentFile.getUri(); + } + + /** + * Return the underlying {@link ExtendedFile} if the path is backed by a real file, + * {@code null} otherwise. + */ + @Nullable + public abstract ExtendedFile getFile(); + + /** + * Same as {@link #getFile()} except it return a raw string. + */ + @Nullable + public abstract String getFilePath(); + + /** + * Same as {@link #getFile()} except it returns the real path if the + * current path is a symbolic link. + */ + @Nullable + public abstract String getRealFilePath() throws IOException; + + /** + * Same as {@link #getFile()} except it returns the real path if the + * current path is a symbolic link. + */ + @Nullable + public abstract Path getRealPath() throws IOException; + + /** + * Return the MIME type of the path + */ + @NonNull + public abstract String getType(); + + /** + * Return the content info of the path. + *

+ * This is an expensive operation and should be done in a non-UI thread. + */ + @NonNull + public abstract PathContentInfo getPathContentInfo(); + + /** + * Returns the length of this path in bytes. Returns 0 if the path does not + * exist, or if the length is unknown. The result for a directory is not + * defined. + */ + @CheckResult + public abstract long length(); + + /** + * Recreate this path if required. + *

+ * This only recreates files and not directories in order to avoid potential mass destructive operation. + * + * @return {@code true} iff the path has been recreated. + */ + @CheckResult + public abstract boolean recreate(); + + /** + * Create a new file as a direct child of this directory. If the file + * already exists, and it is not a directory, it will try to delete it + * and create a new one. + * + * @param displayName Display name for the file with or without extension. + * The name must not contain any file separator. + * @param mimeType Mime type for the new file. Underlying provider may + * choose to add extension based on the mime type. If + * displayName contains an extension, set it to null. + * @return The newly created file. + * @throws IOException If the target is a mount point, a directory, or the current file is not a + * directory, or failed for any other reason. + * @throws IllegalArgumentException If the display name contains file separator. + */ + @NonNull + public abstract Path createNewFile(@NonNull String displayName, @Nullable String mimeType) throws IOException; + + /** + * Create a new directory as a direct child of this directory. + * + * @param displayName Display name for the directory. The name must not + * contain any file separator. + * @return The newly created directory. + * @throws IOException If the target is a mount point or the current file is not a directory, + * or failed for any other reason. + * @throws IllegalArgumentException If the display name contains file separator. + */ + @NonNull + public abstract Path createNewDirectory(@NonNull String displayName) throws IOException; + + /** + * Create a new file at some arbitrary level under this directory, + * non-existing paths are created if necessary. If the file already exists, + * and it isn't a directory, it will try to delete it and create a new one. + * If mount points encountered while iterating through the paths, it will + * try to create a new file under the last mount point. + * + * @param displayName Display name for the file with or without extension + * and/or file separator. + * @param mimeType Mime type for the new file. Underlying provider may + * choose to add extension based on the mime type. If + * displayName contains an extension, set it to null. + * @return The newly created file. + * @throws IOException If the target is a mount point, a directory or failed for any other reason. + * @throws IllegalArgumentException If the display name is malformed. + */ + @NonNull + public abstract Path createNewArbitraryFile(@NonNull String displayName, @Nullable String mimeType) throws IOException; + + + /** + * Create all the non-existing directories under this directory. If mount + * points encountered while iterating through the paths, it will try to + * create a new directory under the last mount point. + * + * @param displayName Relative path to the target directory. + * @return The newly created directory. + * @throws IOException If the target is a mount point, or failed for any other reason. + */ + @NonNull + public abstract Path createDirectoriesIfRequired(@NonNull String displayName) throws IOException; + + /** + * Create all the non-existing directories under this directory. If mount + * points encountered while iterating through the paths, it will try to + * create a new directory under the last mount point. + * + * @param displayName Relative path to the target directory. + * @return The newly created directory. + * @throws IOException If the target exists, or it is a mount point, or failed for any other reason. + */ + @NonNull + public abstract Path createDirectories(@NonNull String displayName) throws IOException; + + /** + * Delete this file. If this is a directory, it is deleted recursively. + * + * @return {@code true} if the file was deleted, {@code false} if the file + * is a mount point or any other error occurred. + */ + public boolean delete() { + if (isMountPoint()) { + return false; + } + return documentFile.delete(); + } + + /** + * Return the parent file of this document. If this is a mount point, + * the parent is the parent of the mount point. For tree-documents, + * the consistency of the parent file isn't guaranteed as the underlying + * directory tree might be altered by another application. + */ + @Nullable + public abstract Path getParent(); + + /** + * Return the parent file of this document. If this is a mount point, + * the parent is the parent of the mount point. For tree-documents, + * the consistency of the parent file isn't guaranteed as the underlying + * directory tree might be altered by another application. + */ + @NonNull + public Path requireParent() { + return Objects.requireNonNull(getParent()); + } + + /** + * Whether this file has a file denoted by this abstract name. The file + * isn't necessarily have to be a direct child of this file. + * + * @param displayName Display name for the file with extension and/or + * file separator if applicable. + * @return {@code true} if the file denoted by this abstract name exists. + */ + public abstract boolean hasFile(@NonNull String displayName); + + /** + * Return the file denoted by this abstract name in this file. File name + * can be either case-sensitive or case-insensitive depending on the file + * provider. + * + * @param displayName Display name for the file with extension and/or + * file separator if applicable. + * @return The first file that matches the name. + * @throws FileNotFoundException If the file was not found. + */ + @NonNull + public abstract Path findFile(@NonNull String displayName) throws FileNotFoundException; + + /** + * Return a file that is a direct child of this directory, creating if necessary. + * + * @param displayName Display name for the file with or without extension. + * The name must not contain any file separator. + * @param mimeType Mime type for the new file. Underlying provider may + * choose to add extension based on the mime type. If + * displayName contains an extension, set it to null. + * @return The existing or newly created file. + * @throws IOException If the target is a mount point, a directory, or the current file is not a + * directory, or failed for any other reason. + * @throws IllegalArgumentException If the display name contains file separator. + */ + @NonNull + public abstract Path findOrCreateFile(@NonNull String displayName, @Nullable String mimeType) throws IOException; + + /** + * Return a directory that is a direct child of this directory, creating + * if necessary. + * + * @param displayName Display name for the directory. The name must not + * contain any file separator. + * @return The existing or newly created directory or mount point. + * @throws IOException If the target directory could not be created, or the existing or the + * current file is not a directory. + * @throws IllegalArgumentException If the display name contains file separator. + */ + @NonNull + public abstract Path findOrCreateDirectory(@NonNull String displayName) throws IOException; + + @NonNull + public abstract PathAttributes getAttributes() throws IOException; + + /** + * Whether this file can be found. This is useful only for the paths + * accessed using Java File API. In other cases, the file has to exist + * before it can be accessed. However, in SAF, the file can be deleted + * by another application in which case the URI becomes non-existent. + * + * @return {@code true} if the file exists. + */ + @CheckResult + public abstract boolean exists(); + + /** + * Whether this file is a directory. A mount point is also considered as a + * directory. + *

+ * Note that the return value {@code false} does not necessarily mean that + * the path is a file. + * + * @return {@code true} if the file is a directory or a mount point. + */ + @CheckResult + public abstract boolean isDirectory(); + + /** + * Whether this file is a file. + *

+ * Note that the return value {@code false} does not necessarily mean that + * the path is a directory. + * + * @return {@code true} if the file is a file. + */ + @CheckResult + public abstract boolean isFile(); + + /** + * Whether the file is a virtual file i.e. it has no physical existence. + * + * @return {@code true} if this is a virtual file. + */ + @CheckResult + public abstract boolean isVirtual(); + + /** + * Whether the file is a symbolic link, only applicable for Java File API. + * + * @return {@code true} iff the file is accessed using Java File API and + * is a symbolic link. + */ + @CheckResult + public abstract boolean isSymbolicLink(); + + /** + * Creates a new symbolic link named by this abstract pathname to a target file if and only if the pathname is a + * physical file and is not yet exist. + * + * @param target the target of the symbolic link. + * @return {@code true} if target did not exist and the link was successfully created, and {@code false} otherwise. + */ + public abstract boolean createNewSymbolicLink(String target); + + /** + * Whether the file can be read. + * + * @return {@code true} if it can be read. + */ + public abstract boolean canRead(); + + /** + * Whether the file can be written. + * + * @return {@code true} if it can be written. + */ + public abstract boolean canWrite(); + + /** + * Whether the file can be executed. + * + * @return {@code true} if it can be executed. + */ + public abstract boolean canExecute(); + + public abstract int getMode(); + + public abstract boolean setMode(int mode); + + @Nullable + public abstract UidGidPair getUidGid(); + + public abstract boolean setUidGid(UidGidPair uidGidPair); + + @Nullable + public abstract String getSelinuxContext(); + + public abstract boolean setSelinuxContext(@Nullable String context); + + /** + * Whether the file is a mount point, thereby, is being overridden by another file system. + * + * @return {@code true} if this is a mount point. + */ + public abstract boolean isMountPoint(); + + public abstract boolean mkdir(); + + public abstract boolean mkdirs(); + + /** + * Renames this file to {@code displayName}, both containing in the same directory. + *

+ * Note that this method does not throw {@code IOException} on + * failure. Callers must check the return value. + *

+ * Some providers may need to create a new document to reflect the rename, + * potentially with a different MIME type, so {@link #getUri()} and + * {@link #getType()} may change to reflect the rename. + *

+ * When renaming a directory, children previously enumerated through + * {@link #listFiles()} may no longer be valid. + * + * @param displayName the new display name. + * @return {@code true} on success. It returns {@code false} if the displayName is invalid or if it already exists. + * @throws UnsupportedOperationException when working with a single document + */ + public abstract boolean renameTo(@NonNull String displayName); + + /** + * Same as {@link #moveTo(Path, boolean)} with override enabled + */ + public boolean moveTo(@NonNull Path dest) { + return moveTo(dest, true); + } + + /** + * Move the given path based on the following criteria: + *

    + *
  1. If both paths are physical (i.e. uses File API), use normal move behaviour + *
  2. If one of the paths is virtual or the above fails, use special copy and delete operation + *
+ *

+ * Move behavior is as follows: + *

    + *
  • If both are directories, move {@code this} inside {@code path} + *
  • If both are files, move {@code this} to {@code path} overriding it + *
  • If {@code this} is a file and {@code path} is a directory, move the file inside the directory + *
  • If {@code path} does not exist, it is created based on {@code this}. + *
+ * + * @param path Target file/directory which may or may not exist + * @param override Whether to override the files in the destination + * @return {@code true} on success and {@code false} on failure + */ + public abstract boolean moveTo(@NonNull Path path, boolean override); + + + @Nullable + public Path copyTo(@NonNull Path path) { + return copyTo(path, true); + } + + @Nullable + public abstract Path copyTo(@NonNull Path path, boolean override); + + public abstract long lastModified(); + + public abstract boolean setLastModified(long time); + + public abstract long lastAccess(); + + public abstract boolean setLastAccess(long millis); + + public abstract long creationTime(); + + @NonNull + public abstract Path[] listFiles(); + + @NonNull + public Path[] listFiles(@Nullable FileFilter filter) { + Path[] ss = listFiles(); + ArrayList files = new ArrayList<>(); + for (Path s : ss) { + if ((filter == null) || filter.accept(s)) { + files.add(s); + } + } + return files.toArray(new Path[0]); + } + + @NonNull + public Path[] listFiles(@Nullable FilenameFilter filter) { + Path[] ss = listFiles(); + ArrayList files = new ArrayList<>(); + for (Path s : ss) { + s.getName(); + if (filter == null || filter.accept(this, s.getName())) { + files.add(s); + } + } + return files.toArray(new Path[0]); + } + + @NonNull + public String[] listFileNames() { + Path[] ss = listFiles(); + ArrayList files = new ArrayList<>(); + for (Path s : ss) { + s.getName(); + files.add(s.getName()); + } + return files.toArray(new String[0]); + } + + @NonNull + public String[] listFileNames(@Nullable FileFilter filter) { + Path[] ss = listFiles(); + ArrayList files = new ArrayList<>(); + for (Path s : ss) { + s.getName(); + if (filter == null || filter.accept(s)) { + files.add(s.getName()); + } + } + return files.toArray(new String[0]); + } + + @NonNull + public abstract ParcelFileDescriptor openFileDescriptor(@NonNull String mode, @NonNull HandlerThread callbackThread) + throws FileNotFoundException; + + public OutputStream openOutputStream() throws IOException { + return openOutputStream(false); + } + + @NonNull + public abstract OutputStream openOutputStream(boolean append) throws IOException; + + @NonNull + public abstract InputStream openInputStream() throws IOException; + + public abstract FileChannel openFileChannel(int mode) throws IOException; + + @NonNull + public byte[] getContentAsBinary() { + return getContentAsBinary(new byte[0]); + } + + @Nullable + @Contract("!null -> !null") + public byte[] getContentAsBinary(byte[] emptyValue) { + try (InputStream inputStream = openInputStream()) { + return IoUtils.readFully(inputStream, -1, true); + } catch (IOException e) { + if (!(e.getCause() instanceof ErrnoException)) { + // This isn't just another EACCESS exception + e.printStackTrace(); + } + } + return emptyValue; + } + + @NonNull + public String getContentAsString() { + return getContentAsString(""); + } + + @Nullable + @Contract("!null -> !null") + public String getContentAsString(@Nullable String emptyValue) { + try (InputStream inputStream = openInputStream()) { + return new String(IoUtils.readFully(inputStream, -1, true), Charset.defaultCharset()); + } catch (Exception e) { + e.printStackTrace(); + return emptyValue; + } + } + + @NonNull + @Override + public String toString() { + return getUri().toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Path)) return false; + Path path = (Path) o; + return documentFile.getUri().equals(path.documentFile.getUri()); + } + + @Override + public int hashCode() { + return documentFile.getUri().hashCode(); + } + + @Override + public int compareTo(@NonNull Path o) { + return documentFile.getUri().compareTo(o.documentFile.getUri()); + } + + @FunctionalInterface + public interface FilenameFilter { + /** + * Tests if a specified file should be included in a file list. + * + * @param dir the directory in which the file was found. + * @param name the name of the file. + * @return true if and only if the name should be + * included in the file list; false otherwise. + */ + boolean accept(Path dir, String name); + } + + @FunctionalInterface + public interface FileFilter { + + /** + * Tests whether or not the specified abstract pathname should be + * included in a pathname list. + * + * @param pathname The abstract pathname to be tested + * @return true if and only if pathname + * should be included + */ + boolean accept(Path pathname); + } +} diff --git a/libcore/io/src/main/java/io/github/muntashirakon/io/PathAttributes.java b/libcore/io/src/main/java/io/github/muntashirakon/io/PathAttributes.java new file mode 100644 index 00000000000..47d2d85d005 --- /dev/null +++ b/libcore/io/src/main/java/io/github/muntashirakon/io/PathAttributes.java @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package io.github.muntashirakon.io; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class PathAttributes { + @NonNull + public final String name; + @Nullable + public final String mimeType; + public final long lastModified; + public final long lastAccess; + public final long creationTime; + public final boolean isRegularFile; + public final boolean isDirectory; + public final boolean isSymbolicLink; + public final boolean isOtherFile; + public final long size; + + protected PathAttributes(@NonNull String displayName, @Nullable String mimeType, long lastModified, long lastAccess, + long creationTime, boolean isRegularFile, boolean isDirectory, boolean isSymbolicLink, + long size) { + this.name = displayName; + this.mimeType = mimeType; + this.lastModified = lastModified; + this.lastAccess = lastAccess; + this.creationTime = creationTime; + this.isRegularFile = isRegularFile; + this.isDirectory = isDirectory; + this.isSymbolicLink = isSymbolicLink; + this.isOtherFile = !isRegularFile && !isDirectory && !isSymbolicLink; + this.size = size; + } +} diff --git a/libcore/io/src/main/java/io/github/muntashirakon/io/PathContentInfo.java b/libcore/io/src/main/java/io/github/muntashirakon/io/PathContentInfo.java new file mode 100644 index 00000000000..e4e47ca5db9 --- /dev/null +++ b/libcore/io/src/main/java/io/github/muntashirakon/io/PathContentInfo.java @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package io.github.muntashirakon.io; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public abstract class PathContentInfo { + public static final String TAG = PathContentInfo.class.getSimpleName(); + + @NonNull + private final String mName; + @Nullable + private final String mMessage; + @Nullable + private final String mMimeType; + @Nullable + private final String[] mFileExtensions; + private final boolean mPartial; + + protected PathContentInfo(@NonNull String name, @Nullable String message, @Nullable String mimeType, + @Nullable String[] fileExtensions, boolean partial) { + mName = name; + mMessage = message; + mMimeType = mimeType; + mFileExtensions = fileExtensions; + mPartial = partial; + } + + /** + * Returns the short name of the content either from the content-type or extracted from the message. If the + * content-type is known then this is a specific name string. Otherwise, this is usually the first word of the + * message generated by the magic file. + */ + @NonNull + public String getName() { + return mName; + } + + /** + * Returns the mime-type or null if none. + */ + @Nullable + public String getMimeType() { + return mMimeType; + } + + /** + * Returns the full message as generated by the magic matching code or null if none. This should be similar to the + * output from the Unix file(1) command. + */ + @Nullable + public String getMessage() { + return mMessage; + } + + /** + * Returns an array of associated file-extensions or null if none. + */ + @Nullable + public String[] getFileExtensions() { + return mFileExtensions; + } + + /** + * Whether this was a partial match. For some types, there is a main matching pattern and then more + * specific patterns which detect additional features of the type. A partial match means that none of the more + * specific patterns fully matched the content. It's probably still of the type but just not a variant that the + * entries from the magic file(s) know about. + */ + public boolean isPartial() { + return mPartial; + } +} diff --git a/app/src/main/java/io/github/muntashirakon/io/PathReader.java b/libcore/io/src/main/java/io/github/muntashirakon/io/PathReader.java similarity index 54% rename from app/src/main/java/io/github/muntashirakon/io/PathReader.java rename to libcore/io/src/main/java/io/github/muntashirakon/io/PathReader.java index 6600dfd6797..64c9e88c64b 100644 --- a/app/src/main/java/io/github/muntashirakon/io/PathReader.java +++ b/libcore/io/src/main/java/io/github/muntashirakon/io/PathReader.java @@ -2,8 +2,6 @@ package io.github.muntashirakon.io; -import android.content.Context; - import androidx.annotation.NonNull; import java.io.FileNotFoundException; @@ -11,21 +9,6 @@ import java.io.InputStreamReader; public class PathReader extends InputStreamReader { - /** - * Creates a new {@link PathReader}, given the name of the - * file to read from. - * - * @param context File context (usually application context) - * @param fileName the name of the file to read from - * @throws FileNotFoundException if the named file does not exist, - * is a directory rather than a regular file, - * or for some other reason cannot be opened for - * reading. - */ - public PathReader(Context context, String fileName) throws IOException { - this(new Path(context, fileName)); - } - /** * Creates a new {@link PathReader}, given the Path * to read from. diff --git a/app/src/main/java/io/github/muntashirakon/io/PathWriter.java b/libcore/io/src/main/java/io/github/muntashirakon/io/PathWriter.java similarity index 55% rename from app/src/main/java/io/github/muntashirakon/io/PathWriter.java rename to libcore/io/src/main/java/io/github/muntashirakon/io/PathWriter.java index d69a150361c..9e9fa9c3922 100644 --- a/app/src/main/java/io/github/muntashirakon/io/PathWriter.java +++ b/libcore/io/src/main/java/io/github/muntashirakon/io/PathWriter.java @@ -2,8 +2,6 @@ package io.github.muntashirakon.io; -import android.content.Context; - import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; @@ -11,20 +9,6 @@ import java.io.OutputStreamWriter; public class PathWriter extends OutputStreamWriter { - /** - * Constructs a ProxyFileWriter object given a file name. - * - * @param context File context (usually application context) - * @param fileName String The system-dependent filename. - * @throws IOException if the named file exists but is a directory rather - * than a regular file, does not exist but cannot be - * created, or cannot be opened for any other reason - */ - @WorkerThread - public PathWriter(Context context, String fileName) throws IOException { - this(new Path(context, fileName)); - } - /** * Constructs a ProxyFileWriter object given a File object. * diff --git a/app/src/main/java/io/github/muntashirakon/io/SplitInputStream.java b/libcore/io/src/main/java/io/github/muntashirakon/io/SplitInputStream.java similarity index 97% rename from app/src/main/java/io/github/muntashirakon/io/SplitInputStream.java rename to libcore/io/src/main/java/io/github/muntashirakon/io/SplitInputStream.java index 5cd9218ca12..12f9c40731f 100644 --- a/app/src/main/java/io/github/muntashirakon/io/SplitInputStream.java +++ b/libcore/io/src/main/java/io/github/muntashirakon/io/SplitInputStream.java @@ -3,6 +3,7 @@ package io.github.muntashirakon.io; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.io.IOException; @@ -30,6 +31,7 @@ public class SplitInputStream extends InputStream { // markBuf.length == markBufSize private int mMarkBufSize; + @Nullable private byte[] mMarkBuf; // Some value ranges: @@ -132,7 +134,7 @@ public boolean markSupported() { } @WorkerThread - private synchronized int read0(byte[] b, int off, int len) throws IOException { + private synchronized int read0(@Nullable byte[] b, int off, int len) throws IOException { int n = 0; while (n < len) { if (mPos < 0) { @@ -199,7 +201,7 @@ private synchronized int readStream(@NonNull byte[] b) throws IOException { int len = b.length; if (len <= 0) return len; try { - if (mFiles.size() == 0) { + if (mFiles.isEmpty()) { // No files supplied, nothing to read return -1; } else if (mCurrentIndex == -1) { diff --git a/app/src/main/java/io/github/muntashirakon/io/SplitOutputStream.java b/libcore/io/src/main/java/io/github/muntashirakon/io/SplitOutputStream.java similarity index 100% rename from app/src/main/java/io/github/muntashirakon/io/SplitOutputStream.java rename to libcore/io/src/main/java/io/github/muntashirakon/io/SplitOutputStream.java