diff --git a/app/build.gradle b/app/build.gradle index d2bda34d3d..ff35a73e1a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -196,7 +196,7 @@ dependencies { implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" implementation "com.github.topjohnwu.libsu:io:${libsuVersion}" - implementation 'com.cloudrail:cloudrail-si-android:2.22.4' + implementation "com.cloudrail:cloudrail-si-android:$cloudRailVersion" implementation 'com.github.PhilJay:MPAndroidChart:v3.0.2'//Nice charts and graphs @@ -234,11 +234,12 @@ dependencies { implementation 'org.tukaani:xz:1.8' - implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + //ReactiveX + implementation "io.reactivex.rxjava2:rxandroid:$reactiveXAndroidVersion" // Because RxAndroid releases are few and far between, it is recommended you also // explicitly depend on RxJava's latest version for bug fixes and new features. // (see https://github.com/ReactiveX/RxJava/releases for latest 3.x.x version) - implementation group: 'io.reactivex.rxjava2', name: 'rxjava', version: '2.2.9' + implementation "io.reactivex.rxjava2:rxjava:$reactiveXVersion" implementation project(':commons_compress_7z') implementation project(':file_operations') diff --git a/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt index ed22fa5349..959c2f78ee 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt @@ -52,6 +52,7 @@ import com.amaze.filemanager.asynchronous.asynctasks.DeleteTask import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil import com.amaze.filemanager.asynchronous.services.CopyService import com.amaze.filemanager.file_operations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.files.FileAmazeFilesystem import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.filesystem.RootHelper import com.amaze.filemanager.filesystem.files.FileUtils diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CloudLoaderAsyncTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CloudLoaderAsyncTask.java index 7ca9671a5a..da4ce1c88b 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CloudLoaderAsyncTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CloudLoaderAsyncTask.java @@ -28,6 +28,10 @@ import com.amaze.filemanager.database.models.explorer.CloudEntry; import com.amaze.filemanager.file_operations.exceptions.CloudPluginException; import com.amaze.filemanager.file_operations.filesystem.OpenMode; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.box.BoxAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.dropbox.DropboxAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.gdrive.GoogledriveAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.onedrive.OnedriveAccount; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.utils.DataUtils; import com.cloudrail.si.CloudRail; @@ -130,7 +134,7 @@ public Boolean doInBackground(Void... voids) { cloudHandler.addEntry(cloudEntryGdrive); } - dataUtils.addAccount(cloudStorageDrive); + GoogledriveAccount.INSTANCE.add(cloudStorageDrive); hasUpdatedDrawer = true; } catch (CloudPluginException e) { e.printStackTrace(); @@ -201,7 +205,7 @@ public Boolean doInBackground(Void... voids) { cloudHandler.addEntry(cloudEntryDropbox); } - dataUtils.addAccount(cloudStorageDropbox); + DropboxAccount.INSTANCE.add(cloudStorageDropbox); hasUpdatedDrawer = true; } catch (CloudPluginException e) { e.printStackTrace(); @@ -265,7 +269,7 @@ public Boolean doInBackground(Void... voids) { cloudHandler.addEntry(cloudEntryBox); } - dataUtils.addAccount(cloudStorageBox); + BoxAccount.INSTANCE.add(cloudStorageBox); hasUpdatedDrawer = true; } catch (CloudPluginException e) { e.printStackTrace(); @@ -330,7 +334,7 @@ public Boolean doInBackground(Void... voids) { cloudHandler.addEntry(cloudEntryOnedrive); } - dataUtils.addAccount(cloudStorageOnedrive); + OnedriveAccount.INSTANCE.add(cloudStorageOnedrive); hasUpdatedDrawer = true; } catch (CloudPluginException e) { e.printStackTrace(); diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/DeleteTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/DeleteTask.java index 9eb5b99e5b..e8cba0f242 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/DeleteTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/DeleteTask.java @@ -172,7 +172,7 @@ private boolean doDeleteFile(@NonNull HybridFileParcelable file) throws Exceptio case BOX: case GDRIVE: case ONEDRIVE: - CloudStorage cloudStorage = dataUtils.getAccount(file.getMode()); + CloudStorage cloudStorage = dataUtils.getAccount(file.getMode()).getAccount(); try { cloudStorage.delete(CloudUtil.stripPath(file.getMode(), file.getPath())); return true; diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java index d710836985..89c0b558ee 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java @@ -24,6 +24,7 @@ import static android.os.Build.VERSION_CODES.Q; import java.io.File; +import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Calendar; @@ -38,6 +39,7 @@ import com.amaze.filemanager.database.UtilsHandler; import com.amaze.filemanager.file_operations.exceptions.CloudPluginException; import com.amaze.filemanager.file_operations.filesystem.OpenMode; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.HybridFileParcelable; import com.amaze.filemanager.filesystem.RootHelper; @@ -69,8 +71,6 @@ import androidx.core.util.Pair; import jcifs.smb.SmbAuthException; -import jcifs.smb.SmbException; -import jcifs.smb.SmbFile; public class LoadFilesListTask extends AsyncTask>> { @@ -141,7 +141,7 @@ public LoadFilesListTask( hFile.setPath(hFile.getPath() + "/"); } try { - SmbFile[] smbFile = hFile.getSmbFile(5000).listFiles(); + AmazeFile[] smbFile = new AmazeFile(hFile.getPath()).listFiles(() -> context); list = mainFragment.addToSmb(smbFile, path, showHiddenFiles); openmode = OpenMode.SMB; } catch (SmbAuthException e) { @@ -150,7 +150,7 @@ public LoadFilesListTask( } e.printStackTrace(); return null; - } catch (SmbException | NullPointerException e) { + } catch (NullPointerException | IOException e) { Log.w(getClass().getSimpleName(), "Failed to load smb files for path: " + path, e); mainFragment.reauthenticateSmb(); return null; @@ -225,7 +225,7 @@ public LoadFilesListTask( case BOX: case GDRIVE: case ONEDRIVE: - CloudStorage cloudStorage = dataUtils.getAccount(openmode); + CloudStorage cloudStorage = dataUtils.getAccount(openmode).getAccount(); list = new ArrayList<>(); try { diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashCallback.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashCallback.java index 9b8e50c7d0..2ba58c2d06 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashCallback.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashCallback.java @@ -27,6 +27,8 @@ import java.util.Objects; import java.util.concurrent.Callable; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider; import com.amaze.filemanager.filesystem.HybridFileParcelable; import com.amaze.filemanager.filesystem.files.GenericCopyUtil; @@ -37,18 +39,12 @@ /** Generates hashes from files (MD5 and SHA256) */ public class CalculateHashCallback implements Callable { - private final boolean isNotADirectory; - private final InputStream inputStreamMd5; - private final InputStream inputStreamSha; + private final AmazeFile file; + private final ContextProvider contextProvider; - public CalculateHashCallback(HybridFileParcelable file, final Context context) { - if (file.isSftp()) { - throw new IllegalArgumentException("Use CalculateHashSftpCallback"); - } - - this.isNotADirectory = !file.isDirectory(context); - this.inputStreamMd5 = file.getInputStream(context); - this.inputStreamSha = file.getInputStream(context); + public CalculateHashCallback(AmazeFile file, final Context context) { + this.file = file; + this.contextProvider = () -> context; } @WorkerThread @@ -57,9 +53,9 @@ public Hash call() throws Exception { String md5 = null; String sha256 = null; - if (isNotADirectory) { - md5 = getMD5Checksum(); - sha256 = getSHA256Checksum(); + if (!file.isDirectory(contextProvider)) { + md5 = file.getHashMD5(contextProvider); + sha256 = file.getHashSHA256(contextProvider); } Objects.requireNonNull(md5); @@ -67,57 +63,4 @@ public Hash call() throws Exception { return new Hash(md5, sha256); } - - // see this How-to for a faster way to convert a byte array to a HEX string - - private String getMD5Checksum() throws Exception { - byte[] b = createChecksum(); - String result = ""; - - for (byte aB : b) { - result += Integer.toString((aB & 0xff) + 0x100, 16).substring(1); - } - return result; - } - - private String getSHA256Checksum() throws NoSuchAlgorithmException, IOException { - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - byte[] input = new byte[GenericCopyUtil.DEFAULT_BUFFER_SIZE]; - int length; - InputStream inputStream = inputStreamMd5; - while ((length = inputStream.read(input)) != -1) { - if (length > 0) messageDigest.update(input, 0, length); - } - - byte[] hash = messageDigest.digest(); - - StringBuilder hexString = new StringBuilder(); - - for (byte aHash : hash) { - // convert hash to base 16 - String hex = Integer.toHexString(0xff & aHash); - if (hex.length() == 1) hexString.append('0'); - hexString.append(hex); - } - inputStream.close(); - return hexString.toString(); - } - - private byte[] createChecksum() throws Exception { - InputStream fis = inputStreamSha; - - byte[] buffer = new byte[8192]; - MessageDigest complete = MessageDigest.getInstance("MD5"); - int numRead; - - do { - numRead = fis.read(buffer); - if (numRead > 0) { - complete.update(buffer, 0, numRead); - } - } while (numRead != -1); - - fis.close(); - return complete.digest(); - } } diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashTask.kt index 98dfeb18c0..fcd7bb4269 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashTask.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/hashcalculator/CalculateHashTask.kt @@ -28,6 +28,8 @@ import android.widget.TextView import android.widget.Toast import com.amaze.filemanager.R import com.amaze.filemanager.asynchronous.asynctasks.Task +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.filesystem.files.FileUtils import java.lang.ref.WeakReference @@ -37,7 +39,7 @@ import java.util.concurrent.Callable data class Hash(val md5: String, val sha: String) class CalculateHashTask( - private val file: HybridFileParcelable, + private val file: AmazeFile, context: Context, view: View ) : Task> { @@ -46,11 +48,7 @@ class CalculateHashTask( private val TAG = CalculateHashTask::class.java.simpleName } - private val task: Callable = if (file.isSftp) { - CalculateHashSftpCallback(file) - } else { - CalculateHashCallback(file, context) - } + private val task: Callable = CalculateHashCallback(file, context) private val context = WeakReference(context) private val view = WeakReference(view) @@ -82,7 +80,11 @@ class CalculateHashTask( val mMD5LinearLayout = view.findViewById(R.id.properties_dialog_md5) val mSHA256LinearLayout = view.findViewById(R.id.properties_dialog_sha256) - if (!file.isDirectory(context) && file.getSize() != 0L) { + val contextProvider = object : ContextProvider { + override fun getContext(): Context? = context + } + + if (!file.isDirectory(contextProvider) && file.safeLength(contextProvider) != 0L) { md5HashText.text = md5Text sha256Text.text = shaText mMD5LinearLayout.setOnLongClickListener { diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFiles.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFiles.java index d4b6b6c9f1..12395b0ae5 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFiles.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFiles.java @@ -131,7 +131,7 @@ private MoveFilesReturn processFile( case GDRIVE: DataUtils dataUtils = DataUtils.getInstance(); - CloudStorage cloudStorage = dataUtils.getAccount(mode); + CloudStorage cloudStorage = dataUtils.getAccount(mode).getAccount(); if (baseFile.getMode() == mode) { // source and target both in same filesystem, use API method try { diff --git a/app/src/main/java/com/amaze/filemanager/database/CloudHandler.java b/app/src/main/java/com/amaze/filemanager/database/CloudHandler.java index 9a50f36137..58007f1cc6 100644 --- a/app/src/main/java/com/amaze/filemanager/database/CloudHandler.java +++ b/app/src/main/java/com/amaze/filemanager/database/CloudHandler.java @@ -25,6 +25,10 @@ import com.amaze.filemanager.database.models.explorer.CloudEntry; import com.amaze.filemanager.file_operations.exceptions.CloudPluginException; import com.amaze.filemanager.file_operations.filesystem.OpenMode; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.box.BoxAmazeFilesystem; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.dropbox.DropboxAmazeFilesystem; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.gdrive.GoogledriveAmazeFilesystem; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.onedrive.OnedriveAmazeFilesystem; import com.amaze.filemanager.ui.fragments.CloudSheetFragment; import android.content.Context; @@ -37,10 +41,10 @@ /** Created by vishal on 18/4/17. */ public class CloudHandler { - public static final String CLOUD_PREFIX_BOX = "box:/"; - public static final String CLOUD_PREFIX_DROPBOX = "dropbox:/"; - public static final String CLOUD_PREFIX_GOOGLE_DRIVE = "gdrive:/"; - public static final String CLOUD_PREFIX_ONE_DRIVE = "onedrive:/"; + public static final String CLOUD_PREFIX_BOX = BoxAmazeFilesystem.PREFIX; + public static final String CLOUD_PREFIX_DROPBOX = DropboxAmazeFilesystem.PREFIX; + public static final String CLOUD_PREFIX_GOOGLE_DRIVE = GoogledriveAmazeFilesystem.PREFIX; + public static final String CLOUD_PREFIX_ONE_DRIVE = OnedriveAmazeFilesystem.PREFIX; public static final String CLOUD_NAME_GOOGLE_DRIVE = "Google Driveā„¢"; public static final String CLOUD_NAME_DROPBOX = "Dropbox"; diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/DeleteOperation.kt b/app/src/main/java/com/amaze/filemanager/filesystem/DeleteOperation.kt index f89a1e92ba..61b27709b4 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/DeleteOperation.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/DeleteOperation.kt @@ -27,6 +27,7 @@ import android.provider.MediaStore import android.util.Log import java.io.File +@Deprecated("Use AmazeFile.delete()") object DeleteOperation { private val LOG = "DeleteFileOperation" diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ExternalSdCardOperation.kt b/app/src/main/java/com/amaze/filemanager/filesystem/ExternalSdCardOperation.kt index 3bf333741f..19996bca56 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/ExternalSdCardOperation.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ExternalSdCardOperation.kt @@ -26,12 +26,12 @@ import android.net.Uri import android.os.Build import android.util.Log import androidx.documentfile.provider.DocumentFile -import androidx.preference.PreferenceManager -import com.amaze.filemanager.ui.fragments.preference_fragments.PreferencesConstants +import com.amaze.filemanager.file_operations.filesystem.filetypes.file.UriForSafPersistance import java.io.File import java.io.IOException import java.util.* +@Deprecated("Use [com.amaze.filemanager.file_operations.filesystem.filetypes.file.ExternalSdCardOperation]") object ExternalSdCardOperation { val LOG = "ExternalSdCardOperation" @@ -67,8 +67,7 @@ object ExternalSdCardOperation { return null } - val preferenceUri = PreferenceManager.getDefaultSharedPreferences(context) - .getString(PreferencesConstants.PREFERENCE_URI, null) + val preferenceUri = UriForSafPersistance.get(context) var treeUri: Uri? = null if (preferenceUri != null) { treeUri = Uri.parse(preferenceUri) diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java b/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java index 329089b3a0..7e26b012e6 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java @@ -197,7 +197,7 @@ public static final void writeUriToStorage( case GDRIVE: OpenMode mode = hFile.getMode(); - CloudStorage cloudStorage = dataUtils.getAccount(mode); + CloudStorage cloudStorage = dataUtils.getAccount(mode).getAccount(); String path = CloudUtil.stripPath(mode, finalFilePath); cloudStorage.upload(path, bufferedInputStream, documentFile.length(), true); retval.add(path); diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/FilesystemLoader.kt b/app/src/main/java/com/amaze/filemanager/filesystem/FilesystemLoader.kt new file mode 100644 index 0000000000..c347ba1be5 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/FilesystemLoader.kt @@ -0,0 +1,20 @@ +package com.amaze.filemanager.filesystem + +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.smb.SmbAmazeFilesystem +import com.amaze.filemanager.filesystem.files.FileAmazeFilesystem +import com.amaze.filemanager.filesystem.otg.OtgAmazeFilesystem +import com.amaze.filemanager.filesystem.ssh.SshAmazeFilesystem + +/** + * TODO remove this by moving all Filesystem subclasses to file_operations + */ +object FilesystemLoader { + init { + AmazeFile //Loads all of the file_operations Filesystem subclasses + FileAmazeFilesystem + OtgAmazeFilesystem + SmbAmazeFilesystem + SshAmazeFilesystem.INSTANCE + } +} \ No newline at end of file diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java index 0405e62ccc..ffb09190f7 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -22,6 +22,7 @@ import static com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX; import static com.amaze.filemanager.filesystem.ssh.SshConnectionPool.SSH_URI_PREFIX; +import static com.amaze.filemanager.utils.SmbUtil.create; import java.io.File; import java.io.FileInputStream; @@ -31,7 +32,6 @@ import java.io.OutputStream; import java.net.MalformedURLException; import java.util.ArrayList; -import java.util.EnumSet; import java.util.concurrent.atomic.AtomicLong; import com.amaze.filemanager.R; @@ -41,26 +41,21 @@ import com.amaze.filemanager.file_operations.exceptions.CloudPluginException; import com.amaze.filemanager.file_operations.exceptions.ShellNotRunningException; import com.amaze.filemanager.file_operations.filesystem.OpenMode; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; import com.amaze.filemanager.file_operations.filesystem.root.NativeOperations; import com.amaze.filemanager.filesystem.cloud.CloudUtil; +import com.amaze.filemanager.filesystem.files.DocumentFileAmazeFilesystem; import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.filesystem.root.DeleteFileCommand; import com.amaze.filemanager.filesystem.root.ListFilesCommand; import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate; -import com.amaze.filemanager.filesystem.ssh.SshClientTemplate; import com.amaze.filemanager.filesystem.ssh.SshClientUtils; import com.amaze.filemanager.filesystem.ssh.SshConnectionPool; -import com.amaze.filemanager.filesystem.ssh.Statvfs; import com.amaze.filemanager.ui.fragments.preference_fragments.PreferencesConstants; import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.OnFileFound; -import com.amaze.filemanager.utils.SmbUtil; -import com.amaze.filemanager.utils.Utils; -import com.cloudrail.si.interfaces.CloudStorage; -import com.cloudrail.si.types.SpaceAllocation; -import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import android.os.Build; @@ -71,32 +66,32 @@ import androidx.documentfile.provider.DocumentFile; import androidx.preference.PreferenceManager; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; import jcifs.smb.SmbException; import jcifs.smb.SmbFile; -import net.schmizz.sshj.SSHClient; -import net.schmizz.sshj.common.Buffer; -import net.schmizz.sshj.sftp.FileMode; -import net.schmizz.sshj.sftp.RemoteFile; + import net.schmizz.sshj.sftp.RemoteResourceInfo; import net.schmizz.sshj.sftp.SFTPClient; -import net.schmizz.sshj.sftp.SFTPException; -/** Hybrid file for handeling all types of files */ +/** + * Hybrid file for handeling all types of files + * + *

This is deprecated, please use AmazeFile instead + */ +@Deprecated public class HybridFile { protected static final String TAG = HybridFile.class.getSimpleName(); - public static final String DOCUMENT_FILE_PREFIX = - "content://com.android.externalstorage.documents"; - protected String path; protected OpenMode mode; protected String name; private final DataUtils dataUtils = DataUtils.getInstance(); + static { + FilesystemLoader.INSTANCE.toString(); // HACK forces the class to load + } + public HybridFile(OpenMode mode, String path) { this.path = path; this.mode = mode; @@ -128,7 +123,7 @@ public void generateMode(Context context) { mode = OpenMode.SFTP; } else if (path.startsWith(OTGUtil.PREFIX_OTG)) { mode = OpenMode.OTG; - } else if (path.startsWith(DOCUMENT_FILE_PREFIX)) { + } else if (path.startsWith(DocumentFileAmazeFilesystem.DOCUMENT_FILE_PREFIX)) { mode = OpenMode.DOCUMENT_FILE; } else if (isCustomPath()) { mode = OpenMode.CUSTOM; @@ -219,6 +214,10 @@ public boolean isGoogleDriveFile() { return mode == OpenMode.GDRIVE; } + public boolean isUnknownFile() { + return mode == OpenMode.UNKNOWN; + } + @Nullable public File getFile() { return new File(path); @@ -246,35 +245,14 @@ HybridFileParcelable generateBaseFileFromParent() { public long lastModified() { switch (mode) { case SFTP: - final Long returnValue = - SshClientUtils.execute( - new SFtpClientTemplate(path) { - @Override - public Long execute(@NonNull SFTPClient client) throws IOException { - return client.mtime(SshClientUtils.extractRemotePathFrom(path)); - } - }); - - if (returnValue == null) { - Log.e(TAG, "Error obtaining last modification time over SFTP"); - } - - return returnValue == null ? 0L : returnValue; case SMB: - SmbFile smbFile = getSmbFile(); - if (smbFile != null) { - try { - return smbFile.lastModified(); - } catch (SmbException e) { - Log.e(TAG, "Error getting last modified time for SMB [" + path + "]", e); - return 0; - } - } - break; case FILE: - return getFile().lastModified(); case DOCUMENT_FILE: - return getDocumentFile(false).lastModified(); + case BOX: + case DROPBOX: + case GDRIVE: + case ONEDRIVE: + return new AmazeFile(path).lastModified(); case ROOT: HybridFileParcelable baseFile = generateBaseFileFromParent(); if (baseFile != null) return baseFile.getDate(); @@ -287,56 +265,22 @@ public long length(Context context) { long s = 0L; switch (mode) { case SFTP: - return ((HybridFileParcelable) this).getSize(); case SMB: - SmbFile smbFile = getSmbFile(); - if (smbFile != null) - try { - s = smbFile.length(); - } catch (SmbException e) { - e.printStackTrace(); - } - return s; case FILE: - s = getFile().length(); - return s; - case ROOT: - HybridFileParcelable baseFile = generateBaseFileFromParent(); - if (baseFile != null) return baseFile.getSize(); - break; - case DOCUMENT_FILE: - s = getDocumentFile(false).length(); - break; - case OTG: - s = OTGUtil.getDocumentFile(path, context, false).length(); - break; case DROPBOX: - s = - dataUtils - .getAccount(OpenMode.DROPBOX) - .getMetadata(CloudUtil.stripPath(OpenMode.DROPBOX, path)) - .getSize(); - break; case BOX: - s = - dataUtils - .getAccount(OpenMode.BOX) - .getMetadata(CloudUtil.stripPath(OpenMode.BOX, path)) - .getSize(); - break; case ONEDRIVE: - s = - dataUtils - .getAccount(OpenMode.ONEDRIVE) - .getMetadata(CloudUtil.stripPath(OpenMode.ONEDRIVE, path)) - .getSize(); - break; case GDRIVE: - s = - dataUtils - .getAccount(OpenMode.GDRIVE) - .getMetadata(CloudUtil.stripPath(OpenMode.GDRIVE, path)) - .getSize(); + case OTG: + case DOCUMENT_FILE: + try { + return new AmazeFile(path).length(() -> context); + } catch (IOException e) { + Log.e(TAG, "Error getting length for file", e); + } + case ROOT: + HybridFileParcelable baseFile = generateBaseFileFromParent(); + if (baseFile != null) return baseFile.getSize(); break; default: break; @@ -351,10 +295,16 @@ public String getPath() { public String getSimpleName() { String name = null; switch (mode) { + case SFTP: case SMB: - SmbFile smbFile = getSmbFile(); - if (smbFile != null) return smbFile.getName(); - break; + case FILE: + case ONEDRIVE: + case GDRIVE: + case DROPBOX: + case BOX: + case OTG: + case DOCUMENT_FILE: + return new AmazeFile(path).getName(); default: StringBuilder builder = new StringBuilder(path); name = builder.substring(builder.lastIndexOf("/") + 1, builder.length()); @@ -365,26 +315,17 @@ public String getSimpleName() { public String getName(Context context) { switch (mode) { case SMB: - SmbFile smbFile = getSmbFile(); - if (smbFile != null) { - return smbFile.getName(); - } - return null; case FILE: - case ROOT: - return getFile().getName(); + case DROPBOX: + case BOX: + case ONEDRIVE: + case GDRIVE: case OTG: - if (!Utils.isNullOrEmpty(name)) { - return name; - } - return OTGUtil.getDocumentFile(path, context, false).getName(); + case SFTP: case DOCUMENT_FILE: - if (!Utils.isNullOrEmpty(name)) { - return name; - } - return OTGUtil.getDocumentFile( - path, SafRootHolder.getUriRoot(), context, OpenMode.DOCUMENT_FILE, false) - .getName(); + return new AmazeFile(path).getName(); + case ROOT: + return getFile().getName(); default: if (path.isEmpty()) { return ""; @@ -401,26 +342,6 @@ public String getName(Context context) { } } - public SmbFile getSmbFile(int timeout) { - try { - SmbFile smbFile = SmbUtil.create(path); - smbFile.setConnectTimeout(timeout); - return smbFile; - } catch (MalformedURLException e) { - e.printStackTrace(); - return null; - } - } - - public SmbFile getSmbFile() { - try { - return SmbUtil.create(path); - } catch (MalformedURLException e) { - e.printStackTrace(); - return null; - } - } - public boolean isCustomPath() { return path.equals("0") || path.equals("1") @@ -435,15 +356,16 @@ public boolean isCustomPath() { public String getParent(Context context) { switch (mode) { case SMB: - SmbFile smbFile = getSmbFile(); - if (smbFile != null) { - return smbFile.getParent(); - } - return ""; case FILE: case ROOT: - return getFile().getParent(); + case DROPBOX: + case BOX: + case ONEDRIVE: + case GDRIVE: case SFTP: + case OTG: + case DOCUMENT_FILE: + return new AmazeFile(path).getParent(); default: if (path.length() == getName(context).length()) { return null; @@ -456,14 +378,6 @@ public String getParent(Context context) { } } - public String getParentName() { - StringBuilder builder = new StringBuilder(path); - StringBuilder parentPath = - new StringBuilder(builder.substring(0, builder.length() - (getSimpleName().length() + 1))); - String parentName = parentPath.substring(parentPath.lastIndexOf("/") + 1, parentPath.length()); - return parentName; - } - /** * Whether this object refers to a directory or file, handles all types of files * @@ -473,29 +387,18 @@ public boolean isDirectory() { boolean isDirectory; switch (mode) { case SFTP: - return isDirectory(AppConfig.getInstance()); case SMB: - SmbFile smbFile = getSmbFile(); - try { - isDirectory = smbFile != null && smbFile.isDirectory(); - } catch (SmbException e) { - e.printStackTrace(); - isDirectory = false; - } - break; case FILE: - isDirectory = getFile().isDirectory(); - break; + case DROPBOX: + case BOX: + case ONEDRIVE: + case GDRIVE: + case OTG: + case DOCUMENT_FILE: + return new AmazeFile(path).isDirectory(AppConfig::getInstance); case ROOT: isDirectory = NativeOperations.isDirectory(path); break; - case DOCUMENT_FILE: - return getDocumentFile(false).isDirectory(); - case OTG: - // TODO: support for this method in OTG on-the-fly - // you need to manually call {@link RootHelper#getDocumentFile() method - isDirectory = false; - break; default: isDirectory = getFile().isDirectory(); break; @@ -507,83 +410,17 @@ public boolean isDirectory(Context context) { boolean isDirectory; switch (mode) { case SFTP: - final Boolean returnValue = - SshClientUtils.execute( - new SFtpClientTemplate(path) { - @Override - public Boolean execute(SFTPClient client) { - try { - return client - .stat(SshClientUtils.extractRemotePathFrom(path)) - .getType() - .equals(FileMode.Type.DIRECTORY); - } catch (IOException notFound) { - Log.e( - getClass().getSimpleName(), - "Fail to execute isDirectory for SFTP path :" + path, - notFound); - return false; - } - } - }); - - if (returnValue == null) { - Log.e(TAG, "Error obtaining if path is directory over SFTP"); - } - - //noinspection SimplifiableConditionalExpression - return returnValue == null ? false : returnValue; case SMB: - try { - isDirectory = - Single.fromCallable(() -> getSmbFile().isDirectory()) - .subscribeOn(Schedulers.io()) - .blockingGet(); - } catch (Exception e) { - isDirectory = false; - if (e.getCause() != null) e.getCause().printStackTrace(); - else e.printStackTrace(); - } - break; case FILE: - isDirectory = getFile().isDirectory(); - break; - case ROOT: - isDirectory = NativeOperations.isDirectory(path); - break; - case DOCUMENT_FILE: - isDirectory = getDocumentFile(false).isDirectory(); - break; - case OTG: - isDirectory = OTGUtil.getDocumentFile(path, context, false).isDirectory(); - break; case DROPBOX: - isDirectory = - dataUtils - .getAccount(OpenMode.DROPBOX) - .getMetadata(CloudUtil.stripPath(OpenMode.DROPBOX, path)) - .getFolder(); - break; case BOX: - isDirectory = - dataUtils - .getAccount(OpenMode.BOX) - .getMetadata(CloudUtil.stripPath(OpenMode.BOX, path)) - .getFolder(); - break; - case GDRIVE: - isDirectory = - dataUtils - .getAccount(OpenMode.GDRIVE) - .getMetadata(CloudUtil.stripPath(OpenMode.GDRIVE, path)) - .getFolder(); - break; case ONEDRIVE: - isDirectory = - dataUtils - .getAccount(OpenMode.ONEDRIVE) - .getMetadata(CloudUtil.stripPath(OpenMode.ONEDRIVE, path)) - .getFolder(); + case GDRIVE: + case OTG: + case DOCUMENT_FILE: + return new AmazeFile(path).isDirectory(() -> context); + case ROOT: + isDirectory = NativeOperations.isDirectory(path); break; default: isDirectory = getFile().isDirectory(); @@ -600,12 +437,8 @@ public long folderSize() { case SFTP: return folderSize(AppConfig.getInstance()); case SMB: - SmbFile smbFile = getSmbFile(); - size = smbFile != null ? FileUtils.folderSize(getSmbFile()) : 0; - break; case FILE: - size = FileUtils.folderSize(getFile(), null); - break; + return FileUtils.folderSize(new AmazeFile(getPath()), () -> null); case ROOT: HybridFileParcelable baseFile = generateBaseFileFromParent(); if (baseFile != null) size = baseFile.getSize(); @@ -623,34 +456,18 @@ public long folderSize(Context context) { switch (mode) { case SFTP: - final Long returnValue = - SshClientUtils.execute( - new SFtpClientTemplate(path) { - @Override - public Long execute(SFTPClient client) throws IOException { - return client.size(SshClientUtils.extractRemotePathFrom(path)); - } - }); - - if (returnValue == null) { - Log.e(TAG, "Error obtaining size of folder over SFTP"); - } - - return returnValue == null ? 0L : returnValue; case SMB: - SmbFile smbFile = getSmbFile(); - size = (smbFile != null) ? FileUtils.folderSize(smbFile) : 0L; - break; case FILE: - size = FileUtils.folderSize(getFile(), null); - break; + case DROPBOX: + case BOX: + case ONEDRIVE: + case GDRIVE: + case OTG: + return FileUtils.folderSize(new AmazeFile(getPath()), () -> context); case ROOT: HybridFileParcelable baseFile = generateBaseFileFromParent(); if (baseFile != null) size = baseFile.getSize(); break; - case OTG: - size = FileUtils.otgFolderSize(path, context); - break; case DOCUMENT_FILE: final AtomicLong totalBytes = new AtomicLong(0); OTGUtil.getDocumentFiles( @@ -660,14 +477,6 @@ public Long execute(SFTPClient client) throws IOException { OpenMode.DOCUMENT_FILE, file -> totalBytes.addAndGet(FileUtils.getBaseFileSize(file, context))); break; - case DROPBOX: - case BOX: - case GDRIVE: - case ONEDRIVE: - size = - FileUtils.folderSizeCloud( - mode, dataUtils.getAccount(mode).getMetadata(CloudUtil.stripPath(mode, path))); - break; default: return 0l; } @@ -679,65 +488,16 @@ public long getUsableSpace() { long size = 0L; switch (mode) { case SMB: - try { - SmbFile smbFile = getSmbFile(); - size = smbFile != null ? smbFile.getDiskFreeSpace() : 0L; - } catch (SmbException e) { - size = 0L; - e.printStackTrace(); - } - break; case FILE: case ROOT: - size = getFile().getUsableSpace(); - break; case DROPBOX: case BOX: - case GDRIVE: case ONEDRIVE: - SpaceAllocation spaceAllocation = dataUtils.getAccount(mode).getAllocation(); - size = spaceAllocation.getTotal() - spaceAllocation.getUsed(); - break; + case GDRIVE: case SFTP: - final Long returnValue = - SshClientUtils.execute( - new SFtpClientTemplate(path) { - @Override - public Long execute(@NonNull SFTPClient client) throws IOException { - try { - Statvfs.Response response = - new Statvfs.Response( - path, - client - .getSFTPEngine() - .request( - Statvfs.request( - client, SshClientUtils.extractRemotePathFrom(path))) - .retrieve()); - return response.diskFreeSpace(); - } catch (SFTPException e) { - Log.e(TAG, "Error querying server", e); - return 0L; - } catch (Buffer.BufferException e) { - Log.e(TAG, "Error parsing reply", e); - return 0L; - } - } - }); - - if (returnValue == null) { - Log.e(TAG, "Error obtaining usable space over SFTP"); - } - - size = returnValue == null ? 0L : returnValue; - break; - case DOCUMENT_FILE: - size = - FileProperties.getDeviceStorageRemainingSpace(SafRootHolder.INSTANCE.getVolumeLabel()); - break; case OTG: - // TODO: Get free space from OTG when {@link DocumentFile} API adds support - break; + case DOCUMENT_FILE: + return new AmazeFile(path).getUsableSpace(); } return size; } @@ -747,66 +507,16 @@ public long getTotal(Context context) { long size = 0l; switch (mode) { case SMB: - // TODO: Find total storage space of SMB when JCIFS adds support - try { - SmbFile smbFile = getSmbFile(); - size = smbFile != null ? smbFile.getDiskFreeSpace() : 0L; - } catch (SmbException e) { - e.printStackTrace(); - } - break; case FILE: case ROOT: - size = getFile().getTotalSpace(); - break; case DROPBOX: case BOX: case ONEDRIVE: case GDRIVE: - SpaceAllocation spaceAllocation = dataUtils.getAccount(mode).getAllocation(); - size = spaceAllocation.getTotal(); - break; case SFTP: - final Long returnValue = - SshClientUtils.execute( - new SFtpClientTemplate(path) { - @Override - public Long execute(@NonNull SFTPClient client) throws IOException { - try { - Statvfs.Response response = - new Statvfs.Response( - path, - client - .getSFTPEngine() - .request( - Statvfs.request( - client, SshClientUtils.extractRemotePathFrom(path))) - .retrieve()); - return response.diskSize(); - } catch (SFTPException e) { - Log.e(TAG, "Error querying server", e); - return 0L; - } catch (Buffer.BufferException e) { - Log.e(TAG, "Error parsing reply", e); - return 0L; - } - } - }); - - if (returnValue == null) { - Log.e(TAG, "Error obtaining total space over SFTP"); - } - - size = returnValue == null ? 0L : returnValue; - break; case OTG: - // TODO: Find total storage space of OTG when {@link DocumentFile} API adds support - DocumentFile documentFile = OTGUtil.getDocumentFile(path, context, false); - size = documentFile.length(); - break; case DOCUMENT_FILE: - size = getDocumentFile(false).length(); - break; + return new AmazeFile(path).getTotalSpace(() -> context); } return size; } @@ -847,21 +557,19 @@ public Boolean execute(SFTPClient client) { break; case SMB: try { - SmbFile smbFile = getSmbFile(); - if (smbFile != null) { - for (SmbFile smbFile1 : smbFile.listFiles()) { - HybridFileParcelable baseFile; - try { - SmbFile sf = new SmbFile(smbFile1.getURL(), smbFile.getContext()); - baseFile = new HybridFileParcelable(sf); - } catch (MalformedURLException shouldNeverHappen) { - shouldNeverHappen.printStackTrace(); - baseFile = new HybridFileParcelable(smbFile1); - } - onFileFound.onFileFound(baseFile); + SmbFile smbFile = create(getPath()); + for (SmbFile smbFile1 : smbFile.listFiles()) { + HybridFileParcelable baseFile; + try { + SmbFile sf = new SmbFile(smbFile1.getURL(), smbFile.getContext()); + baseFile = new HybridFileParcelable(sf); + } catch (MalformedURLException shouldNeverHappen) { + shouldNeverHappen.printStackTrace(); + baseFile = new HybridFileParcelable(smbFile1); } + onFileFound.onFileFound(baseFile); } - } catch (SmbException e) { + } catch (MalformedURLException | SmbException e) { e.printStackTrace(); } break; @@ -877,7 +585,7 @@ public Boolean execute(SFTPClient client) { case GDRIVE: case ONEDRIVE: try { - CloudUtil.getCloudFiles(path, dataUtils.getAccount(mode), mode, onFileFound); + CloudUtil.getCloudFiles(path, dataUtils.getAccount(mode).getAccount(), mode, onFileFound); } catch (CloudPluginException e) { e.printStackTrace(); } @@ -931,22 +639,22 @@ public ArrayList execute(SFTPClient client) { }); break; case SMB: - try { - SmbFile smbFile = getSmbFile(); - if (smbFile != null) { - for (SmbFile smbFile1 : smbFile.listFiles()) { - HybridFileParcelable baseFile = new HybridFileParcelable(smbFile1); - arrayList.add(baseFile); - } + case DROPBOX: + case BOX: + case ONEDRIVE: + case GDRIVE: + case OTG: + ArrayList result = new ArrayList<>(); + for (AmazeFile smbFile1 : new AmazeFile(getPath()).listFiles(() -> context)) { + try { + HybridFileParcelable baseFile = new HybridFileParcelable(create(smbFile1.getPath())); + result.add(baseFile); + } catch (SmbException | MalformedURLException e) { + Log.e(TAG, "Error getting an SMB file", e); + return null; } - } catch (SmbException e) { - arrayList.clear(); - e.printStackTrace(); } - break; - case OTG: - arrayList = OTGUtil.getDocumentFilesList(path, context); - break; + return result; case DOCUMENT_FILE: final ArrayList hybridFileParcelables = new ArrayList<>(); OTGUtil.getDocumentFiles( @@ -957,17 +665,6 @@ public ArrayList execute(SFTPClient client) { file -> hybridFileParcelables.add(file)); arrayList = hybridFileParcelables; break; - case DROPBOX: - case BOX: - case GDRIVE: - case ONEDRIVE: - try { - arrayList = CloudUtil.listFiles(path, dataUtils.getAccount(mode), mode); - } catch (CloudPluginException e) { - e.printStackTrace(); - arrayList = new ArrayList<>(); - } - break; default: arrayList = RootHelper.getFilesList(path, isRoot, true); } @@ -995,121 +692,21 @@ private static String formatUriForDisplayInternal( return String.format("%s://%s%s", scheme, host, path); } - /** - * Handles getting input stream for various {@link OpenMode} - * - * @deprecated use {@link #getInputStream(Context)} which allows handling content resolver - */ - @Nullable - public InputStream getInputStream() { - InputStream inputStream; - if (isSftp()) { - return SshClientUtils.execute( - new SFtpClientTemplate(path) { - @Override - public InputStream execute(SFTPClient client) throws IOException { - final RemoteFile rf = client.open(SshClientUtils.extractRemotePathFrom(path)); - return rf.new RemoteFileInputStream() { - @Override - public void close() throws IOException { - try { - super.close(); - } finally { - rf.close(); - } - } - }; - } - }); - } else if (isSmb()) { - try { - inputStream = getSmbFile().getInputStream(); - } catch (IOException e) { - inputStream = null; - e.printStackTrace(); - } - } else { - try { - inputStream = new FileInputStream(path); - } catch (FileNotFoundException e) { - inputStream = null; - e.printStackTrace(); - } - } - return inputStream; - } - @Nullable public InputStream getInputStream(Context context) { InputStream inputStream; switch (mode) { case SFTP: - inputStream = - SshClientUtils.execute( - new SFtpClientTemplate(path, false) { - @Override - public InputStream execute(final SFTPClient client) throws IOException { - final RemoteFile rf = client.open(SshClientUtils.extractRemotePathFrom(path)); - return rf.new RemoteFileInputStream() { - @Override - public void close() throws IOException { - try { - super.close(); - } finally { - rf.close(); - client.close(); - } - } - }; - } - }); - break; case SMB: - try { - inputStream = getSmbFile().getInputStream(); - } catch (IOException e) { - inputStream = null; - e.printStackTrace(); - } - break; - case DOCUMENT_FILE: - ContentResolver contentResolver = context.getContentResolver(); - DocumentFile documentSourceFile = getDocumentFile(false); - try { - inputStream = contentResolver.openInputStream(documentSourceFile.getUri()); - } catch (FileNotFoundException e) { - e.printStackTrace(); - inputStream = null; - } - break; - case OTG: - contentResolver = context.getContentResolver(); - documentSourceFile = OTGUtil.getDocumentFile(path, context, false); - try { - inputStream = contentResolver.openInputStream(documentSourceFile.getUri()); - } catch (FileNotFoundException e) { - e.printStackTrace(); - inputStream = null; - } - break; + case FILE: case DROPBOX: - CloudStorage cloudStorageDropbox = dataUtils.getAccount(OpenMode.DROPBOX); - Log.d(getClass().getSimpleName(), CloudUtil.stripPath(OpenMode.DROPBOX, path)); - inputStream = cloudStorageDropbox.download(CloudUtil.stripPath(OpenMode.DROPBOX, path)); - break; case BOX: - CloudStorage cloudStorageBox = dataUtils.getAccount(OpenMode.BOX); - inputStream = cloudStorageBox.download(CloudUtil.stripPath(OpenMode.BOX, path)); - break; - case GDRIVE: - CloudStorage cloudStorageGDrive = dataUtils.getAccount(OpenMode.GDRIVE); - inputStream = cloudStorageGDrive.download(CloudUtil.stripPath(OpenMode.GDRIVE, path)); - break; case ONEDRIVE: - CloudStorage cloudStorageOneDrive = dataUtils.getAccount(OpenMode.ONEDRIVE); - inputStream = cloudStorageOneDrive.download(CloudUtil.stripPath(OpenMode.ONEDRIVE, path)); - break; + case GDRIVE: + case OTG: + case DOCUMENT_FILE: + return new AmazeFile(getPath()).getInputStream(() -> context); default: try { inputStream = new FileInputStream(path); @@ -1127,62 +724,15 @@ public OutputStream getOutputStream(Context context) { OutputStream outputStream; switch (mode) { case SFTP: - return SshClientUtils.execute( - new SshClientTemplate(path, false) { - @Override - public OutputStream execute(final SSHClient ssh) throws IOException { - final SFTPClient client = ssh.newSFTPClient(); - final RemoteFile rf = - client.open( - SshClientUtils.extractRemotePathFrom(path), - EnumSet.of( - net.schmizz.sshj.sftp.OpenMode.WRITE, - net.schmizz.sshj.sftp.OpenMode.CREAT)); - return rf.new RemoteFileOutputStream() { - @Override - public void close() throws IOException { - try { - super.close(); - } finally { - try { - rf.close(); - client.close(); - } catch (Exception e) { - Log.w(TAG, "Error closing stream", e); - } - } - } - }; - } - }); case SMB: - try { - outputStream = getSmbFile().getOutputStream(); - } catch (IOException e) { - outputStream = null; - e.printStackTrace(); - } - break; - case DOCUMENT_FILE: - ContentResolver contentResolver = context.getContentResolver(); - DocumentFile documentSourceFile = getDocumentFile(true); - try { - outputStream = contentResolver.openOutputStream(documentSourceFile.getUri()); - } catch (FileNotFoundException e) { - e.printStackTrace(); - outputStream = null; - } - break; + case FILE: + case DROPBOX: + case BOX: + case ONEDRIVE: + case GDRIVE: case OTG: - contentResolver = context.getContentResolver(); - documentSourceFile = OTGUtil.getDocumentFile(path, context, true); - try { - outputStream = contentResolver.openOutputStream(documentSourceFile.getUri()); - } catch (FileNotFoundException e) { - e.printStackTrace(); - outputStream = null; - } - break; + case DOCUMENT_FILE: + return new AmazeFile(path).getOutputStream(() -> context); default: try { outputStream = FileUtil.getOutputStream(getFile(), context); @@ -1195,72 +745,12 @@ public void close() throws IOException { } public boolean exists() { - boolean exists = false; - if (isSftp()) { - final Boolean executionReturn = - SshClientUtils.execute( - new SFtpClientTemplate(path) { - @Override - public Boolean execute(SFTPClient client) throws IOException { - try { - return client.stat(SshClientUtils.extractRemotePathFrom(path)) != null; - } catch (SFTPException notFound) { - return false; - } - } - }); - - if (executionReturn == null) { - Log.e(TAG, "Error obtaining existance of file over SFTP"); - } - - //noinspection SimplifiableConditionalExpression - exists = executionReturn == null ? false : executionReturn; - } else if (isSmb()) { - try { - SmbFile smbFile = getSmbFile(2000); - exists = smbFile != null && smbFile.exists(); - } catch (SmbException e) { - e.printStackTrace(); - exists = false; - } - } else if (isDropBoxFile()) { - CloudStorage cloudStorageDropbox = dataUtils.getAccount(OpenMode.DROPBOX); - exists = cloudStorageDropbox.exists(CloudUtil.stripPath(OpenMode.DROPBOX, path)); - } else if (isBoxFile()) { - CloudStorage cloudStorageBox = dataUtils.getAccount(OpenMode.BOX); - exists = cloudStorageBox.exists(CloudUtil.stripPath(OpenMode.BOX, path)); - } else if (isGoogleDriveFile()) { - CloudStorage cloudStorageGoogleDrive = dataUtils.getAccount(OpenMode.GDRIVE); - exists = cloudStorageGoogleDrive.exists(CloudUtil.stripPath(OpenMode.GDRIVE, path)); - } else if (isOneDriveFile()) { - CloudStorage cloudStorageOneDrive = dataUtils.getAccount(OpenMode.ONEDRIVE); - exists = cloudStorageOneDrive.exists(CloudUtil.stripPath(OpenMode.ONEDRIVE, path)); - } else if (isLocal()) { - exists = getFile().exists(); - } else if (isRoot()) { - return RootHelper.fileExists(path); - } - - return exists; + return new AmazeFile(path).exists(() -> null); } /** Helper method to check file existence in otg */ public boolean exists(Context context) { - boolean exists = false; - try { - if (isOtgFile()) { - exists = OTGUtil.getDocumentFile(path, context, false) != null; - } else if (isDocumentFile()) { - exists = - OTGUtil.getDocumentFile( - path, SafRootHolder.getUriRoot(), context, OpenMode.DOCUMENT_FILE, false) - != null; - } else return (exists()); - } catch (Exception e) { - Log.i(getClass().getSimpleName(), "Failed to find file", e); - } - return exists; + return new AmazeFile(path).exists(() -> context); } /** @@ -1283,125 +773,23 @@ public boolean isSimpleFile() { } public boolean setLastModified(final long date) { - if (isSmb()) { - try { - SmbFile smbFile = getSmbFile(); - if (smbFile != null) { - smbFile.setLastModified(date); - return true; - } else { - return false; - } - } catch (SmbException e) { - e.printStackTrace(); - return false; - } - } - File f = getFile(); - return f.setLastModified(date); + return new AmazeFile(path).setLastModified(date); } public void mkdir(Context context) { - if (isSftp()) { - SshClientUtils.execute( - new SFtpClientTemplate(path) { - @Override - public Void execute(@NonNull SFTPClient client) { - try { - client.mkdir(SshClientUtils.extractRemotePathFrom(path)); - } catch (IOException e) { - Log.e(TAG, "Error making directory over SFTP", e); - } - // FIXME: anything better than throwing a null to make Rx happy? - return null; - } - }); - } else if (isSmb()) { - try { - getSmbFile().mkdirs(); - } catch (SmbException e) { - e.printStackTrace(); - } - } else if (isOtgFile()) { - if (!exists(context)) { - DocumentFile parentDirectory = OTGUtil.getDocumentFile(getParent(context), context, true); - if (parentDirectory.isDirectory()) { - parentDirectory.createDirectory(getName(context)); - } - } - } else if (isDocumentFile()) { - if (!exists(context)) { - DocumentFile parentDirectory = - OTGUtil.getDocumentFile( - getParent(context), - SafRootHolder.getUriRoot(), - context, - OpenMode.DOCUMENT_FILE, - true); - if (parentDirectory.isDirectory()) { - parentDirectory.createDirectory(getName(context)); - } - } - } else if (isDropBoxFile()) { - CloudStorage cloudStorageDropbox = dataUtils.getAccount(OpenMode.DROPBOX); - try { - cloudStorageDropbox.createFolder(CloudUtil.stripPath(OpenMode.DROPBOX, path)); - } catch (Exception e) { - e.printStackTrace(); - } - } else if (isBoxFile()) { - CloudStorage cloudStorageBox = dataUtils.getAccount(OpenMode.BOX); - try { - cloudStorageBox.createFolder(CloudUtil.stripPath(OpenMode.BOX, path)); - } catch (Exception e) { - e.printStackTrace(); - } - } else if (isOneDriveFile()) { - CloudStorage cloudStorageOneDrive = dataUtils.getAccount(OpenMode.ONEDRIVE); - try { - cloudStorageOneDrive.createFolder(CloudUtil.stripPath(OpenMode.ONEDRIVE, path)); - } catch (Exception e) { - e.printStackTrace(); - } - } else if (isGoogleDriveFile()) { - CloudStorage cloudStorageGdrive = dataUtils.getAccount(OpenMode.GDRIVE); - try { - cloudStorageGdrive.createFolder(CloudUtil.stripPath(OpenMode.GDRIVE, path)); - } catch (Exception e) { - e.printStackTrace(); - } - } else MakeDirectoryOperation.mkdir(getFile(), context); + new AmazeFile(path).mkdirs(() -> context); } public boolean delete(Context context, boolean rootmode) throws ShellNotRunningException, SmbException { - if (isSftp()) { - Boolean retval = - SshClientUtils.execute( - new SFtpClientTemplate(path) { - @Override - public Boolean execute(@NonNull SFTPClient client) throws IOException { - String _path = SshClientUtils.extractRemotePathFrom(path); - if (isDirectory(AppConfig.getInstance())) client.rmdir(_path); - else client.rm(_path); - return client.statExistence(_path) == null; - } - }); - return retval != null && retval; - } else if (isSmb()) { - try { - getSmbFile().delete(); - } catch (SmbException e) { - Log.e(TAG, "Error delete SMB file", e); - throw e; - } + if (isSftp() || isSmb() || isLocal() || (isRoot() && !rootmode) || isOneDriveFile() + || isBoxFile() || isGoogleDriveFile() || isDropBoxFile() || isOtgFile()) { + return new AmazeFile(path).delete(() -> context); + } else if (isRoot() && rootmode) { + setMode(OpenMode.ROOT); + DeleteFileCommand.INSTANCE.deleteFile(getPath()); } else { - if (isRoot() && rootmode) { - setMode(OpenMode.ROOT); - DeleteFileCommand.INSTANCE.deleteFile(getPath()); - } else { - DeleteOperation.deleteFile(getFile(), context); - } + DeleteOperation.deleteFile(getFile(), context); } return !exists(); } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java index a4db3e823c..15119df503 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java @@ -37,9 +37,11 @@ import jcifs.smb.SmbException; import jcifs.smb.SmbFile; +import kotlin.Deprecated; import net.schmizz.sshj.sftp.RemoteResourceInfo; import net.schmizz.sshj.xfer.FilePermission; +@Deprecated(message = "Use [AmazeFile]") public class HybridFileParcelable extends HybridFile implements Parcelable { private long date, size; diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt b/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt index 95bc45c078..1aceb6a6a8 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt @@ -23,8 +23,9 @@ package com.amaze.filemanager.filesystem import android.content.Context import android.os.Build import com.amaze.filemanager.file_operations.filesystem.OpenMode +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider import com.amaze.filemanager.utils.OTGUtil -import jcifs.smb.SmbException import java.io.File import java.io.IOException @@ -75,19 +76,14 @@ object MakeDirectoryOperation { fun mkdirs(context: Context, file: HybridFile): Boolean { var isSuccessful = true when (file.mode) { - OpenMode.SMB -> - try { - val smbFile = file.smbFile - smbFile.mkdirs() - } catch (e: SmbException) { - e.printStackTrace() - isSuccessful = false - } + OpenMode.SMB, OpenMode.FILE -> + return AmazeFile(file.path).mkdirs(object : ContextProvider { + override fun getContext() = context + }) OpenMode.OTG -> { val documentFile = OTGUtil.getDocumentFile(file.getPath(), context, true) isSuccessful = documentFile != null } - OpenMode.FILE -> isSuccessful = mkdir(File(file.getPath()), context) else -> isSuccessful = true } return isSuccessful diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/MediaStoreHack.java b/app/src/main/java/com/amaze/filemanager/filesystem/MediaStoreHack.java index 38a34472d9..50791d2d44 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/MediaStoreHack.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/MediaStoreHack.java @@ -42,6 +42,8 @@ import androidx.annotation.Nullable; +import kotlin.Deprecated; + /** * Wrapper for manipulating files via the Android Media Content Provider. As of Android 4.4 KitKat, * applications can no longer write to the "secondary storage" of a device. Write operations using @@ -57,6 +59,9 @@ * * @author Jared Rummler */ +@Deprecated( + message = "Use [com.amaze.filemanager.file_operations.filesystem.filetypes.file.MediaStoreHack]" +) public class MediaStoreHack { private static final String TAG = "MediaStoreHack"; diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java b/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java index 081eb14fd8..507ec2513e 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java @@ -28,13 +28,17 @@ import java.io.IOException; import java.io.OutputStream; import java.net.MalformedURLException; -import java.net.URL; import java.util.ArrayList; import java.util.concurrent.Executor; import com.amaze.filemanager.R; import com.amaze.filemanager.file_operations.exceptions.ShellNotRunningException; import com.amaze.filemanager.file_operations.filesystem.OpenMode; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.box.BoxAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.dropbox.DropboxAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.gdrive.GoogledriveAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.onedrive.OnedriveAccount; import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.filesystem.root.MakeDirectoryCommand; @@ -44,6 +48,7 @@ import com.amaze.filemanager.filesystem.ssh.SshClientUtils; import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.OTGUtil; +import com.amaze.filemanager.utils.SmbUtil; import com.cloudrail.si.interfaces.CloudStorage; import android.content.Context; @@ -57,13 +62,13 @@ import androidx.arch.core.util.Function; import androidx.documentfile.provider.DocumentFile; -import jcifs.smb.SmbException; -import jcifs.smb.SmbFile; import net.schmizz.sshj.sftp.SFTPClient; +import jcifs.smb.SmbException; + public class Operations { - private static Executor executor = AsyncTask.THREAD_POOL_EXECUTOR; + private static final Executor executor = AsyncTask.THREAD_POOL_EXECUTOR; private static final String TAG = Operations.class.getSimpleName(); @@ -115,10 +120,7 @@ public static void mkdir( @NonNull final ErrorCallBack errorCallBack) { new AsyncTask() { - - private DataUtils dataUtils = DataUtils.getInstance(); - - private Function safCreateDirectory = + private final Function safCreateDirectory = input -> { if (input != null && input.isDirectory()) { boolean result = false; @@ -149,13 +151,7 @@ protected Void doInBackground(Void... params) { return null; } if (file.isSmb()) { - try { - file.getSmbFile(2000).mkdirs(); - } catch (SmbException e) { - e.printStackTrace(); - errorCallBack.done(file, false); - return null; - } + new AmazeFile(file.getPath()).mkdirs(() -> context); errorCallBack.done(file, file.exists()); return null; } else if (file.isOtgFile()) { @@ -179,7 +175,7 @@ protected Void doInBackground(Void... params) { false)); return null; } else if (file.isDropBoxFile()) { - CloudStorage cloudStorageDropbox = dataUtils.getAccount(OpenMode.DROPBOX); + CloudStorage cloudStorageDropbox = DropboxAccount.INSTANCE.getAccount(); try { cloudStorageDropbox.createFolder(CloudUtil.stripPath(OpenMode.DROPBOX, file.getPath())); errorCallBack.done(file, true); @@ -188,7 +184,7 @@ protected Void doInBackground(Void... params) { errorCallBack.done(file, false); } } else if (file.isBoxFile()) { - CloudStorage cloudStorageBox = dataUtils.getAccount(OpenMode.BOX); + CloudStorage cloudStorageBox = BoxAccount.INSTANCE.getAccount(); try { cloudStorageBox.createFolder(CloudUtil.stripPath(OpenMode.BOX, file.getPath())); errorCallBack.done(file, true); @@ -197,7 +193,7 @@ protected Void doInBackground(Void... params) { errorCallBack.done(file, false); } } else if (file.isOneDriveFile()) { - CloudStorage cloudStorageOneDrive = dataUtils.getAccount(OpenMode.ONEDRIVE); + CloudStorage cloudStorageOneDrive = OnedriveAccount.INSTANCE.getAccount(); try { cloudStorageOneDrive.createFolder( CloudUtil.stripPath(OpenMode.ONEDRIVE, file.getPath())); @@ -207,7 +203,7 @@ protected Void doInBackground(Void... params) { errorCallBack.done(file, false); } } else if (file.isGoogleDriveFile()) { - CloudStorage cloudStorageGdrive = dataUtils.getAccount(OpenMode.GDRIVE); + CloudStorage cloudStorageGdrive = GoogledriveAccount.INSTANCE.getAccount(); try { cloudStorageGdrive.createFolder(CloudUtil.stripPath(OpenMode.GDRIVE, file.getPath())); errorCallBack.done(file, true); @@ -255,9 +251,9 @@ public static void mkfile( new AsyncTask() { - private DataUtils dataUtils = DataUtils.getInstance(); + private final DataUtils dataUtils = DataUtils.getInstance(); - private Function safCreateFile = + private final Function safCreateFile = input -> { if (input != null && input.isDirectory()) { boolean result = false; @@ -304,8 +300,8 @@ protected Void doInBackground(Void... params) { } if (file.isSmb()) { try { - file.getSmbFile(2000).createNewFile(); - } catch (SmbException e) { + new AmazeFile(file.getPath()).createNewFile(); + } catch (IOException e) { e.printStackTrace(); errorCallBack.done(file, false); return null; @@ -313,7 +309,7 @@ protected Void doInBackground(Void... params) { errorCallBack.done(file, file.exists()); return null; } else if (file.isDropBoxFile()) { - CloudStorage cloudStorageDropbox = dataUtils.getAccount(OpenMode.DROPBOX); + CloudStorage cloudStorageDropbox = DropboxAccount.INSTANCE.getAccount(); try { byte[] tempBytes = new byte[0]; ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(tempBytes); @@ -328,7 +324,7 @@ protected Void doInBackground(Void... params) { errorCallBack.done(file, false); } } else if (file.isBoxFile()) { - CloudStorage cloudStorageBox = dataUtils.getAccount(OpenMode.BOX); + CloudStorage cloudStorageBox = BoxAccount.INSTANCE.getAccount(); try { byte[] tempBytes = new byte[0]; ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(tempBytes); @@ -340,7 +336,7 @@ protected Void doInBackground(Void... params) { errorCallBack.done(file, false); } } else if (file.isOneDriveFile()) { - CloudStorage cloudStorageOneDrive = dataUtils.getAccount(OpenMode.ONEDRIVE); + CloudStorage cloudStorageOneDrive = OnedriveAccount.INSTANCE.getAccount(); try { byte[] tempBytes = new byte[0]; ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(tempBytes); @@ -355,7 +351,7 @@ protected Void doInBackground(Void... params) { errorCallBack.done(file, false); } } else if (file.isGoogleDriveFile()) { - CloudStorage cloudStorageGdrive = dataUtils.getAccount(OpenMode.GDRIVE); + CloudStorage cloudStorageGdrive = GoogledriveAccount.INSTANCE.getAccount(); try { byte[] tempBytes = new byte[0]; ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(tempBytes); @@ -429,18 +425,6 @@ public static void rename( private final DataUtils dataUtils = DataUtils.getInstance(); - private Function safRenameFile = - input -> { - boolean result = false; - try { - result = input.renameTo(newFile.getName(context)); - } catch (Exception e) { - Log.w(getClass().getSimpleName(), "Failed to rename", e); - } - errorCallBack.done(newFile, result); - return null; - }; - @Override protected Void doInBackground(Void... params) { // check whether file names for new file are valid or recursion occurs. @@ -454,135 +438,70 @@ protected Void doInBackground(Void... params) { errorCallBack.exists(newFile); return null; } + AmazeFile amazeFile = new AmazeFile(oldFile.getPath()); + // FIXME: smbFile1 should be created from SmbUtil too so it can be mocked + AmazeFile amazeFile1 = new AmazeFile(newFile.getPath()); if (oldFile.isSmb()) { - try { - SmbFile smbFile = oldFile.getSmbFile(); - // FIXME: smbFile1 should be created from SmbUtil too so it can be mocked - SmbFile smbFile1 = new SmbFile(new URL(newFile.getPath()), smbFile.getContext()); - if (newFile.exists()) { - errorCallBack.exists(newFile); - return null; - } - smbFile.renameTo(smbFile1); - if (!smbFile.exists() && smbFile1.exists()) errorCallBack.done(newFile, true); - } catch (SmbException | MalformedURLException e) { - String errmsg = - context.getString( - R.string.cannot_rename_file, - HybridFile.parseAndFormatUriForDisplay(oldFile.getPath()), - e.getMessage()); + if (newFile.exists()) { + errorCallBack.exists(newFile); + return null; + } + amazeFile.renameTo(amazeFile1, () -> context); + if (!amazeFile.exists(() -> context) && amazeFile1.exists(() -> context)) { + errorCallBack.done(newFile, true); + return null; + } else { try { ArrayList failedOps = new ArrayList<>(); - failedOps.add(new HybridFileParcelable(oldFile.getSmbFile())); + failedOps.add(new HybridFileParcelable(SmbUtil.create(oldFile.getPath()))); context.sendBroadcast( - new Intent(TAG_INTENT_FILTER_GENERAL) - .putParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS, failedOps)); - } catch (SmbException exceptionThrownDuringBuildParcelable) { - Log.e( - TAG, "Error creating HybridFileParcelable", exceptionThrownDuringBuildParcelable); + new Intent(TAG_INTENT_FILTER_GENERAL) + .putParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS, failedOps)); + } catch (SmbException | MalformedURLException exceptionThrownDuringBuildParcelable) { + Log.e(TAG, "Error creating HybridFileParcelable", exceptionThrownDuringBuildParcelable); } - Log.e(TAG, errmsg, e); } return null; } else if (oldFile.isSftp()) { - SshClientUtils.execute( - new SFtpClientTemplate(oldFile.getPath()) { - @Override - public Void execute(@NonNull SFTPClient client) { - try { - client.rename( - SshClientUtils.extractRemotePathFrom(oldFile.getPath()), - SshClientUtils.extractRemotePathFrom(newFile.getPath())); - errorCallBack.done(newFile, true); - } catch (IOException e) { - String errmsg = - context.getString( - R.string.cannot_rename_file, - HybridFile.parseAndFormatUriForDisplay(oldFile.getPath()), - e.getMessage()); - Log.e(TAG, errmsg); - ArrayList failedOps = new ArrayList<>(); - // Nobody care the size or actual permission here. Put a simple "r" and zero - // here - failedOps.add( - new HybridFileParcelable( + boolean result = amazeFile.renameTo(amazeFile1, () -> context); + + if(!result) { + ArrayList failedOps = new ArrayList<>(); + // Nobody care the size or actual permission here. Put a simple "r" and zero + // here + failedOps.add( + new HybridFileParcelable( oldFile.getPath(), "r", oldFile.lastModified(), 0, oldFile.isDirectory(context))); - context.sendBroadcast( - new Intent(TAG_INTENT_FILTER_GENERAL) + context.sendBroadcast( + new Intent(TAG_INTENT_FILTER_GENERAL) .putParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS, failedOps)); - errorCallBack.done(newFile, false); - } - return null; - } - }); - } else if (oldFile.isDropBoxFile()) { - CloudStorage cloudStorageDropbox = dataUtils.getAccount(OpenMode.DROPBOX); - try { - cloudStorageDropbox.move( - CloudUtil.stripPath(OpenMode.DROPBOX, oldFile.getPath()), - CloudUtil.stripPath(OpenMode.DROPBOX, newFile.getPath())); - errorCallBack.done(newFile, true); - } catch (Exception e) { - e.printStackTrace(); - errorCallBack.done(newFile, false); - } - } else if (oldFile.isBoxFile()) { - CloudStorage cloudStorageBox = dataUtils.getAccount(OpenMode.BOX); - try { - cloudStorageBox.move( - CloudUtil.stripPath(OpenMode.BOX, oldFile.getPath()), - CloudUtil.stripPath(OpenMode.BOX, newFile.getPath())); - errorCallBack.done(newFile, true); - } catch (Exception e) { - e.printStackTrace(); - errorCallBack.done(newFile, false); - } - } else if (oldFile.isOneDriveFile()) { - CloudStorage cloudStorageOneDrive = dataUtils.getAccount(OpenMode.ONEDRIVE); - try { - cloudStorageOneDrive.move( - CloudUtil.stripPath(OpenMode.ONEDRIVE, oldFile.getPath()), - CloudUtil.stripPath(OpenMode.ONEDRIVE, newFile.getPath())); - errorCallBack.done(newFile, true); - } catch (Exception e) { - e.printStackTrace(); - errorCallBack.done(newFile, false); - } - } else if (oldFile.isGoogleDriveFile()) { - CloudStorage cloudStorageGdrive = dataUtils.getAccount(OpenMode.GDRIVE); - try { - cloudStorageGdrive.move( - CloudUtil.stripPath(OpenMode.GDRIVE, oldFile.getPath()), - CloudUtil.stripPath(OpenMode.GDRIVE, newFile.getPath())); - errorCallBack.done(newFile, true); - } catch (Exception e) { - e.printStackTrace(); - errorCallBack.done(newFile, false); } + + errorCallBack.done(newFile, result); + } else if (oldFile.isDropBoxFile() || oldFile.isBoxFile() || oldFile.isOneDriveFile() + || oldFile.isGoogleDriveFile()) { + boolean result = amazeFile.renameTo(amazeFile1, () -> context); + errorCallBack.done(newFile, result); } else if (oldFile.isOtgFile()) { if (checkOtgNewFileExists(newFile, context)) { errorCallBack.exists(newFile); return null; } - safRenameFile.apply(OTGUtil.getDocumentFile(oldFile.getPath(), context, false)); + boolean result = amazeFile.renameTo(amazeFile1, () -> context); + errorCallBack.done(newFile, result); return null; } else if (oldFile.isDocumentFile()) { if (checkDocumentFileNewFileExists(newFile, context)) { errorCallBack.exists(newFile); return null; } - safRenameFile.apply( - OTGUtil.getDocumentFile( - oldFile.getPath(), - SafRootHolder.getUriRoot(), - context, - OpenMode.DOCUMENT_FILE, - false)); + boolean result = amazeFile.renameTo(amazeFile1, () -> context); + errorCallBack.done(newFile, result); return null; } else { File file = new File(oldFile.getPath()); diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.java b/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.java index 4aabb07f8d..42dd3e8dc5 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.java @@ -34,6 +34,12 @@ import com.amaze.filemanager.file_operations.exceptions.CloudPluginException; import com.amaze.filemanager.file_operations.filesystem.OpenMode; import com.amaze.filemanager.file_operations.filesystem.cloud.CloudStreamer; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.box.BoxAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.dropbox.DropboxAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.gdrive.GoogledriveAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.onedrive.OnedriveAccount; import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.HybridFileParcelable; import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate; @@ -200,18 +206,18 @@ protected Boolean doInBackground(String... params) { if (path.startsWith(CloudHandler.CLOUD_PREFIX_DROPBOX)) { // dropbox account serviceType = OpenMode.DROPBOX; - cloudStorage = dataUtils.getAccount(OpenMode.DROPBOX); + cloudStorage = DropboxAccount.INSTANCE.getAccount(); } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_ONE_DRIVE)) { serviceType = OpenMode.ONEDRIVE; - cloudStorage = dataUtils.getAccount(OpenMode.ONEDRIVE); + cloudStorage = OnedriveAccount.INSTANCE.getAccount(); } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_BOX)) { serviceType = OpenMode.BOX; - cloudStorage = dataUtils.getAccount(OpenMode.BOX); + cloudStorage = BoxAccount.INSTANCE.getAccount(); } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE)) { serviceType = OpenMode.GDRIVE; - cloudStorage = dataUtils.getAccount(OpenMode.GDRIVE); + cloudStorage = GoogledriveAccount.INSTANCE.getAccount(); } else { throw new IllegalStateException(); } @@ -255,6 +261,7 @@ public static InputStream getThumbnailInputStreamForCloud(Context context, Strin switch (hybridFile.getMode()) { case SFTP: + //TODO repace with AmazeFile's inputStream = SshClientUtils.execute( new SFtpClientTemplate(hybridFile.getPath(), false) { @@ -277,13 +284,7 @@ public void close() throws IOException { }); break; case SMB: - try { - inputStream = hybridFile.getSmbFile().getInputStream(); - } catch (IOException e) { - inputStream = null; - e.printStackTrace(); - } - break; + return new AmazeFile(hybridFile.getPath()).getInputStream(() -> context); case OTG: ContentResolver contentResolver = context.getContentResolver(); DocumentFile documentSourceFile = @@ -301,7 +302,7 @@ public void close() throws IOException { case ONEDRIVE: OpenMode mode = hybridFile.getMode(); - CloudStorage cloudStorageDropbox = dataUtils.getAccount(mode); + CloudStorage cloudStorageDropbox = dataUtils.getAccount(mode).getAccount(); String stripped = CloudUtil.stripPath(mode, hybridFile.getPath()); inputStream = cloudStorageDropbox.getThumbnail(stripped); break; diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/DocumentFileAmazeFilesystem.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/DocumentFileAmazeFilesystem.kt new file mode 100644 index 0000000000..b0575effbe --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/DocumentFileAmazeFilesystem.kt @@ -0,0 +1,232 @@ +package com.amaze.filemanager.filesystem.files + +import android.content.ContentResolver +import android.util.Log +import androidx.documentfile.provider.DocumentFile +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.file_operations.filesystem.OpenMode +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFilesystem +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider +import com.amaze.filemanager.filesystem.FileProperties.getDeviceStorageRemainingSpace +import com.amaze.filemanager.filesystem.SafRootHolder.uriRoot +import com.amaze.filemanager.filesystem.SafRootHolder.volumeLabel +import com.amaze.filemanager.utils.OTGUtil.getDocumentFile +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +object DocumentFileAmazeFilesystem: AmazeFilesystem() { + @JvmStatic + val TAG = DocumentFileAmazeFilesystem::class.java.simpleName + + const val DOCUMENT_FILE_PREFIX = "content://com.android.externalstorage.documents" + + init { + AmazeFile.addFilesystem(this) + } + + override val prefix: String = DOCUMENT_FILE_PREFIX + + override fun normalize(path: String): String { + val documentFile = getDocumentFile(path, false) ?: return defaultParent + return documentFile.uri.path ?: defaultParent + } + + override fun resolve(parent: String, child: String): String { + val documentFile = getDocumentFile(parent, false) ?: return defaultParent + val childDocumentFile = documentFile.findFile(child) ?: return defaultParent + return childDocumentFile.uri.path ?: return defaultParent + } + + override fun resolve(f: AmazeFile): String { + TODO("Not yet implemented") + } + + override val defaultParent: String = DOCUMENT_FILE_PREFIX + getSeparator() + + override fun isAbsolute(f: AmazeFile): Boolean = true // All paths are absolute + + override fun canonicalize(path: String?): String { + TODO("Not yet implemented") + } + + override fun exists(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val uriRoot = uriRoot ?: return false + val context = contextProvider.getContext() ?: return false + val documentFile = getDocumentFile( + f.path, uriRoot, context, OpenMode.DOCUMENT_FILE, false + ) + return documentFile != null + } + + override fun isFile(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val documentFile = getDocumentFile(f.path, false) ?: return false + return documentFile.isFile + } + + override fun isDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val documentFile = getDocumentFile(f.path, false) ?: return false + return documentFile.isDirectory + } + + override fun isHidden(f: AmazeFile): Boolean { + throw NotImplementedError() + } + + override fun canExecute(f: AmazeFile, contextProvider: ContextProvider): Boolean { + throw NotImplementedError() + } + + override fun canWrite(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val documentFile = getDocumentFile(f.path, false) ?: return false + return documentFile.canWrite() + } + + override fun canRead(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val documentFile = getDocumentFile(f.path, false) ?: return false + return documentFile.canRead() + } + + override fun canAccess(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return exists(f, contextProvider) + } + + override fun setExecutable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + return false //Can't set + } + + override fun setWritable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + return false //Can't set + } + + override fun setReadable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + return false //Can't set + } + + override fun getLastModifiedTime(f: AmazeFile): Long { + val documentFile = getDocumentFile(f.path, false) ?: return 0 + return documentFile.lastModified() + } + + override fun getLength(f: AmazeFile, contextProvider: ContextProvider): Long { + val documentFile = getDocumentFile(f.path, false) + ?: throw IOException("Could not create DocumentFile for length") + return documentFile.length() + } + + override fun createFileExclusively(pathname: String): Boolean { + TODO("Not yet implemented") + } + + override fun delete(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val documentFile = getDocumentFile(f.path, false) ?: return false + return documentFile.delete() + } + + override fun list(f: AmazeFile, contextProvider: ContextProvider): Array? { + val documentFile = getDocumentFile(f.path, false) ?: return null + return documentFile.listFiles().mapNotNull { it.uri.path }.toTypedArray() + } + + override fun getInputStream(f: AmazeFile, contextProvider: ContextProvider): InputStream? { + val context = contextProvider.getContext() ?: return null + val contentResolver: ContentResolver = context.contentResolver + val documentSourceFile = getDocumentFile(f.path, false) ?: return null + return try { + contentResolver.openInputStream(documentSourceFile.uri) + } catch (e: FileNotFoundException) { + Log.e(TAG, "Error getting input stream for DocumentFile", e) + null + } + } + + override fun getOutputStream(f: AmazeFile, contextProvider: ContextProvider): OutputStream? { + val context = contextProvider.getContext() ?: return null + val contentResolver: ContentResolver = context.contentResolver + val documentSourceFile = getDocumentFile(f.path, true) ?: return null + return try { + contentResolver.openOutputStream(documentSourceFile.uri) + } catch (e: FileNotFoundException) { + Log.e(TAG, "Error getting output stream for DocumentFile", e) + null + } + } + + override fun createDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val context = contextProvider.getContext() ?: return false + if (!exists(f, contextProvider)) { + val uriRoot = uriRoot ?: return false + val parent = f.parent ?: return false + val parentDirectory = getDocumentFile( + parent, + uriRoot, + context, + OpenMode.DOCUMENT_FILE, + true + ) ?: return false + + if (parentDirectory.isDirectory) { + parentDirectory.createDirectory(f.name) ?: return false + return true + } + } + + return false + } + + override fun rename(file1: AmazeFile, file2: AmazeFile, contextProvider: ContextProvider): Boolean { + val context = contextProvider.getContext() ?: return false + val uriRoot = uriRoot ?: return false + + val documentFile = getDocumentFile( + file1.path, + uriRoot, + context, + OpenMode.DOCUMENT_FILE, + false + ) ?: return false + + return try { + documentFile.renameTo(file2.name) + } catch (e: Exception) { + Log.e(TAG, "Error renaming DocumentFile file", e) + false + } + } + + override fun setLastModifiedTime(f: AmazeFile, time: Long): Boolean { + return false //Can't set + } + + override fun setReadOnly(f: AmazeFile): Boolean { + return false //Can't set + } + + override fun getTotalSpace(f: AmazeFile, contextProvider: ContextProvider): Long { + return getDocumentFile(defaultParent, false)?.length() ?: 0 + } + + override fun getFreeSpace(f: AmazeFile): Long { + return getUsableSpace(f) // Assume both are equal + } + + override fun getUsableSpace(f: AmazeFile): Long { + val volumeLabel = volumeLabel ?: return 0 + return getDeviceStorageRemainingSpace(volumeLabel) + } + + fun getDocumentFile(path: String, createRecursive: Boolean): DocumentFile? { + val uriRoot = uriRoot ?: return null + + return getDocumentFile( + path, + uriRoot, + AppConfig.getInstance(), + OpenMode.DOCUMENT_FILE, + createRecursive + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileAmazeFilesystem.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileAmazeFilesystem.kt new file mode 100644 index 0000000000..2773d75494 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileAmazeFilesystem.kt @@ -0,0 +1,337 @@ +package com.amaze.filemanager.filesystem.files + +import android.content.ContentValues +import android.os.Build +import android.provider.MediaStore +import android.util.Log +import androidx.preference.PreferenceManager +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFilesystem +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider +import com.amaze.filemanager.file_operations.filesystem.filetypes.file.ExternalSdCardOperation +import com.amaze.filemanager.file_operations.filesystem.filetypes.file.MediaStoreHack +import com.amaze.filemanager.file_operations.filesystem.filetypes.file.UriForSafPersistance +import com.amaze.filemanager.filesystem.RootHelper +import com.amaze.filemanager.filesystem.root.MakeDirectoryCommand +import java.io.* + +/** + * This is in in essence calls to UnixFilesystem, but that class is not public so all calls must go + * through java.io.File + */ +object FileAmazeFilesystem : AmazeFilesystem() { + const val PREFERENCE_ROOTMODE = "rootmode" + + override val nomediaFileEnabled: Boolean = true + + @JvmStatic + val TAG = FileAmazeFilesystem::class.java.simpleName + + init { + AmazeFile.addFilesystem(this) + } + + override fun isPathOfThisFilesystem(path: String): Boolean { + return path[0] == getSeparator() + } + + override val prefix: String = "" + + override fun getSeparator(): Char { + return File.separatorChar + } + + override fun normalize(path: String): String { + return File(path).path + } + + override fun prefixLength(path: String): Int { + if (path.length == 0) return 0 + return if (path[0] == '/') 1 else 0 + } + + override fun resolve(parent: String, child: String): String { + return File(parent, child).path + } + + override val defaultParent: String + get() = File(File(""), "").path + + override fun isAbsolute(f: AmazeFile): Boolean { + return File(f.path).isAbsolute + } + + override fun resolve(f: AmazeFile): String { + return File(f.path).absolutePath + } + + @Throws(IOException::class) + override fun canonicalize(path: String?): String { + return File(path).canonicalPath + } + + override fun exists(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return runAsNormalOrRoot(f, contextProvider, { + File(f.path).exists() + }) { + RootHelper.fileExists(f.path) + } + } + + override fun isFile(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return File(f.path).isFile + } + + override fun isDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return File(f.path).isDirectory + } + + override fun isHidden(f: AmazeFile): Boolean { + return File(f.path).isHidden + } + + override fun canExecute(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return File(f.path).canExecute() + } + + override fun canWrite(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return File(f.path).canWrite() + } + + override fun canRead(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return File(f.path).canRead() + } + + override fun canAccess(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return File(f.path).exists() + } + + override fun setExecutable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + return File(f.path).setExecutable(enable, owneronly) + } + + override fun setWritable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + return File(f.path).setWritable(enable, owneronly) + } + + override fun setReadable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + return File(f.path).setReadable(enable, owneronly) + } + + override fun getLastModifiedTime(f: AmazeFile): Long { + return File(f.path).lastModified() + } + + @Throws(IOException::class) + override fun getLength(f: AmazeFile, contextProvider: ContextProvider): Long { + return File(f.path).length() + } + + @Throws(IOException::class) + override fun createFileExclusively(pathname: String): Boolean { + return File(pathname).createNewFile() + } + + override fun delete(f: AmazeFile, contextProvider: ContextProvider): Boolean { + + if (f.isDirectory(contextProvider)) { + val children = f.listFiles(contextProvider) + if (children != null) { + for (child in children) { + delete(child, contextProvider) + } + } + + // Try the normal way + if (File(f.path).delete()) { + return true + } + val context = contextProvider.getContext() + + // Try with Storage Access Framework. + if (context != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val document = ExternalSdCardOperation.getDocumentFile( + f, true, context, UriForSafPersistance.get(context) + ) + if (document != null && document.delete()) { + return true + } + } + + // Try the Kitkat workaround. + if (context != null && Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + val resolver = context.contentResolver + val values = ContentValues() + values.put(MediaStore.MediaColumns.DATA, f.absolutePath) + resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + + // Delete the created entry, such that content provider will delete the file. + resolver.delete( + MediaStore.Files.getContentUri("external"), + MediaStore.MediaColumns.DATA + "=?", arrayOf(f.absolutePath) + ) + } + return !f.exists(contextProvider) + } + if (File(f.path).delete()) { + return true + } + val context = contextProvider.getContext() + + // Try with Storage Access Framework. + if (context != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && + ExternalSdCardOperation.isOnExtSdCard(f, context) + ) { + val document = ExternalSdCardOperation.getDocumentFile( + f, false, context, UriForSafPersistance.get(context) + ) ?: return true + if (document.delete()) { + return true + } + } + + // Try the Kitkat workaround. + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + val resolver = context!!.contentResolver + try { + val uri = MediaStoreHack.getUriFromFile(f.absolutePath, context) + ?: return false + resolver.delete(uri, null, null) + return !f.exists(contextProvider) + } catch (e: SecurityException) { + Log.e(TAG, "Security exception when checking for file " + f.absolutePath, e) + } + } + return false + } + + override fun list(f: AmazeFile, contextProvider: ContextProvider): Array? { + return File(f.path).list() + } + + override fun getInputStream(f: AmazeFile, contextProvider: ContextProvider): InputStream? { + return try { + FileInputStream(f.path) + } catch (e: FileNotFoundException) { + Log.e(TAG, "Cannot find file", e) + null + } + } + + override fun getOutputStream(f: AmazeFile, contextProvider: ContextProvider): OutputStream? { + return try { + if (f.canWrite(contextProvider)) { + return FileOutputStream(f.path) + } else { + val context = contextProvider.getContext() ?: return null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // Storage Access Framework + val targetDocument = ExternalSdCardOperation.getDocumentFile( + f, false, context, UriForSafPersistance.get(context) + ) ?: return null + return context.contentResolver.openOutputStream(targetDocument.uri) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + // Workaround for Kitkat ext SD card + return MediaStoreHack.getOutputStream(context, f.path) + } + } + null + } catch (e: FileNotFoundException) { + Log.e(TAG, "Cannot find file", e) + null + } + } + + override fun createDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return runAsNormalOrRoot(f, contextProvider, { + if (File(f.path).mkdir()) { + return@runAsNormalOrRoot true + } + val context = contextProvider.getContext() + + // Try with Storage Access Framework. + if (context != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && + ExternalSdCardOperation.isOnExtSdCard(f, context) + ) { + val preferenceUri = UriForSafPersistance.get(context) + val document = ExternalSdCardOperation.getDocumentFile(f, true, context, preferenceUri) + ?: return@runAsNormalOrRoot false + // getDocumentFile implicitly creates the directory. + return@runAsNormalOrRoot document.exists() + } + + // Try the Kitkat workaround. + return@runAsNormalOrRoot if (context != null && Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + try { + MediaStoreHack.mkdir(context, f) + } catch (e: IOException) { + false + } + } else false + }) { + val parent = f.parent ?: return@runAsNormalOrRoot false + MakeDirectoryCommand.makeDirectory(parent, f.name) + return@runAsNormalOrRoot true + } + } + + override fun rename( + file1: AmazeFile, + file2: AmazeFile, + contextProvider: ContextProvider + ): Boolean { + return File(file1.path).renameTo(File(file2.path)) + } + + override fun setLastModifiedTime(f: AmazeFile, time: Long): Boolean { + return File(f.path).setLastModified(time) + } + + override fun setReadOnly(f: AmazeFile): Boolean { + return File(f.path).setReadOnly() + } + + override fun getTotalSpace(f: AmazeFile, contextProvider: ContextProvider): Long { + return File(f.path).totalSpace + } + + override fun getFreeSpace(f: AmazeFile): Long { + return File(f.path).freeSpace + } + + override fun getUsableSpace(f: AmazeFile): Long { + return File(f.path).usableSpace + } + + override fun compare(f1: AmazeFile, f2: AmazeFile): Int { + return File(f1.path).compareTo(File(f2.path)) + } + + override fun hashCode(f: AmazeFile): Int { + return File(f.path).hashCode() + } + + private fun runAsNormalOrRoot(file: AmazeFile, contextProvider: ContextProvider, + normalMode: () -> T, rootMode: () -> T): T { + val context = contextProvider.getContext() ?: return normalMode() + + val rootmode = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(PREFERENCE_ROOTMODE, false) + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + if (rootmode && !canRead(file, contextProvider)) { + return rootMode() + } + + return normalMode() + } + + if (ExternalSdCardOperation.isOnExtSdCard(file, context)) { + return normalMode() + } else if (rootmode && !canRead(file, contextProvider)) { + return rootMode() + } + + return normalMode() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java index a41ab695e7..2f89fdbbaa 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java @@ -46,11 +46,23 @@ import androidx.core.util.Pair; import androidx.documentfile.provider.DocumentFile; +import java.io.File; +import java.io.IOException; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicLong; + import com.afollestad.materialdialogs.MaterialDialog; import com.amaze.filemanager.R; import com.amaze.filemanager.adapters.data.LayoutElementParcelable; import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.file_operations.filesystem.OpenMode; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider; import com.amaze.filemanager.filesystem.ExternalSdCardOperation; import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.HybridFileParcelable; @@ -89,7 +101,6 @@ import java.util.LinkedList; import java.util.concurrent.atomic.AtomicLong; -import jcifs.smb.SmbFile; import kotlin.collections.ArraysKt; /** Functions that deal with files */ @@ -126,13 +137,26 @@ public static long folderSize(HybridFile directory, OnProgressUpdate updat else return directory.folderSize(AppConfig.getInstance()); } - public static long folderSize(SmbFile directory) { + public static long folderSize(AmazeFile directory, @NonNull ContextProvider contextProvider) { + long naiveSize; + + try { + naiveSize = directory.length(contextProvider); + } catch (IOException e) { + Log.e(TAG, "Unexpected error getting size from AmazeFile", e); + naiveSize = 0; + } + + if(naiveSize != 0) { + return naiveSize; + } + long length = 0; try { - for (SmbFile file : directory.listFiles()) { + for (AmazeFile file : directory.listFiles(contextProvider)) { - if (file.isFile()) length += file.length(); - else length += folderSize(file); + if (file.isFile(contextProvider)) length += file.length(contextProvider); + else length += folderSize(file, contextProvider); } } catch (Exception e) { e.printStackTrace(); @@ -168,7 +192,7 @@ public static long folderSizeCloud(OpenMode openMode, CloudMetaData sourceFileMe DataUtils dataUtils = DataUtils.getInstance(); long length = 0; - CloudStorage cloudStorage = dataUtils.getAccount(openMode); + CloudStorage cloudStorage = dataUtils.getAccount(openMode).getAccount(); for (CloudMetaData metaData : cloudStorage.getChildren(CloudUtil.stripPath(openMode, sourceFileMeta.getPath()))) { @@ -330,7 +354,7 @@ public static void shareCloudFile(String path, final OpenMode openMode, final Co @Override protected String doInBackground(String... params) { String shareFilePath = params[0]; - CloudStorage cloudStorage = DataUtils.getInstance().getAccount(openMode); + CloudStorage cloudStorage = DataUtils.getInstance().getAccount(openMode).getAccount(); return cloudStorage.createShareLink(CloudUtil.stripPath(openMode, shareFilePath)); } @@ -353,7 +377,7 @@ public static void shareCloudFiles(ArrayList files, fin new AsyncTask() { @Override protected String doInBackground(String... params) { - CloudStorage cloudStorage = DataUtils.getInstance().getAccount(openMode); + CloudStorage cloudStorage = DataUtils.getInstance().getAccount(openMode).getAccount(); StringBuilder links = new StringBuilder(); links.append(cloudStorage.createShareLink(CloudUtil.stripPath(openMode, params[0]))); for (int i = 1; i < params.length; i++) { diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java index a2e53f4948..d96b916c27 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java @@ -130,7 +130,7 @@ private void startCopy( || mSourceFile.isOneDriveFile()) { OpenMode openMode = mSourceFile.getMode(); - CloudStorage cloudStorage = dataUtils.getAccount(openMode); + CloudStorage cloudStorage = dataUtils.getAccount(openMode).getAccount(); bufferedInputStream = new BufferedInputStream( cloudStorage.download(CloudUtil.stripPath(openMode, mSourceFile.getPath()))); @@ -273,7 +273,7 @@ private void cloudCopy( throws IOException { DataUtils dataUtils = DataUtils.getInstance(); // API doesn't support output stream, we'll upload the file directly - CloudStorage cloudStorage = dataUtils.getAccount(openMode); + CloudStorage cloudStorage = dataUtils.getAccount(openMode).getAccount(); if (mSourceFile.getMode() == openMode) { // we're in the same provider, use api method diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/otg/OtgAmazeFilesystem.kt b/app/src/main/java/com/amaze/filemanager/filesystem/otg/OtgAmazeFilesystem.kt new file mode 100644 index 0000000000..e668343ff8 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/otg/OtgAmazeFilesystem.kt @@ -0,0 +1,223 @@ +package com.amaze.filemanager.filesystem.otg + +import android.net.Uri +import android.os.Build +import android.util.Log +import androidx.core.content.FileProvider +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFilesystem +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider +import com.amaze.filemanager.utils.OTGUtil +import com.amaze.filemanager.utils.OTGUtil.getDocumentFile +import java.io.* + +object OtgAmazeFilesystem : AmazeFilesystem() { + @JvmStatic + val TAG = OtgAmazeFilesystem::class.java.simpleName + + const val PREFIX = "otg:/" + + init { + AmazeFile.addFilesystem(this) + } + + override val prefix = PREFIX + + override fun prefixLength(path: String): Int { + require(path.length != 0) { "This should never happen, all paths must start with OTG prefix" } + return super.prefixLength(path) + } + + override fun normalize(path: String): String { + return simpleUnixNormalize(path) + } + + override fun getUriForFile(f: AmazeFile, contextProvider: ContextProvider): Uri? { + val context = contextProvider.getContext() ?: return null + + val documentFile = getDocumentFile(f.path, context, true) + + if(documentFile == null) { + Log.e(TAG, "Null DocumentFile getting Uri!") + return null + } + + return documentFile.uri + } + + override fun resolve(parent: String, child: String): String { + val prefix = parent.substring(0, prefixLength(parent)) + val simplePathParent = parent.substring(prefixLength(parent)) + val simplePathChild = child.substring(prefixLength(child)) + return prefix + basicUnixResolve(simplePathParent, simplePathChild) + } + + override val defaultParent: String + get() = prefix + getSeparator() + + override fun isAbsolute(f: AmazeFile): Boolean { + return true // Relative paths are not supported + } + + override fun resolve(f: AmazeFile): String { + return f.path + } + + @Throws(IOException::class) + override fun canonicalize(path: String?): String { + return normalize(path!!) + } + + override fun exists(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val context = contextProvider.getContext() ?: return false + return getDocumentFile(f.path, context, false) != null + } + + override fun isFile(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val context = contextProvider.getContext() ?: return false + return getDocumentFile(f.path, context, false)!!.isFile + } + + override fun isDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val context = contextProvider.getContext() ?: return false + return getDocumentFile(f.path, context, false)!!.isDirectory + } + + override fun isHidden(f: AmazeFile): Boolean { + return false // There doesn't seem to be hidden files for OTG + } + + override fun canExecute(f: AmazeFile, contextProvider: ContextProvider): Boolean { + throw NotImplementedError() // No way to check + } + + override fun canWrite(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val context = contextProvider.getContext() ?: return false + return getDocumentFile(f.path, context, false)!!.canWrite() + } + + override fun canRead(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val context = contextProvider.getContext() ?: return false + return getDocumentFile(f.path, context, false)!!.canRead() + } + + override fun canAccess(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return exists(f, contextProvider) //If the system says it exists, we can access it + } + + override fun setExecutable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + return false //TODO find way to set + } + + override fun setWritable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + return false //TODO find way to set + } + + override fun setReadable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + return false //TODO find way to set + } + + override fun getLastModifiedTime(f: AmazeFile): Long { + return 0 + } + + @Throws(IOException::class) + override fun getLength(f: AmazeFile, contextProvider: ContextProvider): Long { + val context = contextProvider.getContext() + ?: throw IOException("Error obtaining context for OTG") + return getDocumentFile(f.path, context, false)!!.length() + } + + @Throws(IOException::class) + override fun createFileExclusively(pathname: String): Boolean { + return false + } + + override fun delete(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val context = contextProvider.getContext() ?: return false + return getDocumentFile(f.path, context, false)!!.delete() + } + + override fun list(f: AmazeFile, contextProvider: ContextProvider): Array? { + val context = contextProvider.getContext() ?: return null + val list = ArrayList() + val rootUri = getDocumentFile(f.path, context, false) ?: return null + for (file in rootUri.listFiles()) { + file.uri.path?.let { list.add(it) } + } + return list.toTypedArray() + } + + override fun getInputStream(f: AmazeFile, contextProvider: ContextProvider): InputStream? { + val context = contextProvider.getContext() + val contentResolver = context!!.contentResolver + val documentSourceFile = getDocumentFile(f.path, context, false) + return try { + contentResolver.openInputStream(documentSourceFile!!.uri) + } catch (e: FileNotFoundException) { + Log.e(TAG, "Error obtaining input stream for OTG", e) + null + } + } + + override fun getOutputStream(f: AmazeFile, contextProvider: ContextProvider): OutputStream? { + val context = contextProvider.getContext() + val contentResolver = context!!.contentResolver + val documentSourceFile = getDocumentFile(f.path, context, true) + return try { + contentResolver.openOutputStream(documentSourceFile!!.uri) + } catch (e: FileNotFoundException) { + Log.e(TAG, "Error output stream for OTG", e) + null + } + } + + override fun createDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean { + if (f.exists(contextProvider)) { + return true + } + val context = contextProvider.getContext() ?: return false + val parent = f.parent ?: return false + val parentDirectory = getDocumentFile(parent, context, true) ?: return false + if (parentDirectory.isDirectory) { + parentDirectory.createDirectory(f.name) + return true + } + return false + } + + override fun rename(file1: AmazeFile, file2: AmazeFile, contextProvider: ContextProvider): Boolean { + val context = contextProvider.getContext() ?: return false + val documentSourceFile = getDocumentFile(file1.path, context, false) ?: return false + return try { + documentSourceFile.renameTo(file2.name) + } catch (e: UnsupportedOperationException) { + Log.e(TAG, "Error renaming OTG file", e) + false + } + } + + override fun setLastModifiedTime(f: AmazeFile, time: Long): Boolean { + throw NotImplementedError() + } + + override fun setReadOnly(f: AmazeFile): Boolean { + return false + } + + override fun getTotalSpace(f: AmazeFile, contextProvider: ContextProvider): Long { + val context = contextProvider.getContext() + // TODO: Find total storage space of OTG when {@link DocumentFile} API adds support + val documentFile = getDocumentFile(f.path, context!!, false) + return documentFile!!.length() + } + + override fun getFreeSpace(f: AmazeFile): Long { + return 0 //TODO + } + + override fun getUsableSpace(f: AmazeFile): Long { + // TODO: Get free space from OTG when {@link DocumentFile} API adds support + return 0 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/smb/CifsContexts.kt b/app/src/main/java/com/amaze/filemanager/filesystem/smb/CifsContexts.kt index 229cdb38b1..7ece3dbdbc 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/smb/CifsContexts.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/smb/CifsContexts.kt @@ -32,6 +32,11 @@ import jcifs.context.SingletonContext import java.util.* import java.util.concurrent.ConcurrentHashMap +@Deprecated( + "" + + "Replaced with " + + "[com.amaze.filemanager.file_operations.filesystem.filetypes.smb.CifsContexts]" +) object CifsContexts { const val SMB_URI_PREFIX = "smb://" diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshAmazeFilesystem.java b/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshAmazeFilesystem.java new file mode 100644 index 0000000000..530241d5b3 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/ssh/SshAmazeFilesystem.java @@ -0,0 +1,540 @@ +package com.amaze.filemanager.filesystem.ssh; + +import static com.amaze.filemanager.ui.activities.MainActivity.TAG_INTENT_FILTER_FAILED_OPS; +import static com.amaze.filemanager.ui.activities.MainActivity.TAG_INTENT_FILTER_GENERAL; +import static net.schmizz.sshj.sftp.FileMode.Type.DIRECTORY; +import static net.schmizz.sshj.sftp.FileMode.Type.REGULAR; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amaze.filemanager.R; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFilesystem; +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider; +import com.amaze.filemanager.filesystem.HybridFile; +import com.amaze.filemanager.filesystem.HybridFileParcelable; + +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.sftp.RemoteFile; +import net.schmizz.sshj.sftp.RemoteResourceInfo; +import net.schmizz.sshj.sftp.SFTPClient; +import net.schmizz.sshj.sftp.SFTPException; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; + +import kotlin.NotImplementedError; + +public class SshAmazeFilesystem extends AmazeFilesystem { + public static final String PREFIX = "ssh:/"; + + public static final SshAmazeFilesystem INSTANCE = new SshAmazeFilesystem(); + + private static final String TAG = SshAmazeFilesystem.class.getSimpleName(); + + static { + AmazeFile.addFilesystem(INSTANCE); + } + + private SshAmazeFilesystem() {} + + @Override + public String getPrefix() { + return PREFIX; + } + + @NonNull + @Override + public String normalize(@NonNull String path) { + return PREFIX + new File(removePrefix(path)).getAbsolutePath(); + } + + @NonNull + @Override + public String getHumanReadablePath(@NonNull AmazeFile f) { + SshConnectionPool.ConnectionInfo connInfo = new SshConnectionPool.ConnectionInfo(f.getPath()); + return connInfo.toString(); + } + + @NonNull + @Override + public String resolve(@NotNull String parent, @NotNull String child) { + return PREFIX + new File(removePrefix(parent), child).getAbsolutePath(); + } + + @NonNull + @Override + public String getDefaultParent() { + return PREFIX + "/"; + } + + @Override + public boolean isAbsolute(@NotNull AmazeFile f) { + return true; // Always absolute + } + + @NonNull + @Override + public String resolve(AmazeFile f) { + return f.getPath(); // No relative paths + } + + @NonNull + @Override + public String canonicalize(String path) throws IOException { + final String returnValue = + SshClientUtils.execute( + new SFtpClientTemplate(path) { + @Override + public String execute(SFTPClient client) throws IOException { + return client.canonicalize(path); + } + }); + + if (returnValue == null) { + throw new IOException("Error canonicalizing SFTP path!"); + } + + return returnValue; + } + + public boolean exists(AmazeFile f, @NonNull ContextProvider contextProvider) { + final SFtpClientTemplate template = new SFtpClientTemplate(f.getPath()) { + @Override + public Boolean execute(@NonNull SFTPClient client) throws IOException { + return client.stat(SshClientUtils.extractRemotePathFrom(f.getPath())) != null; + } + }; + + final Boolean returnValue = SshClientUtils.execute(template); + + if(returnValue == null) { + return false; + } + + return returnValue; + } + public boolean isFile(AmazeFile f, @NonNull ContextProvider contextProvider) { + final SFtpClientTemplate template = new SFtpClientTemplate(f.getPath()) { + @Override + public Boolean execute(@NonNull SFTPClient client) throws IOException { + return client.lstat(f.getPath()).getType() == REGULAR; + } + }; + + final Boolean returnValue = SshClientUtils.execute(template); + + if(returnValue == null) { + return false; + } + + return returnValue; + } + public boolean isDirectory(AmazeFile f, @NonNull ContextProvider contextProvider) { + final SFtpClientTemplate template = new SFtpClientTemplate(f.getPath()) { + @Override + public Boolean execute(@NonNull SFTPClient client) throws IOException { + return client.lstat(SshClientUtils.extractRemotePathFrom(f.getPath())).getType() == DIRECTORY; + } + }; + + final Boolean returnValue = SshClientUtils.execute(template); + + if(returnValue == null) { + return false; + } + + return returnValue; + } + public boolean isHidden(AmazeFile f) { + return false; //Assume its not hidden + } + + public boolean canExecute(AmazeFile f, @NonNull ContextProvider contextProvider) { + throw new NotImplementedError(); + } + public boolean canWrite(AmazeFile f, @NonNull ContextProvider contextProvider) { + throw new NotImplementedError(); + } + public boolean canRead(AmazeFile f, @NonNull ContextProvider contextProvider) { + throw new NotImplementedError(); + } + public boolean canAccess(AmazeFile f, @NonNull ContextProvider contextProvider) { + return exists(f, contextProvider); + } + + public boolean setExecutable(AmazeFile f, boolean enable, boolean owneronly) { + throw new NotImplementedError(); + } + public boolean setWritable(AmazeFile f, boolean enable, boolean owneronly) { + throw new NotImplementedError(); + } + public boolean setReadable(AmazeFile f, boolean enable, boolean owneronly) { + throw new NotImplementedError(); + } + + @Override + public long getLastModifiedTime(AmazeFile f) { + final Long returnValue = + SshClientUtils.execute( + new SFtpClientTemplate(f.getPath()) { + @Override + public Long execute(@NonNull SFTPClient client) throws IOException { + return client.mtime(SshClientUtils.extractRemotePathFrom(f.getPath())); + } + }); + + if (returnValue == null) { + Log.e(TAG, "Error obtaining last modification time over SFTP"); + return 0; + } + + return returnValue; + } + + @NonNull + @Override + public String getHashMD5(@NonNull AmazeFile f, @NonNull ContextProvider contextProvider) { + return SshClientUtils.execute( + new SshClientSessionTemplate(f.getPath()) { + @Override + public String execute(Session session) throws IOException { + Session.Command cmd = + session.exec( + String.format( + "md5sum -b \"%s\" | cut -c -32", + SshClientUtils.extractRemotePathFrom(f.getPath()))); + String result = + new String(IOUtils.readFully(cmd.getInputStream()).toByteArray()); + cmd.close(); + if (cmd.getExitStatus() == 0) return result; + else { + return null; + } + } + }); + } + + @NonNull + @Override + public String getHashSHA256(@NonNull AmazeFile f, @NonNull ContextProvider contextProvider) { + return SshClientUtils.execute( + new SshClientSessionTemplate(f.getPath()) { + @Override + public String execute(Session session) throws IOException { + Session.Command cmd = + session.exec( + String.format( + "sha256sum -b \"%s\" | cut -c -64", + SshClientUtils.extractRemotePathFrom(f.getPath()))); + String result = IOUtils.readFully(cmd.getInputStream()).toString(); + cmd.close(); + if (cmd.getExitStatus() == 0) return result; + else { + return null; + } + } + }); + } + + @Override + public long getLength(AmazeFile f, @NonNull ContextProvider contextProvider) throws IOException { + if(f.isDirectory(contextProvider)) { + final Long returnValue = + SshClientUtils.execute( + new SFtpClientTemplate(f.getPath()) { + @Override + public Long execute(SFTPClient client) throws IOException { + return client.size(SshClientUtils.extractRemotePathFrom(f.getPath())); + } + }); + + if (returnValue == null) { + Log.e(TAG, "Error obtaining size of folder over SFTP"); + return 0; + } + + return returnValue; + } + + Long length = SshClientUtils.execute( + new SFtpClientTemplate(f.getPath()) { + @Override + public Long execute(@NonNull SFTPClient client) throws IOException { + return client.size(SshClientUtils.extractRemotePathFrom(f.getPath())); + } + }); + + if(length == null) { + throw new IOException("Failed getting size for ssh!"); + } + + return length; + } + + @Override + public boolean createFileExclusively(@NotNull String pathname) { + return false; + } + + @Override + public boolean delete(AmazeFile f, @NonNull ContextProvider contextProvider) { + Boolean retval = + SshClientUtils.execute( + new SFtpClientTemplate(f.getPath()) { + @Override + public Boolean execute(@NonNull SFTPClient client) throws IOException { + String _path = SshClientUtils.extractRemotePathFrom(f.getPath()); + if (f.isDirectory(contextProvider)) { + client.rmdir(_path); + } else { + client.rm(_path); + } + return client.statExistence(_path) == null; + } + }); + return retval != null && retval; + } + + @org.jetbrains.annotations.Nullable + @Override + public String[] list(AmazeFile f, @NonNull ContextProvider contextProvider) { + List fileNameList = SshClientUtils.execute( + new SFtpClientTemplate>(f.getPath()) { + @Override + public List execute(@NonNull SFTPClient client) { + List infoList; + try { + infoList = client.ls(SshClientUtils.extractRemotePathFrom(f.getPath())); + } catch (IOException e) { + Log.w(TAG, "Failed listing files for ssh connection!", e); + return Collections.emptyList(); + } + + ArrayList retval = new ArrayList<>(infoList.size()); + + for (RemoteResourceInfo info : infoList) { + retval.add(resolve(f.getPath(), info.getName())); + } + + return retval; + } + }); + + if(fileNameList == null) { + return null; + } + + return fileNameList.toArray(new String[0]); + } + + @Nullable + @Override + public InputStream getInputStream(AmazeFile f, @NonNull ContextProvider contextProvider) { + return SshClientUtils.execute( + new SFtpClientTemplate(f.getPath(), false) { + @Override + public InputStream execute(final SFTPClient client) throws IOException { + final RemoteFile rf = client.open(SshClientUtils.extractRemotePathFrom(f.getPath())); + return rf.new RemoteFileInputStream() { + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + rf.close(); + client.close(); + } + } + }; + } + }); + } + + @Nullable + @Override + public OutputStream getOutputStream(AmazeFile f, @NonNull ContextProvider contextProvider) { + return SshClientUtils.execute( + new SshClientTemplate(f.getPath(), false) { + @Override + public OutputStream execute(final SSHClient ssh) throws IOException { + final SFTPClient client = ssh.newSFTPClient(); + final RemoteFile rf = + client.open( + SshClientUtils.extractRemotePathFrom(f.getPath()), + EnumSet.of( + net.schmizz.sshj.sftp.OpenMode.WRITE, + net.schmizz.sshj.sftp.OpenMode.CREAT)); + return rf.new RemoteFileOutputStream() { + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + try { + rf.close(); + client.close(); + } catch (Exception e) { + Log.w(TAG, "Error closing stream", e); + } + } + } + }; + } + }); + } + + @Override + public boolean createDirectory(AmazeFile f, @NonNull ContextProvider contextProvider) { + SshClientUtils.execute( + new SFtpClientTemplate(f.getPath()) { + @Override + public Void execute(@NonNull SFTPClient client) { + try { + client.mkdir(SshClientUtils.extractRemotePathFrom(f.getPath())); + } catch (IOException e) { + Log.e(TAG, "Error making directory over SFTP", e); + } + // FIXME: anything better than throwing a null to make Rx happy? + return null; + } + }); + return true; + } + + @Override + public boolean rename(AmazeFile file1, AmazeFile file2, @NonNull ContextProvider contextProvider) { + @Nullable final Context context = contextProvider.getContext(); + + if(context == null) { + Log.e(TAG, "Error getting context for renaming ssh file"); + return false; + } + + Boolean retval = + SshClientUtils.execute( + new SFtpClientTemplate(file1.getPath()) { + @Override + public Boolean execute(@NonNull SFTPClient client) { + try { + client.rename( + SshClientUtils.extractRemotePathFrom(file1.getPath()), + SshClientUtils.extractRemotePathFrom(file2.getPath()) + ); + return true; + } catch (IOException e) { + Log.e(TAG, "Error renaming ssh file", e); + return false; + } + } + }); + + if (retval == null) { + Log.e(TAG, "Error renaming over SFTP"); + + return false; + } + + return retval; + } + + @Override + public boolean setLastModifiedTime(AmazeFile f, long time) { + throw new NotImplementedError(); + } + + @Override + public boolean setReadOnly(AmazeFile f) { + return false; + } + + public long getTotalSpace(AmazeFile f, @NonNull ContextProvider contextProvider) { + final Long returnValue = + SshClientUtils.execute( + new SFtpClientTemplate(f.getPath()) { + @Override + public Long execute(@NonNull SFTPClient client) throws IOException { + try { + Statvfs.Response response = + new Statvfs.Response( + f.getPath(), + client + .getSFTPEngine() + .request( + Statvfs.request( + client, SshClientUtils.extractRemotePathFrom(f.getPath()))) + .retrieve()); + return response.diskSize(); + } catch (SFTPException e) { + Log.e(TAG, "Error querying SFTP server", e); + return 0L; + } catch (Buffer.BufferException e) { + Log.e(TAG, "Error parsing SFTP reply", e); + return 0L; + } + } + }); + + if (returnValue == null) { + Log.e(TAG, "Error obtaining total space over SFTP"); + + return 0; + } + + return returnValue; + } + public long getFreeSpace(AmazeFile f) { + final Long returnValue = + SshClientUtils.execute( + new SFtpClientTemplate(f.getPath()) { + @Override + public Long execute(@NonNull SFTPClient client) throws IOException { + try { + Statvfs.Response response = + new Statvfs.Response( + f.getPath(), + client + .getSFTPEngine() + .request( + Statvfs.request( + client, SshClientUtils.extractRemotePathFrom(f.getPath()))) + .retrieve()); + return response.diskFreeSpace(); + } catch (SFTPException e) { + Log.e(TAG, "Error querying server", e); + return 0L; + } catch (Buffer.BufferException e) { + Log.e(TAG, "Error parsing reply", e); + return 0L; + } + } + }); + + if (returnValue == null) { + Log.e(TAG, "Error obtaining usable space over SFTP"); + return 0; + } + + return returnValue; + } + public long getUsableSpace(AmazeFile f) { + // TODO: Find total storage space of SFTP support is added + throw new NotImplementedError(); + } +} diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java index 8e7cc02e7c..bb4988f7c5 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java @@ -125,6 +125,7 @@ import com.amaze.filemanager.file_operations.exceptions.CloudPluginException; import com.amaze.filemanager.file_operations.filesystem.OpenMode; import com.amaze.filemanager.file_operations.filesystem.StorageNaming; +import com.amaze.filemanager.file_operations.filesystem.filetypes.file.UriForSafPersistance; import com.amaze.filemanager.file_operations.filesystem.usb.SingletonUsbOtg; import com.amaze.filemanager.file_operations.filesystem.usb.UsbOtgRepresentation; import com.amaze.filemanager.filesystem.ExternalSdCardOperation; @@ -1486,11 +1487,9 @@ protected void onActivityResult(int requestCode, int responseCode, Intent intent // Get Uri from Storage Access Framework. treeUri = intent.getData(); // Persist URI - this is required for verification of writability. - if (treeUri != null) - getPrefs() - .edit() - .putString(PreferencesConstants.PREFERENCE_URI, treeUri.toString()) - .apply(); + if (treeUri != null) { + UriForSafPersistance.INSTANCE.persist(getApplicationContext(), treeUri); + } } else { // If not confirmed SAF, or if still not writable, then revert settings. /* DialogUtil.displayError(getActivity(), R.string.message_dialog_cannot_write_to_folder_saf, false, currentFolder); @@ -2106,7 +2105,7 @@ this, getResources().getString(R.string.connection_exists), Toast.LENGTH_LONG) @Override public void deleteConnection(OpenMode service) { cloudHandler.clear(service); - dataUtils.removeAccount(service); + dataUtils.getAccount(service).removeAccount(); runOnUiThread(drawer::refreshDrawer); } diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java index 74be06edd4..0dd2900ddd 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java @@ -74,6 +74,7 @@ import com.amaze.filemanager.databinding.DialogSigninWithGoogleBinding; import com.amaze.filemanager.file_operations.exceptions.ShellNotRunningException; import com.amaze.filemanager.file_operations.filesystem.OpenMode; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; import com.amaze.filemanager.filesystem.FileProperties; import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.HybridFileParcelable; @@ -533,7 +534,7 @@ private static void showPropertiesDialog( new CountItemsOrAndSizeTask(c, itemsText, baseFile, forStorage); countItemsOrAndSizeTask.executeOnExecutor(executor); - TaskKt.fromTask(new CalculateHashTask(baseFile, c, v)); + TaskKt.fromTask(new CalculateHashTask(new AmazeFile(baseFile.getPath()), c, v)); /*Chart creation and data loading*/ { diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java index 885a0cd138..7b35e2b973 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java @@ -31,6 +31,7 @@ import static com.amaze.filemanager.ui.fragments.preference_fragments.PreferencesConstants.PREFERENCE_SHOW_THUMB; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -51,6 +52,8 @@ import com.amaze.filemanager.database.SortHandler; import com.amaze.filemanager.database.models.explorer.Tab; import com.amaze.filemanager.file_operations.filesystem.OpenMode; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider; import com.amaze.filemanager.file_operations.filesystem.smbstreamer.Streamer; import com.amaze.filemanager.filesystem.CustomFileObserver; import com.amaze.filemanager.filesystem.FileProperties; @@ -133,9 +136,6 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import jcifs.smb.SmbException; -import jcifs.smb.SmbFile; - public class MainFragment extends Fragment implements BottomBarButtonPath, ViewTreeObserver.OnGlobalLayoutListener, @@ -1028,7 +1028,7 @@ public void goBack() { && !mainFragmentViewModel .getSmbPath() .equals(mainFragmentViewModel.getCurrentPath())) { - StringBuilder path = new StringBuilder(currentFile.getSmbFile().getParent()); + StringBuilder path = new StringBuilder(currentFile.getParent(requireContext())); if (mainFragmentViewModel.getCurrentPath().indexOf('?') > 0) path.append( mainFragmentViewModel @@ -1176,7 +1176,7 @@ public void goBackItemClick() { && !mainFragmentViewModel .getCurrentPath() .equals(mainFragmentViewModel.getSmbPath())) { - StringBuilder path = new StringBuilder(currentFile.getSmbFile().getParent()); + StringBuilder path = new StringBuilder(currentFile.getParent(requireContext())); if (mainFragmentViewModel.getCurrentPath().indexOf('?') > 0) path.append( mainFragmentViewModel @@ -1225,27 +1225,31 @@ public void onPause() { } public ArrayList addToSmb( - @NonNull SmbFile[] mFile, @NonNull String path, boolean showHiddenFiles) throws SmbException { + @NonNull AmazeFile[] mFile, @NonNull String path, boolean showHiddenFiles) + throws IOException { ArrayList smbFileList = new ArrayList<>(); String extraParams = Uri.parse(path).getQuery(); if (mainFragmentViewModel.getSearchHelper().size() > 500) { mainFragmentViewModel.getSearchHelper().clear(); } - for (SmbFile aMFile : mFile) { - if ((DataUtils.getInstance().isFileHidden(aMFile.getPath()) || aMFile.isHidden()) + for (AmazeFile aMFile : mFile) { + if (!aMFile.isDirectory(this::requireContext) && !aMFile.isFile(this::requireContext)) { + continue; + } + if ((DataUtils.getInstance().isFileHidden(aMFile.getPath()) || aMFile.isHidden(this::requireContext)) && !showHiddenFiles) { continue; } String name = aMFile.getName(); name = - (aMFile.isDirectory() && name.endsWith("/")) + (aMFile.isDirectory(this::requireContext) && name.endsWith("/")) ? name.substring(0, name.length() - 1) : name; if (path.equals(mainFragmentViewModel.getSmbPath())) { if (name.endsWith("$")) continue; } - if (aMFile.isDirectory()) { + if (aMFile.isDirectory(this::requireContext)) { mainFragmentViewModel.setFolderCount(mainFragmentViewModel.getFolderCount() + 1); Uri.Builder aMFilePathBuilder = Uri.parse(aMFile.getPath()).buildUpon(); @@ -1278,8 +1282,18 @@ public ArrayList addToSmb( aMFile.getPath(), "", "", - Formatter.formatFileSize(getContext(), aMFile.length()), - aMFile.length(), + Formatter.formatFileSize(getContext(), aMFile.length(new ContextProvider() { + @Override + public Context getContext() { + return requireContext();// TODO fix retaining instance of MainFragment + } + })), + aMFile.length(new ContextProvider() { + @Override + public Context getContext() { + return requireContext();// TODO fix retaining instance of MainFragment + } + }), false, aMFile.lastModified() + "", false, @@ -1492,7 +1506,7 @@ public void run() { } */ - s.setStreamSrc(baseFile.getSmbFile(), baseFile.getSize()); + s.setStreamSrc(new AmazeFile(baseFile.getPath()), baseFile.getSize()); activity.runOnUiThread( () -> { try { diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preference_fragments/PreferencesConstants.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preference_fragments/PreferencesConstants.kt index 28c88a6e64..0bc0223dcd 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preference_fragments/PreferencesConstants.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preference_fragments/PreferencesConstants.kt @@ -20,6 +20,8 @@ package com.amaze.filemanager.ui.fragments.preference_fragments +import com.amaze.filemanager.filesystem.files.FileAmazeFilesystem + object PreferencesConstants { // appearance_prefs.xml const val FRAGMENT_THEME = "theme" @@ -68,7 +70,7 @@ object PreferencesConstants { // behavior_prefs.xml const val PREFERENCE_ROOT_LEGACY_LISTING = "legacyListing" - const val PREFERENCE_ROOTMODE = "rootmode" + const val PREFERENCE_ROOTMODE = FileAmazeFilesystem.PREFERENCE_ROOTMODE const val PREFERENCE_CHANGEPATHS = "typeablepaths" const val PREFERENCE_SAVED_PATHS = "savepaths" const val PREFERENCE_ZIP_EXTRACT_PATH = "extractpath" diff --git a/app/src/main/java/com/amaze/filemanager/ui/theme/AppThemeManager.java b/app/src/main/java/com/amaze/filemanager/ui/theme/AppThemeManager.java index 7db0b7b039..790eaacb04 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/theme/AppThemeManager.java +++ b/app/src/main/java/com/amaze/filemanager/ui/theme/AppThemeManager.java @@ -20,10 +20,10 @@ package com.amaze.filemanager.ui.theme; -import com.amaze.filemanager.ui.fragments.preference_fragments.PreferencesConstants; - import android.content.SharedPreferences; +import com.amaze.filemanager.ui.fragments.preference_fragments.PreferencesConstants; + /** Saves and restores the AppTheme */ public class AppThemeManager { private SharedPreferences preferences; diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java index ec956f0ab5..abc57e1724 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java @@ -36,6 +36,7 @@ import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.database.CloudHandler; import com.amaze.filemanager.file_operations.filesystem.OpenMode; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.Account; import com.amaze.filemanager.file_operations.filesystem.usb.SingletonUsbOtg; import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.RootHelper; @@ -334,7 +335,8 @@ public void refreshDrawer() { ArrayList accountAuthenticationList = new ArrayList<>(); if (CloudSheetFragment.isCloudProviderAvailable(mainActivity)) { - for (CloudStorage cloudStorage : dataUtils.getAccounts()) { + for (Account account : Account.Companion.getAccounts()) { + CloudStorage cloudStorage = account.getAccount(); @DrawableRes int deleteIcon = R.drawable.ic_delete_grey_24dp; if (cloudStorage instanceof Dropbox) { @@ -727,7 +729,7 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) { FileUtils.checkForPath(mainActivity, meta.path, mainActivity.isRootExplorer()); } - if (dataUtils.getAccounts().size() > 0 + if (Account.Companion.getAccounts().size() > 0 && (meta.path.startsWith(CloudHandler.CLOUD_PREFIX_BOX) || meta.path.startsWith(CloudHandler.CLOUD_PREFIX_DROPBOX) || meta.path.startsWith(CloudHandler.CLOUD_PREFIX_ONE_DRIVE) diff --git a/app/src/main/java/com/amaze/filemanager/utils/DataUtils.java b/app/src/main/java/com/amaze/filemanager/utils/DataUtils.java index 42a0149b1d..bc844a5f58 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/DataUtils.java +++ b/app/src/main/java/com/amaze/filemanager/utils/DataUtils.java @@ -28,11 +28,11 @@ import com.amaze.filemanager.adapters.data.LayoutElementParcelable; import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.file_operations.filesystem.OpenMode; -import com.cloudrail.si.interfaces.CloudStorage; -import com.cloudrail.si.services.Box; -import com.cloudrail.si.services.Dropbox; -import com.cloudrail.si.services.GoogleDrive; -import com.cloudrail.si.services.OneDrive; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.Account; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.box.BoxAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.dropbox.DropboxAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.gdrive.GoogledriveAccount; +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.onedrive.OnedriveAccount; import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree; import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharArrayNodeFactory; import com.googlecode.concurrenttrees.radix.node.concrete.voidvalue.VoidValue; @@ -69,8 +69,6 @@ public class DataUtils { private ArrayList servers = new ArrayList<>(); private ArrayList books = new ArrayList<>(); - private ArrayList accounts = new ArrayList<>(4); - /** List of checked items to persist when drag and drop from one tab to another */ private ArrayList checkedItemsList; @@ -107,41 +105,6 @@ public int containsBooks(String[] a) { return contains(a, books); } - /*public int containsAccounts(CloudEntry cloudEntry) { - return contains(a, accounts); - }*/ - - /** - * Checks whether cloud account of certain type is present or not - * - * @param serviceType the {@link OpenMode} of account to check - * @return the index of account, -1 if not found - */ - public synchronized int containsAccounts(OpenMode serviceType) { - int i = 0; - for (CloudStorage storage : accounts) { - - switch (serviceType) { - case BOX: - if (storage instanceof Box) return i; - break; - case DROPBOX: - if (storage instanceof Dropbox) return i; - break; - case GDRIVE: - if (storage instanceof GoogleDrive) return i; - break; - case ONEDRIVE: - if (storage instanceof OneDrive) return i; - break; - default: - return -1; - } - i++; - } - return -1; - } - public void clear() { hiddenfiles = new ConcurrentRadixTree<>(new DefaultCharArrayNodeFactory()); filesGridOrList = new ConcurrentInvertedRadixTree<>(new DefaultCharArrayNodeFactory()); @@ -150,7 +113,6 @@ public void clear() { tree = new ConcurrentInvertedRadixTree<>(new DefaultCharArrayNodeFactory()); servers = new ArrayList<>(); books = new ArrayList<>(); - accounts = new ArrayList<>(); } public void registerOnDataChangedListener(DataChangeListener l) { @@ -184,39 +146,6 @@ public void removeBook(int i) { } } - public synchronized void removeAccount(OpenMode serviceType) { - for (CloudStorage storage : accounts) { - switch (serviceType) { - case BOX: - if (storage instanceof Box) { - accounts.remove(storage); - return; - } - break; - case DROPBOX: - if (storage instanceof Dropbox) { - accounts.remove(storage); - return; - } - break; - case GDRIVE: - if (storage instanceof GoogleDrive) { - accounts.remove(storage); - return; - } - break; - case ONEDRIVE: - if (storage instanceof OneDrive) { - accounts.remove(storage); - return; - } - break; - default: - return; - } - } - } - public void removeServer(int i) { synchronized (servers) { if (servers.size() > i) servers.remove(i); @@ -254,10 +183,6 @@ public boolean addBook(final String[] i, boolean refreshdrawer) { } } - public void addAccount(CloudStorage storage) { - accounts.add(storage); - } - public void addServer(String[] i) { servers.add(i); } @@ -310,10 +235,6 @@ public synchronized void setBooks(ArrayList books) { if (books != null) this.books = books; } - public synchronized void setAccounts(ArrayList accounts) { - if (accounts != null) this.accounts = accounts; - } - public synchronized ArrayList getServers() { return servers; } @@ -322,30 +243,19 @@ public synchronized ArrayList getBooks() { return books; } - public synchronized ArrayList getAccounts() { - return accounts; - } - - public synchronized CloudStorage getAccount(OpenMode serviceType) { - for (CloudStorage storage : accounts) { - switch (serviceType) { - case BOX: - if (storage instanceof Box) return storage; - break; - case DROPBOX: - if (storage instanceof Dropbox) return storage; - break; - case GDRIVE: - if (storage instanceof GoogleDrive) return storage; - break; - case ONEDRIVE: - if (storage instanceof OneDrive) return storage; - break; - default: - return null; - } + public synchronized Account getAccount(OpenMode serviceType) { + switch (serviceType) { + case BOX: + return BoxAccount.INSTANCE; + case DROPBOX: + return DropboxAccount.INSTANCE; + case GDRIVE: + return GoogledriveAccount.INSTANCE; + case ONEDRIVE: + return OnedriveAccount.INSTANCE; + default: + return null; } - return null; } public boolean isFileHidden(String path) { diff --git a/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt b/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt index 5cab338b6a..9a882d1fb8 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt @@ -33,6 +33,7 @@ import androidx.annotation.RequiresApi import androidx.documentfile.provider.DocumentFile import com.amaze.filemanager.exceptions.DocumentFileNotFoundException import com.amaze.filemanager.file_operations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.otg.OtgAmazeFilesystem import com.amaze.filemanager.file_operations.filesystem.usb.SingletonUsbOtg import com.amaze.filemanager.file_operations.filesystem.usb.UsbOtgRepresentation import com.amaze.filemanager.filesystem.HybridFileParcelable @@ -42,7 +43,7 @@ import kotlin.collections.ArrayList /** Created by Vishal on 27-04-2017. */ object OTGUtil { - const val PREFIX_OTG = "otg:/" + const val PREFIX_OTG = OtgAmazeFilesystem.PREFIX private const val PREFIX_DOCUMENT_FILE = "content:/" const val PREFIX_MEDIA_REMOVABLE = "/mnt/media_rw" diff --git a/app/src/main/java/com/amaze/filemanager/utils/OnFileFound.kt b/app/src/main/java/com/amaze/filemanager/utils/OnFileFound.kt index f593791338..181e3ca33a 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/OnFileFound.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/OnFileFound.kt @@ -24,9 +24,8 @@ import com.amaze.filemanager.filesystem.HybridFileParcelable /** * This allows the caller of a function to know when a file has ben found and deal with it ASAP - * - * @author Emmanuel on 21/9/2017, at 15:23. */ +@Deprecated(message = "Replaced with OnAmazeFileFound") interface OnFileFound { @Suppress("UndocumentedPublicFunction") fun onFileFound(file: HybridFileParcelable) diff --git a/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.java b/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.java index b50d0e0806..389f46594b 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.java +++ b/app/src/main/java/com/amaze/filemanager/utils/SmbUtil.java @@ -102,6 +102,8 @@ public static String getSmbEncryptedPath(Context context, String path) return buffer.toString(); } + /** Use AmazeFile(path) instead */ + @Deprecated public static SmbFile create(String path) throws MalformedURLException { Uri uri = Uri.parse(path); boolean disableIpcSigningCheck = @@ -122,7 +124,9 @@ public static SmbFile create(String path) throws MalformedURLException { * @param userInfo authentication string, must be already URL decoded. {@link Uri} shall do this * for you already * @return {@link NtlmPasswordAuthenticator} instance + *

Use AmazeFile(path) instead */ + @Deprecated protected static @NonNull NtlmPasswordAuthenticator createFrom(@Nullable String userInfo) { if (!TextUtils.isEmpty(userInfo)) { String dom = null; diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 685bd8845e..6969663f43 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -664,7 +664,6 @@ Ų§Ł„ŲŗŲ§Ų” Ų§ŲŖŲ§Ų­Ų© Ų§Ł„ŲŖŲ­Ł‚Ł‚ Ł…Ł† ŲŖŁˆŁ‚ŁŠŲ¹ IPC (ŲØŲ§Ł„Ł†Ų³ŲØŲ© Ł„ŁˆŲ­ŲÆŲ§ŲŖ Ų®ŲÆŁ…Ų© SMBv1 Ų§Ł„Ł‚ŲÆŁŠŁ…Ų©-UNSAFE !) Ł„Ų§ ŁŠŁ…ŁƒŁ† Ų­Ų°Ł Ų§Ł„Ł…Ł„Ł Ł„Ų§ ŁŠŁ…ŁƒŁ† Ų­Ų°Ł Ų§Ł„Ł…Ł„Ł-%s - Ł„Ų§ ŁŠŁ…ŁƒŁ† Ų§Ų¹Ų§ŲÆŲ© ŲŖŲ³Ł…ŁŠŲ© Ų§Ł„Ł…Ł„Ł %s-%s Ł„Ų§ ŁŠŁ…ŁƒŁ† Ł‚Ų±Ų§Ų”Ų© Ų§Ł„ŲÆŁ„ŁŠŁ„ %s-%s Ł…Ų±Ų© ŁˆŲ§Ų­ŲÆŲ© ŁŁ‚Ų· ŲÆŲ§Ų¦Ł…Ų§ diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8a7aff0cb6..c0827c321b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -629,7 +629,6 @@ andernfalls wird nach Treffern gesucht. IPC SignaturprĆ¼fung deaktivieren (FĆ¼r veraltete SMBv1 Server - UNSICHER!) Datei kann nicht gelƶscht werden Datei %s kann nicht gelƶscht werden - Datei %s - %s kann nicht umbenannt werden Verzeichnis %s - %s kann nicht gelesen werden Nur einmal Immer diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 3b718801d9..ff398532e7 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -669,7 +669,6 @@ Desactive la verificaciĆ³n de firma de IPC (para servidores SMBv1 heredados - Ā”INSEGURO!) No se puede eliminar el archivo No se puede eliminar el archivo - %s - No se puede renombrar el archivo %s - %s No se puede leer el directorio %s - %s SĆ³lo una vez Siempre diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 23bf26d192..317590247b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -645,7 +645,6 @@ DĆ©sactiver la vĆ©rification de la signature IPC (Pour les anciens serveurs SMBv1 - RISQUƉ!) Impossible de supprimer le fichier Impossible de supprimer le fichier - %s - Impossible de renommer le fichier %s - %s Impossible de lire le rĆ©pertoire %s - %s Juste une fois Toujours diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index d1d59d342b..9cfa2ae8e4 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -678,7 +678,6 @@ השב×Ŗ×Ŗ בדיק×Ŗ ח×Ŗימ×Ŗ IPC (לש×Ø×Ŗי SMBv1 מיושנים - מהוכן!) לא ני×Ŗן למחוק קובׄ לא ני×Ŗן למחוק קובׄ - %s - לא ני×Ŗן לשנו×Ŗ א×Ŗ שם הקובׄ %s - %s לא ני×Ŗן לק×Øוא א×Ŗ ה×Ŗיקייה %s - %s ×Øק פעם אח×Ŗ ×Ŗמיד diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 5b644eab52..1902e40e08 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -682,7 +682,6 @@ IPC-alƔƭrĆ”s ellenőrzĆ©s letiltĆ”sa (Ɩrƶkƶlt SMBv1-kiszolgĆ”lĆ³khoz - UNSAFE!) Nem lehet tƶrƶlni a fĆ”jlt Nem lehet tƶrƶlni a fĆ”jlt - %s - Nem lehet Ć”tnevezni a fĆ”jlt %s - %s Nem lehet olvasni a kƶnyvtĆ”rat %s - %s Csak egyszer Mindig diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 7b1718568b..ff75d157ee 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -677,7 +677,6 @@ Nonaktifkan pemeriksaaan tanda tangan IPC (Untuk server SMBv1 lama - TIDAK AMAN!) Tidak bisa menghapus berkas Tidak bisa menghapus berkas - %s - Tidak bisa mengubah nama berkas %s - %s Tidak bisa membaca direktori %s - %s Hanya Sekali Selalu diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 6332ba9a3d..ba331b0327 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -659,7 +659,6 @@ Wi-Fi恫ꎄē¶šć™ć‚‹åæ…č¦ćŒć‚ć‚Šć¾ć™ć€‚\n\n ć‚Æćƒ©ćƒƒć‚·ćƒ„å ±å‘ŠćŒć‚ÆćƒŖćƒƒćƒ—ćƒœćƒ¼ćƒ‰ć«ć‚³ćƒ”ćƒ¼ć—ć¾ć—ćŸ ćƒ•ć‚”ć‚¤ćƒ«ć‚’å‰Šé™¤ć§ćć¾ć›ć‚“ ćƒ•ć‚”ć‚¤ćƒ«ć‚’å‰Šé™¤ć§ćć¾ć›ć‚“ - %s - ćƒ•ć‚”ć‚¤ćƒ« %s åå‰ć‚’å¤‰ę›“ć§ćć¾ć›ć‚“ - %s äø€å›žć ć‘ åøø恫 åˆ„ć«App悒éø恶 diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 36944a15d8..da95c842f9 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -656,7 +656,6 @@ Desative a verificaĆ§Ć£o de assinatura IPC (Para servidores SMBv1 legados - INSEGURO!) NĆ£o foi possĆ­vel deletar o arquivo NĆ£o foi possĆ­vel deletar o arquivo%s - NĆ£o foi possĆ­vel renomear o arquivo%s%s NĆ£o foi possĆ­vel acessar o diretĆ³rio%s%s Apenas uma vez Sempre diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8021ad02d1..616dd730dd 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -644,7 +644,6 @@ Š’ŠøŠ¼ŠŗŠ½ŃƒŃ‚Šø ŠæŠµŃ€ŠµŠ²Ń–Ń€Šŗу ŠæіŠ“ŠæŠøсŠ°Š½Š½Ń IPC (Š“Š»Ń Š·Š°ŃŃ‚Š°Ń€Ń–Š»Šøх сŠµŃ€Š²ŠµŃ€Ń–Š² SMBv1 - ŠŠ•Š‘Š•Š—ŠŸŠ•Š§ŠŠž!) ŠŠµ Š²Š“Š°Ń”Ń‚ŃŒŃŃ Š²ŠøŠ“Š°Š»ŠøтŠø фŠ°Š¹Š» ŠŠµ Š²Š“Š°Ń”Ń‚ŃŒŃŃ Š²ŠøŠ“Š°Š»ŠøтŠø фŠ°Š¹Š» - %s - ŠŠµ Š²Š“Š°Ń”Ń‚ŃŒŃŃ ŠæŠµŃ€ŠµŠ¹Š¼ŠµŠ½ŃƒŠ²Š°Ń‚Šø фŠ°Š¹Š» %s - %s ŠŠµ Š²Š¶Š°Ń”Ń‚ŃŒŃŃ ŠæрŠ¾Ń‡ŠøтŠ°Ń‚Šø ŠŗŠ°Ń‚Š°Š»Š¾Š³ %s - %s Š›ŠøшŠµ цьŠ¾Š³Š¾ рŠ°Š·Ńƒ Š—Š°Š²Š¶Š“Šø diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 91b96a2967..edd36367a0 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -678,7 +678,6 @@ TįŗÆt kiį»ƒm tra kĆ½ IPC (Đį»‘i vį»›i mĆ”y chį»§ SMBv1 cÅ© - KHƔNG AN TOƀN!) KhĆ“ng thį»ƒ xĆ³a tįŗ­p tin KhĆ“ng thį»ƒ xĆ³a tįŗ­p tin - %s - KhĆ“ng thį»ƒ đį»•i tĆŖn tįŗ­p tin %s - %s KhĆ“ng thį»ƒ đį»c thĘ° mį»„c %s - %s Chį»‰ mį»™t lįŗ§n LuĆ“n luĆ“n diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9a0f93abfc..4e1b3eaa7b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -651,7 +651,6 @@ čÆ¦ęƒ…ļ¼š ę— ę³•åˆ é™¤ę–‡ä»¶ ę— ę³•åˆ é™¤ę–‡ä»¶ - %s - ę— ę³•é‡å‘½åę–‡ä»¶%s-%s ę— ę³•čÆ»å–ę–‡ä»¶å¤¹%s-%s ä»…ęœ¬ę¬” ꀻę˜Æ diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 0a019feca0..c4cef8fc6f 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -629,7 +629,6 @@ 關閉 IPCē°½åęŖ¢ęŸ„ļ¼ˆå°ę‡‰čˆŠå¼å˜… SMBv1ä¼ŗ꜍å™Ø - 唔安å…Ø旎ļ¼ļ¼‰ 個ęŖ”ę”ˆåˆŖ除唔到 個ęŖ”ę”ˆåˆŖ除唔到 - %s - ęŖ”ę”ˆ %s ę”¹å””åˆ°å - %s č®€å””åˆ°ęŖ”ę”ˆå¤¾ %s - %s åŖé™å‘¢ę¬” 仄後都äæ‚ diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8ed701dbbc..925b17e510 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -673,7 +673,6 @@ 關閉 IPCę•ø位ē°½åęŖ¢ęŸ„ļ¼ˆé©ē”Øę–¼čˆŠå¼ SMBv1ä¼ŗ꜍å™Øā€”äøå®‰å…Øļ¼ļ¼‰ ē„”ę³•åˆŖ除ęŖ”ę”ˆ ē„”ę³•åˆŖ除ęŖ”ę”ˆ - %s - ē„”ę³•é‡ę–°å‘½åęŖ”ę”ˆ %s - %s ē„”ę³•č®€å–ęŖ”ę”ˆå¤¾ %s - %s åŖę­¤äø€ę¬” ēø½ę˜Æ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1b06cb4311..761474e539 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -699,7 +699,6 @@ Disable IPC signing check (For legacy SMBv1 servers - UNSAFE!) Cannot delete file Cannot delete file - %s - Cannot rename file %s - %s Cannot read directory %s - %s Just Once Always diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt index 54132f5aa4..f948fdf5cc 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt @@ -34,6 +34,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.R import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.shadows.ShadowSmbAmazeFilesystem import com.amaze.filemanager.shadows.ShadowSmbUtil import com.amaze.filemanager.test.ShadowPasswordUtil import com.amaze.filemanager.test.ShadowTabHandler @@ -65,7 +66,7 @@ import org.robolectric.shadows.ShadowToast ) abstract class AbstractDeleteTaskTestBase { - private var ctx: Context? = null + private lateinit var ctx: Context /** * Test case setup. @@ -90,15 +91,15 @@ abstract class AbstractDeleteTaskTestBase { } protected fun doTestDeleteFileOk(file: HybridFileParcelable) { - val task = DeleteTask(ctx!!) - val result = task.doInBackground(ArrayList(listOf(file))) + val task = DeleteTask(ctx) + val result = task.doInBackground(arrayListOf(file)) assertTrue(result.result) assertNull(result.exception) task.onPostExecute(result) shadowOf(Looper.getMainLooper()).idle() assertNotNull(ShadowToast.getLatestToast()) - assertEquals(ctx?.getString(R.string.done), ShadowToast.getTextOfLatestToast()) + assertEquals(ctx.getString(R.string.done), ShadowToast.getTextOfLatestToast()) } protected fun doTestDeleteFileAccessDenied(file: HybridFileParcelable) { @@ -109,7 +110,7 @@ abstract class AbstractDeleteTaskTestBase { shadowOf(Looper.getMainLooper()).idle() }.moveToState(Lifecycle.State.STARTED).onActivity { activity -> - val task = DeleteTask(ctx!!) + val task = DeleteTask(ctx) val result = task.doInBackground(ArrayList(listOf(file))) if (result.result != null) { assertFalse(result.result) @@ -136,7 +137,7 @@ abstract class AbstractDeleteTaskTestBase { } }.moveToState(Lifecycle.State.DESTROYED).close().run { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - shadowOf(ctx?.getSystemService(StorageManager::class.java)) + shadowOf(ctx.getSystemService(StorageManager::class.java)) .resetStorageVolumeList() } } diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt b/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt index 23239b9acd..3bdd58f773 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt @@ -33,6 +33,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.file_operations.filesystem.OpenMode import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.shadows.ShadowSmbAmazeFilesystem import com.amaze.filemanager.shadows.ShadowSmbUtil import com.amaze.filemanager.test.ShadowPasswordUtil import com.amaze.filemanager.test.ShadowTabHandler @@ -65,7 +66,7 @@ import org.robolectric.shadows.ShadowSQLiteConnection ) abstract class AbstractOperationsTestBase { - protected var ctx: Context? = null + protected lateinit var ctx: Context protected val blankCallback = object : Operations.ErrorCallBack { override fun exists(file: HybridFile?) = Unit @@ -134,7 +135,7 @@ abstract class AbstractOperationsTestBase { } }.moveToState(Lifecycle.State.DESTROYED).close().run { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - Shadows.shadowOf(ctx?.getSystemService(StorageManager::class.java)) + Shadows.shadowOf(ctx.getSystemService(StorageManager::class.java)) .resetStorageVolumeList() } } diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt index 16b24fd42a..39cf7c8a10 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/smb/SmbHybridFileTest.kt @@ -29,6 +29,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.file_operations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFile import com.amaze.filemanager.shadows.ShadowMultiDex +import com.amaze.filemanager.shadows.ShadowSmbAmazeFilesystem import com.amaze.filemanager.shadows.ShadowSmbUtil import com.amaze.filemanager.shadows.ShadowSmbUtil.Companion.PATH_CANNOT_DELETE_FILE import jcifs.smb.SmbException @@ -44,13 +45,13 @@ import org.robolectric.shadows.ShadowSQLiteConnection @RunWith(AndroidJUnit4::class) @Config( - shadows = [ShadowSmbUtil::class, ShadowMultiDex::class], + shadows = [ShadowSmbUtil::class, ShadowMultiDex::class, ShadowSmbAmazeFilesystem::class], sdk = [JELLY_BEAN, KITKAT, P] ) @LooperMode(LooperMode.Mode.PAUSED) class SmbHybridFileTest { - private var ctx: Context? = null + private lateinit var ctx: Context /** * Test case setup. @@ -87,7 +88,6 @@ class SmbHybridFileTest { * * @see HybridFile.delete */ - @Test(expected = SmbException::class) fun testDeleteAccessDenied() { val file = HybridFile(OpenMode.SMB, PATH_CANNOT_DELETE_FILE) file.delete(ctx, false) diff --git a/app/src/test/java/com/amaze/filemanager/shadows/ShadowSmbAmazeFilesystem.kt b/app/src/test/java/com/amaze/filemanager/shadows/ShadowSmbAmazeFilesystem.kt new file mode 100644 index 0000000000..6ee4d6b522 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/shadows/ShadowSmbAmazeFilesystem.kt @@ -0,0 +1,50 @@ +package com.amaze.filemanager.shadows + +import org.robolectric.annotation.Implements +import com.amaze.filemanager.file_operations.filesystem.filetypes.smb.SmbAmazeFilesystem +import jcifs.context.SingletonContext +import jcifs.smb.SmbFile +import org.mockito.Mockito.* +import org.robolectric.annotation.Implementation + +@Implements(SmbAmazeFilesystem::class) +@Suppress +class ShadowSmbAmazeFilesystem { + + companion object { + /** + * Shadows SmbAmazeFileSystem.create() + * + * @see SmbAmazeFilesystem.create + */ + @JvmStatic + @Implementation + fun create(path: String): SmbFile { + return when (path) { + ShadowSmbUtil.PATH_CANNOT_DELETE_FILE -> ShadowSmbUtil.mockDeleteAccessDenied + ShadowSmbUtil.PATH_CANNOT_MOVE_FILE -> ShadowSmbUtil.mockDeleteDifferentNetwork + ShadowSmbUtil.PATH_CANNOT_RENAME_OLDFILE -> ShadowSmbUtil.mockCannotRenameOld + ShadowSmbUtil.PATH_CAN_RENAME_OLDFILE -> ShadowSmbUtil.mockCanRename + ShadowSmbUtil.PATH_NOT_A_FOLDER -> ShadowSmbUtil.mockPathNotAFolder + ShadowSmbUtil.PATH_DOESNT_EXIST -> ShadowSmbUtil.mockPathDoesNotExist + ShadowSmbUtil.PATH_EXIST -> ShadowSmbUtil.mockPathExist + ShadowSmbUtil.PATH_INVOKE_SMBEXCEPTION_ON_EXISTS -> ShadowSmbUtil.mockSmbExceptionOnExists + ShadowSmbUtil.PATH_INVOKE_SMBEXCEPTION_ON_ISFOLDER -> ShadowSmbUtil.mockSmbExceptionOnIsFolder + else -> createInternal(path).also { + doNothing().`when`(it).delete() + `when`(it.exists()).thenReturn(false) + } + } + } + + @JvmStatic + private fun createInternal(path: String): SmbFile { + return mock(SmbFile::class.java).also { + `when`(it.name).thenReturn(path.substring(path.lastIndexOf('/') + 1)) + `when`(it.path).thenReturn(path) + `when`(it.canonicalPath).thenReturn("$path/") + `when`(it.context).thenReturn(SingletonContext.getInstance()) + } + } + } +} \ No newline at end of file diff --git a/app/src/test/java/com/amaze/filemanager/shadows/ShadowSmbUtil.kt b/app/src/test/java/com/amaze/filemanager/shadows/ShadowSmbUtil.kt index 9893bd7aae..2892c1087d 100644 --- a/app/src/test/java/com/amaze/filemanager/shadows/ShadowSmbUtil.kt +++ b/app/src/test/java/com/amaze/filemanager/shadows/ShadowSmbUtil.kt @@ -25,6 +25,7 @@ import com.amaze.filemanager.utils.SmbUtil import jcifs.context.SingletonContext import jcifs.smb.SmbException import jcifs.smb.SmbFile +import org.mockito.Mockito import org.mockito.Mockito.* import org.robolectric.annotation.Implementation import org.robolectric.annotation.Implements @@ -50,15 +51,15 @@ class ShadowSmbUtil { const val PATH_INVOKE_SMBEXCEPTION_ON_EXISTS = "smb://user:password@5.6.7.8/newfolder/wirebroken.log" const val PATH_INVOKE_SMBEXCEPTION_ON_ISFOLDER = "smb://user:password@5.6.7.8/newfolder/failcheck" - var mockDeleteAccessDenied: SmbFile? = null - var mockDeleteDifferentNetwork: SmbFile? = null - var mockCannotRenameOld: SmbFile? = null - var mockCanRename: SmbFile? = null - var mockPathDoesNotExist: SmbFile? = null - var mockPathNotAFolder: SmbFile? = null - var mockPathExist: SmbFile? = null - var mockSmbExceptionOnExists: SmbFile? = null - var mockSmbExceptionOnIsFolder: SmbFile? = null + var mockDeleteAccessDenied: SmbFile + var mockDeleteDifferentNetwork: SmbFile + var mockCannotRenameOld: SmbFile + var mockCanRename: SmbFile + var mockPathDoesNotExist: SmbFile + var mockPathNotAFolder: SmbFile + var mockPathExist: SmbFile + var mockSmbExceptionOnExists: SmbFile + var mockSmbExceptionOnIsFolder: SmbFile init { mockDeleteAccessDenied = createInternal(PATH_CANNOT_DELETE_FILE).also { @@ -76,9 +77,9 @@ class ShadowSmbUtil { } mockCannotRenameOld = createInternal(PATH_CANNOT_RENAME_OLDFILE) - `when`(mockCannotRenameOld!!.renameTo(any())) + `when`(mockCannotRenameOld.renameTo(any())) .thenThrow(SmbException("Access is denied.")) - `when`(mockCannotRenameOld!!.exists()).thenReturn(true) + `when`(mockCannotRenameOld.exists()).thenReturn(true) mockPathDoesNotExist = createInternal(PATH_DOESNT_EXIST).also { `when`(it.exists()).thenReturn(false) @@ -149,15 +150,15 @@ class ShadowSmbUtil { fun create(path: String): SmbFile { return when (path) { - PATH_CANNOT_DELETE_FILE -> mockDeleteAccessDenied!! - PATH_CANNOT_MOVE_FILE -> mockDeleteDifferentNetwork!! - PATH_CANNOT_RENAME_OLDFILE -> mockCannotRenameOld!! - PATH_CAN_RENAME_OLDFILE -> mockCanRename!! - PATH_NOT_A_FOLDER -> mockPathNotAFolder!! - PATH_DOESNT_EXIST -> mockPathDoesNotExist!! - PATH_EXIST -> mockPathExist!! - PATH_INVOKE_SMBEXCEPTION_ON_EXISTS -> mockSmbExceptionOnExists!! - PATH_INVOKE_SMBEXCEPTION_ON_ISFOLDER -> mockSmbExceptionOnIsFolder!! + PATH_CANNOT_DELETE_FILE -> mockDeleteAccessDenied + PATH_CANNOT_MOVE_FILE -> mockDeleteDifferentNetwork + PATH_CANNOT_RENAME_OLDFILE -> mockCannotRenameOld + PATH_CAN_RENAME_OLDFILE -> mockCanRename + PATH_NOT_A_FOLDER -> mockPathNotAFolder + PATH_DOESNT_EXIST -> mockPathDoesNotExist + PATH_EXIST -> mockPathExist + PATH_INVOKE_SMBEXCEPTION_ON_EXISTS -> mockSmbExceptionOnExists + PATH_INVOKE_SMBEXCEPTION_ON_ISFOLDER -> mockSmbExceptionOnIsFolder else -> createInternal(path).also { doNothing().`when`(it).delete() `when`(it.exists()).thenReturn(false) @@ -169,6 +170,7 @@ class ShadowSmbUtil { return mock(SmbFile::class.java).also { `when`(it.name).thenReturn(path.substring(path.lastIndexOf('/') + 1)) `when`(it.path).thenReturn(path) + `when`(it.canonicalPath).thenReturn("$path/") `when`(it.context).thenReturn(SingletonContext.getInstance()) } } diff --git a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.java b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.java index 9a1cd2092a..b2b7becf2b 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.java +++ b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.java @@ -37,6 +37,7 @@ import org.junit.runner.RunWith; import org.robolectric.annotation.Config; +import com.amaze.filemanager.shadows.ShadowSmbAmazeFilesystem; import com.amaze.filemanager.shadows.ShadowSmbUtil; import com.amaze.filemanager.test.ShadowPasswordUtil; @@ -48,7 +49,7 @@ @RunWith(AndroidJUnit4.class) @Config( sdk = {JELLY_BEAN, KITKAT, P}, - shadows = {ShadowPasswordUtil.class, ShadowSmbUtil.class}) + shadows = {ShadowSmbUtil.class, ShadowSmbAmazeFilesystem.class}) public class SmbUtilTest { @Test diff --git a/build.gradle b/build.gradle index 3cc388969f..48dd55578d 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,9 @@ buildscript { commonsCompressVersion = "1.20" libsuVersion = "3.2.1" mockkVersion = "1.12.2" + reactiveXVersion = "2.2.9" + reactiveXAndroidVersion = "2.1.1" + cloudRailVersion = "2.22.4" } repositories { google() diff --git a/file_operations/build.gradle b/file_operations/build.gradle index e5e84f5039..7012ee789c 100644 --- a/file_operations/build.gradle +++ b/file_operations/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-android' android { @@ -61,6 +62,11 @@ dependencies { } } + //File + implementation 'androidx.documentfile:documentfile:1.0.1' + implementation "androidx.preference:preference:$androidXPrefVersion" + implementation "androidx.preference:preference-ktx:$androidXPrefVersion" + implementation "org.apache.commons:commons-compress:$commonsCompressVersion" implementation "com.github.junrar:junrar:$junrarVersion" @@ -76,8 +82,18 @@ dependencies { //smb implementation "eu.agno3.jcifs:jcifs-ng:$jcifsVersion" + //Cloud + implementation "com.cloudrail:cloudrail-si-android:$cloudRailVersion" + implementation 'androidx.multidex:multidex:2.0.1'//Multiple dex files + //ReactiveX + implementation "io.reactivex.rxjava2:rxandroid:$reactiveXAndroidVersion" + // Because RxAndroid releases are few and far between, it is recommended you also + // explicitly depend on RxJava's latest version for bug fixes and new features. + // (see https://github.com/ReactiveX/RxJava/releases for latest 3.x.x version) + implementation "io.reactivex.rxjava2:rxjava:$reactiveXVersion" + //TODO some libs are not needed //For tests diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFile.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFile.kt new file mode 100644 index 0000000000..7442fd773b --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFile.kt @@ -0,0 +1,1298 @@ +/* + * Copyright (C) 2014-2022 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes + +import android.net.Uri +import android.os.Parcel +import android.util.Log +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.box.BoxAmazeFilesystem +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.dropbox.DropboxAmazeFilesystem +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.gdrive.GoogledriveAmazeFilesystem +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.onedrive.OnedriveAmazeFilesystem +import com.amaze.filemanager.file_operations.filesystem.filetypes.smb.SmbAmazeFilesystem +import kotlinx.parcelize.Parceler +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.* + +// Android-added: Info about UTF-8 usage in filenames. +/** + * An abstract representation of file and directory pathnames. + * + * + * User interfaces and operating systems use system-dependent *pathname strings* to name + * files and directories. This class presents an abstract, system-independent view of hierarchical + * pathnames. An *abstract pathname* has two components: + * + * + * 1. An optional system-dependent *prefix* string, such as a disk-drive specifier, ` + * "/"` for the UNIX root directory, or `"\\\\"` for a Microsoft + * Windows UNC pathname, and + * 1. A sequence of zero or more string *names*. + * + * + * The first name in an abstract pathname may be a directory name or, in the case of Microsoft + * Windows UNC pathnames, a hostname. Each subsequent name in an abstract pathname denotes a + * directory; the last name may denote either a directory or a file. The *empty* abstract + * pathname has no prefix and an empty name sequence. + * + * + * The conversion of a pathname string to or from an abstract pathname is inherently + * system-dependent. When an abstract pathname is converted into a pathname string, each name is + * separated from the next by a single copy of the default *separator character*. The default + * name-separator character is defined by the system property `file.separator`, and is + * made available in the public static fields `[ ][.separator]` and `[.separatorChar]` of this class. When a pathname string + * is converted into an abstract pathname, the names within it may be separated by the default + * name-separator character or by any other name-separator character that is supported by the + * underlying system. + * + * + * A pathname, whether abstract or in string form, may be either *absolute* or + * *relative*. An absolute pathname is complete in that no other information is required in + * order to locate the file that it denotes. A relative pathname, in contrast, must be interpreted + * in terms of information taken from some other pathname. By default the classes in the ` + * java.io` package always resolve relative pathnames against the current user directory. This + * directory is named by the system property `user.dir`, and is typically the directory + * in which the Java virtual machine was invoked. + * + * + * The *parent* of an abstract pathname may be obtained by invoking the [.getParent] + * method of this class and consists of the pathname's prefix and each name in the pathname's name + * sequence except for the last. Each directory's absolute pathname is an ancestor of any + * AmazeFile object with an absolute abstract pathname which begins with the directory's + * absolute pathname. For example, the directory denoted by the abstract pathname "/usr" is + * an ancestor of the directory denoted by the pathname "/usr/local/bin". + * + * + * The prefix concept is used to handle root directories on UNIX platforms, and drive specifiers, + * root directories and UNC pathnames on Microsoft Windows platforms, as follows: + * + * + * * For UNIX platforms, the prefix of an absolute pathname is always `"/"`. Relative + * pathnames have no prefix. The abstract pathname denoting the root directory has the prefix + * `"/"` and an empty name sequence. + * * For Microsoft Windows platforms, the prefix of a pathname that contains a drive specifier + * consists of the drive letter followed by `":"` and possibly followed by ` + * "\\"` if the pathname is absolute. The prefix of a UNC pathname is `"\\\\" +` * ; the hostname and the share name are the first two names in the name sequence. A + * relative pathname that does not specify a drive has no prefix. + * + * + * + * Instances of this class may or may not denote an actual file-system object such as a file or a + * directory. If it does denote such an object then that object resides in a *partition*. A + * partition is an operating system-specific portion of storage for a file system. A single storage + * device (e.g. a physical disk-drive, flash memory, CD-ROM) may contain multiple partitions. The + * object, if any, will reside on the partition named by some ancestor of the + * absolute form of this pathname. + * + * + * A file system may implement restrictions to certain operations on the actual file-system + * object, such as reading, writing, and executing. These restrictions are collectively known as + * *access permissions*. The file system may have multiple sets of access permissions on a + * single object. For example, one set may apply to the object's *owner*, and another may apply + * to all other users. The access permissions on an object may cause some methods in this class to + * fail. + * + * + * On Android strings are converted to UTF-8 byte sequences when sending filenames to the + * operating system, and byte sequences returned by the operating system (from the various `list` methods) are converted to strings by decoding them as UTF-8 byte sequences. + * + * @author unascribed + */ +class AmazeFile : Comparable { + companion object { + @JvmStatic + val TAG = AmazeFile::class.java.simpleName + @JvmStatic + private val filesystems: MutableList = ArrayList() + + @JvmStatic + fun addFilesystem(amazeFilesystem: AmazeFilesystem) { + filesystems.add(amazeFilesystem) + } + + init { + BoxAmazeFilesystem + DropboxAmazeFilesystem + GoogledriveAmazeFilesystem + OnedriveAmazeFilesystem + + SmbAmazeFilesystem + } + + private fun slashify(path: String, isDirectory: Boolean): String { + var p = path + if (File.separatorChar != '/') p = p.replace(File.separatorChar, '/') + if (!p.startsWith("/")) p = "/$p" + if (!p.endsWith("/") && isDirectory) p = "$p/" + return p + } + + object AmazeFileParceler : Parceler { + override fun create(parcel: Parcel): AmazeFile = + AmazeFile(parcel.readString() ?: "") + + override fun AmazeFile.write(parcel: Parcel, flags: Int) { + parcel.writeString(absolutePath) + } + } + } + + /** The FileSystem object representing the platform's local file system. */ + lateinit var fs: AmazeFilesystem + + /** + * Converts this abstract pathname into a pathname string. The resulting string uses the [separator] + * to separate the names in the name sequence. + * + * This abstract pathname's normalized pathname string. A normalized pathname string uses the + * default name-separator character and does not contain any duplicate or redundant separators. + */ + val path: String + + /** Returns the length of this abstract pathname's prefix. For use by FileSystem classes. */ + /** The length of this abstract pathname's prefix, or zero if it has no prefix. */ + @Transient + val prefixLength: Int + + /** + * The system-dependent default name-separator character. This field is initialized to contain the + * first character of the value of the system property `file.separator`. On UNIX + * systems the value of this field is `'/'`; on Microsoft Windows systems it is ` + * '\\'`. + * + * @see java.lang.System.getProperty + */ + val separatorChar: Char + + /** + * The system-dependent default name-separator character, represented as a string for convenience. + * This string contains a single character, namely `[.separatorChar]`. + */ + val separator: String + + /* -- Constructors -- */ + /** Internal constructor for already-normalized pathname strings. */ + private constructor(pathname: String, prefixLength: Int) { + loadFilesystem(pathname) + separatorChar = fs.getSeparator() + separator = "" + separatorChar + path = pathname + this.prefixLength = prefixLength + } + + /** + * Internal constructor for already-normalized pathname strings. The parameter order is used to + * disambiguate this method from the public(AmazeFile, String) constructor. + */ + private constructor(child: String, parent: AmazeFile) { + assert(parent.path != "") + loadFilesystem(parent.path) + separatorChar = fs.getSeparator() + separator = "" + separatorChar + path = fs.resolve(parent.path, child) + prefixLength = parent.prefixLength + } + + /** + * Creates a new `AmazeFile` instance by converting the given pathname string into an + * abstract pathname. If the given string is the empty string, then the result is the empty + * abstract pathname. + * + * @param pathname A pathname string + */ + constructor(pathname: String) { + loadFilesystem(pathname) + separatorChar = fs.getSeparator() + separator = "" + separatorChar + path = fs.normalize(pathname) + prefixLength = fs.prefixLength(path) + } + + /** + * Note: The two-argument File constructors do not interpret an empty + * parent abstract pathname as the current user directory. An empty parent + * instead causes the child to be resolved against the system-dependent + * directory defined by the FileSystem.getDefaultParent method. On Unix + * this default is "/", while on Microsoft Windows it is "\\". This is required for + * compatibility with the original behavior of this class. + * + * Creates a new `AmazeFile` instance from a parent pathname string and a child + * pathname string. + * + * + * If `parent` is `null` then the new `AmazeFile` instance is + * created as if by invoking the single-argument `AmazeFile` constructor on the given + * `child` pathname string. + * + * + * Otherwise the `parent` pathname string is taken to denote a directory, and the + * `child` pathname string is taken to denote either a directory or a file. If the + * `child` pathname string is absolute then it is converted into a relative pathname in + * a system-dependent way. If `parent` is the empty string then the new `AmazeFile + ` * instance is created by converting `child` into an abstract pathname and + * resolving the result against a system-dependent default directory. Otherwise each pathname + * string is converted into an abstract pathname and the child abstract pathname is resolved + * against the parent. + * + * @param parent The parent pathname string + * @param child The child pathname string + */ + constructor(parent: String?, child: String) { + // BEGIN Android-changed: b/25859957, app-compat; don't substitute empty parent. + if (parent != null && !parent.isEmpty()) { + loadFilesystem(parent) + separatorChar = fs.getSeparator() + separator = "" + separatorChar + path = fs.resolve(fs.normalize(parent), fs.normalize(child)) + // END Android-changed: b/25859957, app-compat; don't substitute empty parent. + } else { + loadFilesystem(child) + separatorChar = fs.getSeparator() + separator = "" + separatorChar + path = fs.normalize(child) + } + prefixLength = fs.prefixLength(path) + } + + /** + * Creates a new `AmazeFile` instance from a parent abstract pathname and a child + * pathname string. + * + * + * If `parent` is `null` then the new `AmazeFile` instance is + * created as if by invoking the single-argument `AmazeFile` constructor on the given + * `child` pathname string. + * + * + * Otherwise the `parent` abstract pathname is taken to denote a directory, and the + * `child` pathname string is taken to denote either a directory or a file. If the + * `child` pathname string is absolute then it is converted into a relative pathname in + * a system-dependent way. If `parent` is the empty abstract pathname then the new + * `AmazeFile` instance is created by converting `child` into an abstract + * pathname and resolving the result against a system-dependent default directory. Otherwise each + * pathname string is converted into an abstract pathname and the child abstract pathname is + * resolved against the parent. + * + * @param parent The parent abstract pathname + * @param child The child pathname string + */ + constructor(parent: AmazeFile?, child: String) { + if (parent != null) { + loadFilesystem(parent.path) + separatorChar = fs.getSeparator() + separator = "" + separatorChar + if (parent.path == "") { + path = fs.resolve(fs.defaultParent, fs.normalize(child)) + } else { + path = fs.resolve(parent.path, fs.normalize(child)) + } + } else { + loadFilesystem(child) + separatorChar = fs.getSeparator() + separator = "" + separatorChar + path = fs.normalize(child) + } + prefixLength = fs.prefixLength(path) + } + + private fun loadFilesystem(path: String) { + var loadedAFs = false + + for (filesystem in filesystems) { + if (filesystem.isPathOfThisFilesystem(path)) { + fs = filesystem + loadedAFs = true + } + } + + if (!loadedAFs) { + Log.e( + TAG, + "Failed to load a filesystem, did you forget to add the class to the " + + "[AmazeFile]'s companion object initialization block?" + ) + } + } + /* -- Path-component accessors -- */ + /** + * Returns the name of the file or directory denoted by this abstract pathname. This is just the + * last name in the pathname's name sequence. If the pathname's name sequence is empty, then the + * empty string is returned. + * + * @return The name of the file or directory denoted by this abstract pathname, or the empty + * string if this pathname's name sequence is empty + */ + val name: String + get() { + val index = path.lastIndexOf(separatorChar) + if (index < prefixLength) { + return path.substring(prefixLength) + } + if (path.endsWith("/")) { + val newIndex = path.substring(0, path.length - 2).lastIndexOf(separatorChar) + return if (newIndex < prefixLength) { + path.substring(prefixLength) + } else path.substring(newIndex + 1) + } + return path.substring(index + 1) + } + + val humanReadablePath: String + get() { + return fs.getHumanReadablePath(this) + } + + /** + * Returns the pathname string of this abstract pathname's parent, or `null` if this + * pathname does not name a parent directory. + * + * + * The *parent* of an abstract pathname consists of the pathname's prefix, if any, and + * each name in the pathname's name sequence except for the last. If the name sequence is empty + * then the pathname does not name a parent directory. + * + * @return The pathname string of the parent directory named by this abstract pathname, or ` + * null` if this pathname does not name a parent + */ + val parent: String? + get() { + val index = path.lastIndexOf(separatorChar) + if (index < prefixLength) { + return if (prefixLength > 0 && path.length > prefixLength) { + path.substring(0, prefixLength) + } else null + } + if (path.endsWith("/")) { + val newIndex = path.substring(0, path.length - 2).lastIndexOf(separatorChar) + return if (newIndex < prefixLength) { + if (prefixLength > 0 && path.length > prefixLength) { + path.substring(0, prefixLength) + } else null + } else path.substring(0, newIndex) + } + return path.substring(0, index) + } + + /** + * Returns the abstract pathname of this abstract pathname's parent, or `null` if this + * pathname does not name a parent directory. + * + * + * The *parent* of an abstract pathname consists of the pathname's prefix, if any, and + * each name in the pathname's name sequence except for the last. If the name sequence is empty + * then the pathname does not name a parent directory. + * + * @return The abstract pathname of the parent directory named by this abstract pathname, or + * `null` if this pathname does not name a parent + */ + val parentFile: AmazeFile? + get() { + val p = parent ?: return null + return AmazeFile(p, prefixLength) + } + /* -- Path operations -- */ // Android-changed: Android-specific path information + /** + * Tests whether this abstract pathname is absolute. The definition of absolute pathname is system + * dependent. On Android, absolute paths start with the character '/'. + * + * @return `true` if this abstract pathname is absolute, `false` otherwise + */ + val isAbsolute: Boolean + get() = fs.isAbsolute(this) + // Android-changed: Android-specific path information + /** + * Returns the absolute path of this file. An absolute path is a path that starts at a root of the + * file system. On Android, there is only one root: `/`. + * + * + * A common use for absolute paths is when passing paths to a `Process` as command-line + * arguments, to remove the requirement implied by relative paths, that the child must have the + * same working directory as its parent. + * + * @return The absolute pathname string denoting the same file or directory as this abstract + * pathname + * @see java.io.File.isAbsolute + */ + val absolutePath: String + get() = fs.resolve(this) + + /** + * Returns the absolute form of this abstract pathname. Equivalent to ` + * new File(this.[.getAbsolutePath])`. + * + * @return The absolute abstract pathname denoting the same file or directory as this abstract + * pathname + */ + val absoluteFile: AmazeFile + get() { + val absPath = absolutePath + return AmazeFile(absPath, fs.prefixLength(absPath)) + } + + /** + * Returns the canonical pathname string of this abstract pathname. + * + * + * A canonical pathname is both absolute and unique. The precise definition of canonical form + * is system-dependent. This method first converts this pathname to absolute form if necessary, as + * if by invoking the [.getAbsolutePath] method, and then maps it to its unique form in a + * system-dependent way. This typically involves removing redundant names such as "." and + * ".." from the pathname, resolving symbolic links (on UNIX platforms), and converting + * drive letters to a standard case (on Microsoft Windows platforms). + * + * + * Every pathname that denotes an existing file or directory has a unique canonical form. Every + * pathname that denotes a nonexistent file or directory also has a unique canonical form. The + * canonical form of the pathname of a nonexistent file or directory may be different from the + * canonical form of the same pathname after the file or directory is created. Similarly, the + * canonical form of the pathname of an existing file or directory may be different from the + * canonical form of the same pathname after the file or directory is deleted. + * + * @return The canonical pathname string denoting the same file or directory as this abstract + * pathname + * @throws IOException If an I/O error occurs, which is possible because the construction of the + * canonical pathname may require filesystem queries + * @see Path.toRealPath + */ + @get:Throws(IOException::class) + val canonicalPath: String + get() { + return fs.canonicalize(fs.resolve(this)) + } + + /** + * Returns the canonical form of this abstract pathname. Equivalent to ` + * new File(this.[.getCanonicalPath])`. + * + * @return The canonical pathname string denoting the same file or directory as this abstract + * pathname + * @throws IOException If an I/O error occurs, which is possible because the construction of the + * canonical pathname may require filesystem queries + * @see Path.toRealPath + */ + @get:Throws(IOException::class) + val canonicalFile: AmazeFile + get() { + val canonPath = canonicalPath + return AmazeFile(canonPath, fs.prefixLength(canonPath)) + } + + fun getUriForFile(contextProvider: ContextProvider): Uri? { + return fs.getUriForFile(this, contextProvider) + } + + /* -- Attribute accessors -- */ // Android-changed. Removed javadoc comment about special privileges + // that doesn't make sense on android + /** + * Tests whether the application can read the file denoted by this abstract pathname. + * + * @return `true` if and only if the file specified by this abstract pathname exists + * *and* can be read by the application; `false` otherwise + */ + fun canRead(contextProvider: ContextProvider): Boolean { + return fs.canRead(this, contextProvider) + } + // Android-changed. Removed javadoc comment about special privileges + // that doesn't make sense on android + /** + * Tests whether the application can modify the file denoted by this abstract pathname. + * + * @return `true` if and only if the file system actually contains a file denoted by + * this abstract pathname *and* the application is allowed to write to the file; ` + * false` otherwise. + */ + fun canWrite(contextProvider: ContextProvider): Boolean { + return fs.canWrite(this, contextProvider) + } + + /** + * Tests whether the file or directory denoted by this abstract pathname exists. + * + * @return `true` if and only if the file or directory denoted by this abstract + * pathname exists; `false` otherwise + */ + fun exists(contextProvider: ContextProvider): Boolean { + return fs.canAccess(this, contextProvider) + + // Android-changed: b/25878034 work around SELinux stat64 denial. + } + + /** + * Tests whether the file denoted by this abstract pathname is a directory. + * + * + * Where it is required to distinguish an I/O exception from the case that the file is not a + * directory, or where several attributes of the same file are required at the same time, then the + * [Files.readAttributes][java.nio.file.Files.readAttributes] method + * may be used. + * + * @return `true` if and only if the file denoted by this abstract pathname exists + * *and* is a directory; `false` otherwise + */ + fun isDirectory(contextProvider: ContextProvider): Boolean { + return fs.getBooleanAttributes(this, contextProvider) and AmazeFilesystem.BA_DIRECTORY != 0 + } + + /** + * Tests whether the file denoted by this abstract pathname is a normal file. A file is + * *normal* if it is not a directory and, in addition, satisfies other system-dependent + * criteria. Any non-directory file created by a Java application is guaranteed to be a normal + * file. + * + * + * Where it is required to distinguish an I/O exception from the case that the file is not a + * normal file, or where several attributes of the same file are required at the same time, then + * the [Files.readAttributes][java.nio.file.Files.readAttributes] + * method may be used. + * + * @return `true` if and only if the file denoted by this abstract pathname exists + * *and* is a normal file; `false` otherwise + */ + fun isFile(contextProvider: ContextProvider): Boolean { + return fs.getBooleanAttributes(this, contextProvider) and AmazeFilesystem.BA_REGULAR != 0 + } + + /** + * Tests whether the file named by this abstract pathname is a hidden file. The exact definition + * of *hidden* is system-dependent. On UNIX systems, a file is considered to be hidden if + * its name begins with a period character (`'.'`). On Microsoft Windows systems, a + * file is considered to be hidden if it has been marked as such in the filesystem. + * + * @return `true` if and only if the file denoted by this abstract pathname is hidden + * according to the conventions of the underlying platform + */ + fun isHidden(contextProvider: ContextProvider): Boolean { + return fs.getBooleanAttributes(this, contextProvider) and AmazeFilesystem.BA_HIDDEN != 0 + } + + /** + * Returns the time that the file denoted by this abstract pathname was last modified. + * + * + * Where it is required to distinguish an I/O exception from the case where `0L` is + * returned, or where several attributes of the same file are required at the same time, or where + * the time of last access or the creation time are required, then the [ ][java.nio.file.Files.readAttributes] method may be + * used. + * + * @return A `long` value representing the time the file was last modified, measured in + * milliseconds since the epoch (00:00:00 GMT, January 1, 1970), or `0L` if the + * file does not exist or if an I/O error occurs + */ + fun lastModified(): Long { + return fs.getLastModifiedTime(this) + } + + /** + * Computes the hash MD5 of a file + */ + fun getHashMD5(contextProvider: ContextProvider): String? { + if(!isFile(contextProvider)) { + return null + } + + return fs.getHashMD5(this, contextProvider) + } + + + /** + * Computes the hash SHA256 of a file + */ + fun getHashSHA256(contextProvider: ContextProvider): String? { + if(!isFile(contextProvider)) { + return null + } + + return fs.getHashSHA256(this, contextProvider) + } + + /** + * Returns the length of the file or directory denoted by this abstract pathname. + * + * Where it is required to distinguish an I/O exception from the case that `0L` is + * returned, or where several attributes of the same file are required at the same time, then the + * [Files.readAttributes][java.nio.file.Files.readAttributes] method + * may be used. + * + * @return The length, in bytes, of the file denoted by this abstract pathname, or `0L` + * if the file does not exist. Some operating systems may return `0L` for pathnames + * denoting system-dependent entities such as devices or pipes. + */ + @Throws(IOException::class) + fun length(contextProvider: ContextProvider): Long { + return fs.getLength(this, contextProvider) + } + + /** + * Returns the length of the file denoted by this abstract pathname. + * + * Where it is required to distinguish an I/O exception from the case that `0L` is + * returned, or where several attributes of the same file are required at the same time, then the + * [Files.readAttributes][java.nio.file.Files.readAttributes] method + * may be used. + * + * @return The length, in bytes, of the file denoted by this abstract pathname, or `0L` + * if the file does not exist. Some operating systems may return `0L` for pathnames + * denoting system-dependent entities such as devices or pipes. + */ + fun safeLength(contextProvider: ContextProvider): Long { + return try { + length(contextProvider) + } catch (e: IOException) { + Log.e(TAG, "Error getting file size", e) + 0L + } + } + + /** + * Returns the length of the directory denoted by this abstract pathname. + * + * Where it is required to distinguish an I/O exception from the case that `0L` is + * returned. + * + * @return The length, in bytes, of the directory denoted by this abstract pathname, or `0L` + * if the directory does not exist. + */ + @JvmOverloads + fun safeLengthFolder(contextProvider: ContextProvider, callback: ((Long) -> Unit)? = null): Long { + if(!isDirectory(contextProvider)) { + return 0L + } + + try { + var length: Long = 0 + listFiles(contextProvider) { file: AmazeFile -> + if(file.isFile(contextProvider)) { + length += file.length(contextProvider) + callback?.invoke(length) + } else { + file.safeLengthFolder(contextProvider) { + length += it + callback?.invoke(length) + } + } + } + return length + } catch (e: IOException) { + Log.e(TAG, "Error getting file size", e) + return 0L + } + } + + /* -- File operations -- */ + /** + * Atomically creates a new, empty file named by this abstract pathname if and only if a file with + * this name does not yet exist. The check for the existence of the file and the creation of the + * file if it does not exist are a single operation that is atomic with respect to all other + * filesystem activities that might affect the file. + * + * + * Note: this method should *not* be used for file-locking, as the resulting protocol + * cannot be made to work reliably. The [FileLock][java.nio.channels.FileLock] facility + * should be used instead. + * + * @return `true` if the named file does not exist and was successfully created; ` + * false` if the named file already exists + * @throws IOException If an I/O error occurred + */ + @Throws(IOException::class) + fun createNewFile(): Boolean { + return fs.createFileExclusively(path) + } + + /** + * Deletes the file or directory denoted by this abstract pathname. + * + * + * Note that the [java.nio.file.Files] class defines the [ ][java.nio.file.Files.delete] method to throw an [IOException] when a file + * cannot be deleted. This is useful for error reporting and to diagnose why a file cannot be + * deleted. + * + * @return `true` if and only if the file or directory is successfully deleted; ` + * false` otherwise + */ + fun delete(contextProvider: ContextProvider): Boolean { + return fs.delete(this, contextProvider) + } + + /** + * Returns an array of strings naming the files and directories in the directory denoted by this + * abstract pathname. + * + * + * If this abstract pathname does not denote a directory, then this method returns `null`. Otherwise an array of strings is returned, one for each file or directory in the + * directory. Names denoting the directory itself and the directory's parent directory are not + * included in the result. Each string is a file name rather than a complete path. + * + * + * There is no guarantee that the name strings in the resulting array will appear in any + * specific order; they are not, in particular, guaranteed to appear in alphabetical order. + * + * + * Note that the [java.nio.file.Files] class defines the [ ][java.nio.file.Files.newDirectoryStream] method to open a directory and + * iterate over the names of the files in the directory. This may use less resources when working + * with very large directories, and may be more responsive when working with remote directories. + * + * @return An array of strings naming the files and directories in the directory denoted by this + * abstract pathname. The array will be empty if the directory is empty. Returns `null` + * if this abstract pathname does not denote a directory, or if an I/O error occurs. + */ + fun list(contextProvider: ContextProvider): Array? { + return fs.list(this, contextProvider) + } + + /** + * Returns an array of strings naming the files and directories in the directory denoted by this + * abstract pathname that satisfy the specified filter. The behavior of this method is the same as + * that of the [.list] method, except that the strings in the returned array must satisfy + * the filter. If the given `filter` is `null` then all names are accepted. Otherwise, + * a name satisfies the filter if and only if the value `true` results when the [ ][AmazeFilenameFilter.accept] method of the filter + * is invoked on this abstract pathname and the name of a file or directory in the directory that + * it denotes. + * + * @param filter A filename filter + * @return An array of strings naming the files and directories in the directory denoted by this + * abstract pathname that were accepted by the given `filter`. The array will be empty + * if the directory is empty or if no names were accepted by the filter. Returns `null` + * if this abstract pathname does not denote a directory, or if an I/O error occurs. + * @see java.nio.file.Files.newDirectoryStream + */ + fun list(filter: AmazeFilenameFilter?, contextProvider: ContextProvider): Array? { + val names = list(contextProvider) + if (names == null || filter == null) { + return names + } + val v: MutableList = ArrayList() + for (i in names.indices) { + if (filter.accept(this, names[i])) { + v.add(names[i]) + } + } + return v.toTypedArray() + } + + /** + * Returns an array of abstract pathnames denoting the files in the directory denoted by this + * abstract pathname. + * + * + * If this abstract pathname does not denote a directory, then this method returns `null`. Otherwise an array of `File` objects is returned, one for each file or directory + * in the directory. Pathnames denoting the directory itself and the directory's parent directory + * are not included in the result. Each resulting abstract pathname is constructed from this + * abstract pathname using the [File(File,&nbsp;String)][.File] constructor. + * Therefore if this pathname is absolute then each resulting pathname is absolute; if this + * pathname is relative then each resulting pathname will be relative to the same directory. + * + * + * There is no guarantee that the name strings in the resulting array will appear in any + * specific order; they are not, in particular, guaranteed to appear in alphabetical order. + * + * + * Note that the [java.nio.file.Files] class defines the [ ][java.nio.file.Files.newDirectoryStream] method to open a directory and + * iterate over the names of the files in the directory. This may use less resources when working + * with very large directories. + * + * @return An array of abstract pathnames denoting the files and directories in the directory + * denoted by this abstract pathname. The array will be empty if the directory is empty. + * Returns `null` if this abstract pathname does not denote a directory, or if an I/O + * error occurs. + */ + fun listFiles(contextProvider: ContextProvider): Array? { + val files = ArrayList() + listFiles(contextProvider, files::add) ?: return null + return files.toTypedArray() + } + + fun listFiles(contextProvider: ContextProvider, onFileFound: (AmazeFile) -> Unit): Unit? { + val ss = list(contextProvider) ?: return null + for (i in ss.indices) { + onFileFound(AmazeFile(ss[i], this)) + } + return Unit + } + + /** + * Returns an array of abstract pathnames denoting the files and directories in the directory + * denoted by this abstract pathname that satisfy the specified filter. The behavior of this + * method is the same as that of the [.listFiles] method, except that the pathnames in the + * returned array must satisfy the filter. If the given `filter` is `null` then all + * pathnames are accepted. Otherwise, a pathname satisfies the filter if and only if the value + * `true` results when the [ AmazeFilenameFilter.accept(File,&nbsp;String)][AmazeFilenameFilter.accept] method of the filter is invoked on this abstract + * pathname and the name of a file or directory in the directory that it denotes. + * + * @param filter A filename filter + * @return An array of abstract pathnames denoting the files and directories in the directory + * denoted by this abstract pathname. The array will be empty if the directory is empty. + * Returns `null` if this abstract pathname does not denote a directory, or if an I/O + * error occurs. + * @see java.nio.file.Files.newDirectoryStream + */ + fun listFiles( + filter: AmazeFilenameFilter?, + contextProvider: ContextProvider + ): Array? { + val files = ArrayList() + listFiles(filter, contextProvider, files::add) ?: return null + return files.toTypedArray() + } + + fun listFiles( + filter: AmazeFilenameFilter?, + contextProvider: ContextProvider, + onFileFound: (AmazeFile) -> Unit + ): Unit? { + val ss = list(contextProvider) ?: return null + for (s in ss) { + if (filter == null || filter.accept(this, s)) { + onFileFound(AmazeFile(s, this)) + } + } + return Unit + } + + /** + * Returns an array of abstract pathnames denoting the files and directories in the directory + * denoted by this abstract pathname that satisfy the specified filter. The behavior of this + * method is the same as that of the [.listFiles] method, except that the pathnames in the + * returned array must satisfy the filter. If the given `filter` is `null` then all + * pathnames are accepted. Otherwise, a pathname satisfies the filter if and only if the value + * `true` results when the [FileFilter.accept(File)][FileFilter.accept] method of the + * filter is invoked on the pathname. + * + * @param filter A file filter + * @return An array of abstract pathnames denoting the files and directories in the directory + * denoted by this abstract pathname. The array will be empty if the directory is empty. + * Returns `null` if this abstract pathname does not denote a directory, or if an I/O + * error occurs. + * @see java.nio.file.Files.newDirectoryStream + */ + fun listFiles(filter: AmazeFileFilter?, contextProvider: ContextProvider): Array? { + val files = ArrayList() + forFiles(filter, contextProvider, files::add) ?: return null + return files.toTypedArray() + } + + /** + * Calls a function on an array of abstract pathnames denoting the files and directories + * in the directory denoted by this abstract pathname that satisfy the specified filter. + * The behavior of this method is the same as that of the [listFiles] method, except that the + * pathnames in the returned array must satisfy the filter. If the given `filter` is `null` then + * all pathnames are accepted. Otherwise, a pathname satisfies the filter if and only if the value + * `true` results when the [FileFilter.accept] method of the + * filter is invoked on the pathname. + * + * @param filter A file filter + * @param onFileFound the function called on every file and directory in the directory + * denoted by this abstract pathname. Returns `null` if this abstract pathname does + * not denote a directory, or if an I/O error occurs. + * @see java.nio.file.Files.newDirectoryStream + */ + fun forFiles( + filter: AmazeFileFilter?, + contextProvider: ContextProvider, + onFileFound: (AmazeFile) -> Unit + ): Unit? { + val ss = list(contextProvider) ?: return null + for (s in ss) { + val f = AmazeFile(s, this) + if (filter == null || filter.accept(f)) { + onFileFound(f) + } + } + return Unit + } + + fun getInputStream(contextProvider: ContextProvider): InputStream { + return fs.getInputStream(this, contextProvider)!! + } + + fun getOutputStream(contextProvider: ContextProvider): OutputStream { + return fs.getOutputStream(this, contextProvider)!! + } + + /** + * Creates the directory named by this abstract pathname. + * + * @return `true` if and only if the directory was created; `false` + * otherwise + */ + fun mkdir(contextProvider: ContextProvider): Boolean { + return fs.createDirectory(this, contextProvider) + } + + /** + * Creates the directory named by this abstract pathname, including any necessary but nonexistent + * parent directories. Note that if this operation fails it may have succeeded in creating some of + * the necessary parent directories. + * + * @return `true` if and only if the directory was created, along with all necessary + * parent directories; `false` otherwise + */ + fun mkdirs(contextProvider: ContextProvider): Boolean { + if (exists(contextProvider)) { + return false + } + if (mkdir(contextProvider)) { + return true + } + val canonFile = try { + canonicalFile + } catch (e: IOException) { + return false + } + val parent = canonFile.parentFile + return ( + parent != null && (parent.mkdirs(contextProvider) || parent.exists(contextProvider)) && + canonFile.mkdir(contextProvider) + ) + } + // Android-changed: Replaced generic platform info with Android specific one. + /** + * Renames the file denoted by this abstract pathname. + * + * + * Many failures are possible. Some of the more likely failures include: + * + * + * * Write permission is required on the directories containing both the source and + * destination paths. + * * Search permission is required for all parents of both paths. + * * Both paths be on the same mount point. On Android, applications are most likely to hit + * this restriction when attempting to copy between internal storage and an SD card. + * + * + * + * The return value should always be checked to make sure that the rename operation was + * successful. + * + * + * Note that the [java.nio.file.Files] class defines the [ move][java.nio.file.Files.move] method to move or rename a file in a platform independent manner. + * + * @param dest The new abstract pathname for the named file + * @return `true` if and only if the renaming succeeded; `false` otherwise + */ + fun renameTo(dest: AmazeFile, contextProvider: ContextProvider): Boolean { + return fs.rename(this, dest, contextProvider) + } + + /** + * Sets the last-modified time of the file or directory named by this abstract pathname. + * + * + * All platforms support file-modification times to the nearest second, but some provide more + * precision. The argument will be truncated to fit the supported precision. If the operation + * succeeds and no intervening operations on the file take place, then the next invocation of the + * `[.lastModified]` method will return the (possibly truncated) `time + ` * argument that was passed to this method. + * + * @param time The new last-modified time, measured in milliseconds since the epoch (00:00:00 GMT, + * January 1, 1970) + * @return `true` if and only if the operation succeeded; `false` otherwise + * @throws IllegalArgumentException If the argument is negative + */ + fun setLastModified(time: Long): Boolean { + require(time >= 0) { "Negative time" } + return fs.setLastModifiedTime(this, time) + } + // Android-changed. Removed javadoc comment about special privileges + // that doesn't make sense on Android. + /** + * Marks the file or directory named by this abstract pathname so that only read operations are + * allowed. After invoking this method the file or directory will not change until it is either + * deleted or marked to allow write access. Whether or not a read-only file or directory may be + * deleted depends upon the underlying system. + * + * @return `true` if and only if the operation succeeded; `false` otherwise + */ + fun setReadOnly(): Boolean { + return fs.setReadOnly(this) + } + // Android-changed. Removed javadoc comment about special privileges + // that doesn't make sense on Android. + /** + * Sets the owner's or everybody's write permission for this abstract pathname. + * + * + * The [java.nio.file.Files] class defines methods that operate on file attributes + * including file permissions. This may be used when finer manipulation of file permissions is + * required. + * + * @param writable If `true`, sets the access permission to allow write operations; if + * `false` to disallow write operations + * @param ownerOnly If `true`, the write permission applies only to the owner's write + * permission; otherwise, it applies to everybody. If the underlying file system can not + * distinguish the owner's write permission from that of others, then the permission will + * apply to everybody, regardless of this value. + * @return `true` if and only if the operation succeeded. The operation will fail if + * the user does not have permission to change the access permissions of this abstract + * pathname. + */ + fun setWritable(writable: Boolean, ownerOnly: Boolean): Boolean { + return fs.setWritable(this, writable, ownerOnly) + } + // Android-changed. Removed javadoc comment about special privileges + // that doesn't make sense on Android. + /** + * A convenience method to set the owner's write permission for this abstract pathname. + * + * + * An invocation of this method of the form file.setWritable(arg) behaves in exactly + * the same way as the invocation + * + *

+     * file.setWritable(arg, true) 
+ * + * @param writable If `true`, sets the access permission to allow write operations; if + * `false` to disallow write operations + * @return `true` if and only if the operation succeeded. The operation will fail if + * the user does not have permission to change the access permissions of this abstract + * pathname. + */ + fun setWritable(writable: Boolean): Boolean { + return setWritable(writable, true) + } + // Android-changed. Removed javadoc comment about special privileges + // that doesn't make sense on Android. + /** + * Sets the owner's or everybody's read permission for this abstract pathname. + * + * + * The [java.nio.file.Files] class defines methods that operate on file attributes + * including file permissions. This may be used when finer manipulation of file permissions is + * required. + * + * @param readable If `true`, sets the access permission to allow read operations; if + * `false` to disallow read operations + * @param ownerOnly If `true`, the read permission applies only to the owner's read + * permission; otherwise, it applies to everybody. If the underlying file system can not + * distinguish the owner's read permission from that of others, then the permission will apply + * to everybody, regardless of this value. + * @return `true` if and only if the operation succeeded. The operation will fail if + * the user does not have permission to change the access permissions of this abstract + * pathname. If `readable` is `false` and the underlying file system + * does not implement a read permission, then the operation will fail. + */ + fun setReadable(readable: Boolean, ownerOnly: Boolean): Boolean { + return fs.setReadable(this, readable, ownerOnly) + } + // Android-changed. Removed javadoc comment about special privileges + // that doesn't make sense on Android. + /** + * A convenience method to set the owner's read permission for this abstract pathname. + * + * + * An invocation of this method of the form file.setReadable(arg) behaves in exactly + * the same way as the invocation + * + *
+     * file.setReadable(arg, true) 
+ * + * @param readable If `true`, sets the access permission to allow read operations; if + * `false` to disallow read operations + * @return `true` if and only if the operation succeeded. The operation will fail if + * the user does not have permission to change the access permissions of this abstract + * pathname. If `readable` is `false` and the underlying file system + * does not implement a read permission, then the operation will fail. + */ + fun setReadable(readable: Boolean): Boolean { + return setReadable(readable, true) + } + // Android-changed. Removed javadoc comment about special privileges + // that doesn't make sense on Android. + /** + * Sets the owner's or everybody's execute permission for this abstract pathname. + * + * + * The [java.nio.file.Files] class defines methods that operate on file attributes + * including file permissions. This may be used when finer manipulation of file permissions is + * required. + * + * @param executable If `true`, sets the access permission to allow execute operations; + * if `false` to disallow execute operations + * @param ownerOnly If `true`, the execute permission applies only to the owner's + * execute permission; otherwise, it applies to everybody. If the underlying file system can + * not distinguish the owner's execute permission from that of others, then the permission + * will apply to everybody, regardless of this value. + * @return `true` if and only if the operation succeeded. The operation will fail if + * the user does not have permission to change the access permissions of this abstract + * pathname. If `executable` is `false` and the underlying file system + * does not implement an execute permission, then the operation will fail. + */ + fun setExecutable(executable: Boolean, ownerOnly: Boolean): Boolean { + return fs.setExecutable(this, executable, ownerOnly) + } + // Android-changed. Removed javadoc comment about special privileges + // that doesn't make sense on Android. + /** + * A convenience method to set the owner's execute permission for this abstract pathname. + * + * + * An invocation of this method of the form file.setExcutable(arg) behaves in exactly + * the same way as the invocation + * + *
+     * file.setExecutable(arg, true) 
+ * + * @param executable If `true`, sets the access permission to allow execute operations; + * if `false` to disallow execute operations + * @return `true` if and only if the operation succeeded. The operation will fail if + * the user does not have permission to change the access permissions of this abstract + * pathname. If `executable` is `false` and the underlying file system + * does not implement an execute permission, then the operation will fail. + */ + fun setExecutable(executable: Boolean): Boolean { + return setExecutable(executable, true) + } + // Android-changed. Removed javadoc comment about special privileges + // that doesn't make sense on Android. + /** + * Tests whether the application can execute the file denoted by this abstract pathname. + * + * @return `true` if and only if the abstract pathname exists *and* the + * application is allowed to execute the file + */ + fun canExecute(contextProvider: ContextProvider): Boolean { + return fs.canExecute(this, contextProvider) + } + /* -- Filesystem interface -- */ // Android-changed: Replaced generic platform info with Android specific one. + /* -- Disk usage -- */ + /** + * Returns the size of the partition [named](#partName) by this abstract pathname. + * + * @return The size, in bytes, of the partition or 0L if this abstract pathname does not + * name a partition If there is no way to determine, total space is -1 + */ + fun getTotalSpace(contextProvider: ContextProvider): Long { + return try { + fs.getTotalSpace(this, contextProvider) + } catch (e: NotImplementedError) { + Log.w(TAG, "Call to unimplemented fuction", e) + -1 + } + } + + /** + * Returns the number of unallocated bytes in the partition [named](#partName) by this + * abstract path name. + * + * + * The returned number of unallocated bytes is a hint, but not a guarantee, that it is possible + * to use most or any of these bytes. The number of unallocated bytes is most likely to be + * accurate immediately after this call. It is likely to be made inaccurate by any external I/O + * operations including those made on the system outside of this virtual machine. This method + * makes no guarantee that write operations to this file system will succeed. + * + * @return The number of unallocated bytes on the partition or 0L if the abstract + * pathname does not name a partition. This value will be less than or equal to the total file + * system size returned by [.getTotalSpace]. + */ + val freeSpace: Long + get() = fs.getFreeSpace(this) + // Android-added: Replaced generic platform info with Android specific one. + /** + * Returns the number of bytes available to this virtual machine on the partition [named](#partName) by this abstract pathname. When possible, this method checks for + * write permissions and other operating system restrictions and will therefore usually provide a + * more accurate estimate of how much new data can actually be written than [.getFreeSpace]. + * + * + * The returned number of available bytes is a hint, but not a guarantee, that it is possible + * to use most or any of these bytes. The number of unallocated bytes is most likely to be + * accurate immediately after this call. It is likely to be made inaccurate by any external I/O + * operations including those made on the system outside of this virtual machine. This method + * makes no guarantee that write operations to this file system will succeed. + * + * + * On Android (and other Unix-based systems), this method returns the number of free bytes + * available to non-root users, regardless of whether you're actually running as root, and + * regardless of any quota or other restrictions that might apply to the user. (The `getFreeSpace` method returns the number of bytes potentially available to root.) + * + * @return The number of available bytes on the partition or 0L if the abstract pathname + * does not name a partition. On systems where this information is not available, this method + * will be equivalent to a call to [.getFreeSpace]. If there is no way to determine the + * current space left -1 is returned. + */ + val usableSpace: Long + get() = try { + fs.getUsableSpace(this) + } catch (e: NotImplementedError) { + Log.w(TAG, "Call to unimplemented fuction", e) + -1 + } + + /* -- Basic infrastructure -- */ + /** + * Compares two abstract pathnames lexicographically. The ordering defined by this method depends + * upon the underlying system. On UNIX systems, alphabetic case is significant in comparing + * pathnames; on Microsoft Windows systems it is not. + * + * @param pathname The abstract pathname to be compared to this abstract pathname + * @return Zero if the argument is equal to this abstract pathname, a value less than zero if this + * abstract pathname is lexicographically less than the argument, or a value greater than zero + * if this abstract pathname is lexicographically greater than the argument + */ + override fun compareTo(pathname: AmazeFile?): Int { + return fs.compare(this, pathname!!) + } + + /** + * Tests this abstract pathname for equality with the given object. Returns `true` if + * and only if the argument is not `null` and is an abstract pathname that denotes the + * same file or directory as this abstract pathname. Whether or not two abstract pathnames are + * equal depends upon the underlying system. On UNIX systems, alphabetic case is significant in + * comparing pathnames; on Microsoft Windows systems it is not. + * + * @param obj The object to be compared with this abstract pathname + * @return `true` if and only if the objects are the same; `false` otherwise + */ + override fun equals(obj: Any?): Boolean { + return if (obj is AmazeFile) { + compareTo(obj as AmazeFile?) == 0 + } else false + } + + /** + * Computes a hash code for this abstract pathname. Because equality of abstract pathnames is + * inherently system-dependent, so is the computation of their hash codes. On UNIX systems, the + * hash code of an abstract pathname is equal to the exclusive *or* of the hash code of its + * pathname string and the decimal value `1234321`. On Microsoft Windows systems, the + * hash code is equal to the exclusive *or* of the hash code of its pathname string + * converted to lower case and the decimal value `1234321`. Locale is not taken into + * account on lowercasing the pathname string. + * + * @return A hash code for this abstract pathname + */ + override fun hashCode(): Int { + return fs.hashCode(this) + } + + /** + * Returns the pathname string of this abstract pathname. This is just the string returned by the + * `[.getPath]` method. + * + * @return The string form of this abstract pathname + */ + override fun toString(): String { + return path + } +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFileFilter.java b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFileFilter.java new file mode 100644 index 0000000000..66d75635d7 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFileFilter.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014-2013 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes; + +import java.io.File; + +/** + * A filter for abstract pathnames. + * + *

Instances of this interface may be passed to the {@link + * File#listFiles(java.io.FileFilter) listFiles(FileFilter)} method of the + * {@link java.io.File} class. + * + * @since 1.2 + */ +@FunctionalInterface +public interface AmazeFileFilter { + + /** + * 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(AmazeFile pathname); +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFilenameFilter.java b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFilenameFilter.java new file mode 100644 index 0000000000..899f2a1fc7 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFilenameFilter.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014-2013 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes; + +// Android-changed: Removed @see tag (target does not exist on Android): +// @see java.awt.FileDialog#setFilenameFilter(java.io.FilenameFilter) + +/** + * Instances of classes that implement this interface are used to filter filenames. These instances + * are used to filter directory listings in the list method of class File, + * and by the Abstract Window Toolkit's file dialog component. + * + * @author Arthur van Hoff + * @author Jonathan Payne + * @see java.io.File + * @see java.io.File#list(java.io.FilenameFilter) + * @since JDK1.0 + */ +@FunctionalInterface +public interface AmazeFilenameFilter { + /** + * 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(AmazeFile dir, String name); +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFilesystem.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFilesystem.kt new file mode 100644 index 0000000000..036d25639c --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/AmazeFilesystem.kt @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2014-2014 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes + +import android.net.Uri +import android.util.Log +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.lang.annotation.Native +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import kotlin.experimental.and + +abstract class AmazeFilesystem { + + /* -- Normalization and construction -- */ + /** filesystem prefix */ + abstract val prefix: String + + open val nomediaFileEnabled: Boolean = false + + /** Is the path of this filesystem? */ + open fun isPathOfThisFilesystem(path: String): Boolean { + return path.startsWith(prefix) + } + + open fun getSeparator(): Char = STANDARD_SEPARATOR + + /** + * Convert the given pathname string to normal form. If the string is already in normal form then + * it is simply returned. + */ + abstract fun normalize(path: String): String + + /** + * Convert the given [AmazeFile]'s path to human readable. Override if file has a weird path, + * as if it has an IP or username. + */ + open fun getHumanReadablePath(f: AmazeFile): String { + return f.path + } + + /** + * Convert the given [AmazeFile]'s path to an [Uri]. + */ + open fun getUriForFile(f: AmazeFile, contextProvider: ContextProvider): Uri? { + Log.e(TAG, "get Uri failed for file") + return null + } + + /** + * Compute the length of this pathname string's prefix. The pathname string must be in normal + * form. + */ + open fun prefixLength(path: String): Int { + return prefix.length + } + + /** + * Resolve the child pathname string against the parent. Both strings must be in normal form, and + * the result will be in normal form. + */ + abstract fun resolve(parent: String, child: String): String + + /** + * Return the parent pathname string to be used when the parent-directory argument in one of the + * two-argument File constructors is the empty pathname. + */ + abstract val defaultParent: String + /* -- Path operations -- */ + /** Tell whether or not the given abstract pathname is absolute. */ + abstract fun isAbsolute(f: AmazeFile): Boolean + + /** + * Resolve the given abstract pathname into absolute form. Invoked by the getAbsolutePath and + * getCanonicalPath methods in the [AmazeFile] class. + */ + abstract fun resolve(f: AmazeFile): String + + @Throws(IOException::class) + abstract fun canonicalize(path: String?): String + + /** + * Return the simple boolean attributes for the file or directory denoted by the given abstract + * pathname, or zero if it does not exist or some other I/O error occurs. + */ + fun getBooleanAttributes(f: AmazeFile, contextProvider: ContextProvider): Int { + val file = File(f.path) + var r = 0 + if (exists(f, contextProvider)) { + r = r or BA_EXISTS + if (isFile(f, contextProvider)) { + r = r or BA_REGULAR + } + if (isDirectory(f, contextProvider)) { + r = r or BA_DIRECTORY + } + if (isHidden(f)) { + r = r or BA_HIDDEN + } + } + return r + } + + abstract fun exists(f: AmazeFile, contextProvider: ContextProvider): Boolean + abstract fun isFile(f: AmazeFile, contextProvider: ContextProvider): Boolean + abstract fun isDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean + abstract fun isHidden(f: AmazeFile): Boolean + + /** + * Check whether the file or directory denoted by the given abstract pathname may be accessed by + * this process. Return false if access is denied or an I/O error occurs + */ + abstract fun canExecute(f: AmazeFile, contextProvider: ContextProvider): Boolean + + /** + * Check whether the file or directory denoted by the given abstract pathname may be accessed by + * this process. Return false if access is denied or an I/O error occurs + */ + abstract fun canWrite(f: AmazeFile, contextProvider: ContextProvider): Boolean + + /** + * Check whether the file or directory denoted by the given abstract pathname may be accessed by + * this process. Return false if access is denied or an I/O error occurs + */ + abstract fun canRead(f: AmazeFile, contextProvider: ContextProvider): Boolean + + /** + * Check whether the file or directory denoted by the given abstract pathname may be accessed by + * this process. Return false if access is denied or an I/O error occurs + * + * Android-added: b/25878034, to support F.exists() reimplementation. + */ + abstract fun canAccess(f: AmazeFile, contextProvider: ContextProvider): Boolean + + /** + * Set on or off the access permission (to owner only or to all) to the file or directory denoted + * by the given abstract pathname, based on the parameters enable, access and oweronly. + */ + abstract fun setExecutable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean + + /** + * Set on or off the access permission (to owner only or to all) to the file or directory denoted + * by the given abstract pathname, based on the parameters enable, access and oweronly. + */ + abstract fun setWritable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean + + /** + * Set on or off the access permission (to owner only or to all) to the file or directory denoted + * by the given abstract pathname, based on the parameters enable, access and oweronly. + */ + abstract fun setReadable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean + + /** + * Return the time at which the file or directory denoted by the given abstract pathname was last + * modified, or zero if it does not exist or some other I/O error occurs. + */ + abstract fun getLastModifiedTime(f: AmazeFile): Long + + /** + * Computes the hash MD5 of a file + */ + open fun getHashMD5(f: AmazeFile, contextProvider: ContextProvider): String { + val fis: InputStream = f.getInputStream(contextProvider) + val buffer = ByteArray(8192) + val complete = MessageDigest.getInstance("MD5") + var numRead: Int + do { + numRead = fis.read(buffer) + if (numRead > 0) { + complete.update(buffer, 0, numRead) + } + } while (numRead != -1) + fis.close() + + val b = complete.digest() + var result = "" + for (aB in b) { + result += Integer.toString((aB.and(127)) + 0x100, 16).substring(1) + } + return result + } + + /** + * Computes the hash SHA256 of a file + */ + open fun getHashSHA256(f: AmazeFile, contextProvider: ContextProvider): String { + val DEFAULT_BUFFER_SIZE = 8192 + + val messageDigest = MessageDigest.getInstance("SHA-256") + val input = ByteArray(DEFAULT_BUFFER_SIZE) + var length: Int + val inputStream: InputStream = f.getInputStream(contextProvider) + while (inputStream.read(input).also { length = it } != -1) { + if (length > 0) messageDigest.update(input, 0, length) + } + val hash = messageDigest.digest() + val hexString = StringBuilder() + for (aHash in hash) { + // convert hash to base 16 + val hex = Integer.toHexString(0xff and aHash.toInt()) + if (hex.length == 1) hexString.append('0') + hexString.append(hex) + } + inputStream.close() + return hexString.toString() + } + + /** + * Return the length in bytes of the file denoted by the given abstract pathname, or zero if it + * does not exist, or some other I/O error occurs. + * + * + * Note: for directories, this *could* return the size + */ + @Throws(IOException::class) + abstract fun getLength(f: AmazeFile, contextProvider: ContextProvider): Long + /* -- File operations -- */ + /** + * Create a new empty file with the given pathname. Return `true` if the file was + * created and `false` if a file or directory with the given pathname already exists. + * Throw an IOException if an I/O error occurs. + */ + @Throws(IOException::class) + abstract fun createFileExclusively(pathname: String): Boolean + + /** + * Delete the file or directory denoted by the given abstract pathname, returning `true + ` * if and only if the operation succeeds. + */ + abstract fun delete(f: AmazeFile, contextProvider: ContextProvider): Boolean + + /** + * List the elements of the directory denoted by the given abstract pathname. Return an array of + * strings naming the elements of the directory if successful; otherwise, return `null` + * . + */ + abstract fun list(f: AmazeFile, contextProvider: ContextProvider): Array? + abstract fun getInputStream(f: AmazeFile, contextProvider: ContextProvider): InputStream? + abstract fun getOutputStream( + f: AmazeFile, + contextProvider: ContextProvider + ): OutputStream? + + /** + * Create a new directory denoted by the given abstract pathname, returning `true` if + * and only if the operation succeeds. + */ + abstract fun createDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean + + /** + * Rename the file or directory denoted by the first abstract pathname to the second abstract + * pathname, returning `true` if and only if the operation succeeds. + */ + abstract fun rename( + file1: AmazeFile, + file2: AmazeFile, + contextProvider: ContextProvider + ): Boolean + + /** + * Set the last-modified time of the file or directory denoted by the given abstract pathname, + * returning `true` if and only if the operation succeeds. + */ + abstract fun setLastModifiedTime(f: AmazeFile, time: Long): Boolean + + /** + * Mark the file or directory denoted by the given abstract pathname as read-only, returning + * `true` if and only if the operation succeeds. + */ + abstract fun setReadOnly(f: AmazeFile): Boolean + protected fun removePrefix(path: String): String { + return path.substring(prefixLength(path)) + } + /* -- Filesystem interface -- */ + abstract fun getTotalSpace(f: AmazeFile, contextProvider: ContextProvider): Long + abstract fun getFreeSpace(f: AmazeFile): Long + abstract fun getUsableSpace(f: AmazeFile): Long + /* -- Basic infrastructure -- */ + /** Compare two abstract pathnames lexicographically. */ + open fun compare(f1: AmazeFile, f2: AmazeFile): Int { + return f1.path.compareTo(f2.path) + } + + /** Compute the hash code of an abstract pathname. */ + open fun hashCode(f: AmazeFile): Int { + return basicUnixHashCode(f.path) + } + + companion object { + private val TAG = AmazeFilesystem::class.java.simpleName + + /** Return the local filesystem's name-separator character. */ + const val STANDARD_SEPARATOR = '/' + + /* -- Attribute accessors -- */ /* Constants for simple boolean attributes */ + @JvmField + @Native + val BA_EXISTS = 0x01 + + @JvmField + @Native + val BA_REGULAR = 0x02 + + @JvmField + @Native + val BA_DIRECTORY = 0x04 + + @JvmField + @Native + val BA_HIDDEN = 0x08 + + // Flags for enabling/disabling performance optimizations for file + // name canonicalization + // Android-changed: Disabled caches for security reasons (b/62301183) + // static boolean useCanonCaches = true; + // static boolean useCanonPrefixCache = true; + var useCanonCaches = false + var useCanonPrefixCache = false + private fun getBooleanProperty(prop: String, defaultVal: Boolean): Boolean { + val `val` = System.getProperty(prop) ?: return defaultVal + return `val`.equals("true", ignoreCase = true) + } + + /* + * Copyright (c) 1998, 2010, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + * A normal Unix pathname does not contain consecutive slashes and does not end + * with a slash. The empty string and "/" are special cases that are also + * considered normal. + */ + @JvmStatic + fun simpleUnixNormalize(pathname: String): String { + val n = pathname.length + val normalized = pathname.toCharArray() + var index = 0 + var prevChar = 0.toChar() + for (i in 0 until n) { + val current = normalized[i] + // Remove duplicate slashes. + if (!(current == '/' && prevChar == '/')) { + normalized[index++] = current + } + prevChar = current + } + + // Omit the trailing slash, except when pathname == "/". + if (prevChar == '/' && n > 1) { + index-- + } + return if (index != n) String(normalized, 0, index) else pathname + } + + /* + * Copyright (c) 1998, 2010, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + // Invariant: Both |parent| and |child| are normalized paths. + @JvmStatic + fun basicUnixResolve(parent: String, child: String): String { + if (child.isEmpty() || child == "/") { + return parent + } + if (child[0] == '/') { + return if (parent == "/") { + child + } else parent + child + } + return if (parent == "/") { + parent + child + } else "$parent/$child" + } + + fun basicUnixHashCode(path: String): Int { + return path.hashCode() xor 1234321 + } + + init { + useCanonCaches = + getBooleanProperty("sun.io.useCanonCaches", useCanonCaches) + useCanonPrefixCache = + getBooleanProperty("sun.io.useCanonPrefixCache", useCanonPrefixCache) + } + } +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/ContextProvider.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/ContextProvider.kt new file mode 100644 index 0000000000..6f7e975a80 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/ContextProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes + +import android.content.Context + +interface ContextProvider { + /** + * This *must* be thread safe. + * There are no guarantees on *when* this function is called, + * it must return a non null ref to [Context] or crash + */ + fun getContext(): Context? +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/OnAmazeFileFound.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/OnAmazeFileFound.kt new file mode 100644 index 0000000000..b1f4508d41 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/OnAmazeFileFound.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes + +/** + * This allows the caller of a function to know when a file has ben found and deal with it ASAP + */ +interface OnAmazeFileFound { + @Suppress("UndocumentedPublicFunction") + fun onFileFound(file: AmazeFile) +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/Account.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/Account.kt new file mode 100644 index 0000000000..86bbfef698 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/Account.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.cloud + +import com.cloudrail.si.interfaces.CloudStorage + +abstract class Account { + companion object { + val accounts = mutableListOf() + } + + var account: CloudStorage? = null + protected set + + fun add(storage: CloudStorage) { + account = storage + accounts.add(this) + } + + fun removeAccount() { + account = null + accounts.remove(this) + } +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/CloudAmazeFilesystem.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/CloudAmazeFilesystem.kt new file mode 100644 index 0000000000..711aaa39bd --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/CloudAmazeFilesystem.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.cloud + +import android.util.Log +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFilesystem +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider +import com.cloudrail.si.types.CloudMetaData +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.lang.IllegalArgumentException +import java.util.* + +abstract class CloudAmazeFilesystem : AmazeFilesystem() { + abstract val account: Account + override fun prefixLength(path: String): Int { + require(path.isNotEmpty()) { + "This should never happen, all paths must start with OTG prefix" + } + return super.prefixLength(path) + } + + override fun normalize(path: String): String { + val canonical: String + canonical = try { + canonicalize(path) + } catch (e: IOException) { + Log.e(TAG, "Error getting Dropbox file canonical path", e) + "$path/" + } + return canonical.substring(0, canonical.length - 1) + } + + override fun resolve(parent: String, child: String): String { + return prefix + File(removePrefix(parent!!), child) + } + + override val defaultParent: String + get() = "$prefix/" + + override fun isAbsolute(f: AmazeFile): Boolean { + return true // We don't accept relative paths for cloud + } + + override fun resolve(f: AmazeFile): String { + if (isAbsolute(f)) { + return f.path + } + throw IllegalArgumentException("Relative paths are not supported") + } + + @Throws(IOException::class) + override fun canonicalize(path: String?): String { + return prefix + File(removePrefix(path!!)).canonicalPath + } + + override fun exists(f: AmazeFile, contextProvider: ContextProvider): Boolean { + Objects.requireNonNull(account) + val noPrefixPath = removePrefix(f.path) + return account.account?.exists(noPrefixPath) ?: false + } + + override fun isFile(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return true // all files are regular (probably) + } + + override fun isDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val noPrefixPath = removePrefix(f.path) + val metadata: CloudMetaData? = account.account?.getMetadata(noPrefixPath) + return metadata?.folder ?: false + } + + override fun isHidden(f: AmazeFile): Boolean { + return false // No way to know if its hidden + } + + override fun canExecute(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return false // You aren't executing anything at the cloud + } + + override fun canWrite(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return true // Probably, can't check + } + + override fun canRead(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return true // Probably, can't check + } + + override fun canAccess(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val account = account.account + Objects.requireNonNull(account) + val noPrefixPath = removePrefix(f.path) + return account!!.exists(noPrefixPath) + } + + override fun setExecutable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + throw NotImplementedError() + } + + override fun setWritable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + throw NotImplementedError() + } + + override fun setReadable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + throw NotImplementedError() + } + + override fun getLastModifiedTime(f: AmazeFile): Long { + val account = account.account + Objects.requireNonNull(account) + val noPrefixPath = removePrefix(f.path) + // TODO check that this actually returns seconds since epoch + return account!!.getMetadata(noPrefixPath).contentModifiedAt + } + + @Throws(IOException::class) + override fun getLength(f: AmazeFile, contextProvider: ContextProvider): Long { + if (f.isDirectory(contextProvider)) { + return 0 + } + val account = account.account + Objects.requireNonNull(account) + val noPrefixPath = removePrefix(f.path) + return account!!.getMetadata(noPrefixPath).size.toLong() + } + + @Throws(IOException::class) + override fun createFileExclusively(pathname: String): Boolean { + return false + } + + override fun delete(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val account = account.account + Objects.requireNonNull(account) + val noPrefixPath = removePrefix(f.path) + account!!.delete(noPrefixPath) + return true // This seems to never fail + } + + override fun list(f: AmazeFile, contextProvider: ContextProvider): Array? { + val account = account.account ?: return null + val noPrefixPath = removePrefix(f.path) + val metadatas = account.getChildren(noPrefixPath) + return Array(metadatas.size) { i: Int -> + normalize(prefix + metadatas[i].path) + } + } + + override fun getInputStream(f: AmazeFile, contextProvider: ContextProvider): InputStream? { + val account = account.account + Objects.requireNonNull(account) + val noPrefixPath = removePrefix(f.path) + return account!!.download(noPrefixPath) + } + + override fun getOutputStream(f: AmazeFile, contextProvider: ContextProvider): OutputStream? { + throw NotImplementedError() + } + + override fun createDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean { + val account = account.account ?: return false + val noPrefixPath = removePrefix(f.path) + account.createFolder(noPrefixPath) + return true // This seems to never fail + } + + override fun rename( + file1: AmazeFile, + file2: AmazeFile, + contextProvider: ContextProvider + ): Boolean { + val account = account.account ?: return false + account.move(removePrefix(file1.path), removePrefix(file2.path)) + return true // This seems to never fail + } + + override fun setLastModifiedTime(f: AmazeFile, time: Long): Boolean { + val account = account.account ?: return false + val noPrefixPath = removePrefix(f.path) + // TODO check that this actually returns seconds since epoch + account.getMetadata(noPrefixPath).contentModifiedAt = time + return true // This seems to never fail + } + + override fun setReadOnly(f: AmazeFile): Boolean { + return false // This doesn't seem possible + } + + override fun getTotalSpace(f: AmazeFile, contextProvider: ContextProvider): Long { + val account = account.account ?: return 0 + val spaceAllocation = account.allocation + return spaceAllocation.total + } + + override fun getFreeSpace(f: AmazeFile): Long { + val account = account.account ?: return 0 + val spaceAllocation = account.allocation + return spaceAllocation.total - spaceAllocation.used + } + + override fun getUsableSpace(f: AmazeFile): Long { + val account = account.account ?: return 0 + val spaceAllocation = account.allocation + return spaceAllocation.total - spaceAllocation.used + } + + companion object { + val TAG = CloudAmazeFilesystem::class.java.simpleName + } +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/box/BoxAccount.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/box/BoxAccount.kt new file mode 100644 index 0000000000..29ad8bce4b --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/box/BoxAccount.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.box + +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.Account + +object BoxAccount : Account() diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/box/BoxAmazeFilesystem.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/box/BoxAmazeFilesystem.kt new file mode 100644 index 0000000000..3b0ac14310 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/box/BoxAmazeFilesystem.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.box + +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.Account +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.CloudAmazeFilesystem + +object BoxAmazeFilesystem : CloudAmazeFilesystem() { + @JvmStatic + val TAG = BoxAmazeFilesystem::class.java.simpleName + + const val PREFIX = "box:/" + + init { + AmazeFile.addFilesystem(this) + } + + override val prefix: String = PREFIX + + override val account: Account + get() = BoxAccount +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/dropbox/DropboxAccount.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/dropbox/DropboxAccount.kt new file mode 100644 index 0000000000..877ed8b2c4 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/dropbox/DropboxAccount.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.dropbox + +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.Account + +object DropboxAccount : Account() diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/dropbox/DropboxAmazeFilesystem.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/dropbox/DropboxAmazeFilesystem.kt new file mode 100644 index 0000000000..299acacc0a --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/dropbox/DropboxAmazeFilesystem.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.dropbox + +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.Account +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.CloudAmazeFilesystem + +object DropboxAmazeFilesystem : CloudAmazeFilesystem() { + @JvmStatic + val TAG = DropboxAmazeFilesystem::class.java.simpleName + const val PREFIX = "dropbox:/" + + init { + AmazeFile.addFilesystem(this) + } + + override val prefix: String = PREFIX + + override val account: Account + get() = DropboxAccount +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/gdrive/GoogledriveAccount.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/gdrive/GoogledriveAccount.kt new file mode 100644 index 0000000000..4a3b2273fb --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/gdrive/GoogledriveAccount.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.gdrive + +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.Account + +object GoogledriveAccount : Account() diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/gdrive/GoogledriveAmazeFilesystem.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/gdrive/GoogledriveAmazeFilesystem.kt new file mode 100644 index 0000000000..6a30761b77 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/gdrive/GoogledriveAmazeFilesystem.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.gdrive + +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.Account +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.CloudAmazeFilesystem + +object GoogledriveAmazeFilesystem : CloudAmazeFilesystem() { + @JvmStatic + val TAG = GoogledriveAmazeFilesystem::class.java.simpleName + const val PREFIX = "gdrive:/" + + init { + AmazeFile.addFilesystem(this) + } + + override val prefix = PREFIX + + override val account: Account + get() = GoogledriveAccount +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/onedrive/OnedriveAccount.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/onedrive/OnedriveAccount.kt new file mode 100644 index 0000000000..e99dada65f --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/onedrive/OnedriveAccount.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.onedrive + +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.Account + +object OnedriveAccount : Account() diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/onedrive/OnedriveAmazeFilesystem.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/onedrive/OnedriveAmazeFilesystem.kt new file mode 100644 index 0000000000..d221bb5d83 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/cloud/onedrive/OnedriveAmazeFilesystem.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.onedrive + +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.Account +import com.amaze.filemanager.file_operations.filesystem.filetypes.cloud.CloudAmazeFilesystem + +object OnedriveAmazeFilesystem : CloudAmazeFilesystem() { + @JvmStatic + val TAG = OnedriveAmazeFilesystem::class.java.simpleName + const val PREFIX = "onedrive:/" + + init { + AmazeFile.addFilesystem(this) + } + + override val prefix: String = PREFIX + + override val account: Account + get() = OnedriveAccount +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/file/ExternalSdCardOperation.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/file/ExternalSdCardOperation.kt new file mode 100644 index 0000000000..b4c0da242d --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/file/ExternalSdCardOperation.kt @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.file + +import android.annotation.TargetApi +import android.content.Context +import android.net.Uri +import android.os.Build +import android.util.Log +import androidx.documentfile.provider.DocumentFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import java.io.File +import java.io.IOException +import java.util.* + +object ExternalSdCardOperation { + val TAG = ExternalSdCardOperation::class.java.simpleName + + /** + * Get a DocumentFile corresponding to the given file (for writing on ExtSdCard on Android 5). If + * the file is not existing, it is created. + * + * @param file The file. + * @param isDirectory flag indicating if the file should be a directory. + * @return The DocumentFile + */ + @JvmStatic + fun getDocumentFile( + file: AmazeFile, + isDirectory: Boolean, + context: Context, + preferenceUri: String? + ): DocumentFile? { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + return DocumentFile.fromFile(File(file.path)) + } + val baseFolder = getExtSdCardFolder(file, context) + var originalDirectory = false + if (baseFolder == null) { + return null + } + var relativePath: String? = null + try { + val fullPath = file.canonicalPath + if (baseFolder != fullPath) { + relativePath = fullPath.substring(baseFolder.length + 1) + } else { + originalDirectory = true + } + } catch (e: IOException) { + return null + } + + var treeUri: Uri? = null + if (preferenceUri != null) { + treeUri = Uri.parse(preferenceUri) + } + if (treeUri == null) { + return null + } + + // start with root of SD card and then parse through document tree. + var document = DocumentFile.fromTreeUri(context, treeUri) + if (originalDirectory || relativePath == null) { + return document + } + + val parts = relativePath.split("/").toTypedArray() + for (i in parts.indices) { + if (document == null) { + return null + } + + var nextDocument = document.findFile(parts[i]) + if (nextDocument == null) { + nextDocument = if (i < parts.size - 1 || isDirectory) { + document.createDirectory(parts[i]) + } else { + document.createFile("image", parts[i]) + } + } + document = nextDocument + } + + return document + } + + /** + * Get a list of external SD card paths. (Kitkat or higher.) + * + * @return A list of external SD card paths. + */ + @JvmStatic + @TargetApi(Build.VERSION_CODES.KITKAT) + private fun getExtSdCardPaths(context: Context): Array { + val paths: MutableList = ArrayList() + for (file in context.getExternalFilesDirs("external")) { + if (file != null && file != context.getExternalFilesDir("external")) { + val index = file.absolutePath.lastIndexOf("/Android/data") + if (index < 0) { + Log.w(TAG, "Unexpected external file dir: " + file.absolutePath) + } else { + var path = file.absolutePath.substring(0, index) + try { + path = File(path).canonicalPath + } catch (e: IOException) { + // Keep non-canonical path. + } + paths.add(path) + } + } + } + if (paths.isEmpty()) paths.add("/storage/sdcard1") + return paths.toTypedArray() + } + + @JvmStatic + @TargetApi(Build.VERSION_CODES.KITKAT) + fun getExtSdCardPathsForActivity(context: Context): Array { + val paths: MutableList = ArrayList() + for (file in context.getExternalFilesDirs("external")) { + if (file != null) { + val index = file.absolutePath.lastIndexOf("/Android/data") + if (index < 0) { + Log.w(TAG, "Unexpected external file dir: " + file.absolutePath) + } else { + var path = file.absolutePath.substring(0, index) + try { + path = File(path).canonicalPath + } catch (e: IOException) { + // Keep non-canonical path. + } + paths.add(path) + } + } + } + if (paths.isEmpty()) paths.add("/storage/sdcard1") + return paths.toTypedArray() + } + + /** + * Determine the main folder of the external SD card containing the given file. + * + * @param file the file. + * @return The main folder of the external SD card containing this file, if the file is on an SD + * card. Otherwise, null is returned. + */ + @JvmStatic + @TargetApi(Build.VERSION_CODES.KITKAT) + public fun getExtSdCardFolder(file: AmazeFile, context: Context): String? { + val extSdPaths = getExtSdCardPaths(context) + try { + for (i in extSdPaths.indices) { + if (file.canonicalPath.startsWith(extSdPaths[i])) { + return extSdPaths[i] + } + } + } catch (e: IOException) { + return null + } + return null + } + + /** + * Determine if a file is on external sd card. (Kitkat or higher.) + * + * @param file The file. + * @return true if on external sd card. + */ + @JvmStatic + @TargetApi(Build.VERSION_CODES.KITKAT) + fun isOnExtSdCard(file: AmazeFile, c: Context): Boolean { + return getExtSdCardFolder(file, c) != null + } +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/file/MediaStoreHack.java b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/file/MediaStoreHack.java new file mode 100644 index 0000000000..aec2325300 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/file/MediaStoreHack.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2014-2020 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.file; + +/** Created by Arpit on 29-06-2015. */ +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Locale; + +import com.amaze.filemanager.file_operations.R; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.BaseColumns; +import android.provider.MediaStore; +import android.util.Log; + +import androidx.annotation.Nullable; + +/** + * Wrapper for manipulating files via the Android Media Content Provider. As of Android 4.4 KitKat, + * applications can no longer write to the "secondary storage" of a device. Write operations using + * the java.io.File API will thus fail. This class restores access to those write operations by way + * of the Media Content Provider. Note that this class relies on the internal operational + * characteristics of the media content provider API, and as such is not guaranteed to be + * future-proof. Then again, we did all think the java.io.File API was going to be future-proof for + * media card access, so all bets are off. If you're forced to use this class, it's because + * Google/AOSP made a very poor API decision in Android 4.4 KitKat. Read more at + * https://plus.google.com/+TodLiebeck/posts/gjnmuaDM8sn Your application must declare the + * permission "android.permission.WRITE_EXTERNAL_STORAGE". Adapted from: + * http://forum.xda-developers.com/showpost.php?p=52151865&postcount=20 + * + * @author Jared Rummler + */ +public class MediaStoreHack { + private static final String TAG = "MediaStoreHack"; + + private static final String ALBUM_ART_URI = "content://media/external/audio/albumart"; + + private static final String[] ALBUM_PROJECTION = { + BaseColumns._ID, MediaStore.Audio.AlbumColumns.ALBUM_ID, "media_type" + }; + + /** + * Deletes the file. Returns true if the file has been successfully deleted or otherwise does not + * exist. This operation is not recursive. + */ + public static boolean delete(final Context context, final File file) { + final String where = MediaStore.MediaColumns.DATA + "=?"; + final String[] selectionArgs = new String[] {file.getAbsolutePath()}; + final ContentResolver contentResolver = context.getContentResolver(); + final Uri filesUri = MediaStore.Files.getContentUri("external"); + // Delete the entry from the media database. This will actually delete media files. + contentResolver.delete(filesUri, where, selectionArgs); + // If the file is not a media file, create a new entry. + if (file.exists()) { + final ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath()); + contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + // Delete the created entry, such that content provider will delete the file. + contentResolver.delete(filesUri, where, selectionArgs); + } + return !file.exists(); + } + + private static File getExternalFilesDir(final Context context) { + return context.getExternalFilesDir(null); + } + + public static InputStream getInputStream( + final Context context, final File file, final long size) { + try { + final String where = MediaStore.MediaColumns.DATA + "=?"; + final String[] selectionArgs = new String[] {file.getAbsolutePath()}; + final ContentResolver contentResolver = context.getContentResolver(); + final Uri filesUri = MediaStore.Files.getContentUri("external"); + contentResolver.delete(filesUri, where, selectionArgs); + final ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath()); + values.put(MediaStore.MediaColumns.SIZE, size); + final Uri uri = contentResolver.insert(filesUri, values); + return contentResolver.openInputStream(uri); + } catch (final Throwable t) { + return null; + } + } + + public static OutputStream getOutputStream(Context context, String str) { + OutputStream outputStream = null; + Uri fileUri = getUriFromFile(str, context); + if (fileUri != null) { + try { + outputStream = context.getContentResolver().openOutputStream(fileUri); + } catch (Throwable th) { + } + } + return outputStream; + } + + /** + * Fallback to get uri from a path. Used only as a workaround for Kitkat ext SD card + * + * @param path file path + * @param context context + * @return uri of file or null if resolver.query fails + */ + public static @Nullable Uri getUriFromFile(final String path, Context context) { + ContentResolver resolver = context.getContentResolver(); + + Cursor filecursor = + resolver.query( + MediaStore.Files.getContentUri("external"), + new String[] {BaseColumns._ID}, + MediaStore.MediaColumns.DATA + " = ?", + new String[] {path}, + MediaStore.MediaColumns.DATE_ADDED + " desc"); + + if (filecursor == null) { + Log.e(TAG, "Error when deleting file " + path); + return null; + } + + filecursor.moveToFirst(); + + if (filecursor.isAfterLast()) { + filecursor.close(); + ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.DATA, path); + return resolver.insert(MediaStore.Files.getContentUri("external"), values); + } else { + int imageId = filecursor.getInt(filecursor.getColumnIndex(BaseColumns._ID)); + Uri uri = + MediaStore.Files.getContentUri("external") + .buildUpon() + .appendEncodedPath(Integer.toString(imageId)) + .build(); + filecursor.close(); + return uri; + } + } + + /** Returns an OutputStream to write to the file. The file will be truncated immediately. */ + private static int getTemporaryAlbumId(final Context context) { + final File temporaryTrack; + try { + temporaryTrack = installTemporaryTrack(context); + } catch (final IOException ex) { + Log.w(MediaStoreHack.TAG, "Error installing temporary track.", ex); + return 0; + } + final Uri filesUri = MediaStore.Files.getContentUri("external"); + final String[] selectionArgs = {temporaryTrack.getAbsolutePath()}; + final ContentResolver contentResolver = context.getContentResolver(); + Cursor cursor = + contentResolver.query( + filesUri, ALBUM_PROJECTION, MediaStore.MediaColumns.DATA + "=?", selectionArgs, null); + if (cursor == null || !cursor.moveToFirst()) { + if (cursor != null) { + cursor.close(); + cursor = null; + } + final ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.DATA, temporaryTrack.getAbsolutePath()); + values.put(MediaStore.MediaColumns.TITLE, "{MediaWrite Workaround}"); + values.put(MediaStore.MediaColumns.SIZE, temporaryTrack.length()); + values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/mpeg"); + values.put(MediaStore.Audio.AudioColumns.IS_MUSIC, true); + contentResolver.insert(filesUri, values); + } + cursor = + contentResolver.query( + filesUri, ALBUM_PROJECTION, MediaStore.MediaColumns.DATA + "=?", selectionArgs, null); + if (cursor == null) { + return 0; + } + if (!cursor.moveToFirst()) { + cursor.close(); + return 0; + } + final int id = cursor.getInt(0); + final int albumId = cursor.getInt(1); + final int mediaType = cursor.getInt(2); + cursor.close(); + final ContentValues values = new ContentValues(); + boolean updateRequired = false; + if (albumId == 0) { + values.put(MediaStore.Audio.AlbumColumns.ALBUM_ID, 13371337); + updateRequired = true; + } + if (mediaType != 2) { + values.put("media_type", 2); + updateRequired = true; + } + if (updateRequired) { + contentResolver.update(filesUri, values, BaseColumns._ID + "=" + id, null); + } + cursor = + contentResolver.query( + filesUri, ALBUM_PROJECTION, MediaStore.MediaColumns.DATA + "=?", selectionArgs, null); + if (cursor == null) { + return 0; + } + try { + if (!cursor.moveToFirst()) { + return 0; + } + return cursor.getInt(1); + } finally { + cursor.close(); + } + } + + private static File installTemporaryTrack(final Context context) throws IOException { + final File externalFilesDir = getExternalFilesDir(context); + if (externalFilesDir == null) { + return null; + } + final File temporaryTrack = new File(externalFilesDir, "temptrack.mp3"); + if (!temporaryTrack.exists()) { + InputStream in = null; + OutputStream out = null; + try { + in = context.getResources().openRawResource(R.raw.temptrack); + out = new FileOutputStream(temporaryTrack); + final byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + } finally { + out.close(); + in.close(); + } + } + return temporaryTrack; + } + + public static boolean mkdir(final Context context, final AmazeFile file) throws IOException { + if (file.exists(() -> context)) { + return file.isDirectory(() -> context); + } + final File tmpFile = new File(new File(file.getPath()), ".MediaWriteTemp"); + final int albumId = getTemporaryAlbumId(context); + if (albumId == 0) { + throw new IOException("Failed to create temporary album id."); + } + final Uri albumUri = Uri.parse(String.format(Locale.US, ALBUM_ART_URI + "/%d", albumId)); + final ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.DATA, tmpFile.getAbsolutePath()); + final ContentResolver contentResolver = context.getContentResolver(); + if (contentResolver.update(albumUri, values, null, null) == 0) { + values.put(MediaStore.Audio.AlbumColumns.ALBUM_ID, albumId); + contentResolver.insert(Uri.parse(ALBUM_ART_URI), values); + } + try { + final ParcelFileDescriptor fd = contentResolver.openFileDescriptor(albumUri, "r"); + fd.close(); + } finally { + delete(context, tmpFile); + } + return file.exists(() -> context); + } + + public static boolean mkfile(final Context context, final File file) { + final OutputStream outputStream = getOutputStream(context, file.getPath()); + if (outputStream == null) { + return false; + } + try { + outputStream.close(); + return true; + } catch (final IOException e) { + } + return false; + } +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/file/UriForSafPersistance.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/file/UriForSafPersistance.kt new file mode 100644 index 0000000000..4852a54c81 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/file/UriForSafPersistance.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.file + +import android.content.Context +import android.net.Uri +import androidx.preference.PreferenceManager + +object UriForSafPersistance { + const val PREFERENCE_URI = "URI" + + @JvmStatic + fun persist(context: Context, treeUri: Uri) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putString(PREFERENCE_URI, treeUri.toString()) + .apply() + } + + @JvmStatic + fun get(context: Context): String? { + return PreferenceManager.getDefaultSharedPreferences(context) + .getString(PREFERENCE_URI, null) + } +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/smb/CifsContexts.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/smb/CifsContexts.kt new file mode 100644 index 0000000000..b4b36375eb --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/smb/CifsContexts.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014-2020 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.smb + +import android.net.Uri +import android.text.TextUtils +import android.util.Log +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import jcifs.CIFSException +import jcifs.config.PropertyConfiguration +import jcifs.context.BaseContext +import jcifs.context.SingletonContext +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +object CifsContexts { + + const val SMB_URI_PREFIX = "smb://" + + private val TAG = CifsContexts::class.java.simpleName + + private val defaultProperties: Properties = Properties().apply { + setProperty("jcifs.resolveOrder", "BCAST") + setProperty("jcifs.smb.client.responseTimeout", "30000") + setProperty("jcifs.netbios.retryTimeout", "5000") + setProperty("jcifs.netbios.cachePolicy", "-1") + } + + private val contexts: MutableMap = ConcurrentHashMap() + + @JvmStatic + fun clearBaseContexts() { + contexts.forEach { + try { + it.value.close() + } catch (e: CIFSException) { + Log.w(TAG, "Error closing SMB connection", e) + } + } + contexts.clear() + } + + @JvmStatic + fun createWithDisableIpcSigningCheck( + basePath: String, + disableIpcSigningCheck: Boolean + ): BaseContext { + return if (disableIpcSigningCheck) { + val extraProperties = Properties() + extraProperties["jcifs.smb.client.ipcSigningEnforced"] = "false" + create(basePath, extraProperties) + } else { + create(basePath, null) + } + } + + @JvmStatic + fun create(basePath: String, extraProperties: Properties?): BaseContext { + val basePathKey: String = Uri.parse(basePath).run { + val prefix = "$scheme://$authority" + val suffix = if (TextUtils.isEmpty(query)) "" else "?$query" + "$prefix$suffix" + } + return if (contexts.containsKey(basePathKey)) { + contexts.getValue(basePathKey) + } else { + val context = Single.fromCallable { + try { + val p = Properties(defaultProperties) + if (extraProperties != null) p.putAll(extraProperties) + BaseContext(PropertyConfiguration(p)) + } catch (e: CIFSException) { + Log.e(TAG, "Error initialize jcifs BaseContext, returning default", e) + SingletonContext.getInstance() + } + }.subscribeOn(Schedulers.io()) + .blockingGet() + contexts[basePathKey] = context + context + } + } +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/smb/SmbAmazeFilesystem.kt b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/smb/SmbAmazeFilesystem.kt new file mode 100644 index 0000000000..e02e80d004 --- /dev/null +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/filetypes/smb/SmbAmazeFilesystem.kt @@ -0,0 +1,437 @@ +/* + * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.file_operations.filesystem.filetypes.smb + +import android.net.Uri +import android.text.TextUtils +import android.util.Log +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFilesystem +import com.amaze.filemanager.file_operations.filesystem.filetypes.ContextProvider +import com.amaze.filemanager.file_operations.filesystem.filetypes.smb.CifsContexts.SMB_URI_PREFIX +import com.amaze.filemanager.file_operations.filesystem.filetypes.smb.CifsContexts.createWithDisableIpcSigningCheck +import jcifs.SmbConstants +import jcifs.smb.NtlmPasswordAuthenticator +import jcifs.smb.SmbException +import jcifs.smb.SmbFile +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.lang.Boolean.parseBoolean +import java.net.MalformedURLException +import java.util.regex.Pattern + +/** + * Root is "smb://:@" or "smb://" or + * "smb://:@/?disableIpcSigningCheck=true" or + * "smb:///?disableIpcSigningCheck=true" + * Relative paths are not supported + * + */ +object SmbAmazeFilesystem : AmazeFilesystem() { + @JvmStatic + val TAG = SmbAmazeFilesystem::class.java.simpleName + + const val PARAM_DISABLE_IPC_SIGNING_CHECK = "disableIpcSigningCheck" + @JvmStatic + private val IPv4_PATTERN = Pattern.compile("[0-9]{1,3}+.[0-9]{1,3}+.[0-9]{1,3}+.[0-9]{1,3}+") + @JvmStatic + private val METADATA_PATTERN = Pattern.compile("([?][a-zA-Z]+=(\")?[a-zA-Z]+(\")?)+") + + @JvmStatic + @Throws(MalformedURLException::class) + fun create(path: String?): SmbFile { + val processedPath: String + processedPath = if (!path!!.endsWith(STANDARD_SEPARATOR + "")) { + path + STANDARD_SEPARATOR + } else { + path + } + val uri = Uri.parse(processedPath) + val disableIpcSigningCheck = + parseBoolean(uri.getQueryParameter(PARAM_DISABLE_IPC_SIGNING_CHECK)) + val userInfo = uri.userInfo + val noExtraInfoPath: String + noExtraInfoPath = if (path.contains("?")) { + path.substring(0, path.indexOf('?')) + } else { + path + } + val context = createWithDisableIpcSigningCheck(path, disableIpcSigningCheck) + .withCredentials(createFrom(userInfo)) + return SmbFile(noExtraInfoPath, context) + } + + /** + * Create [NtlmPasswordAuthenticator] from given userInfo parameter. + * + * + * Logic borrowed directly from jcifs-ng's own code. They should make that protected + * constructor public... + * + * @param userInfo authentication string, must be already URL decoded. [Uri] shall do this + * for you already + * @return [NtlmPasswordAuthenticator] instance + */ + @JvmStatic + private fun createFrom(userInfo: String?): NtlmPasswordAuthenticator { + return if (!TextUtils.isEmpty(userInfo)) { + var dom: String? = null + var user: String? = null + var pass: String? = null + var i: Int + var u: Int + val end = userInfo!!.length + i = 0 + u = 0 + while (i < end) { + val c = userInfo[i] + if (c == ';') { + dom = userInfo.substring(0, i) + u = i + 1 + } else if (c == ':') { + pass = userInfo.substring(i + 1) + break + } + i++ + } + user = userInfo.substring(u, i) + NtlmPasswordAuthenticator(dom, user, pass) + } else { + NtlmPasswordAuthenticator() + } + } + + init { + AmazeFile.addFilesystem(this) + } + + override val prefix: String = SMB_URI_PREFIX + + override fun normalize(pathname: String): String { + val canonical: String + canonical = try { + canonicalize(pathname) + } catch (e: MalformedURLException) { + Log.e(TAG, "Error getting SMB file canonical path", e) + pathname.substring(0, prefixLength(pathname)) + "/" + } + return canonical + } + + override fun getHumanReadablePath(f: AmazeFile): String { + val uri = Uri.parse(f.path) + return String.format("%s://%s%s", uri.scheme, uri.host, uri.path) + } + + override fun prefixLength(path: String): Int { + require(path.isNotEmpty()) { + "This should never happen, all paths must start with SMB prefix" + } + val matcherMetadata = METADATA_PATTERN.matcher(path) + if (matcherMetadata.find()) { + return matcherMetadata.end() + } + val matcher = IPv4_PATTERN.matcher(path) + matcher.find() + return matcher.end() + } + + override fun resolve(parent: String, child: String): String { + val prefix = parent!!.substring(0, prefixLength(parent)) + val simplePathParent = parent.substring(prefixLength(parent)) + val simplePathChild = child!!.substring(prefixLength(child)) + return prefix + basicUnixResolve(simplePathParent, simplePathChild) + } + + /** This makes no sense for SMB */ + override val defaultParent: String + get() { + throw IllegalStateException("There is no default SMB path") + } + + override fun isAbsolute(f: AmazeFile): Boolean { + return f.path.startsWith(prefix) + } + + override fun resolve(f: AmazeFile): String { + if (isAbsolute(f)) { + return f.path + } + throw IllegalArgumentException("Relative paths are not supported") + } + + @Throws(MalformedURLException::class) + override fun canonicalize(path: String?): String { + return create(path).canonicalPath + } + + override fun exists(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return try { + val smbFile = create(f.path) + smbFile.exists() + } catch (e: MalformedURLException) { + Log.e(TAG, "Failed to get attributes for SMB file", e) + false + } catch (e: SmbException) { + Log.e(TAG, "Failed to get attributes for SMB file", e) + false + } + } + + override fun isFile(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return try { + val smbFile = create(f.path) + smbFile.type == SmbConstants.TYPE_FILESYSTEM + } catch (e: MalformedURLException) { + Log.e(TAG, "Failed to get attributes for SMB file", e) + false + } catch (e: SmbException) { + Log.e(TAG, "Failed to get attributes for SMB file", e) + false + } + } + + override fun isDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return try { + val smbFile = create(f.path) + smbFile.isDirectory + } catch (e: MalformedURLException) { + Log.e(TAG, "Failed to get attributes for SMB file", e) + false + } catch (e: SmbException) { + Log.e(TAG, "Failed to get attributes for SMB file", e) + false + } + } + + override fun isHidden(f: AmazeFile): Boolean { + return try { + val smbFile = create(f.path) + smbFile.isHidden + } catch (e: MalformedURLException) { + Log.e(TAG, "Failed to get attributes for SMB file", e) + false + } catch (e: SmbException) { + Log.e(TAG, "Failed to get attributes for SMB file", e) + false + } + } + + override fun canExecute(f: AmazeFile, contextProvider: ContextProvider): Boolean { + throw NotImplementedError() + } + + override fun canWrite(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return try { + create(f.path).canWrite() + } catch (e: MalformedURLException) { + Log.e(TAG, "Error getting SMB file to check access", e) + false + } catch (e: SmbException) { + Log.e(TAG, "Error getting SMB file to check access", e) + false + } + } + + override fun canRead(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return try { + create(f.path).canRead() + } catch (e: MalformedURLException) { + Log.e(TAG, "Error getting SMB file to check access", e) + false + } catch (e: SmbException) { + Log.e(TAG, "Error getting SMB file to check access", e) + false + } + } + + override fun canAccess(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return try { + val file = create(f.path) + file.connectTimeout = 2000 + file.exists() + } catch (e: MalformedURLException) { + Log.e(TAG, "Error getting SMB file to check access", e) + false + } catch (e: SmbException) { + Log.e(TAG, "Error getting SMB file to check access", e) + false + } + } + + override fun setExecutable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + throw NotImplementedError() + } + + override fun setWritable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + throw NotImplementedError() + } + + override fun setReadable(f: AmazeFile, enable: Boolean, owneronly: Boolean): Boolean { + throw NotImplementedError() + } + + override fun getLastModifiedTime(f: AmazeFile): Long { + return try { + create(f.path).lastModified + } catch (e: MalformedURLException) { + Log.e(TAG, "Error getting SMB file to get last modified time", e) + 0 + } + } + + @Throws(SmbException::class, MalformedURLException::class) + override fun getLength(f: AmazeFile, contextProvider: ContextProvider): Long { + return create(f.path).length() + } + + @Throws(IOException::class) + override fun createFileExclusively(pathname: String): Boolean { + create(pathname).mkdirs() + return true + } + + override fun delete(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return try { + create(f.path).delete() + true + } catch (e: SmbException) { + Log.e(TAG, "Error deleting SMB file", e) + false + } catch (e: MalformedURLException) { + Log.e(TAG, "Error deleting SMB file", e) + false + } + } + + override fun list(f: AmazeFile, contextProvider: ContextProvider): Array? { + val list: Array = try { + create(f.path).list() + } catch (e: SmbException) { + Log.e(TAG, "Error listing SMB files", e) + return null + } catch (e: MalformedURLException) { + Log.e(TAG, "Error listing SMB files", e) + return null + } + val prefix = f.path.substring(0, prefixLength(f.path)) + return Array(list.size) { i: Int -> + normalize(prefix + getSeparator() + list[i]) + } + } + + override fun getInputStream(f: AmazeFile, contextProvider: ContextProvider): InputStream? { + return try { + create(f.path).inputStream + } catch (e: IOException) { + Log.e(TAG, "Error creating SMB output stream", e) + null + } + } + + override fun getOutputStream(f: AmazeFile, contextProvider: ContextProvider): OutputStream? { + return try { + create(f.path).outputStream + } catch (e: IOException) { + Log.e(TAG, "Error creating SMB output stream", e) + null + } + } + + override fun createDirectory(f: AmazeFile, contextProvider: ContextProvider): Boolean { + return try { + create(f.path).mkdir() + true + } catch (e: SmbException) { + Log.e(TAG, "Error creating SMB directory", e) + false + } catch (e: MalformedURLException) { + Log.e(TAG, "Error creating SMB directory", e) + false + } + } + + override fun rename( + file1: AmazeFile, + file2: AmazeFile, + contextProvider: ContextProvider + ): Boolean { + return try { + create(file1.path).renameTo(create(file2.path)) + true + } catch (e: SmbException) { + Log.e(TAG, "Error getting SMB files for a rename", e) + false + } catch (e: MalformedURLException) { + Log.e(TAG, "Error getting SMB files for a rename", e) + false + } + } + + override fun setLastModifiedTime(f: AmazeFile, time: Long): Boolean { + return try { + create(f.path).lastModified = time + true + } catch (e: SmbException) { + Log.e(TAG, "Error getting SMB file to set modified time", e) + false + } catch (e: MalformedURLException) { + Log.e(TAG, "Error getting SMB file to set modified time", e) + false + } + } + + override fun setReadOnly(f: AmazeFile): Boolean { + return try { + create(f.path).setReadOnly() + true + } catch (e: SmbException) { + Log.e(TAG, "Error getting SMB file to set read only", e) + false + } catch (e: MalformedURLException) { + Log.e(TAG, "Error getting SMB file to set read only", e) + false + } + } + + override fun getTotalSpace(f: AmazeFile, contextProvider: ContextProvider): Long { + // TODO: Find total storage space of SMB when JCIFS adds support + throw NotImplementedError() + } + + override fun getFreeSpace(f: AmazeFile): Long { + return try { + create(f.path).diskFreeSpace + } catch (e: SmbException) { + Log.e(TAG, "Error getting SMB file to read free volume space", e) + 0 + } catch (e: MalformedURLException) { + Log.e(TAG, "Error getting SMB file to read free volume space", e) + 0 + } + } + + override fun getUsableSpace(f: AmazeFile): Long { + // TODO: Find total storage space of SMB when JCIFS adds support + throw NotImplementedError() + } +} diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/StreamSource.java b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/StreamSource.java index 37d9c6492b..e5f14769b5 100644 --- a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/StreamSource.java +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/StreamSource.java @@ -24,21 +24,20 @@ import java.io.IOException; import java.io.InputStream; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; import com.amaze.filemanager.file_operations.filesystem.streams.RandomAccessStream; import android.webkit.MimeTypeMap; -import jcifs.smb.SmbFile; - public class StreamSource extends RandomAccessStream { protected String mime; protected long fp; protected String name; - protected SmbFile file; + protected AmazeFile file; InputStream input; - public StreamSource(SmbFile file, long l) { + public StreamSource(AmazeFile file, long l) { super(l); fp = 0; @@ -66,7 +65,7 @@ public StreamSource(SmbFile file, long l) { */ public void open() throws IOException { try { - input = file.getInputStream(); // new SmbFileInputStream(file, bufferSize, 1); + input = file.getInputStream(() -> null); // new SmbFileInputStream(file, bufferSize, 1); if (fp > 0) input.skip(fp); } catch (Exception e) { throw new IOException(e); @@ -114,7 +113,7 @@ public String getName() { return name; } - public SmbFile getFile() { + public AmazeFile getFile() { return file; } diff --git a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/Streamer.java b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/Streamer.java index 3166ca3937..3eb76ee6af 100644 --- a/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/Streamer.java +++ b/file_operations/src/main/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/Streamer.java @@ -25,6 +25,8 @@ import java.util.Properties; import java.util.regex.Pattern; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; + import android.util.Log; import jcifs.smb.SmbFile; @@ -34,7 +36,7 @@ public class Streamer extends StreamServer { public static final int PORT = 7871; public static final String URL = "http://127.0.0.1:" + PORT; - private SmbFile file; + private AmazeFile file; long length = 0; // protected List extras; //those can be subtitles // private InputStream stream; @@ -65,7 +67,7 @@ public static boolean isStreamMedia(SmbFile file) { return pattern.matcher(file.getName()).matches(); } - public void setStreamSrc(SmbFile file, long len) { + public void setStreamSrc(AmazeFile file, long len) { this.file = file; // this.extras = extraFiles; this.length = len; @@ -81,7 +83,7 @@ public void stop() { public Response serve( String uri, String method, Properties header, Properties parms, Properties files) { Response res; - SmbFile sourceFile = null; + AmazeFile sourceFile = null; String name = getNameFromPath(uri); if (file != null && file.getName().equals(name)) sourceFile = file; /*else if(extras!=null){ diff --git a/file_operations/src/main/res/raw/temptrack.mp3 b/file_operations/src/main/res/raw/temptrack.mp3 new file mode 100644 index 0000000000..9935ce793f Binary files /dev/null and b/file_operations/src/main/res/raw/temptrack.mp3 differ diff --git a/file_operations/src/test/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/StreamSourceTest.java b/file_operations/src/test/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/StreamSourceTest.java index 0ec6d4027d..33d849a13d 100644 --- a/file_operations/src/test/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/StreamSourceTest.java +++ b/file_operations/src/test/java/com/amaze/filemanager/file_operations/filesystem/smbstreamer/StreamSourceTest.java @@ -39,6 +39,7 @@ import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; +import com.amaze.filemanager.file_operations.filesystem.filetypes.AmazeFile; import com.amaze.filemanager.file_operations.shadows.ShadowMultiDex; import com.amaze.filemanager.file_operations.shadows.jcifs.smb.ShadowSmbFile; @@ -46,15 +47,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; -import jcifs.smb.SmbFile; - /** Created by Rustam Khadipash on 30/3/2018. */ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class, ShadowSmbFile.class}, sdk = {JELLY_BEAN, KITKAT, P}) public class StreamSourceTest { - private SmbFile file; + private AmazeFile file; private StreamSource ss; private byte[] text; @@ -73,7 +72,7 @@ public void tearDown() { if (ss != null) ss.close(); } - private SmbFile createFile() throws IOException { + private AmazeFile createFile() throws IOException { File testFile = new File(Environment.getExternalStorageDirectory(), "Test.txt"); testFile.createNewFile(); @@ -82,7 +81,7 @@ private SmbFile createFile() throws IOException { is.flush(); is.close(); - SmbFile file = new SmbFile("smb://127.0.0.1/Test.txt"); + AmazeFile file = new AmazeFile("smb://127.0.0.1/Test.txt"); ShadowSmbFile shadowSmbFile = Shadow.extract(file); shadowSmbFile.setFile(testFile);