diff --git a/.gitignore b/.gitignore index 95a913e241..94f5df1c90 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Built application files *.apk *.aar +!app/libs/*.aar *.ap_ *.aab diff --git a/app/build.gradle b/app/build.gradle index fdd229d55f..c2e2511237 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,3 +1,5 @@ +import java.util.concurrent.TimeUnit + apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' @@ -54,8 +56,8 @@ android { release { signingConfig signingConfigs.release - - minifyEnabled true + debuggable true + minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.cfg' buildConfigField "String", "CRYPTO_IV", "\"LxbHiJhhUXcj\"" buildConfigField "String", "FTP_SERVER_KEYSTORE_PASSWORD", "\"vishal007\"" @@ -145,6 +147,8 @@ dependencies { implementation libs.androidX.constraintLayout implementation libs.androidX.multidex //Multiple dex files implementation libs.androidX.biometric + implementation libs.androidX.security + implementation libs.androidX.browser.customtabs implementation libs.room.runtime implementation libs.room.rxjava2 @@ -168,7 +172,7 @@ dependencies { testImplementation libs.jsoup testImplementation libs.room.migration testImplementation libs.mockk - testImplementation libs.kotlin.coroutine.test + testImplementation libs.kotlin.coroutines.test testImplementation libs.androidX.core.testing kspTest libs.auto.service testImplementation 'ch.qos.logback:logback-classic:1.4.14' @@ -201,7 +205,6 @@ dependencies { implementation libs.libsu.core implementation libs.libsu.io - playImplementation libs.cloudrail.si.android playImplementation libs.junrar playImplementation libs.google.play.billing @@ -217,6 +220,7 @@ dependencies { implementation libs.commons.net //OkHttp implementation libs.okhttp + implementation libs.okhttp.loggingInterceptor implementation libs.bcpkix.jdk18on implementation libs.bcprov.jdk18on @@ -251,6 +255,10 @@ dependencies { implementation project(':file_operations') implementation project(':portscanner') + implementation libs.kotlin.reflect + implementation platform(libs.kotlin.coroutines.bom) + implementation libs.kotlin.coroutines.android + implementation libs.kotlin.coroutines.rxjava2 implementation libs.kotlin.stdlib.jdk8 implementation libs.acra.core implementation libs.slf4j.api @@ -259,6 +267,10 @@ dependencies { implementation libs.gson implementation libs.amaze.trashBin + implementation libs.retrofit + implementation libs.retrofit.jacksonConverter + implementation libs.jackson.module.kotlin + implementation fileTree(include: ['*.jar','*.aar'], dir: 'libs') } kotlin { @@ -267,12 +279,16 @@ kotlin { configurations.configureEach { resolutionStrategy { + cacheChangingModulesFor(0, TimeUnit.SECONDS) dependencySubstitution { substitute module("commons-logging:commons-logging-api:1.1") using module("commons-logging:commons-logging:1.1.1") substitute module("com.android.support:support-annotations:27.1.1") using module("com.android.support:support-annotations:27.0.2") // These two lines are added to prevent possible class clashes between awaitility (which uses hamcrest 2.1) and junit (which uses hamcrest 1.3). substitute module('org.hamcrest:hamcrest-core:1.3') using module("org.hamcrest:hamcrest:2.1") substitute module('org.hamcrest:hamcrest-library:1.3') using module("org.hamcrest:hamcrest:2.1") + // Force coroutines to a specific version to avoid conflicts + substitute module("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:*") using module("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2") + substitute module("org.jetbrains.kotlinx:kotlinx-coroutines-android:*") using module("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") } } } diff --git a/app/libs/com.openmobilehub.android.auth.core-2.0.4.aar b/app/libs/com.openmobilehub.android.auth.core-2.0.4.aar new file mode 100644 index 0000000000..c1a636c6fd Binary files /dev/null and b/app/libs/com.openmobilehub.android.auth.core-2.0.4.aar differ diff --git a/app/libs/com.openmobilehub.android.auth.core-common-mobileweb-2.0.4.aar b/app/libs/com.openmobilehub.android.auth.core-common-mobileweb-2.0.4.aar new file mode 100644 index 0000000000..d9d133b777 Binary files /dev/null and b/app/libs/com.openmobilehub.android.auth.core-common-mobileweb-2.0.4.aar differ diff --git a/app/libs/com.openmobilehub.android.auth.plugin-box-mobileweb-2.0.4.aar b/app/libs/com.openmobilehub.android.auth.plugin-box-mobileweb-2.0.4.aar new file mode 100644 index 0000000000..f8081f54b4 Binary files /dev/null and b/app/libs/com.openmobilehub.android.auth.plugin-box-mobileweb-2.0.4.aar differ diff --git a/app/libs/com.openmobilehub.android.auth.plugin-dropbox-mobileweb-2.0.4.aar b/app/libs/com.openmobilehub.android.auth.plugin-dropbox-mobileweb-2.0.4.aar new file mode 100644 index 0000000000..5e9b86856a Binary files /dev/null and b/app/libs/com.openmobilehub.android.auth.plugin-dropbox-mobileweb-2.0.4.aar differ diff --git a/app/libs/com.openmobilehub.android.auth.plugin-google-non-gms-2.0.4.aar b/app/libs/com.openmobilehub.android.auth.plugin-google-non-gms-2.0.4.aar new file mode 100644 index 0000000000..c6f84990f5 Binary files /dev/null and b/app/libs/com.openmobilehub.android.auth.plugin-google-non-gms-2.0.4.aar differ diff --git a/app/libs/com.openmobilehub.android.auth.plugin-microsoft-mobileweb-2.0.4.aar b/app/libs/com.openmobilehub.android.auth.plugin-microsoft-mobileweb-2.0.4.aar new file mode 100644 index 0000000000..c398b8ff3f Binary files /dev/null and b/app/libs/com.openmobilehub.android.auth.plugin-microsoft-mobileweb-2.0.4.aar differ diff --git a/app/libs/com.openmobilehub.android.storage.core-2.1.1-alpha+thumbnail.aar b/app/libs/com.openmobilehub.android.storage.core-2.1.1-alpha+thumbnail.aar new file mode 100644 index 0000000000..c7a7c75598 Binary files /dev/null and b/app/libs/com.openmobilehub.android.storage.core-2.1.1-alpha+thumbnail.aar differ diff --git a/app/libs/com.openmobilehub.android.storage.core-restful-common-2.1.0-alpha+thumbnail.aar b/app/libs/com.openmobilehub.android.storage.core-restful-common-2.1.0-alpha+thumbnail.aar new file mode 100644 index 0000000000..a70eeff8e5 Binary files /dev/null and b/app/libs/com.openmobilehub.android.storage.core-restful-common-2.1.0-alpha+thumbnail.aar differ diff --git a/app/libs/com.openmobilehub.android.storage.plugin-dropbox-restful-2.1.0-alpha+thumbnail.aar b/app/libs/com.openmobilehub.android.storage.plugin-dropbox-restful-2.1.0-alpha+thumbnail.aar new file mode 100644 index 0000000000..ad32662a69 Binary files /dev/null and b/app/libs/com.openmobilehub.android.storage.plugin-dropbox-restful-2.1.0-alpha+thumbnail.aar differ diff --git a/app/libs/com.openmobilehub.android.storage.plugin-googledrive-non-gms-2.1.0-alpha+thumbnail.aar b/app/libs/com.openmobilehub.android.storage.plugin-googledrive-non-gms-2.1.0-alpha+thumbnail.aar new file mode 100644 index 0000000000..44d339c84d Binary files /dev/null and b/app/libs/com.openmobilehub.android.storage.plugin-googledrive-non-gms-2.1.0-alpha+thumbnail.aar differ diff --git a/app/libs/com.openmobilehub.android.storage.plugin-onedrive-restful-2.1.0-alpha+thumbnail.aar b/app/libs/com.openmobilehub.android.storage.plugin-onedrive-restful-2.1.0-alpha+thumbnail.aar new file mode 100644 index 0000000000..82eab40074 Binary files /dev/null and b/app/libs/com.openmobilehub.android.storage.plugin-onedrive-restful-2.1.0-alpha+thumbnail.aar differ diff --git a/app/proguard.cfg b/app/proguard.cfg index 738663b299..579ac31ea7 100644 --- a/app/proguard.cfg +++ b/app/proguard.cfg @@ -69,9 +69,6 @@ public static final int define_*; } -#From here CloudRail --keep class com.cloudrail.** { *; } - #From here BouncyCastle -keep class org.bouncycastle.crypto.* {*;} -keep class org.bouncycastle.crypto.agreement.** {*;} diff --git a/app/src/fdroid/java/com/amaze/filemanager/asynchronous/asynctasks/CloudLoaderAsyncTask.java b/app/src/fdroid/java/com/amaze/filemanager/asynchronous/asynctasks/CloudLoaderAsyncTask.java deleted file mode 100644 index e176ef4f38..0000000000 --- a/app/src/fdroid/java/com/amaze/filemanager/asynchronous/asynctasks/CloudLoaderAsyncTask.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.asynchronous.asynctasks; - -import java.lang.ref.WeakReference; - -import com.amaze.filemanager.database.CloudHandler; -import com.amaze.filemanager.ui.activities.MainActivity; - -import android.database.Cursor; -import android.os.AsyncTask; - -import androidx.annotation.NonNull; - -public class CloudLoaderAsyncTask extends AsyncTask { - - private final WeakReference mainActivity; - - public CloudLoaderAsyncTask(MainActivity mainActivity, CloudHandler unused1, Cursor unused2) { - this.mainActivity = new WeakReference<>(mainActivity); - } - - @Override - @NonNull - public Boolean doInBackground(Void... voids) { - return false; - } - - @Override - protected void onCancelled() { - super.onCancelled(); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - mainActivity - .getSupportLoaderManager() - .destroyLoader(MainActivity.REQUEST_CODE_CLOUD_LIST_KEY); - mainActivity - .getSupportLoaderManager() - .destroyLoader(MainActivity.REQUEST_CODE_CLOUD_LIST_KEYS); - } - } - - @Override - public void onPostExecute(@NonNull Boolean result) { - if (result) { - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - mainActivity.getDrawer().refreshDrawer(); - mainActivity.invalidateFragmentAndBundle(null, true); - } - } - } -} diff --git a/app/src/fdroid/java/com/cloudrail/si/CloudRail.java b/app/src/fdroid/java/com/cloudrail/si/CloudRail.java deleted file mode 100644 index 4deb9185e8..0000000000 --- a/app/src/fdroid/java/com/cloudrail/si/CloudRail.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.cloudrail.si; - -import android.content.Intent; - -public class CloudRail { - public static void setAuthenticationResponse(Intent i) {} - - public static void setAppKey(String id) {} -} diff --git a/app/src/fdroid/java/com/cloudrail/si/interfaces/CloudStorage.java b/app/src/fdroid/java/com/cloudrail/si/interfaces/CloudStorage.java deleted file mode 100644 index 853aacb113..0000000000 --- a/app/src/fdroid/java/com/cloudrail/si/interfaces/CloudStorage.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.cloudrail.si.interfaces; - -import java.io.InputStream; -import java.util.Collections; -import java.util.List; - -import com.cloudrail.si.types.CloudMetaData; -import com.cloudrail.si.types.SpaceAllocation; - -public class CloudStorage { - - public List getChildren(String str) { - return Collections.emptyList(); - } - - public void logout() {} - - public void delete(String path) {} - - public void move(String path1, String path2) {} - - public void copy(String path1, String path2) {} - - public boolean exists(String path) { - return false; - } - - public void loadAsString(String str) {} - - public SpaceAllocation getAllocation() { - return new SpaceAllocation(); - } - - public void login() {} - - public String saveAsString() { - return ""; - } - - public String getUserLogin() { - return ""; - } - - public void useAdvancedAuthentication() {} - - public void createFolder(String extSyncFolder) {} - - public InputStream download(String path) { - return null; - } - - public InputStream getThumbnail(String path) { - return null; - } - - public void upload(String extSyncFile, InputStream outStream, long length, boolean b) {} - - public CloudMetaData getMetadata(String str) { - return null; - } - - public String createShareLink(String str) { - return ""; - } -} diff --git a/app/src/fdroid/java/com/cloudrail/si/services/Box.java b/app/src/fdroid/java/com/cloudrail/si/services/Box.java deleted file mode 100644 index e1031449f8..0000000000 --- a/app/src/fdroid/java/com/cloudrail/si/services/Box.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.cloudrail.si.services; - -import com.cloudrail.si.interfaces.CloudStorage; - -import android.content.Context; - -public class Box extends CloudStorage { - public Box(Context unused1, String unused2, String unused3) { - super(); - } -} diff --git a/app/src/fdroid/java/com/cloudrail/si/services/GoogleDrive.java b/app/src/fdroid/java/com/cloudrail/si/services/GoogleDrive.java deleted file mode 100644 index 2af786722c..0000000000 --- a/app/src/fdroid/java/com/cloudrail/si/services/GoogleDrive.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.cloudrail.si.services; - -import com.cloudrail.si.interfaces.CloudStorage; - -import android.content.Context; - -public class GoogleDrive extends CloudStorage { - public GoogleDrive( - Context unused1, String unused2, String unused3, String unused4, String unused5) { - super(); - } -} diff --git a/app/src/fdroid/java/com/cloudrail/si/services/OneDrive.java b/app/src/fdroid/java/com/cloudrail/si/services/OneDrive.java deleted file mode 100644 index b94d8e8069..0000000000 --- a/app/src/fdroid/java/com/cloudrail/si/services/OneDrive.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.cloudrail.si.services; - -import com.cloudrail.si.interfaces.CloudStorage; - -import android.content.Context; - -public class OneDrive extends CloudStorage { - public OneDrive(Context unused1, String unused2, String unused3, String unused4, String unused5) { - super(); - } -} diff --git a/app/src/fdroid/java/com/cloudrail/si/types/SpaceAllocation.java b/app/src/fdroid/java/com/cloudrail/si/types/SpaceAllocation.java deleted file mode 100644 index 54e3b78c68..0000000000 --- a/app/src/fdroid/java/com/cloudrail/si/types/SpaceAllocation.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.cloudrail.si.types; - -public class SpaceAllocation { - public long getUsed() { - return 0; - } - - public long getTotal() { - return 0; - } -} diff --git a/app/src/main/java/com/amaze/filemanager/adapters/HiddenAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/HiddenAdapter.kt index e0dd9db9db..1a1390d4a3 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/HiddenAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/HiddenAdapter.kt @@ -94,7 +94,7 @@ class HiddenAdapter( val task = DeleteTask(context, false) task.execute(filesToDelete) } - DataUtils.getInstance().removeHiddenFile(hiddenFiles[position].path) + DataUtils.removeHiddenFile(hiddenFiles[position].path) hiddenFiles.remove(hiddenFiles[position]) notifyItemRemoved(position) } diff --git a/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java b/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java index 82539cefb3..f1247a5da0 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java @@ -21,7 +21,6 @@ package com.amaze.filemanager.adapters.data; import java.io.File; -import java.util.Calendar; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.filesystem.HybridFileParcelable; @@ -38,9 +37,7 @@ public class LayoutElementParcelable implements Parcelable, ComparableParcelable { - private static final String CURRENT_YEAR = - String.valueOf(Calendar.getInstance().get(Calendar.YEAR)); - + public final String cloudFileId; public final boolean isBack; public final int filetype; public final IconDataParcelable iconData; @@ -62,6 +59,7 @@ public LayoutElementParcelable( this( c, true, + "", new File("..").getName(), "..", "", @@ -77,6 +75,7 @@ public LayoutElementParcelable( public LayoutElementParcelable( @NonNull Context c, + String cloudFileId, String path, String permissions, String symlink, @@ -89,6 +88,7 @@ public LayoutElementParcelable( OpenMode openMode) { this( c, + cloudFileId, new File(path).getName(), path, permissions, @@ -104,6 +104,7 @@ public LayoutElementParcelable( public LayoutElementParcelable( @NonNull Context c, + String cloudFileId, String title, String path, String permissions, @@ -118,6 +119,7 @@ public LayoutElementParcelable( this( c, false, + cloudFileId, title, path, permissions, @@ -134,6 +136,7 @@ public LayoutElementParcelable( public LayoutElementParcelable( @NonNull Context c, boolean isBack, + String cloudFileId, String title, String path, String permissions, @@ -180,7 +183,7 @@ public LayoutElementParcelable( } else { this.iconData = new IconDataParcelable(IconDataParcelable.IMAGE_RES, fallbackIcon); } - + this.cloudFileId = cloudFileId; this.title = title; this.desc = path; this.permissions = permissions.trim(); @@ -210,6 +213,7 @@ public void setMode(OpenMode mode) { public HybridFileParcelable generateBaseFile() { HybridFileParcelable baseFile = new HybridFileParcelable(desc, permissions, date, longSize, isDirectory); + baseFile.setCloudFileId(cloudFileId); baseFile.setMode(mode); baseFile.setName(title); return baseFile; @@ -228,6 +232,7 @@ public String toString() { public LayoutElementParcelable(Parcel im) { filetype = im.readInt(); iconData = im.readParcelable(IconDataParcelable.class.getClassLoader()); + cloudFileId = im.readString(); title = im.readString(); desc = im.readString(); permissions = im.readString(); @@ -253,6 +258,7 @@ public int describeContents() { public void writeToParcel(Parcel p1, int p2) { p1.writeInt(filetype); p1.writeParcelable(iconData, 0); + p1.writeString(cloudFileId); p1.writeString(title); p1.writeString(desc); p1.writeString(permissions); diff --git a/app/src/main/java/com/amaze/filemanager/adapters/glide/cloudicon/CloudIconModelLoader.java b/app/src/main/java/com/amaze/filemanager/adapters/glide/cloudicon/CloudIconModelLoader.java index 33edfb754d..b168f8b977 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/glide/cloudicon/CloudIconModelLoader.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/glide/cloudicon/CloudIconModelLoader.java @@ -23,7 +23,7 @@ import static com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool.SSH_URI_PREFIX; import static com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX; -import com.amaze.filemanager.database.CloudHandler; +import com.amaze.filemanager.database.CloudContract; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.signature.ObjectKey; @@ -55,10 +55,10 @@ public LoadData buildLoadData(String s, int width, int height, Options o @Override public boolean handles(String s) { - return s.startsWith(CloudHandler.CLOUD_PREFIX_BOX) - || s.startsWith(CloudHandler.CLOUD_PREFIX_DROPBOX) - || s.startsWith(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE) - || s.startsWith(CloudHandler.CLOUD_PREFIX_ONE_DRIVE) + return s.startsWith(CloudContract.CLOUD_PREFIX_BOX) + || s.startsWith(CloudContract.CLOUD_PREFIX_DROPBOX) + || s.startsWith(CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE) + || s.startsWith(CloudContract.CLOUD_PREFIX_ONE_DRIVE) || s.startsWith(SMB_URI_PREFIX) || s.startsWith(SSH_URI_PREFIX); } diff --git a/app/src/main/java/com/amaze/filemanager/application/AppConfig.java b/app/src/main/java/com/amaze/filemanager/application/AppConfig.java index 93908a81c3..deb7094771 100644 --- a/app/src/main/java/com/amaze/filemanager/application/AppConfig.java +++ b/app/src/main/java/com/amaze/filemanager/application/AppConfig.java @@ -45,6 +45,7 @@ import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.provider.UtilitiesProvider; import com.amaze.filemanager.utils.ScreenUtils; +import com.amaze.filemanager.utils.omh.AuthTrigger; import com.amaze.trashbin.TrashBin; import com.amaze.trashbin.TrashBinConfig; @@ -74,6 +75,7 @@ public class AppConfig extends GlideApplication { private UtilitiesProvider utilsProvider; private UtilsHandler utilsHandler; + private AuthTrigger cloudAuthTrigger; private WeakReference mainActivityContext; private static ScreenUtils screenUtils; @@ -81,11 +83,11 @@ public class AppConfig extends GlideApplication { private static AppConfig instance; private UtilitiesDatabase utilitiesDatabase; - private ExplorerDatabase explorerDatabase; private TrashBinConfig trashBinConfig; private TrashBin trashBin; + private static final String TRASH_BIN_BASE_PATH = Environment.getExternalStorageDirectory().getPath() + File.separator + ".AmazeData"; @@ -214,6 +216,10 @@ public void setMainActivityContext(@NonNull Activity activity) { screenUtils = new ScreenUtils(activity); } + public void setCloudAuthTrigger(@NonNull AuthTrigger cloudAuthTrigger) { + this.cloudAuthTrigger = cloudAuthTrigger; + } + public ScreenUtils getScreenUtils() { return screenUtils; } @@ -223,6 +229,10 @@ public Context getMainActivityContext() { return mainActivityContext.get(); } + public AuthTrigger getCloudAuthTrigger() { + return cloudAuthTrigger; + } + public ExplorerDatabase getExplorerDatabase() { return explorerDatabase; } diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CountItemsOrAndSizeTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CountItemsOrAndSizeTask.java index 78d2d58681..c70bd4294c 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CountItemsOrAndSizeTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/CountItemsOrAndSizeTask.java @@ -33,6 +33,8 @@ import androidx.appcompat.widget.AppCompatTextView; import androidx.core.util.Pair; +import kotlin.Unit; + /** * @author Emmanuel on 12/5/2017, at 19:40. */ @@ -58,7 +60,13 @@ protected String doInBackground(Void[] params) { if (file.isDirectory(context)) { final AtomicInteger x = new AtomicInteger(0); - file.forEachChildrenFile(context, false, file -> x.incrementAndGet()); + file.forEachChildrenFile( + context, + false, + file -> { + x.incrementAndGet(); + return Unit.INSTANCE; + }); final int folderLength = x.intValue(); long folderSize; 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 5ce3703e62..aaba2fa8b8 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 @@ -36,7 +36,6 @@ import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.HybridFileParcelable; import com.amaze.filemanager.filesystem.SafRootHolder; -import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.CryptUtil; import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.filesystem.files.MediaConnectionUtils; @@ -44,9 +43,9 @@ import com.amaze.filemanager.ui.fragments.CompressedExplorerFragment; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.notifications.NotificationConstants; -import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.OTGUtil; -import com.cloudrail.si.interfaces.CloudStorage; +import com.amaze.filemanager.utils.omh.OMHClientHelper; +import com.openmobilehub.android.storage.core.OmhStorageClient; import android.app.NotificationManager; import android.content.Context; @@ -59,6 +58,8 @@ import androidx.preference.PreferenceManager; import jcifs.smb.SmbException; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlinx.coroutines.BuildersKt; public class DeleteTask extends AsyncTask, String, AsyncTaskResult> { @@ -71,7 +72,6 @@ public class DeleteTask private CompressedExplorerFragment compressedExplorerFragment; private boolean doDeletePermanently; - private final DataUtils dataUtils = DataUtils.getInstance(); public DeleteTask(@NonNull Context applicationContext, @NonNull boolean doDeletePermanently) { this.applicationContext = applicationContext.getApplicationContext(); @@ -180,14 +180,29 @@ private boolean doDeleteFile(@NonNull HybridFileParcelable file) throws Exceptio case BOX: case GDRIVE: case ONEDRIVE: - CloudStorage cloudStorage = dataUtils.getAccount(file.getMode()); - try { - cloudStorage.delete(CloudUtil.stripPath(file.getMode(), file.getPath())); - return true; - } catch (Exception e) { - LOG.warn("failed to delete cloud files", e); - return false; + OmhStorageClient storageClient = OMHClientHelper.getStorageClient(file.getMode()); + if (storageClient != null) { + BuildersKt.runBlocking( + EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> { + try { + storageClient.deleteFile(file.getCloudFileId(), continuation); + return true; + } catch (Exception e) { + LOG.error("Error delete cloud file", e); + return false; + } + }); } + return true; + // CloudStorage cloudStorage = dataUtils.getAccount(file.getMode()); + // try { + // cloudStorage.delete(CloudUtil.stripPath(file.getMode(), file.getPath())); + // return true; + // } catch (Exception e) { + // LOG.warn("failed to delete cloud files", e); + // return false; + // } default: try { /* SMB and SFTP (or any remote files that may support in the future) should not be 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 ecc7cd0d65..5e36e35732 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 @@ -51,19 +51,18 @@ import com.amaze.filemanager.filesystem.files.FileListSorter; import com.amaze.filemanager.filesystem.files.sort.SortType; import com.amaze.filemanager.filesystem.root.ListFilesCommand; +import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.activities.MainActivityViewModel; -import com.amaze.filemanager.ui.fragments.CloudSheetFragment; import com.amaze.filemanager.ui.fragments.MainFragment; import com.amaze.filemanager.ui.fragments.data.MainFragmentViewModel; import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.GenericExtKt; import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.OnAsyncTaskFinished; -import com.amaze.filemanager.utils.OnFileFound; import com.amaze.filemanager.utils.Utils; +import com.amaze.filemanager.utils.cloud.CloudPluginUtil; import com.amaze.trashbin.TrashBin; import com.amaze.trashbin.TrashBinFile; -import com.cloudrail.si.interfaces.CloudStorage; import android.content.ContentResolver; import android.content.Context; @@ -81,11 +80,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.arch.core.util.Function; import androidx.core.util.Pair; import jcifs.smb.SmbAuthException; import jcifs.smb.SmbException; import jcifs.smb.SmbFile; +import kotlin.Unit; import kotlin.collections.CollectionsKt; public class LoadFilesListTask @@ -93,31 +94,55 @@ public class LoadFilesListTask private static final Logger LOG = LoggerFactory.getLogger(LoadFilesListTask.class); + private String cloudFolderId; private String path; private WeakReference mainFragmentReference; private WeakReference context; private OpenMode openmode; private boolean showHiddenFiles, showThumbs; - private DataUtils dataUtils = DataUtils.getInstance(); + private DataUtils dataUtils = DataUtils.INSTANCE; private OnAsyncTaskFinished>> listener; private boolean forceReload; public LoadFilesListTask( - Context context, - String path, - MainFragment mainFragment, - OpenMode openmode, + @NonNull Context context, + @NonNull String path, + @NonNull MainFragment mainFragment, + @NonNull OpenMode openmode, boolean showThumbs, boolean showHiddenFiles, boolean forceReload, - OnAsyncTaskFinished>> l) { + @NonNull OnAsyncTaskFinished>> listener) { + this( + context, + null, + path, + mainFragment, + openmode, + showThumbs, + showHiddenFiles, + forceReload, + listener); + } + + public LoadFilesListTask( + @NonNull Context context, + @Nullable String cloudFolderId, + @NonNull String path, + @NonNull MainFragment mainFragment, + @NonNull OpenMode openmode, + boolean showThumbs, + boolean showHiddenFiles, + boolean forceReload, + OnAsyncTaskFinished>> listener) { + this.cloudFolderId = (cloudFolderId == null) ? "" : cloudFolderId; this.path = path; this.mainFragmentReference = new WeakReference<>(mainFragment); this.openmode = openmode; this.context = new WeakReference<>(context); this.showThumbs = showThumbs; this.showHiddenFiles = showHiddenFiles; - this.listener = l; + this.listener = listener; this.forceReload = forceReload; } @@ -182,8 +207,9 @@ public LoadFilesListTask( case BOX: case GDRIVE: case ONEDRIVE: + final MainActivity mainActivity = mainFragment.requireMainActivity(); try { - list = listCloud(mainActivityViewModel); + list = listCloud(mainActivity, mainActivityViewModel); } catch (CloudPluginException e) { LOG.warn("failed to load cloud files", e); AppConfig.toast(context, context.getResources().getString(R.string.failed_no_connection)); @@ -360,6 +386,7 @@ private void postListCustomPathProcess( LayoutElementParcelable layoutElement = new LayoutElementParcelable( context, + baseFile.getCloudFileId(), baseFile.getName(context), baseFile.getPath(), baseFile.getPermission(), @@ -651,6 +678,7 @@ else if (cursor.getCount() > 0 && cursor.moveToFirst()) { LayoutElementParcelable element = new LayoutElementParcelable( ctx, + "", dir.getAbsolutePath(), "", "", @@ -724,6 +752,7 @@ private List listSftp( list.add(elem); } } + return Unit.INSTANCE; }); mainActivityViewModel.putInCache(path, list); } @@ -736,7 +765,10 @@ private List listOtg() { path, file -> { LayoutElementParcelable elem = createListParcelables(file); - if (elem != null) list.add(elem); + if (elem != null) { + list.add(elem); + } + return Unit.INSTANCE; }); return list; } @@ -752,7 +784,10 @@ private List listDocumentFiles( listDocumentFilesInternal( file -> { LayoutElementParcelable elem = createListParcelables(file); - if (elem != null) list.add(elem); + if (elem != null) { + list.add(elem); + } + return Unit.INSTANCE; }); mainActivityViewModel.putInCache(path, list); } @@ -760,21 +795,24 @@ private List listDocumentFiles( } private List listCloud( - @NonNull MainActivityViewModel mainActivityViewModel) throws CloudPluginException { + @NonNull MainActivity mainActivity, @NonNull MainActivityViewModel mainActivityViewModel) + throws CloudPluginException { List list; List cloudCache = mainActivityViewModel.getFromListCache(path); if (cloudCache != null && !forceReload) { list = cloudCache; } else { - CloudStorage cloudStorage = dataUtils.getAccount(openmode); list = new ArrayList<>(); listCloudInternal( - path, - cloudStorage, + mainActivity, + cloudFolderId, openmode, file -> { LayoutElementParcelable elem = createListParcelables(file); - if (elem != null) list.add(elem); + if (elem != null) { + list.add(elem); + } + return Unit.INSTANCE; }); mainActivityViewModel.putInCache(path, list); } @@ -823,7 +861,7 @@ private List listDefault( * com.amaze.filemanager.utils.OTGUtil#PREFIX_OTG} Independent of URI (or mount point) for the * OTG */ - private void listOtgInternal(String path, OnFileFound fileFound) { + private void listOtgInternal(String path, Function fileFound) { final Context context = this.context.get(); if (context == null) { @@ -834,7 +872,7 @@ private void listOtgInternal(String path, OnFileFound fileFound) { OTGUtil.getDocumentFiles(path, context, fileFound); } - private void listDocumentFilesInternal(OnFileFound fileFound) { + private void listDocumentFilesInternal(Function fileFound) { final Context context = this.context.get(); if (context == null) { @@ -847,7 +885,10 @@ private void listDocumentFilesInternal(OnFileFound fileFound) { } private void listCloudInternal( - String path, CloudStorage cloudStorage, OpenMode openMode, OnFileFound fileFoundCallback) + MainActivity mainActivity, + String cloudFolderId, + OpenMode openMode, + Function fileFoundCallback) throws CloudPluginException { final Context context = this.context.get(); @@ -856,10 +897,18 @@ private void listCloudInternal( return; } - if (!CloudSheetFragment.isCloudProviderAvailable(context)) { + if (!CloudPluginUtil.isCloudProviderAvailable(context)) { throw new CloudPluginException(); } - CloudUtil.getCloudFiles(path, cloudStorage, openMode, fileFoundCallback); + // Use a dedicated thread dispatcher instead of runBlocking - + // dirty hack for using the coroutine inside AsyncTask + try { + CloudUtil.getCloudFilesBlocking( + cloudFolderId, path, openMode, mainActivity, fileFoundCallback); + } catch (Exception e) { + LOG.warn("Cloud listing interrupted", e); + throw new CloudPluginException(); + } } } 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 394a66bec3..d0721f64f3 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 @@ -33,11 +33,8 @@ import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.HybridFileParcelable; import com.amaze.filemanager.filesystem.Operations; -import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.filesystem.root.RenameFileCommand; -import com.amaze.filemanager.utils.DataUtils; -import com.cloudrail.si.interfaces.CloudStorage; import android.content.Context; @@ -133,22 +130,7 @@ private MoveFilesReturn processFile( case BOX: case ONEDRIVE: case GDRIVE: - DataUtils dataUtils = DataUtils.getInstance(); - - CloudStorage cloudStorage = dataUtils.getAccount(mode); - if (baseFile.getMode() == mode) { - // source and target both in same filesystem, use API method - try { - cloudStorage.move( - CloudUtil.stripPath(mode, baseFile.getPath()), CloudUtil.stripPath(mode, destPath)); - } catch (RuntimeException e) { - LOG.warn("failed to move file in cloud filesystem", e); - return new MoveFilesReturn(false, false, destinationSize, totalBytes); - } - } else { - // not in same filesystem, execute service - return new MoveFilesReturn(false, false, destinationSize, totalBytes); - } + // FIXME: OmhStorageClient should support move operation default: return new MoveFilesReturn(false, false, destinationSize, totalBytes); } diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PreparePasteTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PreparePasteTask.kt index 7ee9cb13cb..4cff3679c6 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PreparePasteTask.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PreparePasteTask.kt @@ -30,7 +30,6 @@ import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog import com.amaze.filemanager.R import com.amaze.filemanager.asynchronous.asynctasks.fromTask -import com.amaze.filemanager.asynchronous.asynctasks.movecopy.PreparePasteTask.CopyNode import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil import com.amaze.filemanager.asynchronous.services.CopyService import com.amaze.filemanager.databinding.CopyDialogBinding @@ -46,7 +45,6 @@ import com.amaze.filemanager.filesystem.MakeDirectoryOperation import com.amaze.filemanager.filesystem.files.FileUtils import com.amaze.filemanager.filesystem.files.MediaConnectionUtils import com.amaze.filemanager.ui.activities.MainActivity -import com.amaze.filemanager.utils.OnFileFound import com.amaze.filemanager.utils.Utils import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -184,16 +182,13 @@ class PreparePasteTask(strongRefMain: MainActivity) { destination.forEachChildrenFile( context.get(), isRootMode, - object : OnFileFound { - override fun onFileFound(file: HybridFileParcelable) { - for (fileToCopy in filesToCopy) { - if (file.getName(context.get()) == fileToCopy.getName(context.get())) { - conflictingFiles.add(fileToCopy) - } - } + ) { file: HybridFileParcelable -> + for (fileToCopy in filesToCopy) { + if (file.getName(context.get()) == fileToCopy.getName(context.get())) { + conflictingFiles.add(fileToCopy) } - }, - ) + } + } withContext(Dispatchers.Main) { prepareDialog(conflictingFiles, conflictingDirActionMap) @Suppress("DEPRECATION") diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/DeepSearch.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/DeepSearch.kt index 227626b161..19e7f00491 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/DeepSearch.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/DeepSearch.kt @@ -23,6 +23,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem import android.content.Context import com.amaze.filemanager.fileoperations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFile +import com.amaze.filemanager.filesystem.HybridFileParcelable import kotlinx.coroutines.isActive import org.slf4j.LoggerFactory import kotlin.coroutines.coroutineContext @@ -60,16 +61,7 @@ class DeepSearch( nextFile.forEachChildrenFile( applicationContext, SearchParameter.ROOT in searchParameters, - ) { file -> - if (!file.isHidden || SearchParameter.SHOW_HIDDEN_FILES in searchParameters) { - val resultRange = filter.searchFilter(file.getName(applicationContext)) - if (resultRange != null) { - publishProgress(file, resultRange) - } - if (file.isDirectory(applicationContext)) { - worklist.add(file) - } - } + ) { file: HybridFileParcelable -> } } } else { diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java b/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java index e49e0f26f0..14525210af 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java @@ -71,6 +71,8 @@ import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; +import kotlin.Unit; + public class CopyService extends AbstractProgressiveService { private static final Logger LOG = LoggerFactory.getLogger(CopyService.class); public static final String TAG_IS_ROOT_EXPLORER = "is_root"; @@ -319,6 +321,7 @@ private void findAndReplaceEncryptedEntry(HybridFileParcelable sourceFile) { // iterating each file inside source files which were copied to find instance of // any copied / moved encrypted file findAndReplaceEncryptedEntry(file); + return Unit.INSTANCE; }); } else { @@ -512,6 +515,7 @@ private void copyFiles( } catch (IOException e) { throw new IllegalStateException(e); // throw unchecked exception, no throws needed } + return Unit.INSTANCE; }); } else { if (!Operations.isFileNameValid(sourceFile.getName(c))) { diff --git a/app/src/main/java/com/amaze/filemanager/database/CloudContract.java b/app/src/main/java/com/amaze/filemanager/database/CloudContract.java deleted file mode 100644 index 533f1b2e74..0000000000 --- a/app/src/main/java/com/amaze/filemanager/database/CloudContract.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.database; - -/** Created by vishal on 19/4/17. */ -public class CloudContract { - - public static final String APP_PACKAGE_NAME = "com.filemanager.amazecloud"; - - public static final String PROVIDER_AUTHORITY = "com.amaze.cloud.provider"; - - public static final String PERMISSION_PROVIDER = "com.amaze.cloud.permission.ACCESS_PROVIDER"; - - public static final String DATABASE_NAME = "keys.db"; - public static final String TABLE_NAME = "secret_keys"; - public static final String COLUMN_ID = "_id"; - public static final String COLUMN_CLIENT_ID = "client_id"; - public static final String COLUMN_CLIENT_SECRET_KEY = "client_secret"; - public static final int DATABASE_VERSION = 1; -} diff --git a/app/src/main/java/com/amaze/filemanager/database/CloudContract.kt b/app/src/main/java/com/amaze/filemanager/database/CloudContract.kt new file mode 100644 index 0000000000..3d4be9c322 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/database/CloudContract.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2014-2024 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.database + +import android.net.Uri +import androidx.core.net.toUri +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.utils.cloud.CloudPluginUtil.resolvePluginIdFrom + +/** Created by vishal on 19/4/17. */ +object CloudContract { + const val CLOUD_PREFIX_BOX = "box:/" + const val CLOUD_PREFIX_DROPBOX = "dropbox:/" + const val CLOUD_PREFIX_GOOGLE_DRIVE = "gdrive:/" + const val CLOUD_PREFIX_ONE_DRIVE = "onedrive:/" + + const val CLOUD_NAME_GOOGLE_DRIVE = "Google Drive™" + const val CLOUD_NAME_DROPBOX = "Dropbox" + const val CLOUD_NAME_ONE_DRIVE = "One Drive" + const val CLOUD_NAME_BOX = "Box" + + const val APP_PACKAGE_NAME: String = "com.amaze.cloud" + const val PROVIDER_AUTHORITY: String = "com.amaze.cloud.provider" + const val PERMISSION_PROVIDER: String = "com.amaze.cloud.permission.ACCESS_PROVIDER" + + const val DATABASE_NAME: String = "keys.db" + const val TABLE_NAME: String = "secret_keys" + const val COLUMN_ID: String = "_id" + const val COLUMN_CLIENT_ID: String = "client_id" + const val COLUMN_CLIENT_SECRET_KEY: String = "client_secret" + + val URI: Uri = + Uri.withAppendedPath( + "content://$PROVIDER_AUTHORITY".toUri(), + "keys.db/secret_keys", + ) + + val PROJECTION = arrayOf(COLUMN_ID, COLUMN_CLIENT_ID, COLUMN_CLIENT_SECRET_KEY) + + val ENABLED_PROVIDERS = + arrayOf( + OpenMode.GDRIVE, + OpenMode.DROPBOX, + OpenMode.ONEDRIVE, + ) + + val ENABLED_PROVIDER_IDS: Array = + ENABLED_PROVIDERS.map { + resolvePluginIdFrom(it).toString() + }.toTypedArray() +} diff --git a/app/src/main/java/com/amaze/filemanager/database/CloudHandler.java b/app/src/main/java/com/amaze/filemanager/database/CloudHandler.java deleted file mode 100644 index ad826679de..0000000000 --- a/app/src/main/java/com/amaze/filemanager/database/CloudHandler.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.database; - -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.amaze.filemanager.database.models.explorer.CloudEntry; -import com.amaze.filemanager.fileoperations.exceptions.CloudPluginException; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; -import com.amaze.filemanager.ui.fragments.CloudSheetFragment; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import io.reactivex.schedulers.Schedulers; - -/** 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_NAME_GOOGLE_DRIVE = "Google Drive™"; - public static final String CLOUD_NAME_DROPBOX = "Dropbox"; - public static final String CLOUD_NAME_ONE_DRIVE = "One Drive"; - public static final String CLOUD_NAME_BOX = "Box"; - private final Logger LOG = LoggerFactory.getLogger(CloudHandler.class); - - private final ExplorerDatabase database; - private final Context context; - - public CloudHandler(@NonNull Context context, @NonNull ExplorerDatabase explorerDatabase) { - this.context = context; - this.database = explorerDatabase; - } - - public void addEntry(CloudEntry cloudEntry) throws CloudPluginException { - - if (!CloudSheetFragment.isCloudProviderAvailable(context)) throw new CloudPluginException(); - - database.cloudEntryDao().insert(cloudEntry).subscribeOn(Schedulers.io()).subscribe(); - } - - public void clear(OpenMode serviceType) { - database - .cloudEntryDao() - .findByServiceType(serviceType.ordinal()) - .subscribeOn(Schedulers.io()) - .subscribe( - cloudEntry -> - database - .cloudEntryDao() - .delete(cloudEntry) - .subscribeOn(Schedulers.io()) - .subscribe(), - throwable -> LOG.warn("failed to delete cloud connection", throwable)); - } - - public void clearAllCloudConnections() { - database.cloudEntryDao().clear().subscribeOn(Schedulers.io()).blockingGet(); - } - - public void updateEntry(OpenMode serviceType, CloudEntry newCloudEntry) - throws CloudPluginException { - - if (!CloudSheetFragment.isCloudProviderAvailable(context)) throw new CloudPluginException(); - - database.cloudEntryDao().update(newCloudEntry).subscribeOn(Schedulers.io()).subscribe(); - } - - public CloudEntry findEntry(OpenMode serviceType) throws CloudPluginException { - - if (!CloudSheetFragment.isCloudProviderAvailable(context)) throw new CloudPluginException(); - - try { - return database - .cloudEntryDao() - .findByServiceType(serviceType.ordinal()) - .subscribeOn(Schedulers.io()) - .blockingGet(); - } catch (Exception e) { - // catch error to handle Single#onError for blockingGet - LOG.error(getClass().getSimpleName(), e); - return null; - } - } - - public List getAllEntries() throws CloudPluginException { - - if (!CloudSheetFragment.isCloudProviderAvailable(context)) throw new CloudPluginException(); - return database.cloudEntryDao().list().subscribeOn(Schedulers.io()).blockingGet(); - } -} diff --git a/app/src/main/java/com/amaze/filemanager/database/ExplorerDatabase.kt b/app/src/main/java/com/amaze/filemanager/database/ExplorerDatabase.kt index 52b6fea189..ad33e0f534 100644 --- a/app/src/main/java/com/amaze/filemanager/database/ExplorerDatabase.kt +++ b/app/src/main/java/com/amaze/filemanager/database/ExplorerDatabase.kt @@ -27,11 +27,9 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import com.amaze.filemanager.database.daos.CloudEntryDao import com.amaze.filemanager.database.daos.EncryptedEntryDao import com.amaze.filemanager.database.daos.SortDao import com.amaze.filemanager.database.daos.TabDao -import com.amaze.filemanager.database.models.explorer.CloudEntry import com.amaze.filemanager.database.models.explorer.EncryptedEntry import com.amaze.filemanager.database.models.explorer.Sort import com.amaze.filemanager.database.models.explorer.Tab @@ -43,7 +41,7 @@ import com.amaze.filemanager.database.models.explorer.Tab * @see RoomDatabase */ @Database( - entities = [Tab::class, Sort::class, EncryptedEntry::class, CloudEntry::class], + entities = [Tab::class, Sort::class, EncryptedEntry::class], version = ExplorerDatabase.DATABASE_VERSION, ) @Suppress("StringLiteralDuplication", "ComplexMethod", "LongMethod") @@ -63,14 +61,9 @@ abstract class ExplorerDatabase : RoomDatabase() { */ abstract fun encryptedEntryDao(): EncryptedEntryDao - /** - * Returns DAO for [CloudEntry] objects. - */ - abstract fun cloudEntryDao(): CloudEntryDao - companion object { private const val DATABASE_NAME = "explorer.db" - const val DATABASE_VERSION = 11 + const val DATABASE_VERSION = 12 const val TABLE_TAB = "tab" const val TABLE_CLOUD_PERSIST = "cloud" const val TABLE_ENCRYPTED = "encrypted" @@ -317,7 +310,7 @@ abstract class ExplorerDatabase : RoomDatabase() { } internal val MIGRATION_10_11: Migration = - object : Migration(10, DATABASE_VERSION) { + object : Migration(10, 11) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL( "UPDATE " + @@ -331,6 +324,13 @@ abstract class ExplorerDatabase : RoomDatabase() { } } + internal val MIGRATION_11_12: Migration = + object : Migration(11, DATABASE_VERSION) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE IF EXISTS $TABLE_CLOUD_PERSIST") + } + } + /** * Initialize the database. Optionally, may provide a custom way to create the database * with supplied [Context]. @@ -354,6 +354,7 @@ abstract class ExplorerDatabase : RoomDatabase() { .addMigrations(MIGRATION_8_9) .addMigrations(MIGRATION_9_10) .addMigrations(MIGRATION_10_11) + .addMigrations(MIGRATION_11_12) .allowMainThreadQueries() .build() } diff --git a/app/src/main/java/com/amaze/filemanager/database/daos/CloudEntryDao.java b/app/src/main/java/com/amaze/filemanager/database/daos/CloudEntryDao.java deleted file mode 100644 index 62a83b211f..0000000000 --- a/app/src/main/java/com/amaze/filemanager/database/daos/CloudEntryDao.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.database.daos; - -import static com.amaze.filemanager.database.ExplorerDatabase.COLUMN_CLOUD_SERVICE; -import static com.amaze.filemanager.database.ExplorerDatabase.TABLE_CLOUD_PERSIST; - -import java.util.List; - -import com.amaze.filemanager.database.models.explorer.CloudEntry; - -import androidx.room.Dao; -import androidx.room.Delete; -import androidx.room.Insert; -import androidx.room.Query; -import androidx.room.Transaction; -import androidx.room.Update; - -import io.reactivex.Completable; -import io.reactivex.Single; - -/** - * {@link Dao} interface definition for {@link CloudEntry}. Concrete class is generated by Room - * during build. - * - * @see Dao - * @see CloudEntry - * @see com.amaze.filemanager.database.ExplorerDatabase - */ -@Dao -public interface CloudEntryDao { - - @Insert - Completable insert(CloudEntry entry); - - @Query( - "SELECT * FROM " + TABLE_CLOUD_PERSIST + " WHERE " + COLUMN_CLOUD_SERVICE + " = :serviceType") - Single findByServiceType(int serviceType); - - @Query("SELECT * FROM " + TABLE_CLOUD_PERSIST) - Single> list(); - - @Update - Completable update(CloudEntry entry); - - @Delete - Completable delete(CloudEntry entry); - - @Transaction - @Query("DELETE FROM " + TABLE_CLOUD_PERSIST) - Completable clear(); -} diff --git a/app/src/main/java/com/amaze/filemanager/database/models/explorer/CloudEntry.java b/app/src/main/java/com/amaze/filemanager/database/models/explorer/CloudEntry.java deleted file mode 100644 index b161593856..0000000000 --- a/app/src/main/java/com/amaze/filemanager/database/models/explorer/CloudEntry.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.database.models.explorer; - -import com.amaze.filemanager.database.ExplorerDatabase; -import com.amaze.filemanager.database.models.StringWrapper; -import com.amaze.filemanager.database.typeconverters.EncryptedStringTypeConverter; -import com.amaze.filemanager.database.typeconverters.OpenModeTypeConverter; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; - -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.PrimaryKey; -import androidx.room.TypeConverters; - -/** Created by vishal on 18/4/17. */ -@Entity(tableName = ExplorerDatabase.TABLE_CLOUD_PERSIST) -public class CloudEntry { - - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = ExplorerDatabase.COLUMN_CLOUD_ID) - private int _id; - - @ColumnInfo(name = ExplorerDatabase.COLUMN_CLOUD_SERVICE) - @TypeConverters(OpenModeTypeConverter.class) - private OpenMode serviceType; - - @ColumnInfo(name = ExplorerDatabase.COLUMN_CLOUD_PERSIST) - @TypeConverters(EncryptedStringTypeConverter.class) - private StringWrapper persistData; - - public CloudEntry() {} - - public CloudEntry(OpenMode serviceType, String persistData) { - this.serviceType = serviceType; - this.persistData = new StringWrapper(persistData); - } - - public void setId(int _id) { - this._id = _id; - } - - public int getId() { - return this._id; - } - - public void setPersistData(StringWrapper persistData) { - this.persistData = persistData; - } - - public StringWrapper getPersistData() { - return this.persistData; - } - - /** Set the service type Support values from {@link OpenMode} */ - public void setServiceType(OpenMode openMode) { - this.serviceType = openMode; - } - - /** Returns ordinal value of service from {@link OpenMode} */ - public OpenMode getServiceType() { - return this.serviceType; - } -} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/FileProperties.kt b/app/src/main/java/com/amaze/filemanager/filesystem/FileProperties.kt index 2fbab94011..a1727d915f 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/FileProperties.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/FileProperties.kt @@ -30,7 +30,7 @@ import android.os.Environment import android.os.storage.StorageManager import android.provider.DocumentsContract import com.amaze.filemanager.application.AppConfig -import com.amaze.filemanager.database.CloudHandler +import com.amaze.filemanager.database.CloudContract import com.amaze.filemanager.filesystem.DeleteOperation.deleteFile import com.amaze.filemanager.filesystem.ExternalSdCardOperation.isOnExtSdCard import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool @@ -180,10 +180,10 @@ object FileProperties { f.startsWith(NetCopyClientConnectionPool.FTP_URI_PREFIX) || f.startsWith(NetCopyClientConnectionPool.FTPS_URI_PREFIX) || f.startsWith(OTGUtil.PREFIX_OTG) || - f.startsWith(CloudHandler.CLOUD_PREFIX_BOX) || - f.startsWith(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE) || - f.startsWith(CloudHandler.CLOUD_PREFIX_DROPBOX) || - f.startsWith(CloudHandler.CLOUD_PREFIX_ONE_DRIVE) || + f.startsWith(CloudContract.CLOUD_PREFIX_BOX) || + f.startsWith(CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE) || + f.startsWith(CloudContract.CLOUD_PREFIX_DROPBOX) || + f.startsWith(CloudContract.CLOUD_PREFIX_ONE_DRIVE) || f.startsWith("content://") ) { return 1 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 0c55808a20..2a3427023e 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java @@ -38,13 +38,10 @@ import com.amaze.filemanager.exceptions.NotAllowedException; import com.amaze.filemanager.exceptions.OperationWouldOverwriteException; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; -import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.GenericCopyUtil; import com.amaze.filemanager.ui.activities.MainActivity; -import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.smb.SmbUtil; -import com.cloudrail.si.interfaces.CloudStorage; import android.content.ContentResolver; import android.content.Context; @@ -110,157 +107,149 @@ public static final void writeUriToStorage( @NonNull final String currentPath) { MaybeOnSubscribe> writeUri = - (MaybeOnSubscribe>) - emitter -> { - List retval = new ArrayList<>(); - - for (Uri uri : uris) { - - BufferedInputStream bufferedInputStream = null; - try { - bufferedInputStream = - new BufferedInputStream(contentResolver.openInputStream(uri)); - } catch (FileNotFoundException e) { - emitter.onError(e); - return; - } - - BufferedOutputStream bufferedOutputStream = null; + emitter -> { + List retval = new ArrayList<>(); + + for (Uri uri : uris) { + + BufferedInputStream bufferedInputStream = null; + try { + bufferedInputStream = new BufferedInputStream(contentResolver.openInputStream(uri)); + } catch (FileNotFoundException e) { + emitter.onError(e); + return; + } + + BufferedOutputStream bufferedOutputStream = null; + + try { + DocumentFile documentFile = DocumentFile.fromSingleUri(mainActivity, uri); + String filename = documentFile.getName(); + if (filename == null) { + filename = uri.getLastPathSegment(); + + // For cleaning up slashes. Back in #1217 there is a case of + // Uri.getLastPathSegment() end up with a full file path + if (filename.contains("/")) + filename = filename.substring(filename.lastIndexOf('/') + 1); + } - try { - DocumentFile documentFile = DocumentFile.fromSingleUri(mainActivity, uri); - String filename = documentFile.getName(); - if (filename == null) { - filename = uri.getLastPathSegment(); + String finalFilePath = currentPath + "/" + filename; - // For cleaning up slashes. Back in #1217 there is a case of - // Uri.getLastPathSegment() end up with a full file path - if (filename.contains("/")) - filename = filename.substring(filename.lastIndexOf('/') + 1); - } + HybridFile hFile = new HybridFile(OpenMode.UNKNOWN, currentPath); + hFile.generateMode(mainActivity); - String finalFilePath = currentPath + "/" + filename; - DataUtils dataUtils = DataUtils.getInstance(); - - HybridFile hFile = new HybridFile(OpenMode.UNKNOWN, currentPath); - hFile.generateMode(mainActivity); - - switch (hFile.getMode()) { - case FILE: - case ROOT: - File targetFile = new File(finalFilePath); - if (!FileProperties.isWritableNormalOrSaf( - targetFile.getParentFile(), mainActivity.getApplicationContext())) { - emitter.onError(new NotAllowedException()); - return; - } - - DocumentFile targetDocumentFile = - ExternalSdCardOperation.getDocumentFile( - targetFile, false, mainActivity.getApplicationContext()); - - // Fallback, in case getDocumentFile() didn't properly return a - // DocumentFile - // instance - if (targetDocumentFile == null) { - targetDocumentFile = DocumentFile.fromFile(targetFile); - } - - // Lazy check... and in fact, different apps may pass in URI in different - // formats, so we could only check filename matches - // FIXME?: Prompt overwrite instead of simply blocking - if (targetDocumentFile.exists() && targetDocumentFile.length() > 0) { - emitter.onError(new OperationWouldOverwriteException()); - return; - } - - bufferedOutputStream = - new BufferedOutputStream( - contentResolver.openOutputStream(targetDocumentFile.getUri())); - retval.add(targetFile.getPath()); - break; - case SMB: - SmbFile targetSmbFile = SmbUtil.create(finalFilePath); - if (targetSmbFile.exists()) { - emitter.onError(new OperationWouldOverwriteException()); - return; - } else { - OutputStream outputStream = targetSmbFile.getOutputStream(); - bufferedOutputStream = new BufferedOutputStream(outputStream); - retval.add(HybridFile.parseAndFormatUriForDisplay(targetSmbFile.getPath())); - } - break; - case SFTP: - // FIXME: implement support - AppConfig.toast(mainActivity, mainActivity.getString(R.string.not_allowed)); - emitter.onError(new NotImplementedError()); - return; - case DROPBOX: - case BOX: - case ONEDRIVE: - case GDRIVE: - OpenMode mode = hFile.getMode(); - - CloudStorage cloudStorage = dataUtils.getAccount(mode); - String path = CloudUtil.stripPath(mode, finalFilePath); - cloudStorage.upload(path, bufferedInputStream, documentFile.length(), true); - retval.add(path); - break; - case OTG: - DocumentFile documentTargetFile = - OTGUtil.getDocumentFile(finalFilePath, mainActivity, true); - - if (documentTargetFile.exists()) { - emitter.onError(new OperationWouldOverwriteException()); - return; - } - - bufferedOutputStream = - new BufferedOutputStream( - contentResolver.openOutputStream(documentTargetFile.getUri()), - GenericCopyUtil.DEFAULT_BUFFER_SIZE); - - retval.add(documentTargetFile.getUri().getPath()); - break; - default: - return; + switch (hFile.getMode()) { + case FILE: + case ROOT: + File targetFile = new File(finalFilePath); + if (!FileProperties.isWritableNormalOrSaf( + targetFile.getParentFile(), mainActivity.getApplicationContext())) { + emitter.onError(new NotAllowedException()); + return; } - int count = 0; - byte[] buffer = new byte[GenericCopyUtil.DEFAULT_BUFFER_SIZE]; + DocumentFile targetDocumentFile = + ExternalSdCardOperation.getDocumentFile( + targetFile, false, mainActivity.getApplicationContext()); - while (count != -1) { - count = bufferedInputStream.read(buffer); - if (count != -1) { + // Fallback, in case getDocumentFile() didn't properly return a + // DocumentFile + // instance + if (targetDocumentFile == null) { + targetDocumentFile = DocumentFile.fromFile(targetFile); + } - bufferedOutputStream.write(buffer, 0, count); - } + // Lazy check... and in fact, different apps may pass in URI in different + // formats, so we could only check filename matches + // FIXME?: Prompt overwrite instead of simply blocking + if (targetDocumentFile.exists() && targetDocumentFile.length() > 0) { + emitter.onError(new OperationWouldOverwriteException()); + return; } - bufferedOutputStream.flush(); - } catch (IOException e) { - emitter.onError(e); + bufferedOutputStream = + new BufferedOutputStream( + contentResolver.openOutputStream(targetDocumentFile.getUri())); + retval.add(targetFile.getPath()); + break; + case SMB: + SmbFile targetSmbFile = SmbUtil.create(finalFilePath); + if (targetSmbFile.exists()) { + emitter.onError(new OperationWouldOverwriteException()); + return; + } else { + OutputStream outputStream = targetSmbFile.getOutputStream(); + bufferedOutputStream = new BufferedOutputStream(outputStream); + retval.add(HybridFile.parseAndFormatUriForDisplay(targetSmbFile.getPath())); + } + break; + case SFTP: + // FIXME: implement support + AppConfig.toast(mainActivity, mainActivity.getString(R.string.not_allowed)); + emitter.onError(new NotImplementedError()); return; - } finally { - try { - if (bufferedInputStream != null) { - bufferedInputStream.close(); - } - if (bufferedOutputStream != null) { - bufferedOutputStream.close(); - } - } catch (IOException e) { - emitter.onError(e); + case DROPBOX: + case BOX: + case ONEDRIVE: + case GDRIVE: + LOG.warn("Cloud write Uri not supported yet"); + break; + case OTG: + DocumentFile documentTargetFile = + OTGUtil.getDocumentFile(finalFilePath, mainActivity, true); + + if (documentTargetFile.exists()) { + emitter.onError(new OperationWouldOverwriteException()); + return; } - } + + bufferedOutputStream = + new BufferedOutputStream( + contentResolver.openOutputStream(documentTargetFile.getUri()), + GenericCopyUtil.DEFAULT_BUFFER_SIZE); + + retval.add(documentTargetFile.getUri().getPath()); + break; + default: + return; } - if (retval.size() > 0) { - emitter.onSuccess(retval); - } else { - emitter.onError(new Exception()); + int count = 0; + byte[] buffer = new byte[GenericCopyUtil.DEFAULT_BUFFER_SIZE]; + + while (count != -1) { + count = bufferedInputStream.read(buffer); + if (count != -1) { + + bufferedOutputStream.write(buffer, 0, count); + } + } + bufferedOutputStream.flush(); + + } catch (IOException e) { + emitter.onError(e); + return; + } finally { + try { + if (bufferedInputStream != null) { + bufferedInputStream.close(); + } + if (bufferedOutputStream != null) { + bufferedOutputStream.close(); + } + } catch (IOException e) { + emitter.onError(e); } - }; + } + } + + if (retval.size() > 0) { + emitter.onSuccess(retval); + } else { + emitter.onError(new Exception()); + } + }; Maybe.create(writeUri) .subscribeOn(Schedulers.io()) 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 37b01b01df..45d7ff443f 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -28,6 +28,7 @@ import static com.amaze.filemanager.filesystem.smb.CifsContexts.SMB_URI_PREFIX; import static com.amaze.filemanager.filesystem.ssh.SFTPClientExtKt.READ_AHEAD_MAX_UNCONFIRMED_READS; import static com.amaze.filemanager.filesystem.ssh.SshClientUtils.sftpGetSize; +import static com.amaze.filemanager.utils.omh.OMHClientHelper.MULTI_SLASH_FOR_CLOUD; import java.io.File; import java.io.FileInputStream; @@ -48,7 +49,6 @@ import java.util.EnumSet; import java.util.List; import java.util.Locale; -import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -63,7 +63,7 @@ import com.amaze.filemanager.R; import com.amaze.filemanager.adapters.data.LayoutElementParcelable; import com.amaze.filemanager.application.AppConfig; -import com.amaze.filemanager.database.CloudHandler; +import com.amaze.filemanager.database.CloudContract; import com.amaze.filemanager.fileoperations.exceptions.CloudPluginException; import com.amaze.filemanager.fileoperations.exceptions.ShellNotRunningException; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; @@ -87,15 +87,14 @@ import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; -import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.OTGUtil; -import com.amaze.filemanager.utils.OnFileFound; import com.amaze.filemanager.utils.Utils; +import com.amaze.filemanager.utils.omh.AuthTrigger; import com.amaze.filemanager.utils.smb.SmbUtil; import com.amaze.trashbin.TrashBin; import com.amaze.trashbin.TrashBinFile; -import com.cloudrail.si.interfaces.CloudStorage; -import com.cloudrail.si.types.SpaceAllocation; +import com.openmobilehub.android.storage.core.model.OmhStorageEntity; +import com.openmobilehub.android.storage.core.model.OmhStorageMetadata; import android.content.ContentResolver; import android.content.Context; @@ -118,6 +117,7 @@ import io.reactivex.schedulers.Schedulers; import jcifs.smb.SmbException; import jcifs.smb.SmbFile; +import kotlin.Unit; import kotlin.collections.ArraysKt; import kotlin.io.ByteStreamsKt; import kotlin.text.Charsets; @@ -134,17 +134,16 @@ /** Hybrid file for handeling all types of files */ public class HybridFile { - private static final Logger LOG = LoggerFactory.getLogger(HybridFile.class); + protected static final Logger LOG = LoggerFactory.getLogger(HybridFile.class); public static final String DOCUMENT_FILE_PREFIX = "content://com.android.externalstorage.documents"; + protected String cloudFileId; protected String path; protected OpenMode mode; protected String name; - private final DataUtils dataUtils = DataUtils.getInstance(); - public HybridFile(OpenMode mode, String path) { this.path = path; this.mode = mode; @@ -186,13 +185,13 @@ public void generateMode(Context context) { mode = OpenMode.DOCUMENT_FILE; } else if (isCustomPath()) { mode = OpenMode.CUSTOM; - } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_BOX)) { + } else if (path.startsWith(CloudContract.CLOUD_PREFIX_BOX)) { mode = OpenMode.BOX; - } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_ONE_DRIVE)) { + } else if (path.startsWith(CloudContract.CLOUD_PREFIX_ONE_DRIVE)) { mode = OpenMode.ONEDRIVE; - } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE)) { + } else if (path.startsWith(CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE)) { mode = OpenMode.GDRIVE; - } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_DROPBOX)) { + } else if (path.startsWith(CloudContract.CLOUD_PREFIX_DROPBOX)) { mode = OpenMode.DROPBOX; } else if (path.equals("7") || isTrashBin()) { mode = OpenMode.TRASH_BIN; @@ -353,6 +352,11 @@ public Long execute(@NonNull SFTPClient client) throws IOException { return getFile().lastModified(); case DOCUMENT_FILE: return getDocumentFile(false).lastModified(); + case GDRIVE: + case DROPBOX: + case BOX: + case ONEDRIVE: + return HybridFileOmhStorageExtKt.getCloudLastModified(this); case ROOT: HybridFileParcelable baseFile = generateBaseFileFromParent(); if (baseFile != null) return baseFile.getDate(); @@ -412,16 +416,7 @@ public long length(Context context) { case BOX: case ONEDRIVE: case GDRIVE: - s = - Single.fromCallable( - () -> - dataUtils - .getAccount(mode) - .getMetadata(CloudUtil.stripPath(mode, path)) - .getSize()) - .subscribeOn(Schedulers.io()) - .blockingGet(); - return s; + return HybridFileOmhStorageExtKt.getCloudFileSize(this); default: break; } @@ -609,6 +604,7 @@ public String getParent(Context context) { * * @deprecated use {@link #isDirectory(Context)} to handle content resolvers */ + @Deprecated public boolean isDirectory() { boolean isDirectory; switch (mode) { @@ -685,14 +681,13 @@ public Boolean execute(@NonNull SFTPClient client) { case BOX: case GDRIVE: case ONEDRIVE: - return Single.fromCallable( - () -> - dataUtils - .getAccount(mode) - .getMetadata(CloudUtil.stripPath(mode, path)) - .getFolder()) - .subscribeOn(Schedulers.io()) - .blockingGet(); + try { + OmhStorageMetadata metadata = HybridFileOmhStorageExtKt.getCloudFileMetadata(this); + return metadata != null && metadata.getEntity() instanceof OmhStorageEntity.OmhFolder; + } catch (CloudPluginException e) { + LOG.error("Error obtaining metadata for cloud file", e); + return false; + } case TRASH_BIN: default: // also handles the case `FILE` File file = getFile(); @@ -703,6 +698,7 @@ public Boolean execute(@NonNull SFTPClient client) { /** * @deprecated use {@link #folderSize(Context)} */ + @Deprecated public long folderSize() { long size = 0L; @@ -722,6 +718,11 @@ public long folderSize() { HybridFileParcelable baseFile = generateBaseFileFromParent(); if (baseFile != null) size = baseFile.getSize(); break; + case GDRIVE: + case DROPBOX: + case ONEDRIVE: + case BOX: + return HybridFileOmhStorageExtKt.getCloudFolderSize(this); default: return 0L; } @@ -775,19 +776,19 @@ public long folderSize(Context context) { path, context, OpenMode.DOCUMENT_FILE, - file -> totalBytes.addAndGet(FileUtils.getBaseFileSize(file, context))); + file -> { + totalBytes.addAndGet(FileUtils.getBaseFileSize(file, context)); + return Unit.INSTANCE; + }); break; case DROPBOX: case BOX: case GDRIVE: case ONEDRIVE: - size = - FileUtils.folderSizeCloud( - mode, dataUtils.getAccount(mode).getMetadata(CloudUtil.stripPath(mode, path))); - break; + return HybridFileOmhStorageExtKt.getCloudFolderSize(this); case FTP: default: - return 0l; + return 0L; } return size; } @@ -799,16 +800,15 @@ public long getUsableSpace() { case SMB: size = Single.fromCallable( - (Callable) - () -> { - try { - SmbFile smbFile = getSmbFile(); - return smbFile != null ? smbFile.getDiskFreeSpace() : 0L; - } catch (SmbException e) { - LOG.warn("failed to get usage space for smb file", e); - return 0L; - } - }) + () -> { + try { + SmbFile smbFile = getSmbFile(); + return smbFile != null ? smbFile.getDiskFreeSpace() : 0L; + } catch (SmbException e) { + LOG.warn("failed to get usage space for smb file", e); + return 0L; + } + }) .subscribeOn(Schedulers.io()) .blockingGet(); break; @@ -821,8 +821,7 @@ public long getUsableSpace() { case BOX: case GDRIVE: case ONEDRIVE: - SpaceAllocation spaceAllocation = dataUtils.getAccount(mode).getAllocation(); - size = spaceAllocation.getTotal() - spaceAllocation.getUsed(); + size = HybridFileOmhStorageExtKt.getCloudUsableSpace(this); break; case SFTP: final Long returnValue = @@ -887,7 +886,7 @@ public Long execute(@NonNull SFTPClient client) throws IOException { /** Gets total size of the disk */ public long getTotal(Context context) { - long size = 0l; + long size = 0L; switch (mode) { case SMB: // TODO: Find total storage space of SMB when JCIFS adds support @@ -907,13 +906,12 @@ public long getTotal(Context context) { case BOX: case ONEDRIVE: case GDRIVE: - SpaceAllocation spaceAllocation = dataUtils.getAccount(mode).getAllocation(); - size = spaceAllocation.getTotal(); + size = HybridFileOmhStorageExtKt.getCloudTotalSpace(this); break; case SFTP: final Long returnValue = SshClientUtils.execute( - new SFtpClientTemplate(path, true) { + new SFtpClientTemplate<>(path, true) { @Override public Long execute(@NonNull SFTPClient client) throws IOException { try { @@ -958,7 +956,8 @@ public Long execute(@NonNull SFTPClient client) throws IOException { } /** Helper method to list children of this file */ - public void forEachChildrenFile(Context context, boolean isRoot, OnFileFound onFileFound) { + public void forEachChildrenFile( + Context context, boolean isRoot, Function onFileFound) { switch (mode) { case SFTP: SshClientUtils.execute( @@ -976,7 +975,7 @@ public Boolean execute(@NonNull SFTPClient client) { continue; } HybridFileParcelable f = new HybridFileParcelable(getPath(), isDirectory, info); - onFileFound.onFileFound(f); + onFileFound.apply(f); } } catch (IOException e) { LOG.warn("IOException", e); @@ -1004,7 +1003,7 @@ public Boolean execute(@NonNull SFTPClient client) { LOG.warn("failed to get children file for smb", shouldNeverHappen); baseFile = new HybridFileParcelable(smbFile1); } - onFileFound.onFileFound(baseFile); + onFileFound.apply(baseFile); } } } catch (SmbException e) { @@ -1023,7 +1022,7 @@ public FTPFile[] executeWithFtpClient(@NonNull FTPClient ftpClient) } }); for (FTPFile ftpFile : ftpFiles) { - onFileFound.onFileFound(new HybridFileParcelable(getPath(), ftpFile)); + onFileFound.apply(new HybridFileParcelable(getPath(), ftpFile)); } break; case OTG: @@ -1037,11 +1036,8 @@ public FTPFile[] executeWithFtpClient(@NonNull FTPClient ftpClient) case BOX: case GDRIVE: case ONEDRIVE: - try { - CloudUtil.getCloudFiles(path, dataUtils.getAccount(mode), mode, onFileFound); - } catch (CloudPluginException e) { - LOG.warn("failed to get children file for cloud file", e); - } + AuthTrigger authTrigger = AppConfig.getInstance().getCloudAuthTrigger(); + CloudUtil.getCloudFilesBlocking(cloudFileId, path, mode, authTrigger, onFileFound); break; case TRASH_BIN: default: @@ -1051,7 +1047,7 @@ public FTPFile[] executeWithFtpClient(@NonNull FTPClient ftpClient) true, openMode -> null, hybridFileParcelable -> { - onFileFound.onFileFound(hybridFileParcelable); + onFileFound.apply(hybridFileParcelable); return null; }); } @@ -1064,7 +1060,13 @@ public FTPFile[] executeWithFtpClient(@NonNull FTPClient ftpClient) */ public ArrayList listFiles(Context context, boolean isRoot) { ArrayList arrayList = new ArrayList<>(); - forEachChildrenFile(context, isRoot, arrayList::add); + forEachChildrenFile( + context, + isRoot, + file -> { + arrayList.add(file); + return Unit.INSTANCE; + }); return arrayList; } @@ -1104,7 +1106,7 @@ public InputStream getInputStream(Context context) { case SFTP: inputStream = SshClientUtils.execute( - new SFtpClientTemplate(getPath(), false) { + new SFtpClientTemplate<>(getPath(), false) { @Override public InputStream execute(@NonNull final SFTPClient client) throws IOException { final RemoteFile rf = @@ -1114,12 +1116,12 @@ public InputStream execute(@NonNull final SFTPClient client) throws IOException @Override public void close() throws IOException { try { - LOG.debug("Closing input stream for {}", getPath()); + LOG.trace("Closing input stream for {}", getPath()); super.close(); } catch (Throwable e) { - e.printStackTrace(); + LOG.warn("Error closing stream", e); } finally { - LOG.debug("Closing client for {}", getPath()); + LOG.trace("Closing client for {}", getPath()); rf.close(); client.close(); } @@ -1191,9 +1193,8 @@ public InputStream executeWithFtpClient(@NonNull FTPClient ftpClient) case BOX: case GDRIVE: case ONEDRIVE: - CloudStorage cloudStorageOneDrive = dataUtils.getAccount(mode); - LOG.debug(CloudUtil.stripPath(mode, path)); - inputStream = cloudStorageOneDrive.download(CloudUtil.stripPath(mode, path)); + LOG.trace(CloudUtil.stripCloudPath(mode, path)); + inputStream = HybridFileOmhStorageExtKt.downloadCloudFile(this); break; case TRASH_BIN: default: @@ -1214,7 +1215,7 @@ public OutputStream getOutputStream(Context context) { switch (mode) { case SFTP: return SshClientUtils.execute( - new SFtpClientTemplate(getPath(), false) { + new SFtpClientTemplate<>(getPath(), false) { @Nullable @Override public OutputStream execute(@NonNull SFTPClient client) throws IOException { @@ -1244,7 +1245,7 @@ public void close() throws IOException { case FTP: outputStream = NetCopyClientUtils.INSTANCE.execute( - new FtpClientTemplate(path, false) { + new FtpClientTemplate<>(path, false) { public OutputStream executeWithFtpClient(@NonNull FTPClient ftpClient) throws IOException { ftpClient.setFileType(FTP.BINARY_FILE_TYPE); @@ -1331,18 +1332,16 @@ public Boolean execute(SFTPClient client) throws IOException { else { exists = getFtpFile() != null; } - } 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 (isCloudDriveFile()) { + if (cloudFileId == null) { + return false; + } + try { + OmhStorageMetadata metadata = HybridFileOmhStorageExtKt.getCloudFileMetadata(this); + exists = metadata != null; + } catch (CloudPluginException e) { + LOG.error("Error fetching metadata", e); + } } else if (isLocal()) { exists = getFile().exists(); } else if (isRoot()) { @@ -1438,6 +1437,9 @@ public Boolean execute(@NonNull Session session) throws IOException { return 0 == cmd.getExitStatus(); } })); + } else if (isCloudDriveFile()) { + // do nothing + return true; } else if (isTrashBin()) { // do nothing return true; @@ -1507,12 +1509,7 @@ public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) throws IOExcep } } } else if (isCloudDriveFile()) { - CloudStorage cloudStorageDropbox = dataUtils.getAccount(mode); - try { - cloudStorageDropbox.createFolder(CloudUtil.stripPath(mode, path)); - } catch (Exception e) { - LOG.warn("failed to create folder for cloud file", e); - } + HybridFileOmhStorageExtKt.createCloudFolder(this); } else if (isTrashBin()) { // do nothing } else MakeDirectoryOperation.mkdirs(context, this); } @@ -1550,6 +1547,8 @@ public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) LOG.error("Error delete SMB file", e); throw e; } + } else if (isCloudDriveFile()) { + HybridFileOmhStorageExtKt.deleteCloudFile(this); } else if (isTrashBin()) { try { deletePermanentlyFromBin(context); @@ -1649,6 +1648,7 @@ public LayoutElementParcelable generateLayoutElement(@NonNull Context c, boolean layoutElement = new LayoutElementParcelable( c, + "", path, RootHelper.parseFilePermission(file), "", @@ -1663,6 +1663,7 @@ public LayoutElementParcelable generateLayoutElement(@NonNull Context c, boolean layoutElement = new LayoutElementParcelable( c, + "", file.getPath(), RootHelper.parseFilePermission(file), file.getPath(), @@ -1950,6 +1951,6 @@ private void openFileInternal(MainActivity activity) { } private void sanitizePathAsNecessary() { - this.path = this.path.replaceAll(MULTI_SLASH, "/"); + this.path = this.path.replaceAll(isCloudDriveFile() ? MULTI_SLASH_FOR_CLOUD : MULTI_SLASH, "/"); } } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileOmhStorageExt.kt b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileOmhStorageExt.kt new file mode 100644 index 0000000000..4453b25a7d --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileOmhStorageExt.kt @@ -0,0 +1,145 @@ +package com.amaze.filemanager.filesystem + +import android.content.Context +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.fileoperations.exceptions.CloudPluginException +import com.amaze.filemanager.filesystem.HybridFile.LOG +import com.amaze.filemanager.filesystem.cloud.CloudUtil +import com.amaze.filemanager.utils.omh.OMHClientHelper +import com.amaze.filemanager.utils.omh.createFolderBlocking +import com.amaze.filemanager.utils.omh.deleteFileBlocking +import com.amaze.filemanager.utils.omh.downloadFileBlocking +import com.amaze.filemanager.utils.omh.folderSizeBlocking +import com.amaze.filemanager.utils.omh.getFileMetadataBlocking +import com.amaze.filemanager.utils.omh.getStorageQuotaBlocking +import com.amaze.filemanager.utils.omh.getStorageUsageBlocking +import com.amaze.filemanager.utils.omh.retryOnUnauthorizedBlocking +import com.openmobilehub.android.storage.core.OmhStorageClient +import com.openmobilehub.android.storage.core.model.OmhStorageEntity +import com.openmobilehub.android.storage.core.model.OmhStorageMetadata +import java.io.InputStream + +private fun HybridFile.withStorageClient( + onFailure: (Throwable) -> Unit = { + LOG.error("Error in OmhStorageClient action", it) + throw CloudPluginException(it) + }, + action: (storageClient: OmhStorageClient) -> T, +): T? { + val storageClient = OMHClientHelper.getStorageClient(mode) + if (storageClient == null) { + LOG.error("Storage client is null for mode: $mode") + return null + } + return runCatching { + retryOnUnauthorizedBlocking(mode, AppConfig.getInstance().cloudAuthTrigger) { + action(storageClient) + } + }.onFailure(onFailure).getOrNull() +} + +/** + * Extension function to download a cloud file for a [HybridFile] instance. + * + * Method runs blockingly. + */ +fun HybridFile.downloadCloudFile(): InputStream? = + withStorageClient { storageClient -> + val tmpFilename = "$name.amaze_tmp" + storageClient.downloadFileBlocking(cloudFileId).writeTo( + AppConfig.getInstance().openFileOutput(tmpFilename, Context.MODE_PRIVATE), + ) + AppConfig.getInstance().openFileInput(tmpFilename) + } + +/** + * Extension function to delete a cloud file for a [HybridFile] instance. + * + * Method runs blockingly. + */ +fun HybridFile.deleteCloudFile() { + withStorageClient { storageClient -> + storageClient.deleteFileBlocking(cloudFileId) + } +} + +/** + * Extension function to fetch the last modified time of a cloud file for a [HybridFile] instance. + * + * Method runs blockingly. + */ +fun HybridFile.getCloudLastModified(): Long = getCloudFileMetadata()?.entity?.modifiedTime?.time ?: 0L + +/** + * Extension function to fetch the size of a cloud file for a [HybridFile] instance. + * + * Method runs blockingly. + */ +fun HybridFile.getCloudFileSize(): Long = + when (val entity = getCloudFileMetadata()?.entity) { + is OmhStorageEntity.OmhFile -> entity.size?.toLong() ?: 0L + else -> 0L + } + +/** + * Extension function to fetch the size of a cloud folder for a [HybridFile] instance. + * + * Method runs blockingly. + */ +fun HybridFile.getCloudFolderSize(): Long = + when (getCloudFileMetadata()?.entity) { + is OmhStorageEntity.OmhFolder -> { + withStorageClient { storageClient -> + storageClient.folderSizeBlocking(cloudFileId) + } ?: 0L + } + else -> 0L + } + +/** + * Extension function to fetch the total space of the cloud storage for a [HybridFile] instance. + * + * Method runs blockingly. + */ +fun HybridFile.getCloudTotalSpace(): Long = + withStorageClient { storageClient -> + storageClient.getStorageQuotaBlocking() + } ?: 0L + +/** + * Extension function to fetch the usable space of the cloud storage for a [HybridFile] instance. + * + * Method runs blockingly. + */ +fun HybridFile.getCloudUsableSpace(): Long = + withStorageClient { storageClient -> + val quota = storageClient.getStorageQuotaBlocking() + val used = storageClient.getStorageUsageBlocking() + quota - used + } ?: 0L + +/** + * Extension function to fetch cloud file metadata for a [HybridFile] instance. + * + * Method runs blockingly. + */ +@Throws(CloudPluginException::class) +fun HybridFile.getCloudFileMetadata(): OmhStorageMetadata? { + return withStorageClient { storageClient -> + storageClient.getFileMetadataBlocking(cloudFileId) + } +} + +/** + * Extension function to create a cloud folder for a [HybridFile] instance. + * + * Method runs blockingly. + */ +fun HybridFile.createCloudFolder() { + withStorageClient { storageClient -> + storageClient.createFolderBlocking( + CloudUtil.stripCloudPath(mode, path), + cloudFileId, + ) + } +} 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 46381a08a8..38c06a84a5 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java @@ -30,6 +30,7 @@ import com.amaze.filemanager.filesystem.files.sort.ComparableParcelable; import com.amaze.filemanager.filesystem.ftp.ExtensionsKt; import com.amaze.filemanager.utils.Utils; +import com.openmobilehub.android.storage.core.model.OmhStorageEntity; import android.content.ContentResolver; import android.content.Context; @@ -77,6 +78,7 @@ public HybridFileParcelable(SmbFile smbFile) throws SmbException { setSize(smbFile.isDirectory() ? 0 : smbFile.length()); } + /** Constructor for {@link FTPFile}. */ public HybridFileParcelable(String path, FTPFile ftpFile) { super( OpenMode.FTP, @@ -95,16 +97,41 @@ public HybridFileParcelable(String path, boolean isDirectory, RemoteResourceInfo setName(sshFile.getName()); setDirectory(isDirectory); setDate(sshFile.getAttributes().getMtime() * 1000); + setLastModified(sshFile.getAttributes().getMtime() * 1000); setSize(isDirectory ? 0 : sshFile.getAttributes().getSize()); setPermission( Integer.toString(FilePermission.toMask(sshFile.getAttributes().getPermissions()), 8)); } + /** Constructor for omh-storage {@link OmhStorageEntity}. */ + public HybridFileParcelable(String path, OpenMode openMode, OmhStorageEntity cloudFile) { + super(openMode, String.format("%s/%s", path, cloudFile.getName())); + cloudFileId = cloudFile.getId(); + setName(cloudFile.getName()); + setDirectory(cloudFile instanceof OmhStorageEntity.OmhFolder); + setDate(cloudFile.getModifiedTime() != null ? cloudFile.getModifiedTime().getTime() : 0L); + if (isDirectory) { + setSize(0L); + } else { + OmhStorageEntity.OmhFile file = OmhStorageEntity.OmhFile.class.cast(cloudFile); + setSize(file.getSize() != null ? file.getSize().longValue() : 0L); + } + setPermission(""); + } + @Override public long lastModified() { return date; } + public String getCloudFileId() { + return cloudFileId; + } + + public void setCloudFileId(String cloudFileId) { + this.cloudFileId = cloudFileId; + } + public String getName() { if (!Utils.isNullOrEmpty(name)) return name; else return super.getSimpleName(); @@ -144,13 +171,22 @@ public void setSize(long size) { this.size = size; } + @Override + public long length(Context context) { + if (isCloudDriveFile()) { + return size; + } else { + return super.length(context); + } + } + public boolean isDirectory() { return isDirectory; } @Override public boolean isDirectory(Context context) { - if (isSmb() || isSftp()) return isDirectory; + if (isSmb() || isSftp() || isCloudDriveFile()) return isDirectory; else return super.isDirectory(context); } 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 306eaaf3c7..84d50b074b 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java @@ -20,23 +20,24 @@ package com.amaze.filemanager.filesystem; +import static android.os.Build.VERSION_CODES.KITKAT; 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 java.io.ByteArrayInputStream; import java.io.File; 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 java.util.concurrent.Callable; import org.apache.commons.net.ftp.FTPClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amaze.filemanager.R; +import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.fileoperations.exceptions.ShellNotRunningException; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.filesystem.cloud.CloudUtil; @@ -49,13 +50,17 @@ import com.amaze.filemanager.filesystem.root.RenameFileCommand; import com.amaze.filemanager.filesystem.ssh.SFtpClientTemplate; import com.amaze.filemanager.filesystem.ssh.SshClientUtils; +import com.amaze.filemanager.ui.icons.MimeTypes; import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.OTGUtil; -import com.cloudrail.si.interfaces.CloudStorage; +import com.amaze.filemanager.utils.omh.OMHClientHelper; +import com.amaze.filemanager.utils.omh.OmhAuthClientExtKt; +import com.amaze.filemanager.utils.omh.OmhStorageClientExtKt; +import com.openmobilehub.android.storage.core.OmhStorageClient; +import com.openmobilehub.android.storage.core.model.OmhStorageEntity; import android.content.Context; import android.content.Intent; -import android.os.AsyncTask; import android.os.Build; import android.text.TextUtils; @@ -63,14 +68,18 @@ import androidx.arch.core.util.Function; import androidx.documentfile.provider.DocumentFile; +import io.reactivex.Flowable; +import io.reactivex.schedulers.Schedulers; import jcifs.smb.SmbException; import jcifs.smb.SmbFile; +import kotlin.Unit; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlin.text.StringsKt; +import kotlinx.coroutines.BuildersKt; import net.schmizz.sshj.sftp.SFTPClient; public class Operations { - private static final Executor executor = AsyncTask.THREAD_POOL_EXECUTOR; - private static final Logger LOG = LoggerFactory.getLogger(Operations.class); // reserved characters by OS, shall not be allowed in file names @@ -120,150 +129,131 @@ public static void mkdir( final boolean rootMode, @NonNull final ErrorCallBack errorCallBack) { - new AsyncTask() { - - private DataUtils dataUtils = DataUtils.getInstance(); - - private Function safCreateDirectory = - input -> { - if (input != null && input.isDirectory()) { - boolean result = false; - try { - result = input.createDirectory(file.getName(context)) != null; - } catch (Exception e) { - LOG.warn("Failed to make directory", e); - } - errorCallBack.done(file, result); - } else errorCallBack.done(file, false); - return null; - }; - - @Override - protected Void doInBackground(Void... params) { - // checking whether filename is valid or a recursive call possible - if (!Operations.isFileNameValid(file.getName(context))) { - errorCallBack.invalidName(file); + Function safCreateDirectory = + input -> { + if (input != null && input.isDirectory()) { + boolean result = false; + try { + result = input.createDirectory(file.getName(context)) != null; + } catch (Exception e) { + LOG.warn("Failed to make directory", e); + } + errorCallBack.done(file, result); + } else errorCallBack.done(file, false); return null; - } + }; - if (file.exists()) { - errorCallBack.exists(file); - return null; - } + Flowable.fromCallable( + () -> { + // checking whether filename is valid or a recursive call possible + if (!Operations.isFileNameValid(file.getName(context))) { + errorCallBack.invalidName(file); + return Unit.INSTANCE; + } - // Android data directory, prohibit create directory - if (file.isAndroidDataDir()) { - errorCallBack.done(file, false); - return null; - } + if (file.exists()) { + errorCallBack.exists(file); + return Unit.INSTANCE; + } - if (file.isSftp() || file.isFtp()) { - file.mkdir(context); - /* - FIXME: throw Exceptions from HybridFile.mkdir() so errorCallback can throw Exceptions - here - */ - errorCallBack.done(file, true); - return null; - } - if (file.isSmb()) { - try { - file.getSmbFile(2000).mkdirs(); - } catch (SmbException e) { - LOG.warn("failed to make smb directories", e); - errorCallBack.done(file, false); - return null; - } - errorCallBack.done(file, file.exists()); - return null; - } - if (file.isOtgFile()) { - if (checkOtgNewFileExists(file, context)) { - errorCallBack.exists(file); - return null; - } - safCreateDirectory.apply(OTGUtil.getDocumentFile(parentFile.getPath(), context, false)); - return null; - } - if (file.isDocumentFile()) { - if (checkDocumentFileNewFileExists(file, context)) { - errorCallBack.exists(file); - return null; - } - safCreateDirectory.apply( - OTGUtil.getDocumentFile( - parentFile.getPath(), - SafRootHolder.getUriRoot(), - context, - OpenMode.DOCUMENT_FILE, - false)); - return null; - } else if (file.isDropBoxFile()) { - CloudStorage cloudStorageDropbox = dataUtils.getAccount(OpenMode.DROPBOX); - try { - cloudStorageDropbox.createFolder(CloudUtil.stripPath(OpenMode.DROPBOX, file.getPath())); - errorCallBack.done(file, true); - } catch (Exception e) { - LOG.warn("failed to make directory in cloud connection", e); - errorCallBack.done(file, false); - } - } else if (file.isBoxFile()) { - CloudStorage cloudStorageBox = dataUtils.getAccount(OpenMode.BOX); - try { - cloudStorageBox.createFolder(CloudUtil.stripPath(OpenMode.BOX, file.getPath())); - errorCallBack.done(file, true); - } catch (Exception e) { - LOG.warn("failed to make directory in cloud connection", e); - errorCallBack.done(file, false); - } - } else if (file.isOneDriveFile()) { - CloudStorage cloudStorageOneDrive = dataUtils.getAccount(OpenMode.ONEDRIVE); - try { - cloudStorageOneDrive.createFolder( - CloudUtil.stripPath(OpenMode.ONEDRIVE, file.getPath())); - errorCallBack.done(file, true); - } catch (Exception e) { - LOG.warn("failed to make directory in cloud connection", e); - errorCallBack.done(file, false); - } - } else if (file.isGoogleDriveFile()) { - CloudStorage cloudStorageGdrive = dataUtils.getAccount(OpenMode.GDRIVE); - try { - cloudStorageGdrive.createFolder(CloudUtil.stripPath(OpenMode.GDRIVE, file.getPath())); - errorCallBack.done(file, true); - } catch (Exception e) { - LOG.warn("failed to make directory in cloud connection", e); - errorCallBack.done(file, false); - } - } else { - if (file.isLocal() || file.isRoot()) { - int mode = checkFolder(new File(file.getParent(context)), context); - if (mode == 2) { - errorCallBack.launchSAF(file); - return null; - } - if (mode == 1 || mode == 0) MakeDirectoryOperation.mkdir(file.getFile(), context); - if (!file.exists() && rootMode) { - file.setMode(OpenMode.ROOT); - if (file.exists()) errorCallBack.exists(file); - try { - MakeDirectoryCommand.INSTANCE.makeDirectory( - file.getParent(context), file.getName(context)); - } catch (ShellNotRunningException e) { - LOG.warn("failed to make directory in local filesystem", e); + // Android data directory, prohibit create directory + if (file.isAndroidDataDir()) { + errorCallBack.done(file, false); + return Unit.INSTANCE; } - errorCallBack.done(file, file.exists()); - return null; - } - errorCallBack.done(file, file.exists()); - return null; - } - errorCallBack.done(file, file.exists()); - } - return null; - } - }.executeOnExecutor(executor); + if (file.isSftp() || file.isFtp()) { + file.mkdir(context); + /* + FIXME: throw Exceptions from HybridFile.mkdir() so errorCallback can throw Exceptions + here + */ + errorCallBack.done(file, true); + return Unit.INSTANCE; + } + if (file.isSmb()) { + try { + file.getSmbFile(2000).mkdirs(); + } catch (SmbException e) { + LOG.warn("failed to make smb directories", e); + errorCallBack.done(file, false); + return Unit.INSTANCE; + } + errorCallBack.done(file, file.exists()); + return Unit.INSTANCE; + } + if (file.isOtgFile()) { + if (checkOtgNewFileExists(file, context)) { + errorCallBack.exists(file); + return Unit.INSTANCE; + } + safCreateDirectory.apply( + OTGUtil.getDocumentFile(parentFile.getPath(), context, false)); + return Unit.INSTANCE; + } + if (file.isDocumentFile()) { + if (checkDocumentFileNewFileExists(file, context)) { + errorCallBack.exists(file); + return Unit.INSTANCE; + } + safCreateDirectory.apply( + OTGUtil.getDocumentFile( + parentFile.getPath(), + SafRootHolder.getUriRoot(), + context, + OpenMode.DOCUMENT_FILE, + false)); + return Unit.INSTANCE; + } else if (file.isCloudDriveFile()) { + OmhStorageClient storageClient = OMHClientHelper.getStorageClient(file.getMode()); + if (storageClient != null) { + OmhStorageEntity result = + OmhAuthClientExtKt.retryOnUnauthorizedBlocking( + file.getMode(), + AppConfig.getInstance().getCloudAuthTrigger(), + () -> { + OmhStorageEntity parentFolder = + OmhStorageClientExtKt.resolvePathBlocking( + storageClient, + CloudUtil.stripCloudPath(file.getMode(), parentFile.path)); + return OmhStorageClientExtKt.createFolderBlocking( + storageClient, + file.getSimpleName(), + parentFolder == null || parentFolder.getId() == null + ? storageClient.getRootFolder() + : parentFolder.getId()); + }); + errorCallBack.done(file, result != null); + } + } else { + if (file.isLocal() || file.isRoot()) { + int mode = checkFolder(new File(file.getParent(context)), context); + if (mode == 2) { + errorCallBack.launchSAF(file); + return Unit.INSTANCE; + } + if (mode == 1 || mode == 0) MakeDirectoryOperation.mkdir(file.getFile(), context); + if (!file.exists() && rootMode) { + file.setMode(OpenMode.ROOT); + if (file.exists()) errorCallBack.exists(file); + try { + MakeDirectoryCommand.INSTANCE.makeDirectory( + file.getParent(context), file.getName(context)); + } catch (ShellNotRunningException e) { + LOG.warn("failed to make directory in local filesystem", e); + } + errorCallBack.done(file, file.exists()); + return Unit.INSTANCE; + } + errorCallBack.done(file, file.exists()); + return Unit.INSTANCE; + } + errorCallBack.done(file, file.exists()); + } + return Unit.INSTANCE; + }) + .subscribeOn(Schedulers.io()) + .subscribe(); } public static void mkfile( @@ -273,187 +263,144 @@ public static void mkfile( final boolean rootMode, @NonNull final ErrorCallBack errorCallBack) { - new AsyncTask() { - - private DataUtils dataUtils = DataUtils.getInstance(); - - private Function safCreateFile = - input -> { - if (input != null && input.isDirectory()) { - boolean result = false; - try { - result = - input.createFile( - file.getName(context).substring(file.getName(context).lastIndexOf(".")), - file.getName(context)) - != null; - } catch (Exception e) { - LOG.warn(getClass().getSimpleName(), "Failed to make file", e); - } - errorCallBack.done(file, result); - } else errorCallBack.done(file, false); - return null; - }; - - @Override - protected Void doInBackground(Void... params) { - // check whether filename is valid or not - if (!Operations.isFileNameValid(file.getName(context))) { - errorCallBack.invalidName(file); - return null; - } - - if (file.exists()) { - errorCallBack.exists(file); - return null; - } - - // Android data directory, prohibit create file - if (file.isAndroidDataDir()) { - errorCallBack.done(file, false); + Function safCreateFile = + input -> { + if (input != null && input.isDirectory()) { + boolean result = false; + try { + result = + input.createFile( + file.getName(context).substring(file.getName(context).lastIndexOf(".")), + file.getName(context)) + != null; + } catch (Exception e) { + LOG.warn("Failed to make file", e); + } + errorCallBack.done(file, result); + } else errorCallBack.done(file, false); return null; - } + }; - if (file.isSftp() || file.isFtp()) { - OutputStream out = file.getOutputStream(context); - if (out == null) { - errorCallBack.done(file, false); - return null; - } - try { - out.close(); - errorCallBack.done(file, true); - return null; - } catch (IOException e) { - errorCallBack.done(file, false); - return null; - } - } - if (file.isSmb()) { - try { - file.getSmbFile(2000).createNewFile(); - } catch (SmbException e) { - LOG.warn("failed to make file in smb connection", e); - errorCallBack.done(file, false); - return null; - } - errorCallBack.done(file, file.exists()); - return null; - } else if (file.isDropBoxFile()) { - CloudStorage cloudStorageDropbox = dataUtils.getAccount(OpenMode.DROPBOX); - try { - byte[] tempBytes = new byte[0]; - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(tempBytes); - cloudStorageDropbox.upload( - CloudUtil.stripPath(OpenMode.DROPBOX, file.getPath()), - byteArrayInputStream, - 0l, - true); - errorCallBack.done(file, true); - } catch (Exception e) { - LOG.warn("failed to make file in cloud connection", e); - errorCallBack.done(file, false); - } - } else if (file.isBoxFile()) { - CloudStorage cloudStorageBox = dataUtils.getAccount(OpenMode.BOX); - try { - byte[] tempBytes = new byte[0]; - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(tempBytes); - cloudStorageBox.upload( - CloudUtil.stripPath(OpenMode.BOX, file.getPath()), byteArrayInputStream, 0l, true); - errorCallBack.done(file, true); - } catch (Exception e) { - LOG.warn("failed to make file in cloud connection", e); - errorCallBack.done(file, false); - } - } else if (file.isOneDriveFile()) { - CloudStorage cloudStorageOneDrive = dataUtils.getAccount(OpenMode.ONEDRIVE); - try { - byte[] tempBytes = new byte[0]; - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(tempBytes); - cloudStorageOneDrive.upload( - CloudUtil.stripPath(OpenMode.ONEDRIVE, file.getPath()), - byteArrayInputStream, - 0l, - true); - errorCallBack.done(file, true); - } catch (Exception e) { - LOG.warn("failed to make file in cloud connection", e); - errorCallBack.done(file, false); - } - } else if (file.isGoogleDriveFile()) { - CloudStorage cloudStorageGdrive = dataUtils.getAccount(OpenMode.GDRIVE); - try { - byte[] tempBytes = new byte[0]; - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(tempBytes); - cloudStorageGdrive.upload( - CloudUtil.stripPath(OpenMode.GDRIVE, file.getPath()), - byteArrayInputStream, - 0l, - true); - errorCallBack.done(file, true); - } catch (Exception e) { - LOG.warn("failed to make file in cloud connection", e); - errorCallBack.done(file, false); - } - } else if (file.isOtgFile()) { - if (checkOtgNewFileExists(file, context)) { - errorCallBack.exists(file); - return null; - } - safCreateFile.apply(OTGUtil.getDocumentFile(parentFile.getPath(), context, false)); - return null; - } else if (file.isDocumentFile()) { - if (checkDocumentFileNewFileExists(file, context)) { - errorCallBack.exists(file); - return null; - } - safCreateFile.apply( - OTGUtil.getDocumentFile( - parentFile.getPath(), - SafRootHolder.getUriRoot(), - context, - OpenMode.DOCUMENT_FILE, - false)); - return null; - } else { - if (file.isLocal() || file.isRoot()) { - int mode = checkFolder(new File(file.getParent(context)), context); - if (mode == 2) { - errorCallBack.launchSAF(file); - return null; - } - if (mode == 1 || mode == 0) MakeFileOperation.mkfile(file.getFile(), context); - if (!file.exists() && rootMode) { - file.setMode(OpenMode.ROOT); - if (file.exists()) errorCallBack.exists(file); - try { - MakeFileCommand.INSTANCE.makeFile(file.getPath()); - } catch (ShellNotRunningException e) { - LOG.warn("failed to make file in local filesystem", e); + Flowable.fromCallable( + () -> { + // check whether filename is valid or not + if (!Operations.isFileNameValid(file.getName(context))) { + errorCallBack.invalidName(file); + return Unit.INSTANCE; } - errorCallBack.done(file, file.exists()); - return null; - } - errorCallBack.done(file, file.exists()); - return null; - } - errorCallBack.done(file, file.exists()); - } - return null; - } - @Override - protected void onPostExecute(Void unused) { - super.onPostExecute(unused); + if (file.exists()) { + errorCallBack.exists(file); + return Unit.INSTANCE; + } - // TODO: run this only when the operation gets completed successfully - MediaConnectionUtils.scanFile(context, file.path); + // Android data directory, prohibit create file + if (file.isAndroidDataDir()) { + errorCallBack.done(file, false); + return Unit.INSTANCE; + } - if (file.name.equals(FileUtils.NOMEDIA_FILE)) - MediaConnectionUtils.scanFile(context, file.getParent(context)); - } - }.executeOnExecutor(executor); + if (file.isSftp() || file.isFtp()) { + OutputStream out = file.getOutputStream(context); + if (out == null) { + errorCallBack.done(file, false); + return Unit.INSTANCE; + } + try { + out.close(); + errorCallBack.done(file, true); + return Unit.INSTANCE; + } catch (IOException e) { + errorCallBack.done(file, false); + return Unit.INSTANCE; + } + } + if (file.isSmb()) { + try { + file.getSmbFile(2000).createNewFile(); + } catch (SmbException e) { + LOG.warn("failed to make file in smb connection", e); + errorCallBack.done(file, false); + return Unit.INSTANCE; + } + errorCallBack.done(file, file.exists()); + return Unit.INSTANCE; + } else if (file.isCloudDriveFile()) { + OmhStorageClient storageClient = OMHClientHelper.getStorageClient(file.mode); + if (storageClient != null) { + String filename = file.getSimpleName(); + String extension = MimeTypes.getExtension(filename); + String filenameWithoutExtension = + StringsKt.substringBeforeLast(filename, ".", filename); + OmhAuthClientExtKt.retryOnUnauthorizedBlocking( + file.mode, + AppConfig.getInstance().getCloudAuthTrigger(), + () -> + OmhStorageClientExtKt.createFileWithExtensionBlocking( + storageClient, + filenameWithoutExtension, + extension, + parentFile.cloudFileId == null + ? storageClient.getRootFolder() + : parentFile.cloudFileId)); + return Unit.INSTANCE; + } + } else if (file.isOtgFile()) { + if (checkOtgNewFileExists(file, context)) { + errorCallBack.exists(file); + return Unit.INSTANCE; + } + safCreateFile.apply(OTGUtil.getDocumentFile(parentFile.getPath(), context, false)); + return Unit.INSTANCE; + } else if (file.isDocumentFile()) { + if (checkDocumentFileNewFileExists(file, context)) { + errorCallBack.exists(file); + return Unit.INSTANCE; + } + safCreateFile.apply( + OTGUtil.getDocumentFile( + parentFile.getPath(), + SafRootHolder.getUriRoot(), + context, + OpenMode.DOCUMENT_FILE, + false)); + return Unit.INSTANCE; + } else { + if (file.isLocal() || file.isRoot()) { + int mode = checkFolder(new File(file.getParent(context)), context); + if (mode == 2) { + errorCallBack.launchSAF(file); + return Unit.INSTANCE; + } + if (mode == 1 || mode == 0) MakeFileOperation.mkfile(file.getFile(), context); + if (!file.exists() && rootMode) { + file.setMode(OpenMode.ROOT); + if (file.exists()) errorCallBack.exists(file); + try { + MakeFileCommand.INSTANCE.makeFile(file.getPath()); + } catch (ShellNotRunningException e) { + LOG.warn("failed to make file in local filesystem", e); + } + errorCallBack.done(file, file.exists()); + return Unit.INSTANCE; + } + errorCallBack.done(file, file.exists()); + return Unit.INSTANCE; + } + errorCallBack.done(file, file.exists()); + } + return Unit.INSTANCE; + }) + .subscribeOn(Schedulers.io()) + .doOnComplete( + () -> { + // TODO: run this only when the operation gets completed successfully + MediaConnectionUtils.scanFile(context, file.path); + + if (file.name.equals(FileUtils.NOMEDIA_FILE)) + MediaConnectionUtils.scanFile(context, file.getParent(context)); + }) + .subscribe(); } public static void rename( @@ -463,275 +410,263 @@ public static void rename( @NonNull final Context context, @NonNull final ErrorCallBack errorCallBack) { - new AsyncTask() { - - private final DataUtils dataUtils = DataUtils.getInstance(); - - /** - * Determines whether double rename is required based on original and new file name regardless - * of the case-sensitivity of the filesystem - */ - private final boolean isCaseSensitiveRename = - oldFile.getSimpleName().equalsIgnoreCase(newFile.getSimpleName()) - && !oldFile.getSimpleName().equals(newFile.getSimpleName()); - - /** - * random string that is appended to file to prevent name collision, max file name is 255 - * bytes - */ - private static final String TEMP_FILE_EXT = "u0CtHRqWUnvxIaeBQ@nY2umVm9MDyR1P"; - - private boolean localRename(@NonNull HybridFile oldFile, @NonNull HybridFile newFile) { - File file = new File(oldFile.getPath()); - File file1 = new File(newFile.getPath()); - boolean result = false; - - switch (oldFile.getMode()) { - case FILE: - int mode = checkFolder(file.getParentFile(), context); - if (mode == 1 || mode == 0) { - try { - RenameOperation.renameFolder(file, file1, context); - } catch (ShellNotRunningException e) { - LOG.warn("failed to rename file in local filesystem", e); - } - result = !file.exists() && file1.exists(); - if (!result && rootMode) { - try { - RenameFileCommand.INSTANCE.renameFile(file.getPath(), file1.getPath()); - } catch (ShellNotRunningException e) { - LOG.warn("failed to rename file in local filesystem", e); + Flowable.fromCallable( + new Callable() { + + private final DataUtils dataUtils = DataUtils.INSTANCE; + + /** + * Determines whether double rename is required based on original and new file name + * regardless of the case-sensitivity of the filesystem + */ + private final boolean isCaseSensitiveRename = + oldFile.getSimpleName().equalsIgnoreCase(newFile.getSimpleName()) + && !oldFile.getSimpleName().equals(newFile.getSimpleName()); + + /** + * random string that is appended to file to prevent name collision, max file name is + * 255 bytes + */ + private static final String TEMP_FILE_EXT = "u0CtHRqWUnvxIaeBQ@nY2umVm9MDyR1P"; + + private boolean localRename( + @NonNull HybridFile oldFile, @NonNull HybridFile newFile) { + File file = new File(oldFile.getPath()); + File file1 = new File(newFile.getPath()); + boolean result = false; + + switch (oldFile.getMode()) { + case FILE: + int mode = checkFolder(file.getParentFile(), context); + if (mode == 1 || mode == 0) { + try { + RenameOperation.renameFolder(file, file1, context); + } catch (ShellNotRunningException e) { + LOG.warn("failed to rename file in local filesystem", e); + } + result = !file.exists() && file1.exists(); + if (!result && rootMode) { + try { + RenameFileCommand.INSTANCE.renameFile(file.getPath(), file1.getPath()); + } catch (ShellNotRunningException e) { + LOG.warn("failed to rename file in local filesystem", e); + } + oldFile.setMode(OpenMode.ROOT); + newFile.setMode(OpenMode.ROOT); + result = !file.exists() && file1.exists(); + } + } + break; + case ROOT: + try { + result = + RenameFileCommand.INSTANCE.renameFile(file.getPath(), file1.getPath()); + } catch (ShellNotRunningException e) { + LOG.warn("failed to rename file in root", e); + } + newFile.setMode(OpenMode.ROOT); + break; } - oldFile.setMode(OpenMode.ROOT); - newFile.setMode(OpenMode.ROOT); - result = !file.exists() && file1.exists(); + return result; } - } - break; - case ROOT: - try { - result = RenameFileCommand.INSTANCE.renameFile(file.getPath(), file1.getPath()); - } catch (ShellNotRunningException e) { - LOG.warn("failed to rename file in root", e); - } - newFile.setMode(OpenMode.ROOT); - break; - } - return result; - } - private boolean localDoubleRename(@NonNull HybridFile oldFile, @NonNull HybridFile newFile) { - HybridFile tempFile = new HybridFile(oldFile.mode, oldFile.getPath().concat(TEMP_FILE_EXT)); - if (localRename(oldFile, tempFile)) { - if (localRename(tempFile, newFile)) { - return true; - } else { - // attempts to rollback - // changes the temporary file name back to original file name - LOG.warn("reverting temporary file rename"); - return localRename(tempFile, oldFile); - } - } - return false; - } + private boolean localDoubleRename( + @NonNull HybridFile oldFile, @NonNull HybridFile newFile) { + HybridFile tempFile = + new HybridFile(oldFile.mode, oldFile.getPath().concat(TEMP_FILE_EXT)); + if (localRename(oldFile, tempFile)) { + if (localRename(tempFile, newFile)) { + return true; + } else { + // attempts to rollback + // changes the temporary file name back to original file name + LOG.warn("reverting temporary file rename"); + return localRename(tempFile, oldFile); + } + } + return false; + } - private Function safRenameFile = - input -> { - boolean result = false; - try { - result = input.renameTo(newFile.getName(context)); - } catch (Exception e) { - LOG.warn(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. - // If rename is on OTG, we are skipping - if (!Operations.isFileNameValid(newFile.getName(context))) { - errorCallBack.invalidName(newFile); - return null; - } + private Function safRenameFile = + input -> { + boolean result = false; + try { + result = input.renameTo(newFile.getName(context)); + } catch (Exception e) { + LOG.warn(getClass().getSimpleName(), "Failed to rename", e); + } + errorCallBack.done(newFile, result); + return null; + }; + + @Override + public Unit call() throws Exception { + // check whether file names for new file are valid or recursion occurs. + // If rename is on OTG, we are skipping + if (!Operations.isFileNameValid(newFile.getName(context))) { + errorCallBack.invalidName(newFile); + return Unit.INSTANCE; + } - if (newFile.exists() && !isCaseSensitiveRename) { - errorCallBack.exists(newFile); - return null; - } + if (newFile.exists() && !isCaseSensitiveRename) { + errorCallBack.exists(newFile); + return Unit.INSTANCE; + } - 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()); - try { - ArrayList failedOps = new ArrayList<>(); - failedOps.add(new HybridFileParcelable(oldFile.getSmbFile())); - context.sendBroadcast( - new Intent(TAG_INTENT_FILTER_GENERAL) - .putParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS, failedOps)); - } catch (SmbException exceptionThrownDuringBuildParcelable) { - LOG.error( - "Error creating HybridFileParcelable", exceptionThrownDuringBuildParcelable); - } - LOG.error(errmsg, e); - } - return null; - } else if (oldFile.isSftp()) { - SshClientUtils.execute( - new SFtpClientTemplate(oldFile.getPath(), true) { - @Override - public Void execute(@NonNull SFTPClient client) { + if (oldFile.isSmb()) { try { - client.rename( - NetCopyClientUtils.extractRemotePathFrom(oldFile.getPath()), - NetCopyClientUtils.extractRemotePathFrom(newFile.getPath())); - errorCallBack.done(newFile, true); - } catch (IOException e) { + 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 Unit.INSTANCE; + } + 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()); - LOG.error(errmsg); - 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) - .putParcelableArrayListExtra(TAG_INTENT_FILTER_FAILED_OPS, failedOps)); - errorCallBack.done(newFile, false); + try { + ArrayList failedOps = new ArrayList<>(); + failedOps.add(new HybridFileParcelable(oldFile.getSmbFile())); + context.sendBroadcast( + new Intent(TAG_INTENT_FILTER_GENERAL) + .putParcelableArrayListExtra( + TAG_INTENT_FILTER_FAILED_OPS, failedOps)); + } catch (SmbException exceptionThrownDuringBuildParcelable) { + LOG.error( + "Error creating HybridFileParcelable", + exceptionThrownDuringBuildParcelable); + } + LOG.error(errmsg, e); + } + return Unit.INSTANCE; + } else if (oldFile.isSftp()) { + SshClientUtils.execute( + new SFtpClientTemplate(oldFile.getPath(), true) { + @Override + public Unit execute(@NonNull SFTPClient client) { + try { + client.rename( + NetCopyClientUtils.extractRemotePathFrom(oldFile.getPath()), + NetCopyClientUtils.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.error(errmsg); + 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) + .putParcelableArrayListExtra( + TAG_INTENT_FILTER_FAILED_OPS, failedOps)); + errorCallBack.done(newFile, false); + } + return Unit.INSTANCE; + } + }); + } else if (oldFile.isFtp()) { + NetCopyClientUtils.INSTANCE.execute( + new FtpClientTemplate(oldFile.getPath(), false) { + public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) + throws IOException { + boolean result = + ftpClient.rename( + NetCopyClientUtils.extractRemotePathFrom(oldFile.getPath()), + NetCopyClientUtils.extractRemotePathFrom(newFile.getPath())); + errorCallBack.done(newFile, result); + return result; + } + }); + } else if (oldFile.isCloudDriveFile()) { + OmhStorageClient storageClient = + OMHClientHelper.getStorageClient(oldFile.getMode()); + if (storageClient != null) { + OmhStorageEntity oldCloudFile = + BuildersKt.runBlocking( + EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> + storageClient.resolvePath( + CloudUtil.stripCloudPath(oldFile.getMode(), oldFile.path), + continuation)); + if (oldCloudFile != null) { + BuildersKt.runBlocking( + EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> { + // storageClient.rename(oldCloudFile.getId(), + // newFile.name, continuation); + return Unit.INSTANCE; + }); + } + return Unit.INSTANCE; + } + } else if (oldFile.isOtgFile()) { + if (checkOtgNewFileExists(newFile, context)) { + errorCallBack.exists(newFile); + return Unit.INSTANCE; + } + safRenameFile.apply(OTGUtil.getDocumentFile(oldFile.getPath(), context, false)); + return Unit.INSTANCE; + } else if (oldFile.isDocumentFile()) { + if (checkDocumentFileNewFileExists(newFile, context)) { + errorCallBack.exists(newFile); + return Unit.INSTANCE; + } + safRenameFile.apply( + OTGUtil.getDocumentFile( + oldFile.getPath(), + SafRootHolder.getUriRoot(), + context, + OpenMode.DOCUMENT_FILE, + false)); + return Unit.INSTANCE; + } else { + File file = new File(oldFile.getPath()); + if (oldFile.getMode() == OpenMode.FILE) { + int mode = checkFolder(file.getParentFile(), context); + if (mode == 2) { + errorCallBack.launchSAF(oldFile, newFile); + } + } + + boolean result; + if (isCaseSensitiveRename) { + result = localDoubleRename(oldFile, newFile); + } else { + result = localRename(oldFile, newFile); } - return null; - } - }); - } else if (oldFile.isFtp()) { - NetCopyClientUtils.INSTANCE.execute( - new FtpClientTemplate(oldFile.getPath(), false) { - public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) - throws IOException { - boolean result = - ftpClient.rename( - NetCopyClientUtils.extractRemotePathFrom(oldFile.getPath()), - NetCopyClientUtils.extractRemotePathFrom(newFile.getPath())); errorCallBack.done(newFile, result); - return result; } - }); - } 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) { - LOG.warn("failed to rename file in cloud connection", e); - 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) { - LOG.warn("failed to rename file in cloud connection", e); - 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) { - LOG.warn("failed to rename file in cloud connection", e); - 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) { - LOG.warn("failed to rename file in cloud connection", e); - errorCallBack.done(newFile, false); - } - } else if (oldFile.isOtgFile()) { - if (checkOtgNewFileExists(newFile, context)) { - errorCallBack.exists(newFile); - return null; - } - safRenameFile.apply(OTGUtil.getDocumentFile(oldFile.getPath(), context, false)); - 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)); - return null; - } else { - File file = new File(oldFile.getPath()); - if (oldFile.getMode() == OpenMode.FILE) { - int mode = checkFolder(file.getParentFile(), context); - if (mode == 2) { - errorCallBack.launchSAF(oldFile, newFile); - } - } - - boolean result; - if (isCaseSensitiveRename) { - result = localDoubleRename(oldFile, newFile); - } else { - result = localRename(oldFile, newFile); - } - errorCallBack.done(newFile, result); - } - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - if (newFile != null && oldFile != null) { - HybridFile[] hybridFiles = {newFile, oldFile}; - MediaConnectionUtils.scanFile(context, hybridFiles); - } - } - }.executeOnExecutor(executor); + return Unit.INSTANCE; + } + }) + .subscribeOn(Schedulers.io()) + .doOnComplete( + () -> { + if (newFile.isLocal() || oldFile.isLocal()) { + HybridFile[] hybridFiles = {newFile, oldFile}; + MediaConnectionUtils.scanFile(context, hybridFiles); + } + }) + .subscribe(); } private static boolean checkOtgNewFileExists(HybridFile newFile, Context context) { @@ -778,7 +713,7 @@ private static int checkFolder(final File folder, Context context) { } return 1; } - } else if (Build.VERSION.SDK_INT == 19) { + } else if (Build.VERSION.SDK_INT == KITKAT) { // Assume that Kitkat workaround works if (ExternalSdCardOperation.isOnExtSdCard(folder, context)) return 1; } 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 deleted file mode 100644 index bc7bb48d8e..0000000000 --- a/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.filesystem.cloud; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.amaze.filemanager.R; -import com.amaze.filemanager.adapters.data.IconDataParcelable; -import com.amaze.filemanager.database.CloudHandler; -import com.amaze.filemanager.fileoperations.exceptions.CloudPluginException; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; -import com.amaze.filemanager.fileoperations.filesystem.cloud.CloudStreamer; -import com.amaze.filemanager.filesystem.HybridFile; -import com.amaze.filemanager.filesystem.HybridFileParcelable; -import com.amaze.filemanager.ui.activities.MainActivity; -import com.amaze.filemanager.ui.icons.MimeTypes; -import com.amaze.filemanager.utils.DataUtils; -import com.amaze.filemanager.utils.OTGUtil; -import com.amaze.filemanager.utils.OnFileFound; -import com.cloudrail.si.interfaces.CloudStorage; -import com.cloudrail.si.types.CloudMetaData; - -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.os.AsyncTask; -import android.widget.Toast; - -import androidx.annotation.Nullable; -import androidx.documentfile.provider.DocumentFile; - -/** - * Created by vishal on 19/4/17. - * - *

Class provides helper methods for cloud utilities - */ -public class CloudUtil { - - private static final Logger LOG = LoggerFactory.getLogger(CloudUtil.class); - - /** - * @deprecated use getCloudFiles() - */ - public static ArrayList listFiles( - String path, CloudStorage cloudStorage, OpenMode openMode) throws CloudPluginException { - final ArrayList baseFiles = new ArrayList<>(); - getCloudFiles(path, cloudStorage, openMode, baseFiles::add); - return baseFiles; - } - - public static void getCloudFiles( - String path, CloudStorage cloudStorage, OpenMode openMode, OnFileFound fileFoundCallback) - throws CloudPluginException { - String strippedPath = stripPath(openMode, path); - try { - for (CloudMetaData cloudMetaData : cloudStorage.getChildren(strippedPath)) { - HybridFileParcelable baseFile = - new HybridFileParcelable( - path + "/" + cloudMetaData.getName(), - "", - (cloudMetaData.getModifiedAt() == null) ? 0l : cloudMetaData.getModifiedAt(), - cloudMetaData.getSize(), - cloudMetaData.getFolder()); - baseFile.setName(cloudMetaData.getName()); - baseFile.setMode(openMode); - fileFoundCallback.onFileFound(baseFile); - } - } catch (Exception e) { - LOG.warn("failed to get cloud files", e); - throw new CloudPluginException(); - } - } - - /** Strips down the cloud path to remove any prefix */ - public static String stripPath(OpenMode openMode, String path) { - final String prefix; - - switch (openMode) { - case DROPBOX: - prefix = CloudHandler.CLOUD_PREFIX_DROPBOX; - break; - case BOX: - prefix = CloudHandler.CLOUD_PREFIX_BOX; - break; - case ONEDRIVE: - prefix = CloudHandler.CLOUD_PREFIX_ONE_DRIVE; - break; - case GDRIVE: - prefix = CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE; - break; - default: - return path; - } - - if (path.equals(prefix + "/")) { - // we're at root, just replace the prefix - return path.replace(prefix, ""); - } else { - // we're not at root, replace prefix + / - // handle when paths are in format gdrive:/Documents // TODO: normalize drive paths - String pathReplaced = path.replace(prefix + "/", ""); - if (pathReplaced.equals(path)) { - // we convert gdrive:/Documents to /Documents - return path.replace(prefix.substring(0, prefix.length() - 1), ""); - } - return pathReplaced; - } - } - - public static void launchCloud( - final HybridFile baseFile, final OpenMode serviceType, final Activity activity) { - final CloudStreamer streamer = CloudStreamer.getInstance(); - - new Thread( - () -> { - try { - streamer.setStreamSrc( - baseFile.getInputStream(activity), - baseFile.getName(activity), - baseFile.length(activity)); - activity.runOnUiThread( - () -> { - try { - File file = - new File( - Uri.parse(CloudUtil.stripPath(serviceType, baseFile.getPath())) - .getPath()); - Uri uri = - Uri.parse(CloudStreamer.URL + Uri.fromFile(file).getEncodedPath()); - Intent i = new Intent(Intent.ACTION_VIEW); - i.setDataAndType( - uri, - MimeTypes.getMimeType( - baseFile.getPath(), baseFile.isDirectory(activity))); - PackageManager packageManager = activity.getPackageManager(); - List resInfos = packageManager.queryIntentActivities(i, 0); - if (resInfos != null && resInfos.size() > 0) activity.startActivity(i); - else - Toast.makeText( - activity, - activity.getString(R.string.smb_launch_error), - Toast.LENGTH_SHORT) - .show(); - } catch (ActivityNotFoundException e) { - LOG.warn("failed to launch cloud file in activity", e); - } - }); - } catch (Exception e) { - LOG.warn("failed to launch cloud file", e); - } - }) - .start(); - } - - /** - * Asynctask checks if the item pressed on is a cloud account, and if the token that is saved for - * it is invalid or not, in which case, we'll clear off the saved token and authenticate the user - * again - * - * @param path the path of item in drawer - * @param mainActivity reference to main activity to fire callbacks to delete/add connection - */ - public static void checkToken(String path, final MainActivity mainActivity) { - - new AsyncTask() { - OpenMode serviceType; - - @Override - protected Boolean doInBackground(String... params) { - final DataUtils dataUtils = DataUtils.getInstance(); - boolean isTokenValid = true; - String path = params[0]; - final CloudStorage cloudStorage; - - if (path.startsWith(CloudHandler.CLOUD_PREFIX_DROPBOX)) { - // dropbox account - serviceType = OpenMode.DROPBOX; - cloudStorage = dataUtils.getAccount(OpenMode.DROPBOX); - } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_ONE_DRIVE)) { - - serviceType = OpenMode.ONEDRIVE; - cloudStorage = dataUtils.getAccount(OpenMode.ONEDRIVE); - } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_BOX)) { - - serviceType = OpenMode.BOX; - cloudStorage = dataUtils.getAccount(OpenMode.BOX); - } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE)) { - serviceType = OpenMode.GDRIVE; - cloudStorage = dataUtils.getAccount(OpenMode.GDRIVE); - } else { - throw new IllegalStateException(); - } - - try { - cloudStorage.getUserLogin(); - } catch (RuntimeException e) { - LOG.warn("Failed to validate user token for cloud connection", e); - isTokenValid = false; - } - return isTokenValid; - } - - @Override - protected void onPostExecute(Boolean aBoolean) { - super.onPostExecute(aBoolean); - - if (!aBoolean) { - // delete account and create a new one - Toast.makeText( - mainActivity, - mainActivity.getResources().getString(R.string.cloud_token_lost), - Toast.LENGTH_LONG) - .show(); - mainActivity.deleteConnection(serviceType); - mainActivity.addConnection(serviceType); - } - } - }.execute(path); - } - - /** Get an input stream for thumbnail for a given {@link IconDataParcelable} */ - @Nullable - public static InputStream getThumbnailInputStreamForCloud(Context context, String path) { - InputStream inputStream; - HybridFile hybridFile = new HybridFile(OpenMode.UNKNOWN, path); - hybridFile.generateMode(context); - DataUtils dataUtils = DataUtils.getInstance(); - - switch (hybridFile.getMode()) { - case SFTP: - inputStream = hybridFile.getInputStream(context); - break; - case FTP: - // Until we find a way to properly handle threading issues with thread unsafe FTPClient, - // we refrain from loading any files via FTP as file thumbnail. - TranceLove - inputStream = null; - break; - case SMB: - try { - inputStream = hybridFile.getSmbFile().getInputStream(); - } catch (IOException e) { - inputStream = null; - LOG.warn("failed to get inputstream for smb file for thumbnail", e); - } - break; - case OTG: - ContentResolver contentResolver = context.getContentResolver(); - DocumentFile documentSourceFile = - OTGUtil.getDocumentFile(hybridFile.getPath(), context, false); - try { - inputStream = contentResolver.openInputStream(documentSourceFile.getUri()); - } catch (FileNotFoundException e) { - LOG.warn("failed to get inputstream for otg for thumbnail", e); - inputStream = null; - } - break; - case DROPBOX: - case BOX: - case GDRIVE: - case ONEDRIVE: - OpenMode mode = hybridFile.getMode(); - - CloudStorage cloudStorageDropbox = dataUtils.getAccount(mode); - String stripped = CloudUtil.stripPath(mode, hybridFile.getPath()); - inputStream = cloudStorageDropbox.getThumbnail(stripped); - break; - default: - try { - inputStream = new FileInputStream(hybridFile.getPath()); - } catch (FileNotFoundException e) { - inputStream = null; - LOG.warn("failed to get inputstream for cloud files for thumbnail", e); - } - break; - } - - return inputStream; - } -} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.kt b/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.kt new file mode 100644 index 0000000000..a72a538e3c --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/cloud/CloudUtil.kt @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2014-2024 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.filesystem.cloud + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import androidx.arch.core.util.Function +import androidx.core.net.toUri +import com.amaze.filemanager.R +import com.amaze.filemanager.database.CloudContract +import com.amaze.filemanager.fileoperations.exceptions.CloudPluginException +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.fileoperations.filesystem.cloud.CloudStreamer +import com.amaze.filemanager.filesystem.HybridFile +import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.filesystem.cloud.CloudUtil.getCloudFiles +import com.amaze.filemanager.ui.icons.MimeTypes +import com.amaze.filemanager.utils.OTGUtil.getDocumentFile +import com.amaze.filemanager.utils.omh.AuthTrigger +import com.amaze.filemanager.utils.omh.OMHClientHelper.getStorageClient +import com.amaze.filemanager.utils.omh.retryOnUnauthorized +import com.openmobilehub.android.storage.core.ThumbnailSize +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.net.ProtocolException +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Created by vishal on 19/4/17. + * + * + * Class provides helper methods for cloud utilities + */ +object CloudUtil { + @JvmStatic + private val LOG: Logger = LoggerFactory.getLogger(CloudUtil::class.java) + + /** + * Blocking version of [getCloudFiles]. + * Collects all files first, then passes them to the callback to avoid + * ConcurrentModificationException. + */ + @JvmStatic + @Suppress("TooGenericExceptionCaught") + fun getCloudFilesBlocking( + folderId: String, + path: String, + openMode: OpenMode, + authTrigger: AuthTrigger, + fileFoundCallback: Function, + ) { + val collectedFiles = CopyOnWriteArrayList() + + runBlocking(Dispatchers.IO.limitedParallelism(5)) { + try { + getCloudFiles(folderId, path, openMode, authTrigger) { file -> + collectedFiles.add(file) + } + } catch (e: Exception) { + throw CloudPluginException(e) + } + } + + // Now safely pass all collected files to the callback on the calling thread + for (file in collectedFiles) { + fileFoundCallback.apply(file) + } + } + + /** + * Helper method to get list of files from cloud storage. + */ + @JvmStatic + @Suppress("TooGenericExceptionCaught") + suspend fun getCloudFiles( + folderId: String, + path: String, + openMode: OpenMode, + authTrigger: AuthTrigger, + fileFoundCallback: Function, + ) { + retryOnUnauthorized( + openMode = openMode, + trigger = authTrigger, + ) { + try { + getStorageClient(openMode)?.let { storageClient -> + for (omhStorageEntity in storageClient.listFiles(folderId.ifEmpty { storageClient.rootFolder })) { + val baseFile = HybridFileParcelable(path, openMode, omhStorageEntity) + baseFile.cloudFileId = omhStorageEntity.id + fileFoundCallback.apply(baseFile) + } + } + } catch (e: ProtocolException) { + LOG.warn("Protocol exception while getting cloud files: ", e) + throw CloudPluginException(e) + } catch (e: Exception) { + LOG.warn("failed to get cloud files", e) + throw CloudPluginException(e) + } + } + } + + /** Strips down the cloud path to remove any prefix */ + @JvmStatic + fun stripCloudPath( + openMode: OpenMode, + path: String, + ): String { + val prefix = + when (openMode) { + OpenMode.DROPBOX -> CloudContract.CLOUD_PREFIX_DROPBOX + OpenMode.BOX -> CloudContract.CLOUD_PREFIX_BOX + OpenMode.ONEDRIVE -> CloudContract.CLOUD_PREFIX_ONE_DRIVE + OpenMode.GDRIVE -> CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE + else -> return path + } + if (path == "$prefix/") { + // we're at root, just replace the prefix + return "" + } else { + // we're not at root, replace prefix + / + // handle when paths are in format gdrive:/Documents // TODO: normalize drive paths + val pathReplaced = path.replace("$prefix/", "") + if (pathReplaced == path) { + // we convert gdrive:/Documents to /Documents + return path.replace(prefix.substring(0, prefix.length - 1), "") + } + return pathReplaced + } + } + + /** + * Attempt to launch a cloud file using [CloudStreamer] and an implicit intent + */ + @JvmStatic + @Suppress("TooGenericExceptionCaught") + fun launchCloud( + baseFile: HybridFile, + serviceType: OpenMode, + activity: Activity, + ) { + val streamer = CloudStreamer.getInstance() + + Thread { + try { + streamer.setStreamSrc( + baseFile.getInputStream(activity), + baseFile.getName(activity), + baseFile.length(activity), + ) + activity.runOnUiThread { + try { + val file = + File( + stripCloudPath(serviceType, baseFile.path).toUri() + .path, + ) + val uri = + (CloudStreamer.URL + Uri.fromFile(file).encodedPath).toUri() + val i = Intent(Intent.ACTION_VIEW) + i.setDataAndType( + uri, + MimeTypes.getMimeType( + baseFile.path, + baseFile.isDirectory(activity), + ), + ) + val packageManager = activity.packageManager + val resInfos = packageManager.queryIntentActivities(i, 0) + if (resInfos.isNotEmpty()) { + activity.startActivity(i) + } else { + Toast.makeText( + activity, + activity.getString(R.string.smb_launch_error), + Toast.LENGTH_SHORT, + ) + .show() + } + } catch (e: ActivityNotFoundException) { + LOG.warn("failed to launch cloud file in activity", e) + } + } + } catch (e: Exception) { + LOG.warn("failed to launch cloud file", e) + } + } + .start() + } + + /** + * Get an input stream for thumbnail for a given path. + */ + @Suppress("LabeledExpression") + fun getThumbnailInputStreamForCloud( + context: Context, + path: String?, + ): InputStream? { + var inputStream: InputStream? + val hybridFile = HybridFile(OpenMode.UNKNOWN, path) + hybridFile.generateMode(context) + + when (hybridFile.mode) { + OpenMode.SFTP -> inputStream = hybridFile.getInputStream(context) + OpenMode.FTP -> // Until we find a way to properly handle threading issues with thread unsafe FTPClient, + // we refrain from loading any files via FTP as file thumbnail. - TranceLove + inputStream = null + + OpenMode.SMB -> + try { + inputStream = hybridFile.smbFile.inputStream + } catch (e: IOException) { + inputStream = null + LOG.warn("failed to get inputstream for smb file for thumbnail", e) + } + + OpenMode.OTG -> { + val contentResolver = context.contentResolver + val documentSourceFile = + getDocumentFile(hybridFile.path, context, false) + try { + inputStream = contentResolver.openInputStream(documentSourceFile!!.uri) + } catch (e: FileNotFoundException) { + LOG.warn("failed to get input stream for otg for thumbnail", e) + inputStream = null + } + } + + OpenMode.DROPBOX, OpenMode.BOX, OpenMode.GDRIVE, OpenMode.ONEDRIVE -> { + val storageClient = getStorageClient(openMode = hybridFile.mode) + if (storageClient == null) { + LOG.warn("failed to get input stream for cloud files for thumbnail - no storage client") + inputStream = null + } else { + inputStream = + ByteArrayInputStream( + runBlocking { + val path = stripCloudPath(hybridFile.mode, path!!) + val storageEntity = + storageClient.resolvePath( + if (path.startsWith("/")) path else "/$path", + ) ?: return@runBlocking ByteArray(0) + val cloudFileId = storageEntity.id + storageClient.getFileThumbnail( + cloudFileId, + ThumbnailSize.MEDIUM, + ).toByteArray() + }, + ) + } + } + + else -> + try { + inputStream = FileInputStream(hybridFile.path) + } catch (e: FileNotFoundException) { + inputStream = null + LOG.warn("failed to get inputstream for cloud files for thumbnail", e) + } + } + return inputStream + } +} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/CryptUtil.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/CryptUtil.java index b6ea1a7bbe..8c03462b17 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/CryptUtil.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/CryptUtil.java @@ -57,6 +57,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import kotlin.Unit; import kotlin.io.ByteStreamsKt; import kotlin.io.ConstantsKt; @@ -205,6 +206,7 @@ private void decrypt( } catch (IOException | GeneralSecurityException e) { throw new IllegalStateException(e); // throw unchecked exception, no throws needed } + return Unit.INSTANCE; }); } else { @@ -284,6 +286,7 @@ private void encrypt( } catch (IOException | GeneralSecurityException e) { throw new IllegalStateException(e); // throw unchecked exception, no throws needed } + return Unit.INSTANCE; }); } else { 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 6b6a6bec65..5bbc83b1db 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,7 +46,6 @@ import com.amaze.filemanager.filesystem.HybridFileParcelable; import com.amaze.filemanager.filesystem.Operations; import com.amaze.filemanager.filesystem.RootHelper; -import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.compressed.CompressedHelper; import com.amaze.filemanager.ui.activities.DatabaseViewerActivity; import com.amaze.filemanager.ui.activities.MainActivity; @@ -62,10 +61,11 @@ import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.OnProgressUpdate; import com.amaze.filemanager.utils.PackageInstallValidation; -import com.cloudrail.si.interfaces.CloudStorage; -import com.cloudrail.si.types.CloudMetaData; +import com.amaze.filemanager.utils.omh.OMHClientHelper; import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree; import com.googlecode.concurrenttrees.radix.node.concrete.voidvalue.VoidValue; +import com.openmobilehub.android.storage.core.OmhStorageClient; +import com.openmobilehub.android.storage.core.model.OmhStorageMetadata; import android.Manifest; import android.animation.Animator; @@ -78,7 +78,6 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; -import android.os.AsyncTask; import android.os.Build; import android.view.View; import android.widget.Toast; @@ -89,8 +88,16 @@ import androidx.core.util.Pair; import androidx.documentfile.provider.DocumentFile; +import io.reactivex.Flowable; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; import jcifs.smb.SmbFile; +import kotlin.Unit; import kotlin.collections.ArraysKt; +import kotlin.collections.CollectionsKt; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlinx.coroutines.rx2.RxSingleKt; import net.schmizz.sshj.sftp.RemoteResourceInfo; import net.schmizz.sshj.sftp.SFTPClient; import net.schmizz.sshj.sftp.SFTPException; @@ -169,20 +176,20 @@ public static Long folderSizeSftp(SFTPClient client, String remotePath) { } } - public static long folderSizeCloud(OpenMode openMode, CloudMetaData sourceFileMeta) { + public static long folderSizeCloud(OpenMode openMode, OmhStorageMetadata sourceFileMeta) { - DataUtils dataUtils = DataUtils.getInstance(); + // DataUtils dataUtils = DataUtils.INSTANCE; long length = 0; - CloudStorage cloudStorage = dataUtils.getAccount(openMode); - for (CloudMetaData metaData : - cloudStorage.getChildren(CloudUtil.stripPath(openMode, sourceFileMeta.getPath()))) { - - if (metaData.getFolder()) { - length += folderSizeCloud(openMode, metaData); - } else { - length += metaData.getSize(); - } - } + // CloudStorage cloudStorage = dataUtils.getAccount(openMode); + // for (CloudMetaData metaData : + // cloudStorage.getChildren(CloudUtil.stripPath(openMode, sourceFileMeta.getPath()))) { + // + // if (metaData.getFolder()) { + // length += folderSizeCloud(openMode, metaData); + // } else { + // length += metaData.getSize(); + // } + // } return length; } @@ -191,7 +198,12 @@ public static long folderSizeCloud(OpenMode openMode, CloudMetaData sourceFileMe public static long otgFolderSize(String path, final Context context) { final AtomicLong totalBytes = new AtomicLong(0); OTGUtil.getDocumentFiles( - path, context, file -> totalBytes.addAndGet(getBaseFileSize(file, context))); + path, + context, + file -> { + totalBytes.addAndGet(getBaseFileSize(file, context)); + return Unit.INSTANCE; + }); return totalBytes.longValue(); } @@ -263,55 +275,55 @@ public void onAnimationEnd(Animator animation) { // participate in layout passes, etc.) } - public static void shareCloudFile(String path, final OpenMode openMode, final Context context) { - new AsyncTask() { - - @Override - protected String doInBackground(String... params) { - String shareFilePath = params[0]; - CloudStorage cloudStorage = DataUtils.getInstance().getAccount(openMode); - return cloudStorage.createShareLink(CloudUtil.stripPath(openMode, shareFilePath)); - } - - @Override - protected void onPostExecute(String s) { - super.onPostExecute(s); - - FileUtils.copyToClipboard(context, s); - Toast.makeText(context, context.getString(R.string.cloud_share_copied), Toast.LENGTH_LONG) - .show(); - } - }.execute(path); + public static void shareCloudFile( + String cloudFileId, final OpenMode openMode, final Context context) { + OmhStorageClient storageClient = OMHClientHelper.getStorageClient(openMode); + if (storageClient != null) { + RxSingleKt.rxSingle( + EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> storageClient.getWebUrl(cloudFileId, continuation)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> { + FileUtils.copyToClipboard(context, result); + AppConfig.toast(context, R.string.cloud_share_copied); + }); + } else { + // FIXME: Toast + } } public static void shareCloudFiles( ArrayList files, final OpenMode openMode, final Context context) { - String[] paths = new String[files.size()]; - for (int i = 0; i < files.size(); i++) { - paths[i] = files.get(i).desc; + final OmhStorageClient storageClient = OMHClientHelper.getStorageClient(openMode); + if (storageClient != null) { + List> tasks = + CollectionsKt.map( + files, + layoutElementParcelable -> + RxSingleKt.rxSingle( + EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> + storageClient.getWebUrl( + layoutElementParcelable.cloudFileId, continuation))); + Flowable.fromIterable(tasks) + .flatMap(Single::toFlowable) + .toList() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + result -> { + StringBuilder sb = new StringBuilder(); + for (String line : result) { + sb.append(line).append('\n'); + } + FileUtils.copyToClipboard(context, sb.toString()); + AppConfig.toast(context, R.string.cloud_share_copied); + }); + } else { + // FIXME: Toast } - new AsyncTask() { - @Override - protected String doInBackground(String... params) { - CloudStorage cloudStorage = DataUtils.getInstance().getAccount(openMode); - StringBuilder links = new StringBuilder(); - links.append(cloudStorage.createShareLink(CloudUtil.stripPath(openMode, params[0]))); - for (int i = 1; i < params.length; i++) { - links.append('\n'); - links.append(cloudStorage.createShareLink(CloudUtil.stripPath(openMode, params[i]))); - } - return links.toString(); - } - - @Override - protected void onPostExecute(String s) { - super.onPostExecute(s); - - FileUtils.copyToClipboard(context, s); - Toast.makeText(context, context.getString(R.string.cloud_share_copied), Toast.LENGTH_LONG) - .show(); - } - }.execute(paths); } public static void shareFiles( @@ -941,7 +953,7 @@ public static ArrayList parse(String permLine) { } public static boolean isStorage(String path) { - for (String s : DataUtils.getInstance().getStorages()) if (s.equals(path)) return true; + for (String s : DataUtils.INSTANCE.getStorages()) if (s.equals(path)) return true; return false; } 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 75dc8ab34f..fecfb8a607 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 @@ -39,6 +39,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.fileoperations.utils.OnLowMemory; import com.amaze.filemanager.fileoperations.utils.UpdatePosition; @@ -49,10 +50,11 @@ import com.amaze.filemanager.filesystem.MediaStoreHack; import com.amaze.filemanager.filesystem.SafRootHolder; import com.amaze.filemanager.filesystem.cloud.CloudUtil; -import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.ProgressHandler; -import com.cloudrail.si.interfaces.CloudStorage; +import com.amaze.filemanager.utils.omh.OMHClientHelper; +import com.openmobilehub.android.storage.core.OmhStorageClient; +import com.openmobilehub.android.storage.core.model.OmhStorageEntity; import android.content.ContentResolver; import android.content.Context; @@ -62,14 +64,19 @@ import androidx.annotation.VisibleForTesting; import androidx.documentfile.provider.DocumentFile; +import kotlin.Unit; +import kotlin.coroutines.EmptyCoroutineContext; +import kotlin.io.ByteStreamsKt; +import kotlin.text.StringsKt; +import kotlinx.coroutines.BuildersKt; + /** Base class to handle file copy. */ public class GenericCopyUtil { - private final Logger LOG = LoggerFactory.getLogger(GenericCopyUtil.class); + private static final Logger LOG = LoggerFactory.getLogger(GenericCopyUtil.class); private HybridFileParcelable mSourceFile; private HybridFile mTargetFile; private final Context mContext; // context needed to find the DocumentFile in otg/sd card - private final DataUtils dataUtils = DataUtils.getInstance(); private final ProgressHandler progressHandler; public static final int DEFAULT_BUFFER_SIZE = 8192; @@ -123,21 +130,13 @@ private void startCopy( bufferedInputStream = new BufferedInputStream( contentResolver.openInputStream(documentSourceFile.getUri()), DEFAULT_BUFFER_SIZE); - } else if (mSourceFile.isSmb() || mSourceFile.isSftp() || mSourceFile.isFtp()) { + } else if (mSourceFile.isSmb() + || mSourceFile.isSftp() + || mSourceFile.isFtp() + || mSourceFile.isCloudDriveFile()) { bufferedInputStream = new BufferedInputStream(mSourceFile.getInputStream(mContext), DEFAULT_TRANSFER_QUANTUM); - } else if (mSourceFile.isDropBoxFile() - || mSourceFile.isBoxFile() - || mSourceFile.isGoogleDriveFile() - || mSourceFile.isOneDriveFile()) { - OpenMode openMode = mSourceFile.getMode(); - - CloudStorage cloudStorage = dataUtils.getAccount(openMode); - bufferedInputStream = - new BufferedInputStream( - cloudStorage.download(CloudUtil.stripPath(openMode, mSourceFile.getPath()))); } else { - // source file is neither smb nor otg; getting a channel from direct file instead of stream File file = new File(mSourceFile.getPath()); if (FileProperties.isReadable(file)) { @@ -192,10 +191,7 @@ private void startCopy( bufferedOutputStream = new BufferedOutputStream( mTargetFile.getOutputStream(mContext), DEFAULT_TRANSFER_QUANTUM); - } else if (mTargetFile.isDropBoxFile() - || mTargetFile.isBoxFile() - || mTargetFile.isGoogleDriveFile() - || mTargetFile.isOneDriveFile()) { + } else if (mTargetFile.isCloudDriveFile()) { cloudCopy(mTargetFile.getMode(), bufferedInputStream); return; } else { @@ -277,22 +273,32 @@ private void startCopy( private void cloudCopy( @NonNull OpenMode openMode, @NonNull BufferedInputStream bufferedInputStream) throws IOException { - DataUtils dataUtils = DataUtils.getInstance(); - // API doesn't support output stream, we'll upload the file directly - CloudStorage cloudStorage = dataUtils.getAccount(openMode); - - if (mSourceFile.getMode() == openMode) { - // we're in the same provider, use api method - cloudStorage.copy( - CloudUtil.stripPath(openMode, mSourceFile.getPath()), - CloudUtil.stripPath(openMode, mTargetFile.getPath())); - } else { - cloudStorage.upload( - CloudUtil.stripPath(openMode, mTargetFile.getPath()), - bufferedInputStream, - mSourceFile.getSize(), - true); - bufferedInputStream.close(); + OmhStorageClient storageClient = OMHClientHelper.getStorageClient(openMode); + if (storageClient != null) { + String fullFilename = mTargetFile.getSimpleName(); + String filename = StringsKt.substringBeforeLast(fullFilename, '.', fullFilename); + String extension = StringsKt.substringAfterLast(fullFilename, '.', ""); + File tmpFile = + File.createTempFile(filename, "." + extension, AppConfig.getInstance().getCacheDir()); + tmpFile.deleteOnExit(); + ByteStreamsKt.copyTo(bufferedInputStream, new FileOutputStream(tmpFile), DEFAULT_BUFFER_SIZE); + + final String parent = mTargetFile.getParent(mContext); + try { + BuildersKt.runBlocking( + EmptyCoroutineContext.INSTANCE, + (scope, continuation) -> { + OmhStorageEntity parentFolder = + (OmhStorageEntity) + storageClient.resolvePath( + CloudUtil.stripCloudPath(openMode, parent), continuation); + storageClient.uploadFile(tmpFile, parentFolder.getId(), continuation); + tmpFile.delete(); + return Unit.INSTANCE; + }); + } catch (InterruptedException e) { + + } } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java index d3b0ea387f..70113f0ac7 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java +++ b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java @@ -109,7 +109,7 @@ public boolean onMenuItemClick(MenuItem item) { case BOX: case GDRIVE: case ONEDRIVE: - FileUtils.shareCloudFile(rowItem.desc, rowItem.getMode(), context); + FileUtils.shareCloudFile(rowItem.cloudFileId, rowItem.getMode(), context); break; default: ArrayList arrayList = new ArrayList<>(); @@ -134,7 +134,7 @@ public boolean onMenuItemClick(MenuItem item) { mainActivity.mainActivityHelper.extractFile(new File(rowItem.desc)); return true; } else if (item.getItemId() == R.id.book) { - DataUtils dataUtils = DataUtils.getInstance(); + DataUtils dataUtils = DataUtils.INSTANCE; if (dataUtils.addBook(new String[] {rowItem.title, rowItem.desc}, true)) { mainActivity.getDrawer().refreshDrawer(); Toast.makeText( 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 2e835df77c..4a33cadb39 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 @@ -65,6 +65,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Pattern; import org.slf4j.Logger; @@ -80,20 +83,15 @@ import com.amaze.filemanager.adapters.data.StorageDirectoryParcelable; import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.asynchronous.SaveOnDataUtilsChange; -import com.amaze.filemanager.asynchronous.asynctasks.CloudLoaderAsyncTask; import com.amaze.filemanager.asynchronous.asynctasks.DeleteTask; import com.amaze.filemanager.asynchronous.asynctasks.TaskKt; import com.amaze.filemanager.asynchronous.asynctasks.movecopy.MoveFilesTask; import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil; import com.amaze.filemanager.asynchronous.services.CopyService; -import com.amaze.filemanager.database.CloudContract; -import com.amaze.filemanager.database.CloudHandler; import com.amaze.filemanager.database.SortHandler; import com.amaze.filemanager.database.TabHandler; import com.amaze.filemanager.database.UtilsHandler; import com.amaze.filemanager.database.models.OperationData; -import com.amaze.filemanager.database.models.explorer.CloudEntry; -import com.amaze.filemanager.fileoperations.exceptions.CloudPluginException; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.fileoperations.filesystem.StorageNaming; import com.amaze.filemanager.fileoperations.filesystem.usb.SingletonUsbOtg; @@ -146,8 +144,12 @@ import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.PackageUtils; import com.amaze.filemanager.utils.PreferenceUtils; +import com.amaze.filemanager.utils.StartActivityForResultWithSourceIntent; import com.amaze.filemanager.utils.Utils; -import com.cloudrail.si.CloudRail; +import com.amaze.filemanager.utils.cloud.CloudPluginUtil; +import com.amaze.filemanager.utils.omh.AuthTrigger; +import com.amaze.filemanager.utils.omh.OMHClientHelper; +import com.amaze.filemanager.utils.omh.OmhCredentialsWrapper; import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; @@ -155,6 +157,9 @@ import com.leinardi.android.speeddial.SpeedDialActionItem; import com.leinardi.android.speeddial.SpeedDialOverlayLayout; import com.leinardi.android.speeddial.SpeedDialView; +import com.openmobilehub.android.auth.core.OmhAuthClient; +import com.openmobilehub.android.auth.core.OmhCredentials; +import com.openmobilehub.android.auth.core.utils.EncryptedSharedPreferences; import com.readystatesoftware.systembartint.SystemBarTintManager; import com.topjohnwu.superuser.Shell; @@ -163,18 +168,15 @@ import android.app.Activity; import android.content.BroadcastReceiver; import android.content.ContentResolver; -import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; -import android.database.Cursor; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.hardware.usb.UsbManager; import android.media.RingtoneManager; import android.net.Uri; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Environment; @@ -191,6 +193,7 @@ import android.view.animation.DecelerateInterpolator; import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; import androidx.annotation.NonNull; @@ -202,13 +205,11 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; import io.reactivex.Completable; import io.reactivex.CompletableObserver; import io.reactivex.Flowable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; @@ -220,9 +221,9 @@ public class MainActivity extends PermissionsActivity implements SmbConnectionListener, BookmarkCallback, CloudConnectionCallbacks, - LoaderManager.LoaderCallbacks, FolderChooserDialog.FolderCallback, - PermissionsActivity.OnPermissionGranted { + PermissionsActivity.OnPermissionGranted, + AuthTrigger { private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); @@ -266,6 +267,9 @@ public class MainActivity extends PermissionsActivity private static final String KEY_OPERATION = "operation"; private static final String KEY_SELECTED_LIST_ITEM = "select_list_item"; + private static final String KEY_OPEN_MODE = "OPEN_MODE"; + private static final String KEY_CLOUD_REAUTHENTICATING = "CLOUD_REAUTHENTICATING"; + private AppBar appbar; private Drawer drawer; // private HistoryManager history, grid; @@ -282,15 +286,6 @@ public class MainActivity extends PermissionsActivity private SpeedDialOverlayLayout fabBgView; private UtilsHandler utilsHandler; - private CloudHandler cloudHandler; - private CloudLoaderAsyncTask cloudLoaderAsyncTask; - - /** - * This is for a hack. - * - * @see MainActivity#onLoadFinished(Loader, Cursor) - */ - private Cursor cloudCursorData = null; public static final int REQUEST_CODE_SAF = 223; @@ -322,6 +317,7 @@ public class MainActivity extends PermissionsActivity public static final String CLOUD_AUTHENTICATOR_GDRIVE = "android.intent.category.BROWSABLE"; public static final String CLOUD_AUTHENTICATOR_REDIRECT_URI = "com.amaze.filemanager:/auth"; + private AuthCallback pendingAuthCallback; // the current visible tab, either 0 or 1 public static int currentTab; @@ -329,9 +325,6 @@ public class MainActivity extends PermissionsActivity private String scrollToFileName = null; - public static final int REQUEST_CODE_CLOUD_LIST_KEYS = 5463; - public static final int REQUEST_CODE_CLOUD_LIST_KEY = 5472; - private PasteHelper pasteHelper; public MainActivityActionMode mainActivityActionMode; @@ -344,6 +337,60 @@ public class MainActivity extends PermissionsActivity private static final String INTENT_ACTION_OPEN_APP_MANAGER = "com.amaze.filemanager.openAppManager"; + @SuppressLint("CheckResult") + private final ActivityResultLauncher loginLauncher = + registerForActivityResult( + new StartActivityForResultWithSourceIntent(), + result -> { + if (result.getResultCode() == RESULT_CANCELED) { + String errorMessage = result.getData().getStringExtra("errorMessage"); + LOG.error("auth failed: {}", errorMessage); + if (pendingAuthCallback != null) { + pendingAuthCallback.onAuthFailure(errorMessage); + pendingAuthCallback = null; + } + } else { + // cuz #getCredentials cannot be called from the main thread + Single.fromCallable( + () -> { + OpenMode openMode = + (OpenMode) result.getData().getSerializableExtra(KEY_OPEN_MODE); + Objects.requireNonNull(openMode); + OmhAuthClient authClient = OMHClientHelper.getAuthClient(openMode); + OmhCredentials credentials = authClient.getCredentials(); + if (!result.getData().getBooleanExtra(KEY_CLOUD_REAUTHENTICATING, false)) { + dataUtils.addAccount(new OmhCredentialsWrapper(openMode, credentials)); + } + return true; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + actionResult -> { + getDrawer().refreshDrawer(); + if (pendingAuthCallback != null) { + pendingAuthCallback.onAuthSuccess(); + pendingAuthCallback = null; + } + }, + throwable -> { + throwable.printStackTrace(); + AppConfig.toast(MainActivity.this, R.string.failed_cloud_new_connection); + if (pendingAuthCallback != null) { + pendingAuthCallback.onAuthFailure(throwable.getMessage()); + pendingAuthCallback = null; + } + }); + } + }); + + /** Interface for callbacks when authentication is completed or fails */ + public interface AuthCallback { + void onAuthSuccess(); + + void onAuthFailure(String errorMessage); + } + /** Called when the activity is first created. */ @Override public void onCreate(final Bundle savedInstanceState) { @@ -352,7 +399,7 @@ public void onCreate(final Bundle savedInstanceState) { intent = getIntent(); - dataUtils = DataUtils.getInstance(); + dataUtils = DataUtils.INSTANCE; if (savedInstanceState != null) { listItemSelected = savedInstanceState.getBoolean(KEY_SELECTED_LIST_ITEM, false); } @@ -363,21 +410,27 @@ public void onCreate(final Bundle savedInstanceState) { dataUtils.registerOnDataChangedListener(new SaveOnDataUtilsChange(drawer)); AppConfig.getInstance().setMainActivityContext(this); + AppConfig.getInstance().setCloudAuthTrigger(this); initialiseViews(); utilsHandler = AppConfig.getInstance().getUtilsHandler(); - cloudHandler = new CloudHandler(this, AppConfig.getInstance().getExplorerDatabase()); initialiseFab(); // TODO: 7/12/2017 not init when actionIntent != null mainActivityHelper = new MainActivityHelper(this); mainActivityActionMode = new MainActivityActionMode(new WeakReference<>(MainActivity.this)); - if (CloudSheetFragment.isCloudProviderAvailable(this)) { + if (CloudPluginUtil.isCloudProviderAvailable(this)) { + try { + OMHClientHelper.initializeClients(); + } catch (Exception errorRaised) { + LOG.error("Error initializing OMH clients", errorRaised); + AppConfig.toast(this, R.string.cloud_error_failed_restart); + } try { - LoaderManager.getInstance(this).initLoader(REQUEST_CODE_CLOUD_LIST_KEYS, null, this); + CloudPluginUtil.initializeDataUtils(this); + drawer.refreshDrawer(); } catch (Exception errorRaised) { LOG.error("Error initializing cloud connections", errorRaised); - cloudHandler.clearAllCloudConnections(); AlertDialog.show( this, R.string.cloud_connection_credentials_cleared_msg, @@ -385,7 +438,6 @@ public void onCreate(final Bundle savedInstanceState) { android.R.string.ok, null, false); - LoaderManager.getInstance(this).initLoader(REQUEST_CODE_CLOUD_LIST_KEYS, null, this); } } @@ -2024,14 +2076,6 @@ public void onNewIntent(Intent i) { if (failedOps != null) { mainActivityHelper.showFailedOperationDialog(failedOps, this); } - } else if (i.getCategories() != null - && i.getCategories().contains(CLOUD_AUTHENTICATOR_GDRIVE)) { - // we used an external authenticator instead of APIs. Probably for Google Drive - CloudRail.setAuthenticationResponse(intent); - if (intent.getAction() != null) { - checkForExternalIntent(intent); - invalidateFragmentAndBundle(null, false); - } } else if ((openProcesses = i.getBooleanExtra(KEY_INTENT_PROCESS_VIEWER, false))) { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace( @@ -2260,9 +2304,49 @@ public void modify(String oldpath, String oldname, String newPath, String newnam } @Override - public void addConnection(OpenMode service) { + public boolean triggerAuthBlocking(@NonNull OpenMode openMode) { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicBoolean result = new AtomicBoolean(false); + + runOnUiThread( + () -> { + OmhAuthClient authClient = OMHClientHelper.getAuthClient(openMode); + if (authClient == null) { + result.set(false); + latch.countDown(); + return; + } + + try { + Intent loginIntent = authClient.getLoginIntent(); + loginIntent.putExtra("OPEN_MODE", openMode); + loginLauncher.launch(loginIntent); + } catch (Exception e) { + LOG.warn("Failed to launch auth intent", e); + latch.countDown(); + } + }); + try { - if (cloudHandler.findEntry(service) != null) { + boolean completed = latch.await(120, java.util.concurrent.TimeUnit.SECONDS); + if (!completed) { + LOG.warn("Auth blocking timed out for {}", openMode); + runOnUiThread(() -> pendingAuthCallback = null); + } + return completed && result.get(); + } catch (InterruptedException e) { + LOG.warn("Auth blocking interrupted", e); + return false; + } + } + + @Override + public void addCloudConnection(OpenMode service) { + if (CloudPluginUtil.isCloudProviderAvailable(this)) { + if (EncryptedSharedPreferences.INSTANCE + .getEncryptedSharedPrefs(this, CloudPluginUtil.resolveOmhProviderNameFrom(service)) + .getString("email", null) + != null) { // cloud entry already exists Toast.makeText( this, getResources().getString(R.string.connection_exists), Toast.LENGTH_LONG) @@ -2281,141 +2365,30 @@ this, getResources().getString(R.string.cloud_error_fdroid), Toast.LENGTH_LONG) args.putInt(ARGS_KEY_LOADER, service.ordinal()); // check if we already had done some work on the loader - Loader loader = getSupportLoaderManager().getLoader(REQUEST_CODE_CLOUD_LIST_KEY); - if (loader != null && loader.isStarted()) { - - // making sure that loader is not started - getSupportLoaderManager().destroyLoader(REQUEST_CODE_CLOUD_LIST_KEY); - } - - getSupportLoaderManager().initLoader(REQUEST_CODE_CLOUD_LIST_KEY, args, this); + OmhAuthClient authClient = OMHClientHelper.getAuthClient(service); + loginLauncher.launch( + authClient + .getLoginIntent() + .putExtra(KEY_CLOUD_REAUTHENTICATING, true) + .putExtra(KEY_OPEN_MODE, service)); } - } catch (CloudPluginException e) { - LOG.warn("failure when adding cloud plugin connections", e); + } else { Toast.makeText(this, getResources().getString(R.string.cloud_error_plugin), Toast.LENGTH_LONG) .show(); } } @Override - public void deleteConnection(OpenMode service) { - cloudHandler.clear(service); + public void deleteCloudConnection(OpenMode service) { + EncryptedSharedPreferences.INSTANCE + .getEncryptedSharedPrefs(this, CloudPluginUtil.resolveOmhProviderNameFrom(service)) + .edit() + .clear() + .apply(); dataUtils.removeAccount(service); - runOnUiThread(drawer::refreshDrawer); } - @NonNull - @Override - public Loader onCreateLoader(int id, Bundle args) { - Uri uri = - Uri.withAppendedPath( - Uri.parse("content://" + CloudContract.PROVIDER_AUTHORITY), "/keys.db/secret_keys"); - - String[] projection = - new String[] { - CloudContract.COLUMN_ID, - CloudContract.COLUMN_CLIENT_ID, - CloudContract.COLUMN_CLIENT_SECRET_KEY - }; - - switch (id) { - case REQUEST_CODE_CLOUD_LIST_KEY: - Uri uriAppendedPath = uri; - switch (OpenMode.getOpenMode(args.getInt(ARGS_KEY_LOADER, 2))) { - case GDRIVE: - uriAppendedPath = ContentUris.withAppendedId(uri, 2); - break; - case DROPBOX: - uriAppendedPath = ContentUris.withAppendedId(uri, 3); - break; - case BOX: - uriAppendedPath = ContentUris.withAppendedId(uri, 4); - break; - case ONEDRIVE: - uriAppendedPath = ContentUris.withAppendedId(uri, 5); - break; - } - return new CursorLoader(this, uriAppendedPath, projection, null, null, null); - case REQUEST_CODE_CLOUD_LIST_KEYS: - // we need a list of all secret keys - - try { - List cloudEntries = cloudHandler.getAllEntries(); - - // we want keys for services saved in database, and the cloudrail app key which - // is at index 1 - String ids[] = new String[cloudEntries.size() + 1]; - - ids[0] = 1 + ""; - for (int i = 1; i <= cloudEntries.size(); i++) { - - // we need to get only those cloud details which user wants - switch (cloudEntries.get(i - 1).getServiceType()) { - case GDRIVE: - ids[i] = 2 + ""; - break; - case DROPBOX: - ids[i] = 3 + ""; - break; - case BOX: - ids[i] = 4 + ""; - break; - case ONEDRIVE: - ids[i] = 5 + ""; - break; - } - } - return new CursorLoader(this, uri, projection, CloudContract.COLUMN_ID, ids, null); - } catch (CloudPluginException e) { - LOG.warn("failure when fetching cloud connections", e); - Toast.makeText( - this, getResources().getString(R.string.cloud_error_plugin), Toast.LENGTH_LONG) - .show(); - } - default: - Uri undefinedUriAppendedPath = ContentUris.withAppendedId(uri, 7); - return new CursorLoader(this, undefinedUriAppendedPath, projection, null, null, null); - } - } - - @Override - public void onLoadFinished(Loader loader, final Cursor data) { - if (data == null) { - Toast.makeText( - this, - getResources().getString(R.string.cloud_error_failed_restart), - Toast.LENGTH_LONG) - .show(); - return; - } - - /* - * This is hack for repeated calls to onLoadFinished(), - * we take the Cursor provided to check if the function - * has already been called on it. - * - * TODO: find a fix for repeated callbacks to onLoadFinished() - */ - if (cloudCursorData == null - || cloudCursorData == data - || data.isClosed() - || cloudCursorData.isClosed()) return; - cloudCursorData = data; - - if (cloudLoaderAsyncTask != null - && cloudLoaderAsyncTask.getStatus() == AsyncTask.Status.RUNNING) { - return; - } - cloudLoaderAsyncTask = new CloudLoaderAsyncTask(this, cloudHandler, cloudCursorData); - cloudLoaderAsyncTask.execute(); - } - - @Override - public void onLoaderReset(Loader loader) { - // For passing code check - } - public void initCornersDragListener(boolean destroy, boolean shouldInvokeLeftAndRight) { initBottomDragListener(destroy); initLeftRightAndTopDragListeners(destroy, shouldInvokeLeftAndRight); diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/DragAndDropDialog.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/DragAndDropDialog.kt index f532ca4a7d..9592d91a24 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/DragAndDropDialog.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/DragAndDropDialog.kt @@ -146,7 +146,7 @@ class DragAndDropDialog : DialogFragment() { .title(getString(R.string.choose_operation)) .customView(R.layout.dialog_drag_drop, true) .theme(dialogTheme) - .negativeText(getString(R.string.cancel).toUpperCase()) + .negativeText(getString(R.string.cancel).uppercase()) .negativeColor(accent) .cancelable(false) .onNeutral { _: MaterialDialog?, _: DialogAction? -> 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 4e7f0e0565..81c2b406aa 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 @@ -970,7 +970,7 @@ public static void showCloudDialog( builder.negativeText(mainActivity.getString(R.string.no)); builder.negativeColor(accentColor); - builder.onPositive((dialog, which) -> mainActivity.deleteConnection(openMode)); + builder.onPositive((dialog, which) -> mainActivity.deleteCloudConnection(openMode)); builder.onNegative((dialog, which) -> dialog.cancel()); @@ -1409,7 +1409,7 @@ public static void showSignInWithGoogleDialog(@NonNull MainActivity mainActivity .findViewById(R.id.signin_with_google) .setOnClickListener( v -> { - mainActivity.addConnection(OpenMode.GDRIVE); + mainActivity.addCloudConnection(OpenMode.GDRIVE); dialog.dismiss(); }); diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/HiddenFilesDialog.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/HiddenFilesDialog.kt index 269bd437e9..97b45bbe85 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/HiddenFilesDialog.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/HiddenFilesDialog.kt @@ -47,7 +47,7 @@ object HiddenFilesDialog { mainActivity, mainFragment, sharedPrefs, - FileUtils.toHybridFileConcurrentRadixTree(DataUtils.getInstance().hiddenFiles), + FileUtils.toHybridFileConcurrentRadixTree(DataUtils.hiddenFiles), null, false, ) diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/HistoryDialog.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/HistoryDialog.kt index cb1e6f793a..03b489083f 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/HistoryDialog.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/HistoryDialog.kt @@ -46,7 +46,7 @@ object HistoryDialog { mainActivity, mainFragment, sharedPrefs, - FileUtils.toHybridFileArrayList(DataUtils.getInstance().history), + FileUtils.toHybridFileArrayList(DataUtils.getHistory()), null, true, ) @@ -59,7 +59,7 @@ object HistoryDialog { builder.negativeColor(mainActivity.accent) builder.title(R.string.history) builder.onNegative { _: MaterialDialog?, _: DialogAction? -> - DataUtils.getInstance().clearHistory() + DataUtils.clearHistory() } builder.theme(appTheme.getMaterialDialogTheme()) builder.adapter(adapter, null) diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/RenameBookmark.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/RenameBookmark.java index 850e119f1a..9ea17b7fb1 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/RenameBookmark.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/RenameBookmark.java @@ -44,7 +44,7 @@ public class RenameBookmark extends DialogFragment { private String title; private String path; private BookmarkCallback bookmarkCallback; - private final DataUtils dataUtils = DataUtils.getInstance(); + private final DataUtils dataUtils = DataUtils.INSTANCE; public static RenameBookmark getInstance(String name, String path, int accentColor) { RenameBookmark renameBookmark = new RenameBookmark(); diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt index eeb9c7ad01..a570caa4d1 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/SftpConnectDialog.kt @@ -315,11 +315,11 @@ class SftpConnectDialog : DialogFragment() { edit = true, ) val i = - DataUtils.getInstance().containsServer( + DataUtils.containsServer( arrayOf(connectionName, path), ) if (i > -1) { - DataUtils.getInstance().removeServer(i) + DataUtils.removeServer(i) AppConfig.getInstance() .runInBackground { AppConfig.getInstance().utilsHandler.removeFromDatabase( @@ -721,8 +721,8 @@ class SftpConnectDialog : DialogFragment() { selectedParsedKeyPair, explicitTls, )?.run { - if (DataUtils.getInstance().containsServer(encryptedPath) == -1) { - DataUtils.getInstance().addServer(arrayOf(connectionName, encryptedPath)) + if (DataUtils.containsServer(encryptedPath) == -1) { + DataUtils.addServer(arrayOf(connectionName, encryptedPath)) (activity as MainActivity).drawer.refreshDrawer() AppConfig.getInstance().utilsHandler.saveToDatabase( OperationData( @@ -768,14 +768,14 @@ class SftpConnectDialog : DialogFragment() { hostKeyFingerprint: String?, encryptedPath: String, ): Boolean { - val i = DataUtils.getInstance().containsServer(oldPath) + val i = DataUtils.containsServer(oldPath!!) if (i != -1) { - DataUtils.getInstance().removeServer(i) + DataUtils.removeServer(i) } - DataUtils.getInstance().addServer(arrayOf(connectionName, encryptedPath)) - DataUtils.getInstance().servers.sortWith(BookSorter()) + DataUtils.addServer(arrayOf(connectionName, encryptedPath)) + DataUtils.getServers().sortWith(BookSorter()) (activity as MainActivity).drawer.refreshDrawer() AppConfig.getInstance().runInBackground { AppConfig.getInstance().utilsHandler.updateSsh( diff --git a/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt b/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt index f2f509478d..8ecb8f70c1 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/drag/RecyclerAdapterDragListener.kt @@ -58,8 +58,7 @@ class RecyclerAdapterDragListener( if (dragAndDropPref != PreferencesConstants.PREFERENCE_DRAG_TO_SELECT ) { - val dataUtils = DataUtils.getInstance() - dataUtils.checkedItemsList = null + DataUtils.checkedItemsList = null mainFragment.requireMainActivity() .tabFragment.dragPlaceholder?.visibility = View.INVISIBLE } @@ -188,14 +187,13 @@ class RecyclerAdapterDragListener( if (checkedItems?.size == 0) { // probably because we switched tabs and // this adapter doesn't have any checked items, get from data utils - val dataUtils = DataUtils.getInstance() Log.d( TAG, "Didn't find checked items in adapter, " + "checking dataUtils size ${ - dataUtils.checkedItemsList?.size ?: "null"}", + DataUtils.checkedItemsList?.size ?: "null"}", ) - checkedItems = dataUtils.checkedItemsList + checkedItems = DataUtils.checkedItemsList } val arrayList = ArrayList() checkedItems?.forEach { diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.java deleted file mode 100644 index 45d4e544b1..0000000000 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.ui.fragments; - -import com.amaze.filemanager.BuildConfig; -import com.amaze.filemanager.R; -import com.amaze.filemanager.database.CloudContract; -import com.amaze.filemanager.databinding.FragmentSheetCloudBinding; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; -import com.amaze.filemanager.ui.activities.MainActivity; -import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation; -import com.amaze.filemanager.ui.dialogs.SftpConnectDialog; -import com.amaze.filemanager.ui.dialogs.SmbSearchDialog; -import com.amaze.filemanager.ui.theme.AppTheme; -import com.amaze.filemanager.utils.Utils; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.bottomsheet.BottomSheetDialog; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; - -import android.app.Dialog; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.LinearLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Created by vishal on 18/2/17. - * - *

Class represents implementation of a new cloud connection sheet dialog - */ -public class CloudSheetFragment extends BottomSheetDialogFragment implements View.OnClickListener { - - private View rootView; - private LinearLayout mSmbLayout, - mScpLayout, - mDropboxLayout, - mBoxLayout, - mGoogleDriveLayout, - mOnedriveLayout, - mGetCloudLayout; - - public static final String TAG_FRAGMENT = "cloud_fragment"; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState); - - dialog.setOnShowListener( - dialog1 -> { - BottomSheetDialog d = (BottomSheetDialog) dialog1; - - FrameLayout bottomSheet = - (FrameLayout) d.findViewById(com.google.android.material.R.id.design_bottom_sheet); - BottomSheetBehavior.from(bottomSheet).setState(BottomSheetBehavior.STATE_EXPANDED); - }); - return dialog; - } - - @Override - public void setupDialog(Dialog dialog, int style) { - super.setupDialog(dialog, style); - - rootView = FragmentSheetCloudBinding.inflate(LayoutInflater.from(requireActivity())).getRoot(); - - MainActivity activity = (MainActivity) getActivity(); - - if (activity.getAppTheme().equals(AppTheme.DARK)) { - rootView.setBackgroundColor(Utils.getColor(getContext(), R.color.holo_dark_background)); - } else if (activity.getAppTheme().equals(AppTheme.BLACK)) { - rootView.setBackgroundColor(Utils.getColor(getContext(), android.R.color.black)); - } else { - rootView.setBackgroundColor(Utils.getColor(getContext(), android.R.color.white)); - } - - mSmbLayout = rootView.findViewById(R.id.linear_layout_smb); - mScpLayout = rootView.findViewById(R.id.linear_layout_scp); - mBoxLayout = rootView.findViewById(R.id.linear_layout_box); - mDropboxLayout = rootView.findViewById(R.id.linear_layout_dropbox); - mGoogleDriveLayout = rootView.findViewById(R.id.linear_layout_google_drive); - mOnedriveLayout = rootView.findViewById(R.id.linear_layout_onedrive); - mGetCloudLayout = rootView.findViewById(R.id.linear_layout_get_cloud); - - if (isCloudProviderAvailable(getContext())) { - - mBoxLayout.setVisibility(View.VISIBLE); - mDropboxLayout.setVisibility(View.VISIBLE); - mGoogleDriveLayout.setVisibility(View.VISIBLE); - mOnedriveLayout.setVisibility(View.VISIBLE); - mGetCloudLayout.setVisibility(View.GONE); - } - - if (BuildConfig.IS_VERSION_FDROID) { - mBoxLayout.setVisibility(View.GONE); - mDropboxLayout.setVisibility(View.GONE); - mGoogleDriveLayout.setVisibility(View.GONE); - mOnedriveLayout.setVisibility(View.GONE); - mGetCloudLayout.setVisibility(View.GONE); - } - - mSmbLayout.setOnClickListener(this); - mScpLayout.setOnClickListener(this); - mBoxLayout.setOnClickListener(this); - mDropboxLayout.setOnClickListener(this); - mGoogleDriveLayout.setOnClickListener(this); - mOnedriveLayout.setOnClickListener(this); - mGetCloudLayout.setOnClickListener(this); - - dialog.setContentView(rootView); - } - - /** Determines whether cloud provider is installed or not */ - public static final boolean isCloudProviderAvailable(Context context) { - - PackageManager pm = context.getPackageManager(); - try { - pm.getPackageInfo(CloudContract.APP_PACKAGE_NAME, PackageManager.GET_ACTIVITIES); - return true; - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - - @Override - public void onClick(View v) { - if (v.getId() == R.id.linear_layout_smb) { - dismiss(); - SmbSearchDialog smbDialog = new SmbSearchDialog(); - smbDialog.show(requireActivity().getSupportFragmentManager(), "tab"); - } else if (v.getId() == R.id.linear_layout_scp) { - dismiss(); - SftpConnectDialog sftpConnectDialog = new SftpConnectDialog(); - Bundle args = new Bundle(); - args.putBoolean("edit", false); - sftpConnectDialog.setArguments(args); - sftpConnectDialog.show(getFragmentManager(), "tab"); - } else if (v.getId() == R.id.linear_layout_box) { - ((MainActivity) requireActivity()).addConnection(OpenMode.BOX); - } else if (v.getId() == R.id.linear_layout_dropbox) { - ((MainActivity) requireActivity()).addConnection(OpenMode.DROPBOX); - } else if (v.getId() == R.id.linear_layout_google_drive) { - GeneralDialogCreation.showSignInWithGoogleDialog((MainActivity) requireActivity()); - } else if (v.getId() == R.id.linear_layout_onedrive) { - ((MainActivity) getActivity()).addConnection(OpenMode.ONEDRIVE); - } else if (v.getId() == R.id.linear_layout_get_cloud) { - Intent cloudPluginIntent = new Intent(Intent.ACTION_VIEW); - cloudPluginIntent.setData(Uri.parse(getString(R.string.cloud_plugin_google_play_uri))); - try { - startActivity(cloudPluginIntent); - } catch (ActivityNotFoundException ifGooglePlayIsNotInstalled) { - cloudPluginIntent.setData(Uri.parse(getString(R.string.cloud_plugin_google_play_web_uri))); - startActivity(cloudPluginIntent); - } - } - // dismiss this sheet dialog - dismiss(); - } - - public interface CloudConnectionCallbacks { - void addConnection(OpenMode service); - - void deleteConnection(OpenMode service); - } -} diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.kt new file mode 100644 index 0000000000..9078218169 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/CloudSheetFragment.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2014-2024 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.ui.fragments + +import android.app.Dialog +import android.content.ActivityNotFoundException +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import android.widget.LinearLayout +import com.amaze.filemanager.BuildConfig +import com.amaze.filemanager.R +import com.amaze.filemanager.databinding.FragmentSheetCloudBinding +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.ui.activities.MainActivity +import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation +import com.amaze.filemanager.ui.dialogs.SftpConnectDialog +import com.amaze.filemanager.ui.dialogs.SmbSearchDialog +import com.amaze.filemanager.ui.theme.AppTheme +import com.amaze.filemanager.utils.Utils +import com.amaze.filemanager.utils.cloud.CloudPluginUtil +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +/** + * Created by vishal on 18/2/17. + * + * + * Class represents implementation of a new cloud connection sheet dialog + */ +class CloudSheetFragment : BottomSheetDialogFragment(), View.OnClickListener { + private lateinit var rootView: View + private lateinit var mSmbLayout: LinearLayout + private lateinit var mScpLayout: LinearLayout + private lateinit var mDropboxLayout: LinearLayout + private lateinit var mBoxLayout: LinearLayout + private lateinit var mGoogleDriveLayout: LinearLayout + private lateinit var mOnedriveLayout: LinearLayout + private lateinit var mGetCloudLayout: LinearLayout + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog + val binding = FragmentSheetCloudBinding.inflate(LayoutInflater.from(requireActivity())) + rootView = binding.root + + val activity = activity as MainActivity? + + when (activity?.appTheme) { + AppTheme.DARK -> rootView.setBackgroundColor(Utils.getColor(context, R.color.holo_dark_background)) + AppTheme.BLACK -> rootView.setBackgroundColor(Utils.getColor(context, android.R.color.black)) + else -> rootView.setBackgroundColor(Utils.getColor(context, android.R.color.white)) + } + + mSmbLayout = binding.linearLayoutSmb + mScpLayout = binding.linearLayoutScp + mBoxLayout = binding.linearLayoutBox + mDropboxLayout = binding.linearLayoutDropbox + mGoogleDriveLayout = binding.linearLayoutGoogleDrive + mOnedriveLayout = binding.linearLayoutOnedrive + mGetCloudLayout = binding.linearLayoutGetCloud + + if (CloudPluginUtil.isCloudProviderAvailable(requireContext())) { + mBoxLayout.visibility = View.VISIBLE + mDropboxLayout.visibility = View.VISIBLE + mGoogleDriveLayout.visibility = View.VISIBLE + mOnedriveLayout.visibility = View.VISIBLE + mGetCloudLayout.visibility = View.GONE + } + + if (BuildConfig.IS_VERSION_FDROID) { + mBoxLayout.visibility = View.GONE + mDropboxLayout.visibility = View.GONE + mGoogleDriveLayout.visibility = View.GONE + mOnedriveLayout.visibility = View.GONE + mGetCloudLayout.visibility = View.GONE + } + + mSmbLayout.setOnClickListener(this) + mScpLayout.setOnClickListener(this) + mBoxLayout.setOnClickListener(this) + mDropboxLayout.setOnClickListener(this) + mGoogleDriveLayout.setOnClickListener(this) + mOnedriveLayout.setOnClickListener(this) + mGetCloudLayout.setOnClickListener(this) + + dialog.setContentView(binding.root) + dialog.setOnShowListener { dialog1: DialogInterface -> + val d = dialog1 as BottomSheetDialog + val bottomSheet = + d.findViewById(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout? + BottomSheetBehavior.from(bottomSheet!!).setState(BottomSheetBehavior.STATE_EXPANDED) + } + return dialog + } + + override fun onClick(v: View) { + when (v.id) { + R.id.linear_layout_smb -> { + dismiss() + val smbDialog = SmbSearchDialog() + smbDialog.show(requireActivity().supportFragmentManager, "tab") + return + } + + R.id.linear_layout_scp -> { + dismiss() + val sftpConnectDialog = SftpConnectDialog() + val args = Bundle() + args.putBoolean("edit", false) + sftpConnectDialog.arguments = args + sftpConnectDialog.show(parentFragmentManager, "tab") + return + } + + R.id.linear_layout_box -> requireMainActivity().addCloudConnection(OpenMode.BOX) + R.id.linear_layout_dropbox -> requireMainActivity().addCloudConnection(OpenMode.DROPBOX) + R.id.linear_layout_google_drive -> GeneralDialogCreation.showSignInWithGoogleDialog((activity as MainActivity?)!!) + R.id.linear_layout_onedrive -> requireMainActivity().addCloudConnection(OpenMode.ONEDRIVE) + R.id.linear_layout_get_cloud -> { + val cloudPluginIntent = Intent(Intent.ACTION_VIEW) + cloudPluginIntent.setData(Uri.parse(getString(R.string.cloud_plugin_google_play_uri))) + try { + startActivity(cloudPluginIntent) + } catch (ifGooglePlayIsNotInstalled: ActivityNotFoundException) { + cloudPluginIntent.setData( + Uri.parse(getString(R.string.cloud_plugin_google_play_web_uri)), + ) + startActivity(cloudPluginIntent) + } + } + } + // dismiss this sheet dialog + dismiss() + } + + private fun requireMainActivity(): MainActivity = requireActivity() as MainActivity + + interface CloudConnectionCallbacks { + /** + * Callback to add a new cloud connection of type [service] + */ + fun addCloudConnection(service: OpenMode?) + + /** + * Callback to delete an existing cloud connection of type [service] + */ + fun deleteCloudConnection(service: OpenMode?) + } + + companion object { + const val TAG_FRAGMENT: String = "cloud_fragment" + } +} 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 8dddfa8579..f0e0068646 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 @@ -378,8 +378,8 @@ void switchToList() { public void switchView() { boolean isPathLayoutGrid = - DataUtils.getInstance() - .getListOrGridForPath(mainFragmentViewModel.getCurrentPath(), DataUtils.LIST) + DataUtils.INSTANCE.getListOrGridForPath( + mainFragmentViewModel.getCurrentPath(), DataUtils.LIST) == DataUtils.GRID; reloadListElements(false, isPathLayoutGrid); } @@ -482,7 +482,16 @@ public void onListItemClicked( adapter.toggleChecked(position, imageView); } else { computeScroll(); - loadlist(path, false, mainFragmentViewModel.getOpenMode(), false); + if (mainFragmentViewModel.getIsCloudOpenMode()) { + loadlist( + layoutElementParcelable.cloudFileId, + path, + false, + mainFragmentViewModel.getOpenMode(), + false); + } else { + loadlist(path, false, mainFragmentViewModel.getOpenMode(), false); + } } } else if (layoutElementParcelable.desc.endsWith(CryptUtil.CRYPT_EXTENSION) || layoutElementParcelable.desc.endsWith(CryptUtil.AESCRYPT_EXTENSION)) { @@ -513,7 +522,7 @@ public void onListItemClicked( new HybridFileParcelable[] {layoutElementParcelable.generateBaseFile()}); } else { layoutElementParcelable.generateBaseFile().openFile(getMainActivity(), false); - DataUtils.getInstance().addHistoryFile(layoutElementParcelable.desc); + DataUtils.INSTANCE.addHistoryFile(layoutElementParcelable.desc); } } } @@ -621,15 +630,30 @@ public void returnIntentResults(HybridFileParcelable[] baseFiles) { LoadFilesListTask loadFilesListTask; + public void loadlist( + final String providedPath, + final boolean back, + final OpenMode providedOpenMode, + boolean forceReload) { + loadlist( + mainFragmentViewModel.getCloudFolderId(), + providedPath, + back, + providedOpenMode, + forceReload); + } + /** * This loads a path into the MainFragment. * + * @param cloudFolderId cloud folder ID. Leave it blank if not used. * @param providedPath the path to be loaded * @param back if we're coming back from any directory and want the scroll to be restored * @param providedOpenMode the mode in which the directory should be opened * @param forceReload whether use cached list or force reload the list items */ public void loadlist( + @NonNull final String cloudFolderId, final String providedPath, final boolean back, final OpenMode providedOpenMode, @@ -653,7 +677,10 @@ && getMainActivity().getActionModeHelper().getActionMode() != null) { } OpenMode openMode = providedOpenMode; - String actualPath = FileProperties.remapPathForApi30OrAbove(providedPath, false); + String actualPath; + if ((OpenMode.FILE.equals(openMode)) && SDK_INT > Q) + actualPath = FileProperties.remapPathForApi30OrAbove(providedPath, false); + else actualPath = providedPath; if (SDK_INT >= Q && ArraysKt.any(ANDROID_DATA_DIRS, providedPath::contains)) { openMode = loadPathInQ(actualPath, providedPath, providedOpenMode); @@ -667,7 +694,8 @@ else if (actualPath.startsWith("/") loadFilesListTask = new LoadFilesListTask( - getActivity(), + requireMainActivity(), + cloudFolderId, actualPath, this, openMode, @@ -678,9 +706,10 @@ else if (actualPath.startsWith("/") mSwipeRefreshLayout.setRefreshing(false); if (data != null && data.second != null) { boolean isPathLayoutGrid = - DataUtils.getInstance().getListOrGridForPath(providedPath, DataUtils.LIST) + DataUtils.INSTANCE.getListOrGridForPath(providedPath, DataUtils.LIST) == DataUtils.GRID; - setListElements(data.second, back, providedPath, data.first, isPathLayoutGrid); + setListElements( + data.second, back, cloudFolderId, providedPath, data.first, isPathLayoutGrid); } else { LOG.warn("Load list operation cancelled"); } @@ -799,11 +828,13 @@ void initNoFileLayout() { public void setListElements( List bitmap, boolean back, - String path, + @Nullable String cloudFolderId, + @NonNull String path, final OpenMode openMode, boolean grid) { if (bitmap != null) { mainFragmentViewModel.setListElements(bitmap); + mainFragmentViewModel.setCloudFolderId(cloudFolderId); mainFragmentViewModel.setCurrentPath(path); mainFragmentViewModel.setOpenMode(openMode); reloadListElements(back, grid); @@ -885,7 +916,7 @@ public void reloadListElements(boolean back, boolean grid) { if (mainFragmentViewModel.getOpenMode() != OpenMode.CUSTOM && mainFragmentViewModel.getOpenMode() != OpenMode.TRASH_BIN) { - DataUtils.getInstance().addHistoryFile(mainFragmentViewModel.getCurrentPath()); + DataUtils.INSTANCE.addHistoryFile(mainFragmentViewModel.getCurrentPath()); } listView.setAdapter(adapter); @@ -1187,13 +1218,11 @@ public void reauthenticateSmb() { () -> { int i; AppConfig.toast(requireContext(), getString(R.string.unknown_error)); - if ((i = - DataUtils.getInstance() - .containsServer(mainFragmentViewModel.getSmbPath())) + if ((i = DataUtils.INSTANCE.containsServer(mainFragmentViewModel.getSmbPath())) != -1) { requireMainActivity() .showSMBDialog( - DataUtils.getInstance().getServers().get(i)[0], + DataUtils.INSTANCE.getServers().get(i)[0], mainFragmentViewModel.getSmbPath(), true); } @@ -1289,7 +1318,7 @@ public ArrayList addToSmb( mainFragmentViewModel.getSearchHelper().clear(); } for (SmbFile aMFile : mFile) { - if ((DataUtils.getInstance().isFileHidden(aMFile.getPath()) || aMFile.isHidden()) + if ((DataUtils.INSTANCE.isFileHidden(aMFile.getPath()) || aMFile.isHidden()) && !showHiddenFiles) { continue; } @@ -1358,7 +1387,7 @@ public void onDestroy() { } public void hide(String path) { - DataUtils.getInstance().addHiddenFile(path); + DataUtils.INSTANCE.addHiddenFile(path); File file = new File(path); if (file.isDirectory()) { File f1 = new File(path + "/" + ".nomedia"); diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java index 1ac810562d..833cec0c63 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java @@ -525,7 +525,7 @@ private void initLeftAndRightDragListeners(boolean destroy) { View leftPlaceholder = rootView.findViewById(R.id.placeholder_drag_left); View rightPlaceholder = rootView.findViewById(R.id.placeholder_drag_right); AppCompatImageView dragToTrash = rootView.findViewById(R.id.placeholder_trash_bottom); - DataUtils dataUtils = DataUtils.getInstance(); + DataUtils dataUtils = DataUtils.INSTANCE; if (destroy) { leftPlaceholder.setOnDragListener(null); rightPlaceholder.setOnDragListener(null); diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt index 69b61e42cb..02f7b8bb49 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt @@ -28,7 +28,7 @@ import androidx.lifecycle.viewModelScope import com.amaze.filemanager.adapters.RecyclerAdapter import com.amaze.filemanager.adapters.data.IconDataParcelable import com.amaze.filemanager.adapters.data.LayoutElementParcelable -import com.amaze.filemanager.database.CloudHandler +import com.amaze.filemanager.database.CloudContract import com.amaze.filemanager.fileoperations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.filesystem.files.sort.DirSortBy @@ -45,6 +45,7 @@ import java.util.Objects class MainFragmentViewModel : ViewModel() { var currentPath: String? = null + var cloudFolderId: String? = null /** This is not an exact copy of the elements in the adapter */ var listElements: List = ArrayList() @@ -139,7 +140,7 @@ class MainFragmentViewModel : ViewModel() { * Initialize isList from dataUtils */ fun initIsList() { - isList = DataUtils.getInstance().getListOrGridForPath( + isList = DataUtils.getListOrGridForPath( currentPath, DataUtils.LIST, ) == DataUtils.LIST @@ -193,10 +194,10 @@ class MainFragmentViewModel : ViewModel() { * Check if current path is cloud root path */ fun getIsOnCloudRoot(): Boolean { - return CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE + "/" == currentPath || - CloudHandler.CLOUD_PREFIX_ONE_DRIVE + "/" == currentPath || - CloudHandler.CLOUD_PREFIX_BOX + "/" == currentPath || - CloudHandler.CLOUD_PREFIX_DROPBOX + "/" == currentPath + return CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE + "/" == currentPath || + CloudContract.CLOUD_PREFIX_ONE_DRIVE + "/" == currentPath || + CloudContract.CLOUD_PREFIX_BOX + "/" == currentPath || + CloudContract.CLOUD_PREFIX_DROPBOX + "/" == currentPath } /** diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BookmarksPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BookmarksPrefsFragment.kt index 3ce9e62e85..049f2e2d51 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BookmarksPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BookmarksPrefsFragment.kt @@ -42,10 +42,6 @@ import com.amaze.filemanager.utils.SimpleTextWatcher class BookmarksPrefsFragment : BasePrefsFragment() { override val title = R.string.show_bookmarks_pref - companion object { - private val dataUtils = DataUtils.getInstance()!! - } - private val position: MutableMap = HashMap() private var bookmarksList: PreferenceCategory? = null @@ -80,10 +76,10 @@ class BookmarksPrefsFragment : BasePrefsFragment() { } position.clear() - for (i in dataUtils.books.indices) { + for (i in DataUtils.getBooks().indices) { val p = PathSwitchPreference(activity, itemOnEditListener, itemOnDeleteListener) - p.title = dataUtils.books[i][0] - p.summary = dataUtils.books[i][1] + p.title = DataUtils.getBooks()[i][0] + p.summary = DataUtils.getBooks()[i][1] position[p] = i bookmarksList?.addPreference(p) } @@ -126,14 +122,14 @@ class BookmarksPrefsFragment : BasePrefsFragment() { val p = PathSwitchPreference(activity, itemOnEditListener, itemOnDeleteListener) p.title = txtShortcutName.text p.summary = txtShortcutPath.text - position[p] = dataUtils.books.size + position[p] = DataUtils.getBooks().size bookmarksList?.addPreference(p) val values = arrayOf( txtShortcutName.text.toString(), txtShortcutPath.text.toString(), ) - dataUtils.addBook(values) + DataUtils.addBook(values) utilsHandler.saveToDatabase( OperationData( UtilsHandler.Operation.BOOKMARKS, @@ -153,7 +149,7 @@ class BookmarksPrefsFragment : BasePrefsFragment() { ): Pair { return when { name.isEmpty() -> Pair(false, R.string.invalid_name) - dataUtils.containsBooks(arrayOf(name, path)) != -1 -> Pair(false, R.string.bookmark_exists) + DataUtils.containsBooks(arrayOf(name, path)) != -1 -> Pair(false, R.string.bookmark_exists) !FileUtils.isPathAccessible(path, activity.prefs) -> Pair(false, R.string.ftp_path_change_error_invalid) else -> Pair(true, 0) } @@ -190,7 +186,7 @@ class BookmarksPrefsFragment : BasePrefsFragment() { .setOnClickListener { val oldName = p.title.toString() val oldPath = p.summary.toString() - dataUtils.removeBook(position[p]!!) + DataUtils.removeBook(position[p]!!) position.remove(p) bookmarksList?.removePreference(p) p.title = editText1.text @@ -198,7 +194,7 @@ class BookmarksPrefsFragment : BasePrefsFragment() { position[p] = position.size bookmarksList?.addPreference(p) val values = arrayOf(editText1.text.toString(), editText2.text.toString()) - dataUtils.addBook(values) + DataUtils.addBook(values) AppConfig.getInstance() .runInBackground { utilsHandler.renameBookmark( @@ -228,7 +224,7 @@ class BookmarksPrefsFragment : BasePrefsFragment() { .build() dialog.getActionButton(DialogAction.POSITIVE) .setOnClickListener { - dataUtils.removeBook(position[p]!!) + DataUtils.removeBook(position[p]!!) utilsHandler.removeFromDatabase( OperationData( UtilsHandler.Operation.BOOKMARKS, 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 358bfeb869..1e7bdb1a78 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 @@ -39,12 +39,11 @@ import com.amaze.filemanager.R; import com.amaze.filemanager.adapters.data.StorageDirectoryParcelable; import com.amaze.filemanager.application.AppConfig; -import com.amaze.filemanager.database.CloudHandler; +import com.amaze.filemanager.database.CloudContract; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.fileoperations.filesystem.usb.SingletonUsbOtg; import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.RootHelper; -import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.ui.ExtensionsKt; import com.amaze.filemanager.ui.activities.AboutActivity; @@ -53,7 +52,6 @@ import com.amaze.filemanager.ui.activities.UtilitiesAliasActivity; import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation; import com.amaze.filemanager.ui.fragments.AppsListFragment; -import com.amaze.filemanager.ui.fragments.CloudSheetFragment; import com.amaze.filemanager.ui.fragments.FtpServerFragment; import com.amaze.filemanager.ui.fragments.MainFragment; import com.amaze.filemanager.ui.fragments.preferencefragments.QuickAccessesPrefsFragment; @@ -66,11 +64,8 @@ import com.amaze.filemanager.utils.ScreenUtils; import com.amaze.filemanager.utils.TinyDB; import com.amaze.filemanager.utils.Utils; -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.utils.cloud.CloudPluginUtil; +import com.amaze.filemanager.utils.omh.OmhCredentialsWrapper; import com.google.android.material.navigation.NavigationView; import android.content.ActivityNotFoundException; @@ -98,12 +93,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.AppCompatImageButton; import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatTextView; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.FragmentTransaction; -import androidx.legacy.app.ActionBarDrawerToggle; import androidx.lifecycle.ViewModelProvider; public class Drawer implements NavigationView.OnNavigationItemSelectedListener { @@ -148,7 +143,7 @@ public class Drawer implements NavigationView.OnNavigationItemSelectedListener { public Drawer(MainActivity mainActivity) { this.mainActivity = mainActivity; - dataUtils = DataUtils.getInstance(); + dataUtils = DataUtils.INSTANCE; drawerHeaderLayout = mainActivity.getLayoutInflater().inflate(R.layout.drawerheader, null); drawerHeaderParent = drawerHeaderLayout.findViewById(R.id.drawer_header_parent); @@ -164,28 +159,8 @@ public Drawer(MainActivity mainActivity) { telegramImageView.setOnClickListener(v -> Utils.openTelegramURL(mainActivity)); instagramImageView.setOnClickListener(v -> Utils.openInstagramURL(mainActivity)); initDrawerFocusItems(); - /*drawerHeaderView.setOnLongClickListener( - v -> { - Intent intent1; - if (SDK_INT < Build.VERSION_CODES.KITKAT) { - intent1 = new Intent(); - intent1.setAction(Intent.ACTION_GET_CONTENT); - } else { - intent1 = new Intent(Intent.ACTION_OPEN_DOCUMENT); - } - intent1.addCategory(Intent.CATEGORY_OPENABLE); - intent1.setType("image/*"); - mainActivity.startActivityForResult(intent1, image_selector_request_code); - return false; - });*/ navView = mainActivity.findViewById(R.id.navigation); - - // set width of drawer in portrait to follow material guidelines - /*if(!Utils.isDeviceInLandScape(mainActivity)){ - setNavViewDimension(navView); - }*/ - navView.setNavigationItemSelectedListener(this); int accentColor = mainActivity.getAccent(), idleColor; @@ -236,7 +211,6 @@ public Drawer(MainActivity mainActivity) { new ActionBarDrawerToggle( mainActivity, /* host Activity */ mDrawerLayout, /* DrawerLayout object */ - R.drawable.ic_drawer_l, /* nav drawer image to replace 'Up' caret */ R.string.drawer_open, /* "open drawer" description for accessibility */ R.string.drawer_close /* "close drawer" description for accessibility */) { public void onDrawerClosed(View view) { @@ -248,7 +222,9 @@ public void onDrawerOpened(View drawerView) { // creates call to onPrepareOptionsMenu() } }; - mDrawerLayout.setDrawerListener(mDrawerToggle); + mDrawerToggle.setHomeAsUpIndicator( + R.drawable.ic_drawer_l); /* nav drawer image to replace 'Up' caret */ + mDrawerLayout.addDrawerListener(mDrawerToggle); mainActivity.getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_drawer_l); mainActivity.getSupportActionBar().setDisplayHomeAsUpEnabled(true); mainActivity.getSupportActionBar().setHomeButtonEnabled(true); @@ -353,65 +329,66 @@ public void refreshDrawer() { ArrayList accountAuthenticationList = new ArrayList<>(); - if (CloudSheetFragment.isCloudProviderAvailable(mainActivity)) { - for (CloudStorage cloudStorage : dataUtils.getAccounts()) { + if (CloudPluginUtil.isCloudProviderAvailable(mainActivity)) { + for (OmhCredentialsWrapper cloudStorage : dataUtils.getAccounts()) { @DrawableRes int deleteIcon = R.drawable.ic_delete_grey_24dp; - if (cloudStorage instanceof Dropbox) { + if (OpenMode.DROPBOX.equals(cloudStorage.getOpenMode())) { addNewItem( menu, CLOUDS_GROUP, order++, - CloudHandler.CLOUD_NAME_DROPBOX, - new MenuMetadata(CloudHandler.CLOUD_PREFIX_DROPBOX + "/", false), + CloudContract.CLOUD_NAME_DROPBOX, + new MenuMetadata(CloudContract.CLOUD_PREFIX_DROPBOX + "/", false), R.drawable.ic_dropbox_white_24dp, deleteIcon); accountAuthenticationList.add( new String[] { - CloudHandler.CLOUD_NAME_DROPBOX, CloudHandler.CLOUD_PREFIX_DROPBOX + "/", + CloudContract.CLOUD_NAME_DROPBOX, CloudContract.CLOUD_PREFIX_DROPBOX + "/", }); - } else if (cloudStorage instanceof Box) { + } else if (OpenMode.BOX.equals(cloudStorage.getOpenMode())) { addNewItem( menu, CLOUDS_GROUP, order++, - CloudHandler.CLOUD_NAME_BOX, - new MenuMetadata(CloudHandler.CLOUD_PREFIX_BOX + "/", false), + CloudContract.CLOUD_NAME_BOX, + new MenuMetadata(CloudContract.CLOUD_PREFIX_BOX + "/", false), R.drawable.ic_box_white_24dp, deleteIcon); accountAuthenticationList.add( new String[] { - CloudHandler.CLOUD_NAME_BOX, CloudHandler.CLOUD_PREFIX_BOX + "/", + CloudContract.CLOUD_NAME_BOX, CloudContract.CLOUD_PREFIX_BOX + "/", }); - } else if (cloudStorage instanceof OneDrive) { + } else if (OpenMode.ONEDRIVE.equals(cloudStorage.getOpenMode())) { addNewItem( menu, CLOUDS_GROUP, order++, - CloudHandler.CLOUD_NAME_ONE_DRIVE, - new MenuMetadata(CloudHandler.CLOUD_PREFIX_ONE_DRIVE + "/", false), + CloudContract.CLOUD_NAME_ONE_DRIVE, + new MenuMetadata(CloudContract.CLOUD_PREFIX_ONE_DRIVE + "/", false), R.drawable.ic_onedrive_white_24dp, deleteIcon); accountAuthenticationList.add( new String[] { - CloudHandler.CLOUD_NAME_ONE_DRIVE, CloudHandler.CLOUD_PREFIX_ONE_DRIVE + "/", + CloudContract.CLOUD_NAME_ONE_DRIVE, CloudContract.CLOUD_PREFIX_ONE_DRIVE + "/", }); - } else if (cloudStorage instanceof GoogleDrive) { + } else if (OpenMode.GDRIVE.equals(cloudStorage.getOpenMode())) { addNewItem( menu, CLOUDS_GROUP, order++, - CloudHandler.CLOUD_NAME_GOOGLE_DRIVE, - new MenuMetadata(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE + "/", false), + CloudContract.CLOUD_NAME_GOOGLE_DRIVE, + new MenuMetadata(CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE + "/", false), R.drawable.ic_google_drive_white_24dp, deleteIcon); accountAuthenticationList.add( new String[] { - CloudHandler.CLOUD_NAME_GOOGLE_DRIVE, CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE + "/", + CloudContract.CLOUD_NAME_GOOGLE_DRIVE, + CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE + "/", }); } } @@ -817,12 +794,11 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) { } if (dataUtils.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) - || meta.path.startsWith(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE))) { + && (meta.path.startsWith(CloudContract.CLOUD_PREFIX_BOX) + || meta.path.startsWith(CloudContract.CLOUD_PREFIX_DROPBOX) + || meta.path.startsWith(CloudContract.CLOUD_PREFIX_ONE_DRIVE) + || meta.path.startsWith(CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE))) { // we have cloud accounts, try see if token is expired or not - CloudUtil.checkToken(meta.path, mainActivity); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP @@ -890,16 +866,16 @@ public void onNavigationItemActionClick(MenuItem item) { || path.startsWith(FTP_URI_PREFIX) || path.startsWith(FTPS_URI_PREFIX)) { mainActivity.showSftpDialog(title, path, true); - } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_DROPBOX)) { + } else if (path.startsWith(CloudContract.CLOUD_PREFIX_DROPBOX)) { GeneralDialogCreation.showCloudDialog( mainActivity, mainActivity.getAppTheme(), OpenMode.DROPBOX); - } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE)) { + } else if (path.startsWith(CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE)) { GeneralDialogCreation.showCloudDialog( mainActivity, mainActivity.getAppTheme(), OpenMode.GDRIVE); - } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_BOX)) { + } else if (path.startsWith(CloudContract.CLOUD_PREFIX_BOX)) { GeneralDialogCreation.showCloudDialog( mainActivity, mainActivity.getAppTheme(), OpenMode.BOX); - } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_ONE_DRIVE)) { + } else if (path.startsWith(CloudContract.CLOUD_PREFIX_ONE_DRIVE)) { GeneralDialogCreation.showCloudDialog( mainActivity, mainActivity.getAppTheme(), OpenMode.ONEDRIVE); } diff --git a/app/src/main/java/com/amaze/filemanager/utils/DataUtils.java b/app/src/main/java/com/amaze/filemanager/utils/DataUtils.java deleted file mode 100644 index 74b83d8f1a..0000000000 --- a/app/src/main/java/com/amaze/filemanager/utils/DataUtils.java +++ /dev/null @@ -1,578 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.utils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.amaze.filemanager.R; -import com.amaze.filemanager.adapters.data.LayoutElementParcelable; -import com.amaze.filemanager.application.AppConfig; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; -import com.amaze.filemanager.models.LanguageModel; -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.googlecode.concurrenttrees.radix.ConcurrentRadixTree; -import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharArrayNodeFactory; -import com.googlecode.concurrenttrees.radix.node.concrete.voidvalue.VoidValue; -import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree; -import com.googlecode.concurrenttrees.radixinverted.InvertedRadixTree; - -import android.content.Context; -import android.text.TextUtils; -import android.view.MenuItem; - -import androidx.annotation.Nullable; - -/** Singleton class to handle data for various services */ - -// Central data being used across activity,fragments and classes -public class DataUtils { - - private static final Logger LOG = LoggerFactory.getLogger(DataUtils.class); - - private ConcurrentRadixTree hiddenfiles = - new ConcurrentRadixTree<>(new DefaultCharArrayNodeFactory()); - - public static final int LIST = 0, GRID = 1; - - private InvertedRadixTree filesGridOrList = - new ConcurrentInvertedRadixTree<>(new DefaultCharArrayNodeFactory()); - - private LinkedList history = new LinkedList<>(); - private ArrayList storages = new ArrayList<>(); - - private InvertedRadixTree tree = - new ConcurrentInvertedRadixTree<>(new DefaultCharArrayNodeFactory()); - - 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; - - private DataChangeListener dataChangeListener; - - private DataUtils() {} - - private static class DataUtilsHolder { - private static final DataUtils INSTANCE = new DataUtils(); - } - - public static List getLanguages(Context context) { - - var data = new ArrayList(); - - data.add( - new LanguageModel( - context.getString(R.string.german_translation_title), - context.getString(R.string.german_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.italian_translation_title), - context.getString(R.string.italian_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.french_translation_title), - context.getString(R.string.french_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.russian_translation_title), - context.getString(R.string.russian_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.spanish_translation_title), - context.getString(R.string.spanish_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.basque_translation_title), - context.getString(R.string.basque_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.chinese_translation_title), - context.getString(R.string.chinese_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.serbian_translation_title), - context.getString(R.string.serbian_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.turkish_translation_title), - context.getString(R.string.turkish_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.ukrainian_translation_title), - context.getString(R.string.ukrainian_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.portuguese_translation_title), - context.getString(R.string.portuguese_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.polish_translation_title), - context.getString(R.string.polish_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.korean_translation_title), - context.getString(R.string.korean_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.greek_translation_title), - context.getString(R.string.greek_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.dutch_translation_title), - context.getString(R.string.dutch_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.romanian_translation_title), - context.getString(R.string.romanian_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.vietnamese_translation_title), - context.getString(R.string.vietnamese_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.japanese_translation_title), - context.getString(R.string.japanese_translation_summary))); - data.add( - new LanguageModel( - context.getString(R.string.tamil_translation_title), - context.getString(R.string.tamil_translation_summary))); - - return data; - } - - public static List getContributors(Context context) { - - var data = new ArrayList(); - - data.add( - new LanguageModel( - context.getString(R.string.contributor_1_title), - context.getString(R.string.contributors_1_summary))); - data.add( - new LanguageModel( - context.getString(R.string.contributor_2_title), - context.getString(R.string.contributors_2_summary))); - data.add( - new LanguageModel( - context.getString(R.string.contributor_3_title), - context.getString(R.string.contributors_3_summary))); - data.add( - new LanguageModel( - context.getString(R.string.contributor_4_title), - context.getString(R.string.contributors_4_summary))); - data.add( - new LanguageModel( - context.getString(R.string.contributor_5_title), - context.getString(R.string.contributors_5_summary))); - data.add( - new LanguageModel( - context.getString(R.string.contributor_6_title), - context.getString(R.string.contributors_6_summary))); - data.add( - new LanguageModel( - context.getString(R.string.contributor_7_title), - context.getString(R.string.contributors_7_summary))); - - return data; - } - - public static DataUtils getInstance() { - return DataUtilsHolder.INSTANCE; - } - - public int containsServer(String[] a) { - return contains(a, servers); - } - - public int containsServer(String path) { - - synchronized (servers) { - if (servers == null) return -1; - int i = 0; - for (String[] x : servers) { - if (x[1].equals(path)) return i; - i++; - } - } - return -1; - } - - 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()); - history.clear(); - storages = new ArrayList<>(); - tree = new ConcurrentInvertedRadixTree<>(new DefaultCharArrayNodeFactory()); - servers = new ArrayList<>(); - books = new ArrayList<>(); - accounts = new ArrayList<>(); - } - - public void registerOnDataChangedListener(DataChangeListener l) { - - dataChangeListener = l; - clear(); - } - - int contains(String a, ArrayList b) { - int i = 0; - for (String[] x : b) { - if (x[1].equals(a)) return i; - i++; - } - return -1; - } - - int contains(String[] a, ArrayList b) { - if (b == null) return -1; - int i = 0; - for (String[] x : b) { - if (x[0].equals(a[0]) && x[1].equals(a[1])) return i; - i++; - } - return -1; - } - - public void removeBook(int i) { - synchronized (books) { - if (books.size() > i) books.remove(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); - } - } - - public void addBook(String[] i) { - if (containsBooks(i) != -1) { - return; - } - synchronized (books) { - books.add(i); - } - } - - /** - * @param i The bookmark name and path. - * @param refreshdrawer boolean flag to indicate if drawer refresh is desired. - * @return True if operation successful, false if failure. - */ - public boolean addBook(final String[] i, boolean refreshdrawer) { - if (containsBooks(i) != -1) { - // book exists - return false; - } else { - synchronized (books) { - books.add(i); - } - - if (dataChangeListener != null) { - dataChangeListener.onBookAdded(i, refreshdrawer); - } - - return true; - } - } - - public void addAccount(CloudStorage storage) { - accounts.add(storage); - } - - public void addServer(String[] i) { - servers.add(i); - } - - public void addHiddenFile(final String i) { - - synchronized (hiddenfiles) { - hiddenfiles.put(i, VoidValue.SINGLETON); - } - if (dataChangeListener != null) { - dataChangeListener.onHiddenFileAdded(i); - } - } - - public void removeHiddenFile(final String i) { - - synchronized (hiddenfiles) { - hiddenfiles.remove(i); - } - if (dataChangeListener != null) { - dataChangeListener.onHiddenFileRemoved(i); - } - } - - public void setHistory(LinkedList s) { - history.clear(); - history.addAll(s); - } - - public LinkedList getHistory() { - return history; - } - - public void addHistoryFile(final String i) { - history.push(i); - if (dataChangeListener != null) { - dataChangeListener.onHistoryAdded(i); - } - } - - public void sortBook() { - Collections.sort(books, new BookSorter()); - } - - public synchronized void setServers(ArrayList servers) { - if (servers != null) this.servers = servers; - } - - 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; - } - - 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: - LOG.error("Unable to determine service type of storage {}", storage); - return null; - } - } - return null; - } - - public boolean isFileHidden(String path) { - try { - return getHiddenFiles().getValueForExactKey(path) != null; - } catch (IllegalStateException e) { - LOG.warn("failed to get hidden file", e); - return false; - } - } - - public ConcurrentRadixTree getHiddenFiles() { - return hiddenfiles; - } - - public synchronized void setHiddenFiles(ConcurrentRadixTree hiddenfiles) { - if (hiddenfiles != null) this.hiddenfiles = hiddenfiles; - } - - public synchronized void setGridfiles(ArrayList gridfiles) { - if (gridfiles != null) { - for (String gridfile : gridfiles) { - setPathAsGridOrList(gridfile, GRID); - } - } - } - - public synchronized void setListfiles(ArrayList listfiles) { - if (listfiles != null) { - for (String gridfile : listfiles) { - setPathAsGridOrList(gridfile, LIST); - } - } - } - - public void setPathAsGridOrList(String path, int value) { - filesGridOrList.put(path, value); - } - - public int getListOrGridForPath(String path, int defaultValue) { - Integer value = filesGridOrList.getValueForLongestKeyPrefixing(path); - return value != null ? value : defaultValue; - } - - public void clearHistory() { - history.clear(); - if (dataChangeListener != null) { - AppConfig.getInstance().runInBackground(() -> dataChangeListener.onHistoryCleared()); - } - } - - public synchronized List getStorages() { - return storages; - } - - public synchronized void setStorages(ArrayList storages) { - this.storages = storages; - } - - public boolean putDrawerPath(MenuItem item, String path) { - if (!TextUtils.isEmpty(path)) { - try { - tree.put(path, item.getItemId()); - return true; - } catch (IllegalStateException e) { - LOG.warn("failed to put drawer path", e); - return false; - } - } - return false; - } - - /** - * @param path the path to find - * @return the id of the longest containing MenuMetadata.path in getDrawerMetadata() or null - */ - public @Nullable Integer findLongestContainingDrawerItem(CharSequence path) { - return tree.getValueForLongestKeyPrefixing(path); - } - - public ArrayList getCheckedItemsList() { - return this.checkedItemsList; - } - - public void setCheckedItemsList(ArrayList layoutElementParcelables) { - this.checkedItemsList = layoutElementParcelables; - } - - /** - * Callbacks to do original changes in database (and ui if required) The callbacks are called in a - * main thread - */ - public interface DataChangeListener { - void onHiddenFileAdded(String path); - - void onHiddenFileRemoved(String path); - - void onHistoryAdded(String path); - - void onBookAdded(String path[], boolean refreshdrawer); - - void onHistoryCleared(); - } -} diff --git a/app/src/main/java/com/amaze/filemanager/utils/DataUtils.kt b/app/src/main/java/com/amaze/filemanager/utils/DataUtils.kt new file mode 100644 index 0000000000..02088e9357 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/DataUtils.kt @@ -0,0 +1,569 @@ +/* + * Copyright (C) 2014-2024 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.utils + +import android.content.Context +import android.text.TextUtils +import android.view.MenuItem +import com.amaze.filemanager.R +import com.amaze.filemanager.adapters.data.LayoutElementParcelable +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.models.LanguageModel +import com.amaze.filemanager.utils.omh.OmhCredentialsWrapper +import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree +import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharArrayNodeFactory +import com.googlecode.concurrenttrees.radix.node.concrete.voidvalue.VoidValue +import com.googlecode.concurrenttrees.radixinverted.ConcurrentInvertedRadixTree +import com.googlecode.concurrenttrees.radixinverted.InvertedRadixTree +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.Collections +import java.util.LinkedList + +/** Singleton class to handle data for various services */ +// Central data being used across activity,fragments and classes +object DataUtils { + @JvmStatic + private val LOG: Logger = LoggerFactory.getLogger(DataUtils::class.java) + + const val LIST: Int = 0 + const val GRID: Int = 1 + + private var hiddenfiles = ConcurrentRadixTree(DefaultCharArrayNodeFactory()) + + private var filesGridOrList: InvertedRadixTree = + ConcurrentInvertedRadixTree(DefaultCharArrayNodeFactory()) + + private val history = LinkedList() + + private var tree: InvertedRadixTree = + ConcurrentInvertedRadixTree(DefaultCharArrayNodeFactory()) + + private var servers: ArrayList> = ArrayList() + private var books = ArrayList>() + + private var _accounts: MutableList = mutableListOf() + val accounts: List + @Synchronized get() = _accounts + + /** List of checked items to persist when drag and drop from one tab to another */ + var checkedItemsList: ArrayList? = null + + private var dataChangeListener: DataChangeListener? = null + + /** + * Returns list of available languages. + */ + @JvmStatic + @Suppress("LongMethod") + fun getLanguages(context: Context): List = + arrayListOf( + LanguageModel( + context.getString(R.string.german_translation_title), + context.getString(R.string.german_translation_summary), + ), + LanguageModel( + context.getString(R.string.spanish_translation_title), + context.getString(R.string.spanish_translation_summary), + ), + LanguageModel( + context.getString(R.string.basque_translation_title), + context.getString(R.string.basque_translation_summary), + ), + LanguageModel( + context.getString(R.string.chinese_translation_title), + context.getString(R.string.chinese_translation_summary), + ), + LanguageModel( + context.getString(R.string.serbian_translation_title), + context.getString(R.string.serbian_translation_summary), + ), + LanguageModel( + context.getString(R.string.turkish_translation_title), + context.getString(R.string.turkish_translation_summary), + ), + LanguageModel( + context.getString(R.string.ukrainian_translation_title), + context.getString(R.string.ukrainian_translation_summary), + ), + LanguageModel( + context.getString(R.string.portuguese_translation_title), + context.getString(R.string.portuguese_translation_summary), + ), + LanguageModel( + context.getString(R.string.polish_translation_title), + context.getString(R.string.polish_translation_summary), + ), + LanguageModel( + context.getString(R.string.korean_translation_title), + context.getString(R.string.korean_translation_summary), + ), + LanguageModel( + context.getString(R.string.greek_translation_title), + context.getString(R.string.greek_translation_summary), + ), + LanguageModel( + context.getString(R.string.dutch_translation_title), + context.getString(R.string.dutch_translation_summary), + ), + LanguageModel( + context.getString(R.string.romanian_translation_title), + context.getString(R.string.romanian_translation_summary), + ), + LanguageModel( + context.getString(R.string.vietnamese_translation_title), + context.getString(R.string.vietnamese_translation_summary), + ), + LanguageModel( + context.getString(R.string.japanese_translation_title), + context.getString(R.string.japanese_translation_summary), + ), + LanguageModel( + context.getString(R.string.tamil_translation_title), + context.getString(R.string.tamil_translation_summary), + ), + ) + + /** + * Returns list of contributors. + */ + @JvmStatic + fun getContributors(context: Context): List = + arrayListOf( + LanguageModel( + context.getString(R.string.contributor_1_title), + context.getString(R.string.contributors_1_summary), + ), + LanguageModel( + context.getString(R.string.contributor_2_title), + context.getString(R.string.contributors_2_summary), + ), + LanguageModel( + context.getString(R.string.contributor_3_title), + context.getString(R.string.contributors_3_summary), + ), + LanguageModel( + context.getString(R.string.contributor_4_title), + context.getString(R.string.contributors_4_summary), + ), + LanguageModel( + context.getString(R.string.contributor_5_title), + context.getString(R.string.contributors_5_summary), + ), + LanguageModel( + context.getString(R.string.contributor_6_title), + context.getString(R.string.contributors_6_summary), + ), + LanguageModel( + context.getString(R.string.contributor_7_title), + context.getString(R.string.contributors_7_summary), + ), + ) + + /** + * Check if server path(s) exists + */ + fun containsServer(a: Array): Int { + return contains(a, servers) + } + + /** + * Check if server path exists + */ + fun containsServer(path: String): Int { + synchronized(servers) { + return servers.indexOfFirst { + it[1] == path + } + } + } + + /** + * Check if bookmark exists + */ + fun containsBooks(a: Array): Int { + return contains(a, books) + } + + /** + * Clear all data. + */ + fun clear() { + hiddenfiles = ConcurrentRadixTree(DefaultCharArrayNodeFactory()) + filesGridOrList = ConcurrentInvertedRadixTree(DefaultCharArrayNodeFactory()) + history.clear() + storages = ArrayList() + tree = ConcurrentInvertedRadixTree(DefaultCharArrayNodeFactory()) + servers = ArrayList() + books = ArrayList() + _accounts = mutableListOf() + } + + /** + * Register a DataChangeListener, then clear all data. + */ + fun registerOnDataChangedListener(l: DataChangeListener?) { + dataChangeListener = l + clear() + } + + private fun contains( + a: String, + b: ArrayList>, + ): Int { + for ((i, x) in b.withIndex()) { + if (x[1] == a) return i + } + return -1 + } + + private fun contains( + a: Array, + b: ArrayList>?, + ): Int { + if (b == null) return -1 + for ((i, x) in b.withIndex()) { + if (x[0] == a[0] && x[1] == a[1]) return i + } + return -1 + } + + /** + * Remove bookmark at given index. + */ + fun removeBook(i: Int) { + synchronized(books) { + if (books.size > i) books.removeAt(i) + } + } + + /** + * Remove account for given service type. + */ + fun removeAccount(serviceType: OpenMode) { + accounts.let { + val newValue = it.toMutableList() + newValue.removeAll { entry: OmhCredentialsWrapper -> + entry.openMode == serviceType + } + } + } + + /** + * Remove server path at given index. + */ + fun removeServer(i: Int) { + synchronized(servers) { + if (servers.size > i) servers.removeAt(i) + } + } + + /** + * Add bookmark if not exist. + */ + fun addBook(i: Array) { + if (containsBooks(i) != -1) { + return + } + synchronized(books) { + books.add(i) + } + } + + /** + * @param i The bookmark name and path. + * @param refreshdrawer boolean flag to indicate if drawer refresh is desired. + * @return True if operation successful, false if failure. + */ + fun addBook( + i: Array, + refreshdrawer: Boolean, + ): Boolean { + if (containsBooks(i) != -1) { + // book exists + return false + } else { + synchronized(books) { + books.add(i) + } + + if (dataChangeListener != null) { + dataChangeListener!!.onBookAdded(i, refreshdrawer) + } + + return true + } + } + + /** + * Add account if not exist. + */ + fun addAccount(account: OmhCredentialsWrapper) { + _accounts.let { + if (!it.contains(account)) { + it.add(account) + } + } + } + + /** + * Add server path. + */ + fun addServer(i: Array) { + synchronized(servers) { + servers.add(i) + } + } + + /** + * Add hidden file path. + */ + fun addHiddenFile(i: String) { + synchronized(hiddenfiles) { + hiddenfiles.put(i, VoidValue.SINGLETON) + } + if (dataChangeListener != null) { + dataChangeListener!!.onHiddenFileAdded(i) + } + } + + /** + * Remove hidden file path. + */ + fun removeHiddenFile(i: String) { + synchronized(hiddenfiles) { + hiddenfiles.remove(i) + } + if (dataChangeListener != null) { + dataChangeListener!!.onHiddenFileRemoved(i) + } + } + + /** + * Set history collection. + */ + fun setHistory(s: LinkedList?) { + history.clear() + history.addAll(s!!) + } + + /** + * Get history collection. + */ + fun getHistory(): LinkedList { + return history + } + + /** + * Add path to history. + */ + fun addHistoryFile(i: String) { + history.push(i) + if (dataChangeListener != null) { + dataChangeListener!!.onHistoryAdded(i) + } + } + + /** + * Sort bookmarks alphabetically. + */ + fun sortBook() { + Collections.sort(books, BookSorter()) + } + + /** + * Set servers collection. + */ + @Synchronized + fun setServers(servers: ArrayList>?) { + if (servers != null) this.servers = servers + } + + /** + * Set bookmarks collection. + */ + @Synchronized + fun setBooks(books: ArrayList>?) { + if (books != null) this.books = books + } + + /** + * Get servers collection. + */ + @Synchronized + fun getServers(): ArrayList> { + return servers + } + + /** + * Get bookmarks collection. + */ + @Synchronized + fun getBooks(): ArrayList> { + return books + } + + /** + * Get account for given service type. + */ + @Synchronized + fun getAccount(serviceType: OpenMode): OmhCredentialsWrapper? { + val retval = accounts.find { it.openMode == serviceType } + if (retval == null) { + LOG.error("Unable to determine service type of cloudEntry {}", serviceType) + } + return retval + } + + /** + * Check if file is hidden. + */ + fun isFileHidden(path: String): Boolean { + try { + return hiddenFiles?.getValueForExactKey(path) != null + } catch (e: IllegalStateException) { + LOG.warn("failed to get hidden file", e) + return false + } + } + + @set:Synchronized + var hiddenFiles: ConcurrentRadixTree? + get() = hiddenfiles + set(hiddenfiles) { + if (hiddenfiles != null) this.hiddenfiles = hiddenfiles + } + + @set:Synchronized + var gridfiles: ArrayList? + get() = gridfiles + set(gridfiles) { + if (gridfiles != null) { + for (gridfile in gridfiles) { + setPathAsGridOrList(gridfile, GRID) + } + } + } + + /** + * Set list of paths that should be displayed in list mode. + */ + @Synchronized + fun setListfiles(listfiles: ArrayList?) { + if (listfiles != null) { + for (gridfile in listfiles) { + setPathAsGridOrList(gridfile, LIST) + } + } + } + + /** + * Set path to be displayed in grid or list mode. + */ + fun setPathAsGridOrList( + path: String?, + value: Int, + ) { + filesGridOrList.put(path, value) + } + + /** + * Get if path should be displayed in grid or list mode. + * + * @param path the path to find + * @param defaultValue the default value to return if no value found + * @return the value for the longest prefix matching path or defaultValue if none found + */ + fun getListOrGridForPath( + path: String?, + defaultValue: Int, + ): Int { + val value = filesGridOrList.getValueForLongestKeyPrefixing(path) + return value ?: defaultValue + } + + /** + * Clear history collection. Also notifies listener in background thread. + */ + fun clearHistory() { + history.clear() + AppConfig.getInstance().runInBackground { dataChangeListener?.onHistoryCleared() } + } + + @set:Synchronized + var storages: ArrayList = ArrayList() + + /** + * Put drawer path with its menu id. + */ + fun putDrawerPath( + item: MenuItem, + path: String?, + ): Boolean { + if (!TextUtils.isEmpty(path)) { + try { + tree.put(path, item.itemId) + return true + } catch (e: IllegalStateException) { + LOG.warn("failed to put drawer path", e) + return false + } + } + return false + } + + /** + * @param path the path to find + * @return the id of the longest containing MenuMetadata.path in getDrawerMetadata() or null + */ + fun findLongestContainingDrawerItem(path: CharSequence?): Int? { + return tree.getValueForLongestKeyPrefixing(path) + } + + /** + * Callbacks to do original changes in database (and ui if required) The callbacks are called in a + * main thread + */ + interface DataChangeListener { + /** Called when a hidden file is added */ + fun onHiddenFileAdded(path: String) + + /** Called when a hidden file is removed */ + fun onHiddenFileRemoved(path: String) + + /** Called when a history entry is added */ + fun onHistoryAdded(path: String) + + /** + * Called when a bookmark is added + * + * @param path The bookmark name and path. + * @param refreshDrawer boolean flag to indicate if drawer refresh is desired. + */ + fun onBookAdded( + path: Array, + refreshDrawer: Boolean, + ) + + /** Called when history is cleared */ + fun onHistoryCleared() + } +} diff --git a/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java b/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java index a6010b06ad..e6347bf0a9 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java +++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java @@ -42,7 +42,7 @@ import com.amaze.filemanager.asynchronous.asynctasks.DeleteTask; import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil; import com.amaze.filemanager.asynchronous.services.ZipService; -import com.amaze.filemanager.database.CloudHandler; +import com.amaze.filemanager.database.CloudContract; import com.amaze.filemanager.database.CryptHandler; import com.amaze.filemanager.database.models.explorer.EncryptedEntry; import com.amaze.filemanager.fileoperations.filesystem.FolderState; @@ -91,7 +91,7 @@ public class MainActivityHelper { private static final Logger LOG = LoggerFactory.getLogger(MainActivityHelper.class); private MainActivity mainActivity; - private DataUtils dataUtils = DataUtils.getInstance(); + private DataUtils dataUtils = DataUtils.INSTANCE; private int accentColor; private SpeedDialView.OnActionSelectedListener fabActionListener; @@ -670,7 +670,7 @@ public void invalidName(final HybridFile file) { public void deleteFiles(ArrayList files, boolean doDeletePermanently) { if (files == null || files.size() == 0) return; - if (files.get(0).isSmb() || files.get(0).isFtp()) { + if (files.get(0).isSmb() || files.get(0).isFtp() || files.get(0).isCloudDriveFile()) { new DeleteTask(mainActivity, doDeletePermanently).execute(files); return; } @@ -723,17 +723,17 @@ public String parseOTGPath(String path) { public String parseCloudPath(OpenMode serviceType, String path) { switch (serviceType) { case DROPBOX: - if (path.contains(CloudHandler.CLOUD_PREFIX_DROPBOX)) return path; - else return CloudHandler.CLOUD_PREFIX_DROPBOX + path.substring(path.indexOf(":") + 1); + if (path.contains(CloudContract.CLOUD_PREFIX_DROPBOX)) return path; + else return CloudContract.CLOUD_PREFIX_DROPBOX + path.substring(path.indexOf(":") + 1); case BOX: - if (path.contains(CloudHandler.CLOUD_PREFIX_BOX)) return path; - else return CloudHandler.CLOUD_PREFIX_BOX + path.substring(path.indexOf(":") + 1); + if (path.contains(CloudContract.CLOUD_PREFIX_BOX)) return path; + else return CloudContract.CLOUD_PREFIX_BOX + path.substring(path.indexOf(":") + 1); case GDRIVE: - if (path.contains(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE)) return path; - else return CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE + path.substring(path.indexOf(":") + 1); + if (path.contains(CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE)) return path; + else return CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE + path.substring(path.indexOf(":") + 1); case ONEDRIVE: - if (path.contains(CloudHandler.CLOUD_PREFIX_ONE_DRIVE)) return path; - else return CloudHandler.CLOUD_PREFIX_ONE_DRIVE + path.substring(path.indexOf(":") + 1); + if (path.contains(CloudContract.CLOUD_PREFIX_ONE_DRIVE)) return path; + else return CloudContract.CLOUD_PREFIX_ONE_DRIVE + path.substring(path.indexOf(":") + 1); default: return 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 cad7ae00d1..2e0c595e59 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/OTGUtil.kt @@ -30,6 +30,7 @@ import android.os.Build.VERSION_CODES.LOLLIPOP import android.provider.DocumentsContract import android.util.Log import androidx.annotation.RequiresApi +import androidx.arch.core.util.Function import androidx.documentfile.provider.DocumentFile import com.amaze.filemanager.exceptions.DocumentFileNotFoundException import com.amaze.filemanager.fileoperations.filesystem.OpenMode @@ -70,12 +71,9 @@ object OTGUtil { getDocumentFiles( path, context, - object : OnFileFound { - override fun onFileFound(file: HybridFileParcelable) { - files.add(file) - } - }, - ) + ) { file: HybridFileParcelable -> + files.add(file) + } return files } @@ -90,7 +88,7 @@ object OTGUtil { fun getDocumentFiles( path: String, context: Context, - fileFound: OnFileFound, + fileFound: Function, ) { val rootUriString = SingletonUsbOtg.getInstance().usbOtgRoot @@ -104,7 +102,7 @@ object OTGUtil { path: String, context: Context, openMode: OpenMode, - fileFound: OnFileFound, + fileFound: Function, ) { var rootUri = DocumentFile.fromTreeUri(context, rootUriString) @@ -145,7 +143,7 @@ object OTGUtil { baseFile.name = file.name baseFile.mode = openMode baseFile.fullUri = file.uri - fileFound.onFileFound(baseFile) + fileFound.apply(baseFile) } } } diff --git a/app/src/main/java/com/amaze/filemanager/utils/OnFileFound.kt b/app/src/main/java/com/amaze/filemanager/utils/OnFileFound.kt deleted file mode 100644 index 4c4b68fabf..0000000000 --- a/app/src/main/java/com/amaze/filemanager/utils/OnFileFound.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.utils - -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. - */ -fun interface OnFileFound { - @Suppress("UndocumentedPublicFunction") - fun onFileFound(file: HybridFileParcelable) -} diff --git a/app/src/main/java/com/amaze/filemanager/utils/StartActivityForResultWithSourceIntent.kt b/app/src/main/java/com/amaze/filemanager/utils/StartActivityForResultWithSourceIntent.kt new file mode 100644 index 0000000000..432ccddb41 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/StartActivityForResultWithSourceIntent.kt @@ -0,0 +1,40 @@ +package com.amaze.filemanager.utils + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContract + +/** + * Custom [ActivityResultContract] with specifically input = [Intent] and output = [ActivityResult] + * to allow arguments that is added to the intent to be accessible at the activity result. + */ +class StartActivityForResultWithSourceIntent : ActivityResultContract() { + private var sourceArguments: Bundle? = null + + override fun createIntent( + context: Context, + input: Intent, + ): Intent { + sourceArguments = Bundle(input.extras) + return input + } + + override fun parseResult( + resultCode: Int, + intent: Intent?, + ): ActivityResult = + ActivityResult( + resultCode, + // If source argument is not null, return them to the requestor. + // If activity result is null, we create an empty intent to store the arguments + if (sourceArguments != null) { + intent.also { + it?.putExtras(sourceArguments!!) + } ?: Intent().putExtras(sourceArguments!!) + } else { + intent + }, + ) +} diff --git a/app/src/main/java/com/amaze/filemanager/utils/cloud/CloudPluginUtil.kt b/app/src/main/java/com/amaze/filemanager/utils/cloud/CloudPluginUtil.kt new file mode 100644 index 0000000000..df884e4e93 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/cloud/CloudPluginUtil.kt @@ -0,0 +1,75 @@ +package com.amaze.filemanager.utils.cloud + +import android.content.Context +import android.content.pm.PackageManager +import com.amaze.filemanager.database.CloudContract.APP_PACKAGE_NAME +import com.amaze.filemanager.database.CloudContract.ENABLED_PROVIDERS +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.utils.DataUtils +import com.amaze.filemanager.utils.omh.OMHClientHelper +import com.amaze.filemanager.utils.omh.OmhCredentialsWrapper +import com.openmobilehub.android.auth.core.utils.EncryptedSharedPreferences + +/** + * Utility object for cloud plugin related operations. + */ +object CloudPluginUtil { + /** + * Initializes DataUtils with accounts from enabled cloud providers, if exist. + */ + @JvmStatic + fun initializeDataUtils(context: Context) { + ENABLED_PROVIDERS.forEach { + val prefs = EncryptedSharedPreferences.getEncryptedSharedPrefs(context, resolveOmhProviderNameFrom(it)) + if (prefs.getString("email", null) != null) { + DataUtils.addAccount( + OmhCredentialsWrapper( + openMode = it, + credentials = OMHClientHelper.getAuthClient(it).getCredentials(), + ), + ) + } + } + } + + /** + * Resolves the plugin ID from the given [OpenMode]. + * + */ + @JvmStatic + fun resolvePluginIdFrom(openMode: OpenMode): Int { + return when (openMode) { + OpenMode.GDRIVE -> 1 + OpenMode.BOX -> 3 + OpenMode.DROPBOX -> 2 + OpenMode.ONEDRIVE -> 4 + else -> throw IllegalArgumentException("Invalid open mode: $openMode") + } + } + + /** + * Resolves the Open Mobile Hub provider name from the given [OpenMode]. + */ + @JvmStatic + fun resolveOmhProviderNameFrom(openMode: OpenMode): String { + return when (openMode) { + OpenMode.GDRIVE -> "google" + OpenMode.BOX -> "box" + OpenMode.DROPBOX -> "dropbox" + OpenMode.ONEDRIVE -> "microsoft" + else -> throw IllegalArgumentException("Invalid open mode: $openMode") + } + } + + /** Determines whether cloud provider is installed or not */ + @JvmStatic + fun isCloudProviderAvailable(context: Context): Boolean { + val pm = context.packageManager + try { + pm.getPackageInfo(APP_PACKAGE_NAME, PackageManager.GET_ACTIVITIES) + return true + } catch (_: PackageManager.NameNotFoundException) { + return false + } + } +} diff --git a/app/src/main/java/com/amaze/filemanager/utils/omh/OMHClientHelper.kt b/app/src/main/java/com/amaze/filemanager/utils/omh/OMHClientHelper.kt new file mode 100644 index 0000000000..6d3b116466 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/omh/OMHClientHelper.kt @@ -0,0 +1,248 @@ +package com.amaze.filemanager.utils.omh + +import android.content.Context +import android.database.Cursor +import android.os.CancellationSignal +import androidx.core.content.ContentResolverCompat +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.database.CloudContract +import com.amaze.filemanager.fileoperations.exceptions.CloudPluginException +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.utils.cloud.CloudPluginUtil +import com.openmobilehub.android.auth.core.OmhAuthClient +import com.openmobilehub.android.auth.plugin.dropbox.mobileweb.presentation.DropboxMobileWebAuthClient +import com.openmobilehub.android.auth.plugin.google.nongms.presentation.OmhAuthFactoryImpl +import com.openmobilehub.android.auth.plugin.microsoft.mobileweb.presentation.MicrosoftMobileWebAuthClient +import com.openmobilehub.android.storage.core.OmhStorageClient +import com.openmobilehub.android.storage.core.OmhStorageProvider +import com.openmobilehub.android.storage.plugin.dropbox.restful.DropboxRestfulOmhStorageClientFactory +import com.openmobilehub.android.storage.plugin.googledrive.nongms.GoogleDriveNonGmsConstants +import com.openmobilehub.android.storage.plugin.onedrive.restful.OneDriveRestfulOmhStorageClientFactory +import java.util.EnumMap + +/** + * Helper object to manage Open Mobile Hub auth and storage clients for different cloud providers. + * + */ +object OMHClientHelper { + const val MULTI_SLASH_FOR_CLOUD = "(?<=[^:])(///+)" + + private val authClients: MutableMap = + EnumMap(OpenMode::class.java) + + private val storageClients: MutableMap = + EnumMap(OpenMode::class.java) + + /** + * Initializes all available cloud clients at once. This should be called when the app starts. + */ + @JvmStatic + fun initializeClients() { + AppConfig.getInstance().let { context: Context -> + val cursor = + ContentResolverCompat.query( + context.contentResolver, + CloudContract.URI, + CloudContract.PROJECTION, + CloudContract.COLUMN_ID, + CloudContract.ENABLED_PROVIDER_IDS, + null, + null as CancellationSignal?, + ) + if (cursor == null || !cursor.moveToFirst()) { + throw CloudPluginException() + } else { + do { + when (cursor.getInt(0)) { + 1 -> getAuthClient(OpenMode.GDRIVE, cursor.getString(1)) + 2 -> getAuthClient(OpenMode.DROPBOX, cursor.getString(1)) + 3 -> getAuthClient(OpenMode.BOX, cursor.getString(1)) + 4 -> getAuthClient(OpenMode.ONEDRIVE, cursor.getString(1)) + } + } while (cursor.moveToNext()) + cursor.close() + } + } + } + + /** + * Retrieves the auth client for the specified [openMode]. + */ + @JvmStatic + fun getAuthClient(openMode: OpenMode): OmhAuthClient { + return if (authClients.containsKey(openMode)) { + authClients[openMode]!! + } else { + val cursor = getCloudPluginCredentialsOf(openMode) + if (cursor == null || !cursor.moveToFirst()) { + throw CloudPluginException() + } else { + getAuthClient(openMode, cursor.getString(1)) + } + } + } + + /** + * Retrieves the auth client for the specified [openMode] using the provided [apiKey]. + */ + @JvmStatic + fun getAuthClient( + openMode: OpenMode, + apiKey: String, + ): OmhAuthClient { + val context = AppConfig.getInstance() + if (authClients.containsKey(openMode)) { + return authClients[openMode]!! + } else { + synchronized(authClients) { + val authClient = + when (openMode) { + OpenMode.GDRIVE -> { + getGoogleAuthClient(context, apiKey) + } + OpenMode.DROPBOX -> { + getDropboxAuthClient(context, apiKey) + } + OpenMode.ONEDRIVE -> { + getOnedriveAuthClient(context, apiKey) + } + else -> throw IllegalArgumentException("Unsupported OpenMode $openMode") + } + authClients.put(openMode, authClient) + } + return authClients[openMode]!! + } + } + + /** + * Retrieves the storage client for the specified [openMode]. + */ + @JvmStatic + fun getStorageClient(openMode: OpenMode): OmhStorageClient? { + if (storageClients.containsKey(openMode)) { + return storageClients[openMode]!! + } else { + synchronized(storageClients) { + val context = AppConfig.getInstance() + val credentials = getCloudPluginCredentialsOf(openMode) + if (credentials != null && credentials.moveToFirst()) { + val storageClient = + when (openMode) { + OpenMode.GDRIVE -> { + getGoogleStorageClient(context, credentials) + } + OpenMode.DROPBOX -> { + getDropboxStorageClient(context, credentials) + } + OpenMode.ONEDRIVE -> { + getOnedriveStorageClient(context, credentials) + } + else -> throw IllegalArgumentException("Unsupported OpenMode $openMode") + } + credentials.close() + storageClients.put(openMode, storageClient) + } else { + throw CloudPluginException("Unable to obtain API secrets from Cloud plugin for $openMode") + } + } + return storageClients[openMode] + } + } + + /** + * Utility method to fetch cloud plugin credentials from the cloud plugin. + */ + fun getCloudPluginCredentialsOf(openMode: OpenMode): Cursor? { + val context = AppConfig.getInstance() + return ContentResolverCompat.query( + context.contentResolver, + CloudContract.URI, + CloudContract.PROJECTION, + CloudContract.COLUMN_ID, + arrayOf(CloudPluginUtil.resolvePluginIdFrom(openMode).toString()), + null, + null as CancellationSignal?, + ) + } + + private fun getDropboxAuthClient( + context: Context, + apiKey: String, + ): OmhAuthClient { + return DropboxMobileWebAuthClient.Builder(apiKey).also { builder -> + arrayOf( + "account_info.read", + "files.metadata.read", + "files.content.write", + "files.content.read", + "sharing.write", + "sharing.read", + ).forEach { scope -> + builder.addScope(scope) + } + }.build(context) + } + + private fun getDropboxStorageClient( + context: Context, + cursor: Cursor, + ): OmhStorageClient { + val apiKey = cursor.getString(1) + return DropboxRestfulOmhStorageClientFactory() + .getStorageClient(getDropboxAuthClient(context, apiKey)) + } + + private fun getOnedriveAuthClient( + context: Context, + apiKey: String, + ): OmhAuthClient { + return MicrosoftMobileWebAuthClient.Builder(apiKey).also { builder -> + arrayListOf("User.Read", "openid", "profile", "email").forEach { scope -> + builder.addScope(scope) + } + }.build(context) + } + + private fun getOnedriveStorageClient( + context: Context, + cursor: Cursor, + ): OmhStorageClient { + val apiKey = cursor.getString(1) + return OneDriveRestfulOmhStorageClientFactory() + .getStorageClient(getOnedriveAuthClient(context, apiKey)) + } + + private fun getGoogleAuthClient( + context: Context, + apiKey: String, + ): OmhAuthClient { + val authClientInstance = + OmhAuthFactoryImpl.getAuthClient( + context, + listOf( + "openid", + "email", + "profile", + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/drive.file", + ), + apiKey, + null, + ) + authClientInstance.initialize() + return authClientInstance + } + + private fun getGoogleStorageClient( + context: Context, + cursor: Cursor, + ): OmhStorageClient { + val authClient = getAuthClient(OpenMode.GDRIVE, cursor.getString(1)) + val storageClientInstance = + OmhStorageProvider.Builder() + .addNonGmsPath(GoogleDriveNonGmsConstants.IMPLEMENTATION_PATH) + .build() + .provideStorageClient(authClient, context) + return storageClientInstance + } +} diff --git a/app/src/main/java/com/amaze/filemanager/utils/omh/OmhAuthClientExt.kt b/app/src/main/java/com/amaze/filemanager/utils/omh/OmhAuthClientExt.kt new file mode 100644 index 0000000000..9f29dc84d8 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/omh/OmhAuthClientExt.kt @@ -0,0 +1,205 @@ +package com.amaze.filemanager.utils.omh + +import com.amaze.filemanager.fileoperations.exceptions.CloudPluginException +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.openmobilehub.android.auth.core.OmhAuthClient +import com.openmobilehub.android.auth.core.OmhCredentials +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.net.ProtocolException +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +/** + * Callback interface for triggering authentication when needed. + */ +interface AuthTrigger { + /** + * Triggers authentication for the specified OpenMode. + * @param openMode The cloud service to authenticate with + * @return true if authentication was successful, false otherwise + */ + fun triggerAuthBlocking(openMode: OpenMode): Boolean +} + +private val LOG: Logger = LoggerFactory.getLogger(OmhAuthClient::class.java) + +private val authExecutor = + Executors.newSingleThreadExecutor { + Thread(it, "OmhAuth").apply { + isDaemon = true + } + } + +/** + * Check if the exception has a [ProtocolException] anywhere in its cause chain. + */ +private fun hasProtocolExceptionCause(e: Throwable): Boolean { + var cause: Throwable? = e.cause + while (cause != null) { + if (cause is ProtocolException) return true + cause = cause.cause + } + return false +} + +private fun defaultRefreshAction(openMode: OpenMode): () -> Unit = + { + val omhAuthClient = OMHClientHelper.getAuthClient(openMode) + omhAuthClient.getCredentials().blockingRefreshAccessToken() + } + +/** + * Blocking version of [retryOnUnauthorized], for better interoperability with Java. + * + * This calls [retryOnUnauthorizedBlocking()] with default maxRetries of 2 + * and default refreshAction. + */ +fun retryOnUnauthorizedBlocking( + openMode: OpenMode, + trigger: AuthTrigger, + action: () -> T, +): T = + runBlocking { + retryOnUnauthorized( + openMode, + 2, + trigger, + defaultRefreshAction(openMode), + ) { action() } + } + +/** + * Blocking version of [retryOnUnauthorized], for better interoperability with Java. + */ +fun retryOnUnauthorizedBlocking( + openMode: OpenMode, + maxRetries: Int = 2, + trigger: AuthTrigger, + refreshAction: () -> Unit = defaultRefreshAction(openMode), + action: () -> T, +): T = + runBlocking { + retryOnUnauthorized( + openMode, + maxRetries, + trigger, + refreshAction, + ) { action() } + } + +/** + * Retries the given [action] up to [maxRetries] times if it throws a [CloudPluginException] + * caused by an unauthorized access (e.g., expired token). It attempts to refresh the access token + * using the provided [refreshAction] and triggers re-authentication via [trigger] if necessary. + * + * @param openMode The cloud service to authenticate with + * @param maxRetries The maximum number of retry attempts (default is 2) + * @param trigger The [AuthTrigger] to invoke for re-authentication + * @param refreshAction The action to refresh the access token (default implementation provided) + * @param action The suspend function to execute that may throw [CloudPluginException] + * @return The result of the successful [action] + * @throws CloudPluginException if all retry attempts fail + */ +@Suppress("LongMethod", "TooGenericExceptionCaught") +suspend fun retryOnUnauthorized( + openMode: OpenMode, + maxRetries: Int = 2, + trigger: AuthTrigger, + refreshAction: () -> Unit = defaultRefreshAction(openMode), + action: suspend () -> T, +): T { + var attempt = 1 + var lastException: Exception? = null + + while (attempt <= maxRetries) { + try { + return action() + } catch (e: CancellationException) { + throw e + } catch (e: CloudPluginException) { + lastException = e + if (hasProtocolExceptionCause(e)) { + if (attempt == maxRetries) { + LOG.debug("Max attempts reached, attempting re-authentication") + + // Run auth on a completely separate thread, no coroutines involved + val authFuture = CompletableFuture() + authExecutor.execute { + try { + val result = trigger.triggerAuthBlocking(openMode) + authFuture.complete(result) + } catch (ex: Exception) { + authFuture.completeExceptionally(ex) + } + } + + val authSuccess = + try { + authFuture.get(120, TimeUnit.SECONDS) // 2 min timeout for browser auth + } catch (ex: Exception) { + LOG.warn("Auth trigger failed", ex) + false + } + + if (!authSuccess) { + throw e + } + return action() + } + + LOG.debug("Token unauthorized, attempting to refresh token (attempt $attempt/$maxRetries)") + // Run refresh on separate thread + val refreshFuture = CompletableFuture() + authExecutor.execute { + try { + refreshAction() + refreshFuture.complete(Unit) + } catch (ex: Exception) { + refreshFuture.completeExceptionally(ex) + } + } + refreshFuture.get(30, TimeUnit.SECONDS) + attempt++ + } else { + if (attempt == maxRetries) { + throw e // Don't wrap again, throw original exception + } + + LOG.debug("Network error, retrying operation (attempt $attempt/$maxRetries)") + delay(500L * attempt) // Exponential backoff + + attempt++ + } + } catch (e: Exception) { + LOG.warn("Unexpected exception in retryOnUnauthorized", e) + throw e + } + } + throw lastException ?: error("Unreachable code reached in retryOnUnauthorized") +} + +/** + * Borrowed from omh-storage, to resume an ongoing coroutine after access token is refreshed. + */ +@Suppress("TooGenericExceptionCaught") +fun OmhCredentials.blockingRefreshAccessToken(): String? { + val future = CompletableFuture() + + val cancellable = + refreshAccessToken() + .addOnSuccess { result -> future.complete(result) } + .addOnFailure { e -> future.completeExceptionally(e) } + .execute() + + return try { + future.get(30, TimeUnit.SECONDS) + } catch (e: Exception) { + cancellable.cancel() + throw e + } +} diff --git a/app/src/main/java/com/amaze/filemanager/utils/omh/OmhCredentialsWrapper.kt b/app/src/main/java/com/amaze/filemanager/utils/omh/OmhCredentialsWrapper.kt new file mode 100644 index 0000000000..e5709f87ef --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/omh/OmhCredentialsWrapper.kt @@ -0,0 +1,9 @@ +package com.amaze.filemanager.utils.omh + +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.openmobilehub.android.auth.core.OmhCredentials + +data class OmhCredentialsWrapper( + val openMode: OpenMode, + val credentials: OmhCredentials, +) diff --git a/app/src/main/java/com/amaze/filemanager/utils/omh/OmhStorageClientExt.kt b/app/src/main/java/com/amaze/filemanager/utils/omh/OmhStorageClientExt.kt new file mode 100644 index 0000000000..7f8e4b0c3c --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/utils/omh/OmhStorageClientExt.kt @@ -0,0 +1,111 @@ +package com.amaze.filemanager.utils.omh + +import com.openmobilehub.android.storage.core.OmhStorageClient +import com.openmobilehub.android.storage.core.model.OmhStorageEntity +import com.openmobilehub.android.storage.core.model.OmhStorageMetadata +import com.openmobilehub.android.storage.core.utils.folderSize +import kotlinx.coroutines.runBlocking +import java.io.ByteArrayOutputStream + +/** + * Blocking version of [OmhStorageClient.search]. + */ +fun OmhStorageClient.searchBlocking(query: String): List = runBlocking { search(query) } + +/** + * Blocking version of [OmhStorageClient.createFileWithExtension]. + */ +fun OmhStorageClient.createFileWithExtensionBlocking( + name: String, + extension: String, + parentId: String, +): OmhStorageEntity? = + runBlocking { + createFileWithExtension(name, extension, parentId) + } + +/** + * Blocking version of [OmhStorageClient.createFolder]. + */ +fun OmhStorageClient.createFolderBlocking( + name: String, + parentId: String, +): OmhStorageEntity? = + runBlocking { + createFolder(name, parentId) + } + +/** + * Blocking version of [OmhStorageClient.deleteFile]. + */ +fun OmhStorageClient.deleteFileBlocking(fileId: String) = + runBlocking { + deleteFile(fileId) + } + +/** + * Blocking version of [OmhStorageClient.permanentlyDeleteFile]. + */ +fun OmhStorageClient.permanentlyDeleteFileBlocking(id: String) = + runBlocking { + permanentlyDeleteFile(id) + } + +/** + * Blocking version of [OmhStorageClient.downloadFile]. + */ +fun OmhStorageClient.downloadFileBlocking(fileId: String): ByteArrayOutputStream = + runBlocking { + downloadFile(fileId) + } + +/** + * Blocking version of [OmhStorageClient.downloadFileVersion]. + */ +fun OmhStorageClient.downloadFileVersionBlocking( + fileId: String, + versionId: String, +): ByteArrayOutputStream = + runBlocking { + downloadFileVersion(fileId, versionId) + } + +/** + * Blocking version of [OmhStorageClient.getFileMetadata]. + */ +fun OmhStorageClient.getFileMetadataBlocking(fileId: String): OmhStorageMetadata? = + runBlocking { + getFileMetadata(fileId) + } + +/** + * Blocking version of [OmhStorageClient.resolvePath]. + */ +fun OmhStorageClient.resolvePathBlocking(path: String): OmhStorageEntity? = + runBlocking { + resolvePath(path) + } + +/** + * Blocking version of [OmhStorageClient.folderSize]. + */ +fun OmhStorageClient.folderSizeBlocking(folderId: String): Long = + runBlocking { + folderSize(folderId) + } + +/** + * Blocking version of [OmhStorageClient.getStorageUsage]. + */ +fun OmhStorageClient.getStorageUsageBlocking(): Long = + runBlocking { + getStorageUsage() + } + +/** + * Blocking version of [OmhStorageClient.getStorageQuota]. + */ +fun OmhStorageClient.getStorageQuotaBlocking(): Long = + runBlocking { + getStorageQuota() + } diff --git a/app/src/main/res/layout/drawerheader.xml b/app/src/main/res/layout/drawerheader.xml index e91fd51be6..85600c9f32 100644 --- a/app/src/main/res/layout/drawerheader.xml +++ b/app/src/main/res/layout/drawerheader.xml @@ -18,7 +18,7 @@ android:minHeight="@dimen/drawer_header_height" android:layout_alignParentStart="true" android:layout_alignParentLeft="true"/> - Cleanup interval Trigger auto-cleanup interval (hours) File Deletion + https + auth.teamamaze.xyz + /filemanager/microsoft/oauth2/callback + https + auth.teamamaze.xyz + /filemanager/dropbox/oauth2/callback + https + auth.teamamaze.xyz + /filemanager/google/oauth2/callback diff --git a/app/src/play/java/com/amaze/filemanager/asynchronous/asynctasks/CloudLoaderAsyncTask.java b/app/src/play/java/com/amaze/filemanager/asynchronous/asynctasks/CloudLoaderAsyncTask.java deleted file mode 100644 index 83adc97e31..0000000000 --- a/app/src/play/java/com/amaze/filemanager/asynchronous/asynctasks/CloudLoaderAsyncTask.java +++ /dev/null @@ -1,404 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.asynchronous.asynctasks; - -import java.lang.ref.WeakReference; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.amaze.filemanager.R; -import com.amaze.filemanager.application.AppConfig; -import com.amaze.filemanager.database.CloudHandler; -import com.amaze.filemanager.database.models.explorer.CloudEntry; -import com.amaze.filemanager.fileoperations.exceptions.CloudPluginException; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; -import com.amaze.filemanager.ui.activities.MainActivity; -import com.amaze.filemanager.utils.DataUtils; -import com.cloudrail.si.CloudRail; -import com.cloudrail.si.exceptions.AuthenticationException; -import com.cloudrail.si.exceptions.ParseException; -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 android.database.Cursor; -import android.os.AsyncTask; -import android.widget.Toast; - -import androidx.annotation.NonNull; - -public class CloudLoaderAsyncTask extends AsyncTask { - - private static final Logger LOG = LoggerFactory.getLogger(CloudLoaderAsyncTask.class); - - private final Cursor data; - private final WeakReference mainActivity; - private final CloudHandler cloudHandler; - private final DataUtils dataUtils; - - public CloudLoaderAsyncTask(MainActivity mainActivity, CloudHandler cloudHandler, Cursor data) { - this.data = data; - this.mainActivity = new WeakReference<>(mainActivity); - this.cloudHandler = cloudHandler; - this.dataUtils = DataUtils.getInstance(); - } - - @Override - @NonNull - public Boolean doInBackground(Void... voids) { - boolean hasUpdatedDrawer = false; - - if (data == null) return false; - if (data.getCount() > 0 && data.moveToFirst()) { - do { - if (mainActivity.get() == null || isCancelled()) { - cancel(true); - return false; - } - - switch (data.getInt(0)) { - case 1: - try { - CloudRail.setAppKey(data.getString(1)); - } catch (Exception e) { - // any other exception due to network conditions or other error - LOG.warn("failed to set app key", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.failed_cloud_api_key); - } else { - cancel(true); - } - return false; - } - break; - case 2: - // DRIVE - try { - CloudEntry cloudEntryGdrive = null; - CloudEntry savedCloudEntryGdrive; - GoogleDrive cloudStorageDrive; - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - cloudStorageDrive = - new GoogleDrive( - mainActivity.getApplicationContext(), - data.getString(1), - "", - MainActivity.CLOUD_AUTHENTICATOR_REDIRECT_URI, - data.getString(2)); - } else { - cancel(true); - return false; - } - cloudStorageDrive.useAdvancedAuthentication(); - - if ((savedCloudEntryGdrive = cloudHandler.findEntry(OpenMode.GDRIVE)) != null) { - // we already have the entry and saved state, get it - - try { - cloudStorageDrive.loadAsString(savedCloudEntryGdrive.getPersistData().value); - } catch (ParseException e) { - LOG.warn("failed to load cloud storage connection", e); - // we need to update the persist string as existing one is been compromised - - cloudStorageDrive.login(); - cloudEntryGdrive = - new CloudEntry(OpenMode.GDRIVE, cloudStorageDrive.saveAsString()); - cloudHandler.updateEntry(OpenMode.GDRIVE, cloudEntryGdrive); - } - } else { - cloudStorageDrive.login(); - cloudEntryGdrive = - new CloudEntry(OpenMode.GDRIVE, cloudStorageDrive.saveAsString()); - cloudHandler.addEntry(cloudEntryGdrive); - } - - dataUtils.addAccount(cloudStorageDrive); - hasUpdatedDrawer = true; - } catch (CloudPluginException e) { - LOG.warn("failed to find cloud entry", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.cloud_error_plugin); - mainActivity.deleteConnection(OpenMode.GDRIVE); - } else { - cancel(true); - } - return false; - } catch (AuthenticationException e) { - LOG.warn("failed to authenticate cloud connection", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.cloud_fail_authenticate); - mainActivity.deleteConnection(OpenMode.GDRIVE); - } else { - cancel(true); - } - return false; - } catch (Exception e) { - // any other exception due to network conditions or other error - LOG.warn("Failed to load cloud conn due to network conditions or other error", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.failed_cloud_new_connection); - mainActivity.deleteConnection(OpenMode.GDRIVE); - } else { - cancel(true); - } - return false; - } - break; - case 3: - // DROPBOX - try { - CloudEntry cloudEntryDropbox = null; - CloudEntry savedCloudEntryDropbox; - CloudStorage cloudStorageDropbox; - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - cloudStorageDropbox = - new Dropbox( - mainActivity.getApplicationContext(), data.getString(1), data.getString(2)); - } else { - cancel(true); - return false; - } - - if ((savedCloudEntryDropbox = cloudHandler.findEntry(OpenMode.DROPBOX)) != null) { - // we already have the entry and saved state, get it - try { - cloudStorageDropbox.loadAsString(savedCloudEntryDropbox.getPersistData().value); - } catch (ParseException e) { - LOG.warn("failed to load cloud storage connection", e); - // we need to persist data again - - cloudStorageDropbox.login(); - cloudEntryDropbox = - new CloudEntry(OpenMode.DROPBOX, cloudStorageDropbox.saveAsString()); - cloudHandler.updateEntry(OpenMode.DROPBOX, cloudEntryDropbox); - } - } else { - cloudStorageDropbox.login(); - cloudEntryDropbox = - new CloudEntry(OpenMode.DROPBOX, cloudStorageDropbox.saveAsString()); - cloudHandler.addEntry(cloudEntryDropbox); - } - - dataUtils.addAccount(cloudStorageDropbox); - hasUpdatedDrawer = true; - } catch (CloudPluginException e) { - LOG.warn("failed to find cloud entry", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.cloud_error_plugin); - mainActivity.deleteConnection(OpenMode.DROPBOX); - } else { - cancel(true); - } - return false; - } catch (AuthenticationException e) { - LOG.warn("failed to authenticate cloud connection", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.cloud_fail_authenticate); - mainActivity.deleteConnection(OpenMode.DROPBOX); - } else cancel(true); - return false; - } catch (Exception e) { - // any other exception due to network conditions or other error - LOG.warn("Failed to load cloud conn due to network conditions or other error", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.failed_cloud_new_connection); - mainActivity.deleteConnection(OpenMode.DROPBOX); - } else cancel(true); - return false; - } - break; - case 4: - // BOX - try { - CloudEntry cloudEntryBox = null; - CloudEntry savedCloudEntryBox; - CloudStorage cloudStorageBox; - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - cloudStorageBox = - new Box( - mainActivity.getApplicationContext(), data.getString(1), data.getString(2)); - } else { - cancel(true); - return false; - } - - if ((savedCloudEntryBox = cloudHandler.findEntry(OpenMode.BOX)) != null) { - // we already have the entry and saved state, get it - try { - cloudStorageBox.loadAsString(savedCloudEntryBox.getPersistData().value); - } catch (ParseException e) { - LOG.warn("failed to load cloud storage connection", e); - // we need to persist data again - cloudStorageBox.login(); - cloudEntryBox = new CloudEntry(OpenMode.BOX, cloudStorageBox.saveAsString()); - cloudHandler.updateEntry(OpenMode.BOX, cloudEntryBox); - } - } else { - cloudStorageBox.login(); - cloudEntryBox = new CloudEntry(OpenMode.BOX, cloudStorageBox.saveAsString()); - cloudHandler.addEntry(cloudEntryBox); - } - - dataUtils.addAccount(cloudStorageBox); - hasUpdatedDrawer = true; - } catch (CloudPluginException e) { - LOG.warn("failed to find cloud entry", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.cloud_error_plugin); - mainActivity.deleteConnection(OpenMode.BOX); - } else cancel(true); - return false; - } catch (AuthenticationException e) { - LOG.warn("failed to authenticate cloud connection", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.cloud_fail_authenticate); - mainActivity.deleteConnection(OpenMode.BOX); - } else cancel(true); - return false; - } catch (Exception e) { - // any other exception due to network conditions or other error - LOG.warn("Failed to load cloud conn due to network conditions or other error", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.failed_cloud_new_connection); - mainActivity.deleteConnection(OpenMode.BOX); - } else cancel(true); - return false; - } - break; - case 5: - // ONEDRIVE - try { - CloudEntry cloudEntryOnedrive = null; - CloudEntry savedCloudEntryOnedrive; - CloudStorage cloudStorageOnedrive; - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - cloudStorageOnedrive = - new OneDrive( - mainActivity.getApplicationContext(), data.getString(1), data.getString(2)); - } else { - cancel(true); - return false; - } - - if ((savedCloudEntryOnedrive = cloudHandler.findEntry(OpenMode.ONEDRIVE)) != null) { - // we already have the entry and saved state, get it - try { - cloudStorageOnedrive.loadAsString(savedCloudEntryOnedrive.getPersistData().value); - } catch (ParseException e) { - LOG.warn("failed to load cloud storage connection", e); - // we need to persist data again - - cloudStorageOnedrive.login(); - cloudEntryOnedrive = - new CloudEntry(OpenMode.ONEDRIVE, cloudStorageOnedrive.saveAsString()); - cloudHandler.updateEntry(OpenMode.ONEDRIVE, cloudEntryOnedrive); - } - } else { - cloudStorageOnedrive.login(); - cloudEntryOnedrive = - new CloudEntry(OpenMode.ONEDRIVE, cloudStorageOnedrive.saveAsString()); - cloudHandler.addEntry(cloudEntryOnedrive); - } - - dataUtils.addAccount(cloudStorageOnedrive); - hasUpdatedDrawer = true; - } catch (CloudPluginException e) { - LOG.warn("failed to find cloud entry", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.cloud_error_plugin); - mainActivity.deleteConnection(OpenMode.ONEDRIVE); - } else cancel(true); - return false; - } catch (AuthenticationException e) { - LOG.warn("failed to authenticate cloud connection", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.cloud_fail_authenticate); - mainActivity.deleteConnection(OpenMode.ONEDRIVE); - } else cancel(true); - return false; - } catch (Exception e) { - // any other exception due to network conditions or other error - LOG.warn("Failed to load cloud conn due to network conditions or other error", e); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - AppConfig.toast(mainActivity, R.string.failed_cloud_new_connection); - mainActivity.deleteConnection(OpenMode.ONEDRIVE); - } else cancel(true); - return false; - } - break; - default: - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - Toast.makeText(mainActivity, R.string.cloud_error_failed_restart, Toast.LENGTH_LONG) - .show(); - } else cancel(true); - return false; - } - } while (data.moveToNext()); - } - return hasUpdatedDrawer; - } - - @Override - protected void onCancelled() { - super.onCancelled(); - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - mainActivity - .getSupportLoaderManager() - .destroyLoader(MainActivity.REQUEST_CODE_CLOUD_LIST_KEY); - mainActivity - .getSupportLoaderManager() - .destroyLoader(MainActivity.REQUEST_CODE_CLOUD_LIST_KEYS); - } - } - - @Override - public void onPostExecute(@NonNull Boolean result) { - if (result) { - final MainActivity mainActivity = this.mainActivity.get(); - if (mainActivity != null) { - mainActivity.getDrawer().refreshDrawer(); - mainActivity.invalidateFragmentAndBundle(null, true); - } - } - } -} diff --git a/app/src/play/java/com/amaze/filemanager/asynchronous/asynctasks/cloud/CloudLoaderCallable.kt b/app/src/play/java/com/amaze/filemanager/asynchronous/asynctasks/cloud/CloudLoaderCallable.kt new file mode 100644 index 0000000000..7718eb2303 --- /dev/null +++ b/app/src/play/java/com/amaze/filemanager/asynchronous/asynctasks/cloud/CloudLoaderCallable.kt @@ -0,0 +1,70 @@ +package com.amaze.filemanager.asynchronous.asynctasks.cloud + +import android.database.Cursor +import com.amaze.filemanager.R +import com.amaze.filemanager.application.AppConfig +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.ui.activities.MainActivity +import com.amaze.filemanager.utils.DataUtils +import com.amaze.filemanager.utils.omh.OMHClientHelper +import com.amaze.filemanager.utils.omh.OmhCredentialsWrapper +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.lang.ref.WeakReference +import java.util.concurrent.Callable + +class CloudLoaderCallable( + mainActivity: MainActivity, + private val data: Cursor?, +) : Callable { + private val mainActivity: WeakReference = WeakReference(mainActivity) + + companion object { + @JvmStatic + private val LOG: Logger = LoggerFactory.getLogger(CloudLoaderCallable::class.java) + } + + override fun call(): Boolean { + var hasUpdatedDrawer = false + if (data == null) { + return false + } else if (data.count > 0 && data.moveToFirst()) { + do { + val v = data.getInt(0) + when (v) { + 1 -> Unit + 2, 3, 4 -> { + val openMode = + when (v) { + 2 -> OpenMode.GDRIVE + 3 -> OpenMode.DROPBOX + else -> OpenMode.ONEDRIVE + } + val authClient = + OMHClientHelper.getAuthClient( + openMode, + data.getString(1), + ) + val credentials = authClient.getCredentials() + if (credentials.accessToken != null) { + DataUtils.addAccount( + OmhCredentialsWrapper( + openMode, + credentials, + ), + ) + hasUpdatedDrawer = true + } else { + mainActivity.get()?.deleteCloudConnection(openMode) + } + } + else -> { + AppConfig.toast(mainActivity.get(), R.string.cloud_error_failed_restart) + return false + } + } + } while (data.moveToNext()) + } + return hasUpdatedDrawer + } +} diff --git a/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java b/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java index de57e3ea1b..0e714f6f2b 100644 --- a/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java +++ b/app/src/test/java/com/amaze/filemanager/application/AppConfigTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.application; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static android.os.Looper.getMainLooper; import static org.awaitility.Awaitility.await; @@ -51,7 +51,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) +@Config(sdk = {O, P, Build.VERSION_CODES.R}) public class AppConfigTest { @After 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 c77388920d..46fcec0402 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 @@ -23,7 +23,7 @@ package com.amaze.filemanager.asynchronous.asynctasks import android.Manifest import android.content.Context import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Looper import android.os.storage.StorageManager @@ -70,7 +70,7 @@ import org.robolectric.shadows.ShadowToast ShadowFileUtils::class, ShadowPasswordUtil::class, ], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) abstract class AbstractDeleteTaskTestBase { private var ctx: Context? = null diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java index 343e16b743..03fb707084 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/DbViewerTaskTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.asynchronous.asynctasks; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static android.os.Looper.getMainLooper; import static android.view.View.VISIBLE; @@ -58,7 +58,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) + sdk = {O, P, Build.VERSION_CODES.R}) public class DbViewerTaskTest { private WebView webView; diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt index ca6c8959f5..93092f94f0 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/AbstractCompressedHelperCallableTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.compress import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Environment import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -37,7 +37,7 @@ import java.io.FileOutputStream import java.util.TimeZone @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [LOLLIPOP, P, Build.VERSION_CODES.R]) +@Config(shadows = [ShadowMultiDex::class], sdk = [O, P, Build.VERSION_CODES.R]) abstract class AbstractCompressedHelperCallableTest { private lateinit var systemTz: TimeZone diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/CompressedHelperForBadArchiveTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/CompressedHelperForBadArchiveTest.kt index 335f975c18..9cff971fe2 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/CompressedHelperForBadArchiveTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/compress/CompressedHelperForBadArchiveTest.kt @@ -20,7 +20,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.compress -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Environment import androidx.test.core.app.ApplicationProvider @@ -42,7 +42,7 @@ import java.io.FileOutputStream * Test behaviour of CompressedHelpers in handling corrupt archives. */ @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [LOLLIPOP, P]) +@Config(shadows = [ShadowMultiDex::class], sdk = [O, P]) class CompressedHelperForBadArchiveTest { /** * Test handling of corrupt archive with random junk. diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResultListSorterTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResultListSorterTest.kt index 097eeb34fd..e53ce37689 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResultListSorterTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResultListSorterTest.kt @@ -41,7 +41,7 @@ import java.util.regex.Pattern @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.P, Build.VERSION_CODES.R], + sdk = [Build.VERSION_CODES.O, Build.VERSION_CODES.P, Build.VERSION_CODES.R], ) @Suppress("StringLiteralDuplication", "ComplexMethod", "LongMethod", "LargeClass") class SearchResultListSorterTest { @@ -338,6 +338,7 @@ class SearchResultListSorterTest { val file1 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title1, "C:\\AmazeFileManager\\abc-efg", "user", @@ -353,6 +354,7 @@ class SearchResultListSorterTest { val file2 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title2, "C:\\AmazeFileManager\\ABCD-FG", "user", @@ -401,6 +403,7 @@ class SearchResultListSorterTest { val file1 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title1, "C:\\AmazeFileManager\\abcdefg", "user", @@ -416,6 +419,7 @@ class SearchResultListSorterTest { val file2 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title2, "C:\\AmazeFileManager\\ABC_EFG", "user", @@ -465,6 +469,7 @@ class SearchResultListSorterTest { val file1 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title1, "C:\\AmazeFileManager\\abcdefg", "user", @@ -480,6 +485,7 @@ class SearchResultListSorterTest { val file2 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title2, "C:\\AmazeFileManager\\ABC EFG", "user", @@ -529,6 +535,7 @@ class SearchResultListSorterTest { val file1 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title1, "C:\\AmazeFileManager\\abcdefg", "user", @@ -544,6 +551,7 @@ class SearchResultListSorterTest { val file2 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title2, "C:\\AmazeFileManager\\ABC.EFG", "user", @@ -594,6 +602,7 @@ class SearchResultListSorterTest { val file1 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title1, "C:\\AmazeFileManager\\abc.efg", "user", @@ -609,6 +618,7 @@ class SearchResultListSorterTest { val file2 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title2, "C:\\AmazeFileManager\\ABC_EFG", "user", @@ -659,6 +669,7 @@ class SearchResultListSorterTest { val file1 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title1, "C:\\AmazeFileManager\\abc.efg", "user", @@ -674,6 +685,7 @@ class SearchResultListSorterTest { val file2 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title2, "C:\\AmazeFileManager\\ABC_EFG", "user", @@ -723,6 +735,7 @@ class SearchResultListSorterTest { val file1 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title1, "C:\\AmazeFileManager\\abc.efg", "user", @@ -738,6 +751,7 @@ class SearchResultListSorterTest { val file2 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title2, "C:\\AmazeFileManager\\ABC_EFG", "user", @@ -793,6 +807,7 @@ class SearchResultListSorterTest { val file1 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title1, "C:\\AmazeFileManager\\abc.efghij", "user", @@ -812,6 +827,7 @@ class SearchResultListSorterTest { val file2 = LayoutElementParcelable( ApplicationProvider.getApplicationContext(), + "", title2, "C:\\AmazeFileManager\\EFGABC", "user", diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt index 1cfc834af1..5ac86e666f 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableEd25519Test.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.ssh import android.os.Build.VERSION_CODES -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.shadows.ShadowFileUtils @@ -45,7 +45,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowTabHandler::class, ShadowFileUtils::class], - sdk = [LOLLIPOP, P, VERSION_CODES.R], + sdk = [O, P, VERSION_CODES.R], ) class PemToKeyPairObservableEd25519Test { companion object { diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt index 3590c52573..3c82e82d69 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/PemToKeyPairObservableRsaTest.kt @@ -22,8 +22,8 @@ package com.amaze.filemanager.asynchronous.asynctasks.ssh import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES -import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Build.VERSION_CODES.N +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.appcompat.widget.AppCompatEditText import androidx.lifecycle.Lifecycle @@ -65,7 +65,7 @@ import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowTabHandler::class, ShadowFileUtils::class], - sdk = [LOLLIPOP, P, VERSION_CODES.R], + sdk = [O, P, VERSION_CODES.R], ) class PemToKeyPairObservableRsaTest { companion object { diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt index a176c0fdd6..730e81b306 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/ssh/SshAuthenticationTaskTest.kt @@ -22,7 +22,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.ssh import android.content.Context import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -66,7 +66,7 @@ import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) @Suppress("StringLiteralDuplication") class SshAuthenticationTaskTest { diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt index f120e7f542..cd31738d99 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/read/ReadTextFileCallableTest.kt @@ -23,7 +23,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.texteditor.read import android.content.Context import android.net.Uri import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -46,7 +46,7 @@ import kotlin.random.Random @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) class ReadTextFileCallableTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java index 8fcc850e5d..ddddf94063 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/texteditor/write/WriteTextFileCallableTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.asynchronous.asynctasks.texteditor.write; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -65,7 +65,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class, ShadowContentResolver.class}, - sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) + sdk = {O, P, Build.VERSION_CODES.R}) public class WriteTextFileCallableTest { private static final String contents = "This is modified data"; diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/DecryptServiceTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/DecryptServiceTest.kt index eca0bf4fd4..025c0cd287 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/DecryptServiceTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/DecryptServiceTest.kt @@ -25,8 +25,8 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Build.VERSION_CODES.M +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Environment import android.util.Log @@ -64,7 +64,7 @@ import java.util.concurrent.TimeUnit import kotlin.random.Random @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [LOLLIPOP, P, Build.VERSION_CODES.R]) +@Config(shadows = [ShadowMultiDex::class], sdk = [O, P, Build.VERSION_CODES.R]) @Suppress("StringLiteralDuplication") class DecryptServiceTest { private lateinit var source: ByteArray diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt index 7443d72e6d..55072aa148 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/EncryptServiceTest.kt @@ -25,8 +25,8 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Build.VERSION_CODES.M +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Environment import android.util.Log @@ -68,7 +68,7 @@ import java.util.concurrent.TimeUnit import kotlin.random.Random @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [LOLLIPOP, P, Build.VERSION_CODES.R]) +@Config(shadows = [ShadowMultiDex::class], sdk = [O, P, Build.VERSION_CODES.R]) class EncryptServiceTest { private lateinit var service: EncryptService private lateinit var notificationManager: ShadowNotificationManager @@ -128,7 +128,9 @@ class EncryptServiceTest { assertTrue(notificationManager.activeNotifications.isNotEmpty()) notificationManager.activeNotifications.first().let { assertEquals(NotificationConstants.ENCRYPT_ID, it.id) - assertEquals(NotificationConstants.CHANNEL_NORMAL_ID, it.notification.channelId) + if (SDK_INT >= O) { + assertEquals(NotificationConstants.CHANNEL_NORMAL_ID, it.notification.channelId) + } } await().atMost(10, TimeUnit.SECONDS).until { targetFile.length() > 0 && notificationManager.activeNotifications.isEmpty() @@ -171,7 +173,9 @@ class EncryptServiceTest { assertTrue(notificationManager.activeNotifications.isNotEmpty()) notificationManager.activeNotifications.first().let { assertEquals(NotificationConstants.ENCRYPT_ID, it.id) - assertEquals(NotificationConstants.CHANNEL_NORMAL_ID, it.notification.channelId) + if (SDK_INT >= O) { + assertEquals(NotificationConstants.CHANNEL_NORMAL_ID, it.notification.channelId) + } } await().atMost(10, TimeUnit.SECONDS).until { targetFile.length() > 0 && notificationManager.activeNotifications.isEmpty() diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/FtpServiceAndroidFileSystemIntegrationTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/FtpServiceAndroidFileSystemIntegrationTest.kt index 3dbfc48c0d..a12a1ef40f 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/FtpServiceAndroidFileSystemIntegrationTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/FtpServiceAndroidFileSystemIntegrationTest.kt @@ -26,7 +26,7 @@ import android.net.NetworkInfo import android.net.Uri import android.net.wifi.WifiInfo import android.net.wifi.WifiManager -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Environment import androidx.preference.PreferenceManager import androidx.test.core.app.ApplicationProvider @@ -62,9 +62,9 @@ import java.io.FileOutputStream import java.net.InetAddress import kotlin.random.Random -@Ignore("Pending fix for testing against newer Androids") +@Ignore("FIXME - compatibility with higher Androids") @RunWith(AndroidJUnit4::class) -@Config(sdk = [LOLLIPOP], shadows = [ShadowMultiDex::class]) +@Config(sdk = [O], shadows = [ShadowMultiDex::class]) @LooperMode(LooperMode.Mode.PAUSED) @Suppress("StringLiteralDuplication") class FtpServiceAndroidFileSystemIntegrationTest { diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt index 9faa8029cf..a5a45372fc 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/ZipServiceTest.kt @@ -23,7 +23,7 @@ package com.amaze.filemanager.asynchronous.services import android.content.Context import android.content.Intent import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Looper.getMainLooper import androidx.test.core.app.ApplicationProvider @@ -59,7 +59,7 @@ import kotlin.random.Random @RunWith(RobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) -@Config(shadows = [ShadowMultiDex::class], sdk = [LOLLIPOP, P, Build.VERSION_CODES.R]) +@Config(shadows = [ShadowMultiDex::class], sdk = [O, P, Build.VERSION_CODES.R]) class ZipServiceTest { val dt = DateTimeFormatter.ofPattern("yyyyMMddkkmm") val dates = diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/services/ftp/FtpReceiverTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/services/ftp/FtpReceiverTest.kt index b08a421056..f08b096e10 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/services/ftp/FtpReceiverTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/services/ftp/FtpReceiverTest.kt @@ -22,7 +22,6 @@ package com.amaze.filemanager.asynchronous.services.ftp import android.content.Intent import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Build.VERSION_CODES.N import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P @@ -44,7 +43,7 @@ import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [LOLLIPOP, P, Build.VERSION_CODES.R]) +@Config(shadows = [ShadowMultiDex::class], sdk = [O, P, Build.VERSION_CODES.R]) @Suppress("StringLiteralDuplication") class FtpReceiverTest { private lateinit var receiver: FtpReceiver @@ -81,7 +80,7 @@ class FtpReceiverTest { * Test [Context.startService()] called for pre-Oreo Androids. */ @Test - @Config(sdk = [N]) + @Config(maxSdk = N) fun testStartServiceCalled() { val ctx = AppConfig.getInstance() val spy = spyk(ctx) diff --git a/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt b/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt index 69c1e48289..9abebee84f 100644 --- a/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/ExplorerDatabaseMigrationTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.database import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.room.Room import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory @@ -30,8 +30,6 @@ import androidx.test.platform.app.InstrumentationRegistry import com.amaze.filemanager.fileoperations.filesystem.OpenMode import com.amaze.filemanager.shadows.ShadowMultiDex import com.amaze.filemanager.test.ShadowPasswordUtil -import io.reactivex.schedulers.Schedulers -import org.junit.Assert import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -44,7 +42,7 @@ import java.io.IOException @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) @Suppress("StringLiteralDuplication", "ComplexMethod", "LongMethod") class ExplorerDatabaseMigrationTest { @@ -82,6 +80,7 @@ class ExplorerDatabaseMigrationTest { ExplorerDatabase.MIGRATION_8_9, ExplorerDatabase.MIGRATION_9_10, ExplorerDatabase.MIGRATION_10_11, + ExplorerDatabase.MIGRATION_11_12, ) .build() explorerDatabase.openHelper.writableDatabase @@ -109,6 +108,7 @@ class ExplorerDatabaseMigrationTest { ExplorerDatabase.MIGRATION_8_9, ExplorerDatabase.MIGRATION_9_10, ExplorerDatabase.MIGRATION_10_11, + ExplorerDatabase.MIGRATION_11_12, ) .build() explorerDatabase.openHelper.writableDatabase @@ -135,6 +135,7 @@ class ExplorerDatabaseMigrationTest { ExplorerDatabase.MIGRATION_8_9, ExplorerDatabase.MIGRATION_9_10, ExplorerDatabase.MIGRATION_10_11, + ExplorerDatabase.MIGRATION_11_12, ) .build() explorerDatabase.openHelper.writableDatabase @@ -212,41 +213,9 @@ class ExplorerDatabaseMigrationTest { ExplorerDatabase.MIGRATION_8_9, ExplorerDatabase.MIGRATION_9_10, ExplorerDatabase.MIGRATION_10_11, + ExplorerDatabase.MIGRATION_11_12, ).allowMainThreadQueries() .build() - explorerDatabase.openHelper.writableDatabase - var verify = - explorerDatabase - .cloudEntryDao() - .findByServiceType(OpenMode.GDRIVE.ordinal) - .subscribeOn(Schedulers.trampoline()) - .blockingGet() - Assert.assertEquals(1, verify.id.toLong()) - Assert.assertEquals("abcd", verify.persistData.toString()) - verify = - explorerDatabase - .cloudEntryDao() - .findByServiceType(OpenMode.BOX.ordinal) - .subscribeOn(Schedulers.trampoline()) - .blockingGet() - Assert.assertEquals(3, verify.id.toLong()) - Assert.assertEquals("ijkl", verify.persistData.toString()) - verify = - explorerDatabase - .cloudEntryDao() - .findByServiceType(OpenMode.DROPBOX.ordinal) - .subscribeOn(Schedulers.trampoline()) - .blockingGet() - Assert.assertEquals(2, verify.id.toLong()) - Assert.assertEquals("efgh", verify.persistData.toString()) - verify = - explorerDatabase - .cloudEntryDao() - .findByServiceType(OpenMode.ONEDRIVE.ordinal) - .subscribeOn(Schedulers.trampoline()) - .blockingGet() - Assert.assertEquals(4, verify.id.toLong()) - Assert.assertEquals("mnop", verify.persistData.toString()) explorerDatabase.close() } @@ -320,42 +289,10 @@ class ExplorerDatabaseMigrationTest { ExplorerDatabase.MIGRATION_8_9, ExplorerDatabase.MIGRATION_9_10, ExplorerDatabase.MIGRATION_10_11, + ExplorerDatabase.MIGRATION_11_12, ) .allowMainThreadQueries() .build() - explorerDatabase.openHelper.writableDatabase - var verify = - explorerDatabase - .cloudEntryDao() - .findByServiceType(OpenMode.GDRIVE.ordinal) - .subscribeOn(Schedulers.trampoline()) - .blockingGet() - Assert.assertEquals(1, verify.id.toLong()) - Assert.assertEquals("abcd", verify.persistData.toString()) - verify = - explorerDatabase - .cloudEntryDao() - .findByServiceType(OpenMode.BOX.ordinal) - .subscribeOn(Schedulers.trampoline()) - .blockingGet() - Assert.assertEquals(3, verify.id.toLong()) - Assert.assertEquals("ijkl", verify.persistData.toString()) - verify = - explorerDatabase - .cloudEntryDao() - .findByServiceType(OpenMode.DROPBOX.ordinal) - .subscribeOn(Schedulers.trampoline()) - .blockingGet() - Assert.assertEquals(2, verify.id.toLong()) - Assert.assertEquals("efgh", verify.persistData.toString()) - verify = - explorerDatabase - .cloudEntryDao() - .findByServiceType(OpenMode.ONEDRIVE.ordinal) - .subscribeOn(Schedulers.trampoline()) - .blockingGet() - Assert.assertEquals(4, verify.id.toLong()) - Assert.assertEquals("mnop", verify.persistData.toString()) explorerDatabase.close() } diff --git a/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt b/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt index f3167fcaf0..af54ea69be 100644 --- a/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/UtilitiesDatabaseMigrationTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.database import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.util.Base64 import androidx.room.Room @@ -52,7 +52,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) class UtilitiesDatabaseMigrationTest { companion object { diff --git a/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt b/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt index 7b8a5e433c..7cc602211a 100644 --- a/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/UtilsHandlerTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.database import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Environment import android.os.Environment.DIRECTORY_DCIM @@ -53,7 +53,7 @@ import java.io.File @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) class UtilsHandlerTest { companion object { diff --git a/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt b/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt index f5adf4c7bd..4389e2d402 100644 --- a/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt +++ b/app/src/test/java/com/amaze/filemanager/database/typeconverters/EncryptedStringTypeConverterTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.database.typeconverters import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.database.models.StringWrapper @@ -38,7 +38,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) class EncryptedStringTypeConverterTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/AbstractFilenameHelperIncrementNameTests.kt b/app/src/test/java/com/amaze/filemanager/filesystem/AbstractFilenameHelperIncrementNameTests.kt index f306e23d7a..c6ec359712 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/AbstractFilenameHelperIncrementNameTests.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/AbstractFilenameHelperIncrementNameTests.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.filesystem import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.application.AppConfig @@ -42,7 +42,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) @Suppress("StringLiteralDuplication") abstract class AbstractFilenameHelperIncrementNameTests { 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 1aafbc6a79..edf4c38677 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/AbstractOperationsTestBase.kt @@ -23,7 +23,7 @@ package com.amaze.filemanager.filesystem import android.Manifest import android.content.Context import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Looper import android.os.storage.StorageManager @@ -68,7 +68,7 @@ import org.robolectric.shadows.ShadowSQLiteConnection ShadowFileUtils::class, ShadowPasswordUtil::class, ], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) abstract class AbstractOperationsTestBase { private var ctx: Context? = null diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java index 32897ee00e..968438478d 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/EditableFileAbstractionTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.filesystem; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static com.amaze.filemanager.filesystem.EditableFileAbstraction.Scheme.CONTENT; import static com.amaze.filemanager.filesystem.EditableFileAbstraction.Scheme.FILE; @@ -49,7 +49,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) + sdk = {O, P, Build.VERSION_CODES.R}) public class EditableFileAbstractionTest { @Test(expected = IllegalArgumentException.class) diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt index 7d1f6267b0..6e4f220737 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/HybridFileTest.kt @@ -23,7 +23,7 @@ package com.amaze.filemanager.filesystem import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Environment import androidx.test.core.app.ApplicationProvider @@ -40,7 +40,7 @@ import java.net.URLDecoder import kotlin.random.Random @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [LOLLIPOP, P, Build.VERSION_CODES.R]) +@Config(shadows = [ShadowMultiDex::class], sdk = [O, P, Build.VERSION_CODES.R]) @Suppress("StringLiteralDuplication") class HybridFileTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java index 329e3107e3..ac8fe59fa4 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/OperationsTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.filesystem; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -45,7 +45,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) + sdk = {O, P, Build.VERSION_CODES.R}) public class OperationsTest { private File storageRoot = Environment.getExternalStorageDirectory(); diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java index f3004396db..54bb5a310a 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/RootHelperTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.filesystem; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.fail; @@ -51,7 +51,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) + sdk = {O, P, Build.VERSION_CODES.R}) @Ignore("FIXME: should not ignore - please implement a shadow") public class RootHelperTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/cloud/CloudUtilTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/cloud/CloudUtilTest.kt index 83078a553a..d6c5d1810b 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/cloud/CloudUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/cloud/CloudUtilTest.kt @@ -20,7 +20,7 @@ package com.amaze.filemanager.filesystem.cloud -import com.amaze.filemanager.database.CloudHandler +import com.amaze.filemanager.database.CloudContract import com.amaze.filemanager.fileoperations.filesystem.OpenMode import com.amaze.filemanager.fileoperations.filesystem.OpenMode.BOX import com.amaze.filemanager.fileoperations.filesystem.OpenMode.DROPBOX @@ -33,21 +33,21 @@ import kotlin.random.Random class CloudUtilTest { /** - * Tests [CloudUtil.stripPath] + * Tests [CloudUtil.stripCloudPath] */ @Test fun stripPathTest() { val assertForTest = { mode: OpenMode, path: String, completePath: String -> - Assert.assertEquals(path, CloudUtil.stripPath(mode, completePath)) + Assert.assertEquals(path, CloudUtil.stripCloudPath(mode, completePath)) } val generatePathForMode = { mode: OpenMode, path: String -> val prefix = when (mode) { - DROPBOX -> CloudHandler.CLOUD_PREFIX_DROPBOX - BOX -> CloudHandler.CLOUD_PREFIX_BOX - GDRIVE -> CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE - ONEDRIVE -> CloudHandler.CLOUD_PREFIX_ONE_DRIVE + DROPBOX -> CloudContract.CLOUD_PREFIX_DROPBOX + BOX -> CloudContract.CLOUD_PREFIX_BOX + GDRIVE -> CloudContract.CLOUD_PREFIX_GOOGLE_DRIVE + ONEDRIVE -> CloudContract.CLOUD_PREFIX_ONE_DRIVE else -> null } requireNotNull(prefix) @@ -60,7 +60,7 @@ class CloudUtilTest { val path = RandomPathGenerator.generateRandomPath(r, 50) val genAndStrip = { mode: OpenMode -> - CloudUtil.stripPath(mode, generatePathForMode(mode, path)) + CloudUtil.stripCloudPath(mode, generatePathForMode(mode, path)) } assertForTest(DROPBOX, path, genAndStrip(DROPBOX)) diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java index 1047900c01..e5069e3a94 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/B0rkenZipTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.filesystem.compressed; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static kotlin.io.ConstantsKt.DEFAULT_BUFFER_SIZE; import static org.junit.Assert.assertEquals; @@ -56,7 +56,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) + sdk = {O, P, Build.VERSION_CODES.R}) public class B0rkenZipTest { private File zipfile1 = new File(Environment.getExternalStorageDirectory(), "zip-slip.zip"); diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java index 8208508b35..58047cd394 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/CompressedHelperTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.filesystem.compressed; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -66,7 +66,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) + sdk = {O, P, Build.VERSION_CODES.R}) public class CompressedHelperTest { private Context context; diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt index 8e2e8e1c45..999bd40bed 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/AbstractExtractorTest.kt @@ -22,7 +22,7 @@ package com.amaze.filemanager.filesystem.compressed.extractcontents import android.content.Context import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Environment import androidx.test.core.app.ApplicationProvider @@ -50,7 +50,7 @@ import java.nio.file.Paths import java.util.TimeZone @RunWith(AndroidJUnit4::class) -@Config(shadows = [ShadowMultiDex::class], sdk = [LOLLIPOP, P, Build.VERSION_CODES.R]) +@Config(shadows = [ShadowMultiDex::class], sdk = [O, P, Build.VERSION_CODES.R]) abstract class AbstractExtractorTest { protected abstract fun extractorClass(): Class diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt index e2890ed677..6c1c11fbf6 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt @@ -43,7 +43,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.P, Build.VERSION_CODES.R], + sdk = [Build.VERSION_CODES.O, Build.VERSION_CODES.P, Build.VERSION_CODES.R], ) @Suppress("StringLiteralDuplication", "ComplexMethod", "LongMethod", "LargeClass") class FileListSorterTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt index a75c54e152..d07506c496 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileUtilsTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.filesystem.files import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.filesystem.files.FileUtils.getPathsInPath @@ -38,7 +38,7 @@ import java.util.TimeZone @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) -@Config(sdk = [LOLLIPOP, P, Build.VERSION_CODES.R]) +@Config(sdk = [O, P, Build.VERSION_CODES.R]) @Suppress("TooManyFunctions", "StringLiteralDuplication") class FileUtilsTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/DirSortByTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/DirSortByTest.kt index fb00c32fdb..a2f0e615ab 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/DirSortByTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/DirSortByTest.kt @@ -32,7 +32,7 @@ import kotlin.random.Random @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.P, Build.VERSION_CODES.R], + sdk = [Build.VERSION_CODES.O, Build.VERSION_CODES.P, Build.VERSION_CODES.R], ) class DirSortByTest { /** Tests if [DirSortBy.getDirSortBy] returns the correct [DirSortBy] corresponding to the given index */ diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/SortByTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/SortByTest.kt index a851d96afb..784dcced93 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/SortByTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/SortByTest.kt @@ -32,7 +32,7 @@ import kotlin.random.Random @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.P, Build.VERSION_CODES.R], + sdk = [Build.VERSION_CODES.O, Build.VERSION_CODES.P, Build.VERSION_CODES.R], ) class SortByTest { /** Tests if [SortBy.getSortBy] returns the correct [SortBy] corresponding to the given index */ diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/SortTypeTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/SortTypeTest.kt index ee9bae2172..6cd1469556 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/SortTypeTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/files/sort/SortTypeTest.kt @@ -31,7 +31,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.P, Build.VERSION_CODES.R], + sdk = [Build.VERSION_CODES.O, Build.VERSION_CODES.P, Build.VERSION_CODES.R], ) class SortTypeTest { /** Tests if the Int returned from [SortType.toDirectorySortInt] is as expected */ diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt index 35d51316ac..fddb848773 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftp/NetCopyClientConnectionPoolFtpTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.filesystem.ftp import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.application.AppConfig @@ -64,7 +64,7 @@ import kotlin.text.Charsets.UTF_8 @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) class NetCopyClientConnectionPoolFtpTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt index 6aed30ecb4..32cc3394aa 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ftpserver/commands/AbstractFtpserverCommandTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.filesystem.ftpserver.commands import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.shadows.ShadowMultiDex @@ -38,7 +38,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) abstract class AbstractFtpserverCommandTest { protected lateinit var logger: LogMessageFilter diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt index b00c868784..1db6514e0d 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest.kt @@ -22,7 +22,7 @@ package com.amaze.filemanager.filesystem.root import android.content.SharedPreferences import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.preference.PreferenceManager import androidx.test.core.app.ApplicationProvider @@ -54,7 +54,7 @@ import java.io.InputStreamReader @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowNativeOperations::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) class ListFilesCommandTest { private val sharedPreferences: SharedPreferences = diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt index 6b828df226..1f99babc8d 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/root/ListFilesCommandTest2.kt @@ -22,7 +22,7 @@ package com.amaze.filemanager.filesystem.root import android.content.SharedPreferences import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.preference.PreferenceManager import androidx.test.core.app.ApplicationProvider @@ -54,7 +54,7 @@ import java.io.InputStreamReader @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowNativeOperations::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) class ListFilesCommandTest2 { private val sharedPreferences: SharedPreferences = diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java b/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java index bb83e42eb4..8f8f3875c8 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/smb/CifsContextsTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.filesystem.smb; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -44,7 +44,7 @@ import jcifs.ResolverType; import jcifs.context.BaseContext; -@Config(sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) +@Config(sdk = {O, P, Build.VERSION_CODES.R}) @RunWith(AndroidJUnit4.class) public class CifsContextsTest { 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 e6995e4ef4..e6464d90d6 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 @@ -22,7 +22,7 @@ package com.amaze.filemanager.filesystem.smb import android.content.Context import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -45,7 +45,7 @@ import org.robolectric.shadows.ShadowSQLiteConnection @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowSmbUtil::class, ShadowMultiDex::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) @LooperMode(LooperMode.Mode.PAUSED) class SmbHybridFileTest { diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt index a7cce7c3f1..9398b99ad4 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/AbstractSftpServerTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.filesystem.ssh import android.os.Build.VERSION_CODES -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Environment import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -65,7 +65,7 @@ import kotlin.text.Charsets.UTF_8 @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [LOLLIPOP, P, VERSION_CODES.R], + sdk = [O, P, VERSION_CODES.R], ) abstract class AbstractSftpServerTest { protected var encryptedPassword: String? = diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/FilesOnSshdTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/FilesOnSshdTest.kt index 076e7149ee..bccde8a639 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/FilesOnSshdTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/FilesOnSshdTest.kt @@ -25,9 +25,7 @@ import androidx.test.core.app.ApplicationProvider import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.fileoperations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFile -import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.test.randomBytes -import com.amaze.filemanager.utils.OnFileFound import org.awaitility.Awaitility.await import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers @@ -125,13 +123,10 @@ class FilesOnSshdTest : AbstractSftpServerTest() { file.forEachChildrenFile( ApplicationProvider.getApplicationContext(), false, - object : OnFileFound { - override fun onFileFound(fileFound: HybridFileParcelable) { - assertTrue("${fileFound.path} not seen as directory", fileFound.isDirectory) - result.add(fileFound.name) - } - }, - ) + ) { fileFound -> + assertTrue("${fileFound.path} not seen as directory", fileFound.isDirectory) + result.add(fileFound.name) + } await().until { result.size == 8 } assertThat>( result, @@ -150,12 +145,9 @@ class FilesOnSshdTest : AbstractSftpServerTest() { file.forEachChildrenFile( ApplicationProvider.getApplicationContext(), false, - object : OnFileFound { - override fun onFileFound(fileFound: HybridFileParcelable) { - result.add(fileFound.name) - } - }, - ) + ) { fileFound -> + result.add(fileFound.name) + } await().atMost(90, TimeUnit.SECONDS).until { result.size == 2 } assertThat>( result, @@ -170,12 +162,9 @@ class FilesOnSshdTest : AbstractSftpServerTest() { file.forEachChildrenFile( ApplicationProvider.getApplicationContext(), false, - object : OnFileFound { - override fun onFileFound(fileFound: HybridFileParcelable) { - result.add(fileFound.name) - } - }, - ) + ) { fileFound -> + result.add(fileFound.name) + } await().until { result.size == 1 } assertThat>( result, @@ -190,12 +179,9 @@ class FilesOnSshdTest : AbstractSftpServerTest() { file.forEachChildrenFile( ApplicationProvider.getApplicationContext(), false, - object : OnFileFound { - override fun onFileFound(fileFound: HybridFileParcelable) { - result.add(fileFound.name) - } - }, - ) + ) { fileFound -> + result.add(fileFound.name) + } await().until { result.size == 1 } assertThat>( result, @@ -234,24 +220,21 @@ class FilesOnSshdTest : AbstractSftpServerTest() { file.forEachChildrenFile( ApplicationProvider.getApplicationContext(), false, - object : OnFileFound { - override fun onFileFound(fileFound: HybridFileParcelable) { - if (!fileFound.name.endsWith(".txt")) { - assertTrue( - fileFound.path + " not seen as directory", - fileFound.isDirectory, - ) - dirs.add(fileFound.name) - } else { - assertFalse( - fileFound.path + " not seen as file", - fileFound.isDirectory, - ) - files.add(fileFound.name) - } - } - }, - ) + ) { fileFound -> + if (!fileFound.name.endsWith(".txt")) { + assertTrue( + fileFound.path + " not seen as directory", + fileFound.isDirectory, + ) + dirs.add(fileFound.name) + } else { + assertFalse( + fileFound.path + " not seen as file", + fileFound.isDirectory, + ) + files.add(fileFound.name) + } + } await().until { dirs.size == 8 } assertThat>( dirs, @@ -311,13 +294,10 @@ class FilesOnSshdTest : AbstractSftpServerTest() { file.forEachChildrenFile( ApplicationProvider.getApplicationContext(), false, - object : OnFileFound { - override fun onFileFound(fileFound: HybridFileParcelable) { - assertFalse("${fileFound.path} not seen as directory", fileFound.isDirectory) - result.add(fileFound.name) - } - }, - ) + ) { fileFound -> + assertFalse("${fileFound.path} not seen as directory", fileFound.isDirectory) + result.add(fileFound.name) + } await().until { result.size == 4 } assertThat>( result, @@ -329,13 +309,10 @@ class FilesOnSshdTest : AbstractSftpServerTest() { file.forEachChildrenFile( ApplicationProvider.getApplicationContext(), false, - object : OnFileFound { - override fun onFileFound(fileFound: HybridFileParcelable) { - assertTrue("${fileFound.path} not seen as directory", fileFound.isDirectory) - result2.add(fileFound.name) - } - }, - ) + ) { fileFound -> + assertTrue("${fileFound.path} not seen as directory", fileFound.isDirectory) + result2.add(fileFound.name) + } await().until { result2.size == 8 } assertThat>( result2, diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt index 27b8e5c406..01f4a94546 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/NetCopyClientConnectionPoolSshTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.filesystem.ssh import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.application.AppConfig @@ -75,7 +75,7 @@ import kotlin.text.Charsets.UTF_8 @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) class NetCopyClientConnectionPoolSshTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt index 5c10d396a8..b55950cfa4 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/ssh/SshHybridFileTest.kt @@ -22,7 +22,7 @@ package com.amaze.filemanager.filesystem.ssh import android.content.Context import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -46,7 +46,7 @@ import org.robolectric.annotation.LooperMode @LooperMode(LooperMode.Mode.PAUSED) @Config( shadows = [ShadowMultiDex::class, ShadowPasswordUtil::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) class SshHybridFileTest { private var ctx: Context? = null diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/usb/ReflectionHelpers.java b/app/src/test/java/com/amaze/filemanager/filesystem/usb/ReflectionHelpers.java index f6d8a958b5..a44f3257b2 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/usb/ReflectionHelpers.java +++ b/app/src/test/java/com/amaze/filemanager/filesystem/usb/ReflectionHelpers.java @@ -107,7 +107,7 @@ static void configureUsbConfiguration(UsbConfiguration usbConfiguration) configureMethod.invoke(usbConfiguration, (Object) new Parcelable[] {usbInterface}); } - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) static Parcelable[] configureUsbDevice() throws ClassNotFoundException, NoSuchMethodException, @@ -207,7 +207,7 @@ static UsbDevice callUsbDeviceConstructor( serialNumber); } - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) static UsbDevice callUsbDeviceConstructor( @NonNull String name, int vendorId, @@ -271,7 +271,7 @@ static UsbInterface callUsbInterfaceConstructor( return constructor.newInstance(id, alternateSetting, name, usbClass, subClass, protocol); } - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M) static UsbInterface callUsbInterfaceConstructor( int id, int usbClass, int subClass, int protocol, @Nullable Parcelable[] endpoints) throws ClassNotFoundException, diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt b/app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt index 307828c771..5d2677ae11 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/AbstractMainActivityTestBase.kt @@ -24,7 +24,7 @@ import android.Manifest import android.content.Context import android.os.Build import android.os.Build.VERSION_CODES -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.storage.StorageManager import androidx.annotation.NonNull @@ -54,7 +54,7 @@ import org.robolectric.shadows.ShadowStorageManager */ @RunWith(AndroidJUnit4::class) @Config( - sdk = [LOLLIPOP, P, VERSION_CODES.R], + sdk = [O, P, VERSION_CODES.R], shadows = [ ShadowMultiDex::class, ShadowStorageManager::class, diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivityTest.kt b/app/src/test/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivityTest.kt index 55a7909931..d0000e59fc 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivityTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/DatabaseViewerActivityTest.kt @@ -2,7 +2,7 @@ package com.amaze.filemanager.ui.activities import android.content.Intent import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.shadows.ShadowMultiDex @@ -23,7 +23,7 @@ import java.util.concurrent.TimeUnit */ @RunWith(AndroidJUnit4::class) @Config( - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], shadows = [ShadowMultiDex::class, ShadowStorageManager::class], ) class DatabaseViewerActivityTest { diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityAuthTriggerTest.kt b/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityAuthTriggerTest.kt new file mode 100644 index 0000000000..4c2c84fe60 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/MainActivityAuthTriggerTest.kt @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2014-2024 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.ui.activities + +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.ActivityScenario +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.utils.omh.AuthTrigger +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.robolectric.shadows.ShadowLooper +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference + +/** + * Unit tests for MainActivity's AuthTrigger implementation and AuthCallback behavior. + */ +class MainActivityAuthTriggerTest : AbstractMainActivityTestBase() { + /** + * Test that MainActivity implements AuthTrigger interface. + */ + @Test + fun testMainActivityImplementsAuthTrigger() { + val scenario = ActivityScenario.launch(MainActivity::class.java) + ShadowLooper.idleMainLooper() + scenario.moveToState(Lifecycle.State.STARTED) + + scenario.onActivity { activity -> + assertTrue("MainActivity should implement AuthTrigger", activity is AuthTrigger) + } + + scenario.moveToState(Lifecycle.State.DESTROYED) + scenario.close() + } + + /** + * Test AuthCallback interface contract - success case. + */ + @Test + fun testAuthCallbackSuccess() { + val successCalled = AtomicBoolean(false) + val failureCalled = AtomicBoolean(false) + + val callback = + object : MainActivity.AuthCallback { + override fun onAuthSuccess() { + successCalled.set(true) + } + + override fun onAuthFailure(errorMessage: String?) { + failureCalled.set(true) + } + } + + callback.onAuthSuccess() + + assertTrue("onAuthSuccess should be called", successCalled.get()) + assertFalse("onAuthFailure should not be called", failureCalled.get()) + } + + /** + * Test AuthCallback interface contract - failure case. + */ + @Test + fun testAuthCallbackFailure() { + val successCalled = AtomicBoolean(false) + val failureCalled = AtomicBoolean(false) + val errorReceived = AtomicReference() + + val callback = + object : MainActivity.AuthCallback { + override fun onAuthSuccess() { + successCalled.set(true) + } + + override fun onAuthFailure(errorMessage: String?) { + failureCalled.set(true) + errorReceived.set(errorMessage) + } + } + + callback.onAuthFailure("Test error message") + + assertFalse("onAuthSuccess should not be called", successCalled.get()) + assertTrue("onAuthFailure should be called", failureCalled.get()) + assertEquals("Test error message", errorReceived.get()) + } + + /** + * Test AuthCallback with null error message. + */ + @Test + fun testAuthCallbackFailureNullMessage() { + val errorReceived = AtomicReference("not-null") + + val callback = + object : MainActivity.AuthCallback { + override fun onAuthSuccess() = Unit + + override fun onAuthFailure(errorMessage: String?) { + errorReceived.set(errorMessage) + } + } + + callback.onAuthFailure(null) + + assertEquals(null, errorReceived.get()) + } + + /** + * Test that OpenMode values used for cloud services are valid. + */ + @Test + fun testCloudOpenModeValues() { + // Verify cloud-related OpenMode values exist + assertNotNull(OpenMode.DROPBOX) + assertNotNull(OpenMode.GDRIVE) + assertNotNull(OpenMode.ONEDRIVE) + assertNotNull(OpenMode.BOX) + + // Verify they are distinct + val modes = setOf(OpenMode.DROPBOX, OpenMode.GDRIVE, OpenMode.ONEDRIVE, OpenMode.BOX) + assertEquals(4, modes.size) + } + + /** + * Test CountDownLatch behavior used in triggerAuthBlocking. + * This validates the threading mechanism used for blocking auth. + */ + @Test + fun testCountDownLatchMechanism() { + val latch = CountDownLatch(1) + val result = AtomicBoolean(false) + + // Simulate async operation completing + Thread { + Thread.sleep(50) + result.set(true) + latch.countDown() + }.start() + + val completed = latch.await(5, TimeUnit.SECONDS) + + assertTrue("Latch should complete", completed) + assertTrue("Result should be set", result.get()) + } + + /** + * Test CountDownLatch timeout behavior. + */ + @Test + fun testCountDownLatchTimeout() { + val latch = CountDownLatch(1) + val result = AtomicBoolean(false) + + // Don't countdown, simulating auth never completing + val completed = latch.await(100, TimeUnit.MILLISECONDS) + + assertFalse("Latch should timeout", completed) + assertFalse("Result should not be set", result.get()) + } + + /** + * Test that multiple AuthCallback calls are handled. + */ + @Test + fun testMultipleAuthCallbackCalls() { + val callCount = AtomicInteger(0) + + val callback = + object : MainActivity.AuthCallback { + override fun onAuthSuccess() { + callCount.incrementAndGet() + } + + override fun onAuthFailure(errorMessage: String?) { + callCount.incrementAndGet() + } + } + + callback.onAuthSuccess() + callback.onAuthSuccess() + callback.onAuthFailure("error") + + assertEquals(3, callCount.get()) + } + + /** + * Test concurrent AuthCallback access pattern. + * Simulates multiple threads trying to call callbacks. + */ + @Test + fun testConcurrentAuthCallbackAccess() { + val callCount = AtomicInteger(0) + val latch = CountDownLatch(10) + + val callback = + object : MainActivity.AuthCallback { + override fun onAuthSuccess() { + callCount.incrementAndGet() + latch.countDown() + } + + override fun onAuthFailure(errorMessage: String?) { + callCount.incrementAndGet() + latch.countDown() + } + } + + // Launch multiple threads calling the callback + repeat(10) { index -> + Thread { + if (index % 2 == 0) { + callback.onAuthSuccess() + } else { + callback.onAuthFailure("error-$index") + } + }.start() + } + + val completed = latch.await(5, TimeUnit.SECONDS) + + assertTrue("All callbacks should complete", completed) + assertEquals(10, callCount.get()) + } + + /** + * Test the blocking pattern used in triggerAuthBlocking without actual Activity. + * This validates the core synchronization logic. + */ + @Test + fun testBlockingAuthPattern() { + val latch = CountDownLatch(1) + val result = AtomicBoolean(false) + val callbackExecuted = AtomicBoolean(false) + + // Simulate the pattern used in triggerAuthBlocking + val simulateUiThread = + Thread { + // This represents runOnUiThread content + val callback = + object : MainActivity.AuthCallback { + override fun onAuthSuccess() { + result.set(true) + latch.countDown() + } + + override fun onAuthFailure(errorMessage: String?) { + result.set(false) + latch.countDown() + } + } + + // Simulate auth completing after some delay + Thread.sleep(50) + callbackExecuted.set(true) + callback.onAuthSuccess() + } + simulateUiThread.start() + + // This represents the blocking wait + val completed = latch.await(5, TimeUnit.SECONDS) + + assertTrue("Should complete", completed) + assertTrue("Callback should have executed", callbackExecuted.get()) + assertTrue("Result should be true (success)", result.get()) + } + + /** + * Test the blocking pattern with failure. + */ + @Test + fun testBlockingAuthPatternFailure() { + val latch = CountDownLatch(1) + val result = AtomicBoolean(true) // Start with true to verify it changes + + val simulateUiThread = + Thread { + val callback = + object : MainActivity.AuthCallback { + override fun onAuthSuccess() { + result.set(true) + latch.countDown() + } + + override fun onAuthFailure(errorMessage: String?) { + result.set(false) + latch.countDown() + } + } + + Thread.sleep(50) + callback.onAuthFailure("Auth denied") + } + simulateUiThread.start() + + val completed = latch.await(5, TimeUnit.SECONDS) + + assertTrue("Should complete", completed) + assertFalse("Result should be false (failure)", result.get()) + } + + /** + * Test interruption handling in blocking auth. + */ + @Test + fun testBlockingAuthInterruption() { + val latch = CountDownLatch(1) + val wasInterrupted = AtomicBoolean(false) + + val blockingThread = + Thread { + try { + latch.await(10, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + wasInterrupted.set(true) + Thread.currentThread().interrupt() + } + } + blockingThread.start() + + // Give it a moment to start waiting + Thread.sleep(50) + + // Interrupt the thread + blockingThread.interrupt() + blockingThread.join(1000) + + assertTrue("Thread should have been interrupted", wasInterrupted.get()) + } + + /** + * Test that result is correctly returned based on callback. + */ + @Test + fun testBlockingAuthResultMapping() { + // Test success -> true + assertEquals(true, simulateBlockingAuth { it.onAuthSuccess() }) + + // Test failure -> false + assertEquals(false, simulateBlockingAuth { it.onAuthFailure("error") }) + } + + /** + * Helper to simulate the blocking auth pattern. + */ + private fun simulateBlockingAuth(authAction: (MainActivity.AuthCallback) -> Unit): Boolean { + val latch = CountDownLatch(1) + val result = AtomicBoolean(false) + + Thread { + val callback = + object : MainActivity.AuthCallback { + override fun onAuthSuccess() { + result.set(true) + latch.countDown() + } + + override fun onAuthFailure(errorMessage: String?) { + result.set(false) + latch.countDown() + } + } + authAction(callback) + }.start() + + latch.await(5, TimeUnit.SECONDS) + return result.get() + } + + /** + * Test OpenMode enum ordinal values for cloud services. + * This ensures serialization consistency. + */ + @Test + fun testOpenModeOrdinalStability() { + // These ordinal values are used in Intent extras + // Changes would break serialization + val dropboxOrdinal = OpenMode.DROPBOX.ordinal + val gdriveOrdinal = OpenMode.GDRIVE.ordinal + val onedriveOrdinal = OpenMode.ONEDRIVE.ordinal + val boxOrdinal = OpenMode.BOX.ordinal + + // Verify ordinals are distinct + val ordinals = setOf(dropboxOrdinal, gdriveOrdinal, onedriveOrdinal, boxOrdinal) + assertEquals("All cloud mode ordinals should be distinct", 4, ordinals.size) + } + + /** + * Test that all cloud OpenModes can be converted to/from ordinal. + */ + @Test + fun testOpenModeOrdinalRoundTrip() { + for (mode in listOf(OpenMode.DROPBOX, OpenMode.GDRIVE, OpenMode.ONEDRIVE, OpenMode.BOX)) { + val ordinal = mode.ordinal + val restored = OpenMode.values()[ordinal] + assertEquals("Round-trip should preserve OpenMode", mode, restored) + } + } +} diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/PermissionsActivityTest.kt b/app/src/test/java/com/amaze/filemanager/ui/activities/PermissionsActivityTest.kt index c4f178d511..068f44d920 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/activities/PermissionsActivityTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/PermissionsActivityTest.kt @@ -24,7 +24,7 @@ import android.app.AppOpsManager import android.content.Context import android.net.Uri import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Build.VERSION_CODES.R import android.os.storage.StorageManager @@ -64,7 +64,7 @@ import org.robolectric.shadows.ShadowStorageManager */ @RunWith(AndroidJUnit4::class) @Config( - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], shadows = [ShadowMultiDex::class, ShadowStorageManager::class], ) class PermissionsActivityTest { diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java b/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java index c65a67166c..69e042237f 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.ui.activities; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -58,7 +58,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) + sdk = {O, P, Build.VERSION_CODES.R}) public class TextEditorActivityTest { private final String fileContents = "fsdfsdfs"; diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt index 7c4b684f95..4be6d2437d 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/AbstractEncryptDialogTests.kt @@ -23,8 +23,8 @@ package com.amaze.filemanager.ui.dialogs import android.Manifest import android.os.Build import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Build.VERSION_CODES.N +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.annotation.RequiresApi import androidx.lifecycle.Lifecycle @@ -48,7 +48,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowTabHandler::class, ShadowFileUtils::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) abstract class AbstractEncryptDialogTests { protected lateinit var scenario: ActivityScenario diff --git a/app/src/test/java/com/amaze/filemanager/ui/dialogs/ColorPickerDialogTest.kt b/app/src/test/java/com/amaze/filemanager/ui/dialogs/ColorPickerDialogTest.kt index da53a03bee..528629a967 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/dialogs/ColorPickerDialogTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/dialogs/ColorPickerDialogTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.ui.dialogs import android.os.Build.VERSION_CODES -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.application.AppConfig @@ -42,7 +42,7 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class, ShadowTabHandler::class, ShadowFileUtils::class], - sdk = [LOLLIPOP, P, VERSION_CODES.R], + sdk = [O, P, VERSION_CODES.R], ) class ColorPickerDialogTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/ui/fragments/CloudSheetFragmentTest.java b/app/src/test/java/com/amaze/filemanager/ui/fragments/CloudSheetFragmentTest.java deleted file mode 100644 index 28ca6edcb3..0000000000 --- a/app/src/test/java/com/amaze/filemanager/ui/fragments/CloudSheetFragmentTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.ui.fragments; - -import static android.os.Build.VERSION_CODES.LOLLIPOP; -import static android.os.Build.VERSION_CODES.P; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.robolectric.Shadows.shadowOf; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.annotation.Config; - -import com.amaze.filemanager.database.CloudContract; - -import android.content.pm.PackageInfo; -import android.os.Build; - -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -@RunWith(AndroidJUnit4.class) -@Config(sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) -public class CloudSheetFragmentTest { - - @Test - public void testIsCloudProviderAvailable() { - assertFalse( - CloudSheetFragment.isCloudProviderAvailable(ApplicationProvider.getApplicationContext())); - - PackageInfo pi = new PackageInfo(); - pi.packageName = CloudContract.APP_PACKAGE_NAME; - shadowOf(ApplicationProvider.getApplicationContext().getPackageManager()).installPackage(pi); - - assertTrue( - CloudSheetFragment.isCloudProviderAvailable(ApplicationProvider.getApplicationContext())); - } -} diff --git a/app/src/test/java/com/amaze/filemanager/ui/fragments/preferencefragments/UiPrefsFragmentTest.kt b/app/src/test/java/com/amaze/filemanager/ui/fragments/preferencefragments/UiPrefsFragmentTest.kt index 277aaccb33..2563525b77 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/fragments/preferencefragments/UiPrefsFragmentTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/fragments/preferencefragments/UiPrefsFragmentTest.kt @@ -52,8 +52,7 @@ import kotlin.random.Random */ @Config( sdk = [ - Build.VERSION_CODES.LOLLIPOP, - Build.VERSION_CODES.N, + Build.VERSION_CODES.O, Build.VERSION_CODES.P, Build.VERSION_CODES.R, ], diff --git a/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java b/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java index 117e3da274..f90252b826 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/icons/IconsTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.ui.icons; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; @@ -41,7 +41,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) + sdk = {O, P, Build.VERSION_CODES.R}) public class IconsTest { @Before diff --git a/app/src/test/java/com/amaze/filemanager/ui/notifications/NotificationConstantsTest.java b/app/src/test/java/com/amaze/filemanager/ui/notifications/NotificationConstantsTest.java index 78468e5358..00c8f38cbc 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/notifications/NotificationConstantsTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/notifications/NotificationConstantsTest.java @@ -22,7 +22,10 @@ import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_MIN; +import static android.os.Build.VERSION_CODES.JELLY_BEAN; import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.N; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static com.amaze.filemanager.ui.notifications.NotificationConstants.CHANNEL_FTP_ID; import static com.amaze.filemanager.ui.notifications.NotificationConstants.CHANNEL_NORMAL_ID; @@ -54,7 +57,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) +@Config(sdk = {O, P, Build.VERSION_CODES.R}) public class NotificationConstantsTest { private Context context; @@ -89,7 +92,7 @@ public void testSetMetadataIllegalType() { } @Test - @Config(sdk = {LOLLIPOP}) // max sdk is N + @Config(maxSdk = N) // test only runs up to Android N public void testNormalNotification() { NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_NORMAL_ID) @@ -104,7 +107,7 @@ public void testNormalNotification() { if (Build.VERSION.SDK_INT >= LOLLIPOP) { assertEquals(Notification.CATEGORY_SERVICE, result.category); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + if (Build.VERSION.SDK_INT >= JELLY_BEAN) { assertEquals(Notification.PRIORITY_MIN, result.priority); } else { assertEquals(Notification.PRIORITY_DEFAULT, result.priority); @@ -112,7 +115,7 @@ public void testNormalNotification() { } @Test - @Config(sdk = {LOLLIPOP}) // max sdk is N + @Config(maxSdk = N) // test only runs up to Android N public void testFtpNotification() { NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_FTP_ID) @@ -128,7 +131,7 @@ public void testFtpNotification() { assertEquals(Notification.CATEGORY_SERVICE, result.category); assertEquals(Notification.VISIBILITY_PUBLIC, result.visibility); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + if (Build.VERSION.SDK_INT >= JELLY_BEAN) { assertEquals(Notification.PRIORITY_MAX, result.priority); } else { assertEquals(Notification.PRIORITY_DEFAULT, result.priority); diff --git a/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt b/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt index 7d614b8431..a37f248a79 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt +++ b/app/src/test/java/com/amaze/filemanager/ui/theme/AppThemeTest.kt @@ -22,7 +22,7 @@ package com.amaze.filemanager.ui.theme import android.content.Context import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.PowerManager import androidx.preference.PreferenceManager @@ -41,7 +41,7 @@ import java.util.Calendar @RunWith(AndroidJUnit4::class) @Config( - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], shadows = [ShadowMultiDex::class], ) class AppThemeTest { @@ -180,7 +180,7 @@ class AppThemeTest { @Config( shadows = [ShadowPowerManager::class, ShadowMultiDex::class], qualifiers = "notnight", - minSdk = Build.VERSION_CODES.LOLLIPOP, + minSdk = O, ) fun testSimpleAppThemeWithFollowBatterySaverAndBatterySaverOn() { val context = ApplicationProvider.getApplicationContext() @@ -215,7 +215,7 @@ class AppThemeTest { @Config( shadows = [ShadowPowerManager::class, ShadowMultiDex::class], qualifiers = "notnight", - minSdk = Build.VERSION_CODES.LOLLIPOP, + minSdk = O, ) fun testSimpleAppThemeWithFollowBatterySaverAndBatterySaverOff() { val context = ApplicationProvider.getApplicationContext() @@ -233,7 +233,7 @@ class AppThemeTest { @Config( shadows = [ShadowPowerManager::class, ShadowMultiDex::class], qualifiers = "notnight", - minSdk = Build.VERSION_CODES.LOLLIPOP, + minSdk = O, ) fun testMaterialDialogThemeWithFollowBatterySaverAndBatterySaverOn() { val context = ApplicationProvider.getApplicationContext() @@ -268,7 +268,7 @@ class AppThemeTest { @Config( shadows = [ShadowPowerManager::class, ShadowMultiDex::class], qualifiers = "notnight", - minSdk = Build.VERSION_CODES.LOLLIPOP, + minSdk = O, ) fun testMaterialDialogThemeWithFollowBatterySaverAndBatterySaverOff() { val context = ApplicationProvider.getApplicationContext() diff --git a/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java b/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java index 37070cd432..d8a5887a93 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/views/WarnableTextInputValidatorTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.ui.views; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -44,7 +44,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) +@Config(sdk = {O, P, Build.VERSION_CODES.R}) public class WarnableTextInputValidatorTest { private Context context; diff --git a/app/src/test/java/com/amaze/filemanager/utils/AESCryptTest.kt b/app/src/test/java/com/amaze/filemanager/utils/AESCryptTest.kt index 7559fa97d4..e8fd28d982 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/AESCryptTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/AESCryptTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.utils import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertArrayEquals @@ -36,7 +36,7 @@ import kotlin.random.Random * Unit test for [AESCrypt] */ @RunWith(AndroidJUnit4::class) -@Config(sdk = [LOLLIPOP, P, Build.VERSION_CODES.R]) +@Config(sdk = [O, P, Build.VERSION_CODES.R]) class AESCryptTest { /** * Simple sanity test on [AESCrypt]. diff --git a/app/src/test/java/com/amaze/filemanager/utils/AnimUtilsTest.java b/app/src/test/java/com/amaze/filemanager/utils/AnimUtilsTest.java index 92b65fdc29..ad492d647a 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/AnimUtilsTest.java +++ b/app/src/test/java/com/amaze/filemanager/utils/AnimUtilsTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.utils; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -45,7 +45,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) +@Config(sdk = {O, P, Build.VERSION_CODES.R}) public class AnimUtilsTest { @Test diff --git a/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt index d7e116593f..b01829a969 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/CryptUtilTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.utils import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.Environment import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -48,7 +48,7 @@ import kotlin.random.Random @RunWith(AndroidJUnit4::class) @Config( - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) class CryptUtilTest { /** diff --git a/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt b/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt index 0e882b12c8..d3ae4e76b4 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/MinMaxInputFilterTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.utils import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.text.SpannedString import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -32,7 +32,7 @@ import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) -@Config(sdk = [LOLLIPOP, P, Build.VERSION_CODES.R]) +@Config(sdk = [O, P, Build.VERSION_CODES.R]) class MinMaxInputFilterTest { /** * Test MinMaxInputFilter functioning diff --git a/app/src/test/java/com/amaze/filemanager/utils/NetworkUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/NetworkUtilTest.kt index 5f2a071a64..3751215857 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/NetworkUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/NetworkUtilTest.kt @@ -10,7 +10,7 @@ import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.os.Build import android.os.Build.VERSION_CODES -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import io.mockk.every @@ -33,7 +33,7 @@ import java.util.Collections * Tests for [NetworkUtil]. */ @RunWith(AndroidJUnit4::class) -@Config(sdk = [LOLLIPOP, P, VERSION_CODES.R]) +@Config(sdk = [O, P, VERSION_CODES.R]) @Suppress("StringLiteralDuplication") class NetworkUtilTest { private lateinit var context: Context diff --git a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt index ccc65a0110..2dc96449ea 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/SmbUtilTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.utils import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -46,7 +46,7 @@ import org.robolectric.annotation.Config @Suppress("StringLiteralDuplication") @RunWith(AndroidJUnit4::class) @Config( - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], shadows = [ShadowPasswordUtil::class, ShadowSmbUtil::class], ) class SmbUtilTest { diff --git a/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java b/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java index 12e9f3cd13..dbf9f09ffa 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java +++ b/app/src/test/java/com/amaze/filemanager/utils/TinyDBTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.utils; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNull; @@ -38,7 +38,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) +@Config(sdk = {O, P, Build.VERSION_CODES.R}) public class TinyDBTest { private SharedPreferences prefs; diff --git a/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java b/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java index fa90467fca..eaf422d67b 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java +++ b/app/src/test/java/com/amaze/filemanager/utils/UtilsTest.java @@ -20,8 +20,8 @@ package com.amaze.filemanager.utils; -import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.N; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static com.amaze.filemanager.utils.Utils.formatTimer; import static com.amaze.filemanager.utils.Utils.sanitizeInput; @@ -56,7 +56,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; @RunWith(AndroidJUnit4.class) -@Config(sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) +@Config(sdk = {O, P, Build.VERSION_CODES.R}) public class UtilsTest { @Test diff --git a/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt index fd74085121..93b7fff0f8 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/X509CertificateUtilTest.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.utils import android.os.Build -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.shadows.ShadowMultiDex @@ -41,7 +41,7 @@ import java.security.cert.X509Certificate @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [LOLLIPOP, P, Build.VERSION_CODES.R], + sdk = [O, P, Build.VERSION_CODES.R], ) class X509CertificateUtilTest { private lateinit var cert: X509Certificate diff --git a/app/src/test/java/com/amaze/filemanager/utils/cloud/CloudPluginUtilTest.kt b/app/src/test/java/com/amaze/filemanager/utils/cloud/CloudPluginUtilTest.kt new file mode 100644 index 0000000000..f1c7791639 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/utils/cloud/CloudPluginUtilTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014-2024 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.utils.cloud + +import android.content.Context +import android.content.pm.PackageInfo +import android.os.Build.VERSION_CODES +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.database.CloudContract +import com.amaze.filemanager.utils.cloud.CloudPluginUtil.isCloudProviderAvailable +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Shadows +import org.robolectric.annotation.Config + +/** + * Test class for [CloudPluginUtil]. + */ +@RunWith(AndroidJUnit4::class) +@Config(sdk = [VERSION_CODES.O, VERSION_CODES.P, VERSION_CODES.R]) +class CloudPluginUtilTest { + /** + * Test for [isCloudProviderAvailable] for app not installed and installed. + */ + @Test + fun testIsCloudProviderAvailable() { + assertFalse( + isCloudProviderAvailable(ApplicationProvider.getApplicationContext()), + ) + + val pi = PackageInfo() + pi.packageName = CloudContract.APP_PACKAGE_NAME + Shadows.shadowOf(ApplicationProvider.getApplicationContext().packageManager) + .installPackage(pi) + + assertTrue( + isCloudProviderAvailable(ApplicationProvider.getApplicationContext()), + ) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/utils/omh/OmhAuthClientExtTest.kt b/app/src/test/java/com/amaze/filemanager/utils/omh/OmhAuthClientExtTest.kt new file mode 100644 index 0000000000..2f828bd5fe --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/utils/omh/OmhAuthClientExtTest.kt @@ -0,0 +1,1293 @@ +/* + * Copyright (C) 2014-2024 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.utils.omh + +import com.amaze.filemanager.fileoperations.exceptions.CloudPluginException +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test +import java.net.ProtocolException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +/** + * Unit tests for [retryOnUnauthorized] function. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress( + "StringLiteralDuplication", + "ComplexMethod", + "LongMethod", + "LargeClass", + "TooGenericExceptionCaught", + "TooGenericExceptionThrown", +) +class OmhAuthClientExtTest { + private val testOpenMode = OpenMode.DROPBOX + + /** + * Test that action succeeds on first try without any retries. + */ + @Test + fun testSuccessOnFirstTry() = + runTest { + val actionCallCount = AtomicInteger(0) + + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(shouldSucceed = true), + refreshAction = { fail("refreshAction should not be called") }, + action = { + actionCallCount.incrementAndGet() + "success" + }, + ) + + assertEquals("success", result) + assertEquals(1, actionCallCount.get()) + } + + /** + * Test that action is retried when CloudPluginException with ProtocolException cause is thrown. + */ + @Test + fun testRetryOnProtocolException() = + runTest { + val actionCallCount = AtomicInteger(0) + val refreshCallCount = AtomicInteger(0) + + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 3, + trigger = createMockAuthTrigger(shouldSucceed = true), + refreshAction = { refreshCallCount.incrementAndGet() }, + action = { + val count = actionCallCount.incrementAndGet() + if (count < 3) { + throw createProtocolException() + } + "success after retry" + }, + ) + + assertEquals("success after retry", result) + assertEquals(3, actionCallCount.get()) + assertEquals(2, refreshCallCount.get()) // Refresh called on first 2 failures + } + + /** + * Test that auth trigger is called when max retries are exhausted with ProtocolException. + */ + @Test + fun testAuthTriggerCalledOnMaxRetries() = + runTest { + val actionCallCount = AtomicInteger(0) + val refreshCallCount = AtomicInteger(0) + val authTriggerCallCount = AtomicInteger(0) + + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + authTriggerCallCount.incrementAndGet() + assertEquals(testOpenMode, openMode) + return true + } + }, + refreshAction = { refreshCallCount.incrementAndGet() }, + action = { + val count = actionCallCount.incrementAndGet() + if (count <= 2) { + throw createProtocolException() + } + "success after auth" + }, + ) + + assertEquals("success after auth", result) + assertEquals(3, actionCallCount.get()) // 2 initial attempts + 1 after auth + assertEquals(1, refreshCallCount.get()) // Refresh called once before maxRetries + assertEquals(1, authTriggerCallCount.get()) // Auth triggered once at maxRetries + } + + /** + * Test that exception is thrown when auth trigger fails. + */ + @Test + fun testExceptionThrownWhenAuthTriggerFails() = + runTest { + val actionCallCount = AtomicInteger(0) + + try { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(shouldSucceed = false), + refreshAction = { /* do nothing */ }, + action = { + actionCallCount.incrementAndGet() + throw createProtocolException() + }, + ) + fail("Expected CloudPluginException to be thrown") + } catch (e: CloudPluginException) { + // Expected + assertTrue(e.cause is ProtocolException) + } + + assertEquals(2, actionCallCount.get()) + } + + /** + * Test that non-ProtocolException CloudPluginException triggers retry with backoff. + */ + @Test + fun testRetryWithBackoffOnNetworkError() = + runTest { + val actionCallCount = AtomicInteger(0) + + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 3, + trigger = createMockAuthTrigger(shouldSucceed = true), + refreshAction = { fail("refreshAction should not be called for network error") }, + action = { + val count = actionCallCount.incrementAndGet() + if (count < 3) { + // CloudPluginException without ProtocolException cause + throw CloudPluginException(RuntimeException("Network error")) + } + "success" + }, + ) + + assertEquals("success", result) + assertEquals(3, actionCallCount.get()) + } + + /** + * Test that non-ProtocolException CloudPluginException is thrown after max retries. + */ + @Test + fun testNetworkErrorThrownAfterMaxRetries() = + runTest { + val actionCallCount = AtomicInteger(0) + + try { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(shouldSucceed = true), + refreshAction = { fail("refreshAction should not be called for network error") }, + action = { + actionCallCount.incrementAndGet() + throw CloudPluginException(RuntimeException("Network error")) + }, + ) + fail("Expected CloudPluginException to be thrown") + } catch (e: CloudPluginException) { + // Expected + assertTrue(e.cause is RuntimeException) + assertEquals("Network error", e.cause?.message) + } + + assertEquals(2, actionCallCount.get()) + } + + /** + * Test that CancellationException is propagated immediately without retry. + */ + @Test + fun testCancellationExceptionPropagated() = + runTest { + val actionCallCount = AtomicInteger(0) + + try { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 3, + trigger = createMockAuthTrigger(shouldSucceed = true), + refreshAction = { fail("refreshAction should not be called") }, + action = { + actionCallCount.incrementAndGet() + throw CancellationException("Cancelled") + }, + ) + fail("Expected CancellationException to be thrown") + } catch (e: CancellationException) { + // Expected + assertEquals("Cancelled", e.message) + } + + assertEquals(1, actionCallCount.get()) + } + + /** + * Test that unexpected exceptions are propagated immediately. + */ + @Test + fun testUnexpectedExceptionPropagated() = + runTest { + val actionCallCount = AtomicInteger(0) + + try { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 3, + trigger = createMockAuthTrigger(shouldSucceed = true), + refreshAction = { fail("refreshAction should not be called") }, + action = { + actionCallCount.incrementAndGet() + throw IllegalStateException("Unexpected error") + }, + ) + fail("Expected IllegalStateException to be thrown") + } catch (e: IllegalStateException) { + // Expected + assertEquals("Unexpected error", e.message) + } + + assertEquals(1, actionCallCount.get()) + } + + /** + * Test that refresh action exception is propagated. + */ + @Test + fun testRefreshActionExceptionPropagated() = + runTest { + val actionCallCount = AtomicInteger(0) + + try { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(shouldSucceed = true), + refreshAction = { throw RuntimeException("Refresh failed") }, + action = { + actionCallCount.incrementAndGet() + throw createProtocolException() + }, + ) + fail("Expected exception from refresh action") + } catch (e: Exception) { + // The exception from CompletableFuture.get() wraps the original exception + assertTrue( + e.message?.contains("Refresh failed") == true || + e.cause?.message?.contains("Refresh failed") == true, + ) + } + + assertEquals(1, actionCallCount.get()) + } + + /** + * Test with maxRetries = 1, should trigger auth on first failure with ProtocolException. + */ + @Test + fun testSingleRetryTriggersAuth() = + runTest { + val actionCallCount = AtomicInteger(0) + val authTriggerCallCount = AtomicInteger(0) + + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 1, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + authTriggerCallCount.incrementAndGet() + return true + } + }, + refreshAction = { fail("refreshAction should not be called with maxRetries=1") }, + action = { + val count = actionCallCount.incrementAndGet() + if (count == 1) { + throw createProtocolException() + } + "success" + }, + ) + + assertEquals("success", result) + assertEquals(2, actionCallCount.get()) + assertEquals(1, authTriggerCallCount.get()) + } + + /** + * Test calling retryOnUnauthorized from runBlocking context (simulating Java interop). + * This simulates the actual usage in CloudUtil.getCloudFilesBlocking. + */ + @Test + fun testFromRunBlockingContext() { + val actionCallCount = AtomicInteger(0) + val authTriggerCallCount = AtomicInteger(0) + + val result = + runBlocking(Dispatchers.IO.limitedParallelism(1)) { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + authTriggerCallCount.incrementAndGet() + return true + } + }, + refreshAction = { /* do nothing */ }, + action = { + val count = actionCallCount.incrementAndGet() + if (count <= 2) { + throw createProtocolException() + } + "success after auth" + }, + ) + } + + assertEquals("success after auth", result) + assertEquals(3, actionCallCount.get()) + assertEquals(1, authTriggerCallCount.get()) + } + + /** + * Test concurrent calls to retryOnUnauthorized. + * This tests thread-safety of the retry mechanism. + */ + @Test + fun testConcurrentCalls() = + runTest { + val totalCalls = 5 + val successCount = AtomicInteger(0) + + val results = + (1..totalCalls).map { callIndex -> + async(Dispatchers.IO) { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(shouldSucceed = true), + refreshAction = { /* do nothing */ }, + action = { + successCount.incrementAndGet() + "result-$callIndex" + }, + ) + } + }.awaitAll() + + assertEquals(totalCalls, results.size) + assertEquals(totalCalls, successCount.get()) + } + + /** + * Test that auth trigger blocks correctly and completes. + * Simulates a slow auth process. + */ + @Test + fun testAuthTriggerBlocksAndCompletes() = + runTest { + val authStarted = CountDownLatch(1) + val authCompleted = CountDownLatch(1) + val actionCallCount = AtomicInteger(0) + + val result = + withContext(Dispatchers.IO) { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 1, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + authStarted.countDown() + // Simulate slow auth (100ms) + Thread.sleep(100) + authCompleted.countDown() + return true + } + }, + refreshAction = { fail("Should not refresh with maxRetries=1") }, + action = { + val count = actionCallCount.incrementAndGet() + if (count == 1) { + throw createProtocolException() + } + "success" + }, + ) + } + + assertTrue("Auth should have started", authStarted.await(5, TimeUnit.SECONDS)) + assertTrue("Auth should have completed", authCompleted.await(5, TimeUnit.SECONDS)) + assertEquals("success", result) + assertEquals(2, actionCallCount.get()) + } + + /** + * Test nested ProtocolException causes are detected correctly. + */ + @Test + fun testNestedProtocolExceptionDetection() = + runTest { + val actionCallCount = AtomicInteger(0) + + try { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 1, + trigger = createMockAuthTrigger(shouldSucceed = false), + refreshAction = { fail("Should not be called") }, + action = { + actionCallCount.incrementAndGet() + // Nested ProtocolException: RuntimeException -> ProtocolException + val protocolEx = ProtocolException("401 Unauthorized") + val wrapperEx = RuntimeException("Wrapper", protocolEx) + throw CloudPluginException(wrapperEx) + }, + ) + fail("Expected exception") + } catch (e: CloudPluginException) { + // Expected - should detect nested ProtocolException and trigger auth + } + + assertEquals(1, actionCallCount.get()) + } + + // ========== Additional Tests for Different OpenMode Types ========== + + /** + * Test with OpenMode.GDRIVE + */ + @Test + fun testWithGDriveOpenMode() = + runTest { + val openModeUsed = AtomicInteger(-1) + + val result = + retryOnUnauthorized( + openMode = OpenMode.GDRIVE, + maxRetries = 1, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + openModeUsed.set(openMode.ordinal) + return true + } + }, + refreshAction = { }, + action = { + if (openModeUsed.get() == -1) { + throw createProtocolException() + } + "gdrive-success" + }, + ) + + assertEquals("gdrive-success", result) + assertEquals(OpenMode.GDRIVE.ordinal, openModeUsed.get()) + } + + /** + * Test with OpenMode.ONEDRIVE + */ + @Test + fun testWithOneDriveOpenMode() = + runTest { + val openModeUsed = AtomicInteger(-1) + + val result = + retryOnUnauthorized( + openMode = OpenMode.ONEDRIVE, + maxRetries = 1, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + openModeUsed.set(openMode.ordinal) + return true + } + }, + refreshAction = { }, + action = { + if (openModeUsed.get() == -1) { + throw createProtocolException() + } + "onedrive-success" + }, + ) + + assertEquals("onedrive-success", result) + assertEquals(OpenMode.ONEDRIVE.ordinal, openModeUsed.get()) + } + + /** + * Test with OpenMode.BOX + */ + @Test + fun testWithBoxOpenMode() = + runTest { + val openModeUsed = AtomicInteger(-1) + + val result = + retryOnUnauthorized( + openMode = OpenMode.BOX, + maxRetries = 1, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + openModeUsed.set(openMode.ordinal) + return true + } + }, + refreshAction = { }, + action = { + if (openModeUsed.get() == -1) { + throw createProtocolException() + } + "box-success" + }, + ) + + assertEquals("box-success", result) + assertEquals(OpenMode.BOX.ordinal, openModeUsed.get()) + } + + // ========== Tests for Auth Trigger Exception Scenarios ========== + + /** + * Test when auth trigger throws an exception. + */ + @Test + fun testAuthTriggerThrowsException() = + runTest { + val actionCallCount = AtomicInteger(0) + + try { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 1, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + throw RuntimeException("Auth system error") + } + }, + refreshAction = { }, + action = { + actionCallCount.incrementAndGet() + throw createProtocolException() + }, + ) + fail("Expected CloudPluginException") + } catch (e: CloudPluginException) { + // Auth trigger exception is caught, authSuccess becomes false, original exception thrown + assertTrue(e.cause is ProtocolException) + } + + assertEquals(1, actionCallCount.get()) + } + + /** + * Test when auth trigger throws RuntimeException after successful refresh attempts. + */ + @Test + fun testAuthTriggerExceptionAfterRefreshAttempts() = + runTest { + val actionCallCount = AtomicInteger(0) + val refreshCallCount = AtomicInteger(0) + + try { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 3, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + throw IllegalStateException("Auth unavailable") + } + }, + refreshAction = { refreshCallCount.incrementAndGet() }, + action = { + actionCallCount.incrementAndGet() + throw createProtocolException() + }, + ) + fail("Expected CloudPluginException") + } catch (e: CloudPluginException) { + assertTrue(e.cause is ProtocolException) + } + + assertEquals(3, actionCallCount.get()) + assertEquals(2, refreshCallCount.get()) // Called for attempts 1 and 2 + } + + // ========== Tests for Multiple Failures and Recovery ========== + + /** + * Test multiple protocol exceptions followed by success on final action after auth. + */ + @Test + fun testMultipleFailuresThenSuccessAfterAuth() = + runTest { + val actionCallCount = AtomicInteger(0) + val refreshCallCount = AtomicInteger(0) + val authCallCount = AtomicInteger(0) + + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 5, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + authCallCount.incrementAndGet() + return true + } + }, + refreshAction = { refreshCallCount.incrementAndGet() }, + action = { + val count = actionCallCount.incrementAndGet() + if (count <= 5) { + throw createProtocolException() + } + "finally success" + }, + ) + + assertEquals("finally success", result) + assertEquals(6, actionCallCount.get()) // 5 failures + 1 success after auth + assertEquals(4, refreshCallCount.get()) // Refresh for attempts 1-4 + assertEquals(1, authCallCount.get()) // Auth triggered at attempt 5 + } + + /** + * Test that refresh is called correct number of times before auth trigger. + */ + @Test + fun testRefreshCalledCorrectTimes() = + runTest { + val refreshCallCount = AtomicInteger(0) + val authCallCount = AtomicInteger(0) + val actionCallCount = AtomicInteger(0) + + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 4, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + authCallCount.incrementAndGet() + return true + } + }, + refreshAction = { refreshCallCount.incrementAndGet() }, + action = { + val count = actionCallCount.incrementAndGet() + if (count <= 4) { + throw createProtocolException() + } + "done" + }, + ) + + assertEquals(5, actionCallCount.get()) + assertEquals(3, refreshCallCount.get()) // Refresh for attempts 1, 2, 3 (not 4, which triggers auth) + assertEquals(1, authCallCount.get()) + } + + // ========== Tests for Deeply Nested Exception Chains ========== + + /** + * Test detection of ProtocolException deeply nested in exception chain. + */ + @Test + fun testDeeplyNestedProtocolException() = + runTest { + val authTriggerCalled = AtomicInteger(0) + + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 1, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + authTriggerCalled.incrementAndGet() + return true + } + }, + refreshAction = { }, + action = { + if (authTriggerCalled.get() == 0) { + // Create deeply nested exception: CloudPluginException -> RuntimeException -> IOException -> ProtocolException + val protocolEx = ProtocolException("401") + val ioEx = java.io.IOException("IO error", protocolEx) + val runtimeEx = RuntimeException("Runtime wrapper", ioEx) + throw CloudPluginException(runtimeEx) + } + "success" + }, + ) + + assertEquals("success", result) + assertEquals(1, authTriggerCalled.get()) + } + + /** + * Test that non-ProtocolException nested chain does not trigger auth. + */ + @Test + fun testDeeplyNestedNonProtocolException() = + runTest { + val authTriggerCalled = AtomicInteger(0) + val actionCallCount = AtomicInteger(0) + + try { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + authTriggerCalled.incrementAndGet() + return true + } + }, + refreshAction = { fail("Should not refresh for non-protocol exception") }, + action = { + actionCallCount.incrementAndGet() + // Deeply nested but no ProtocolException + val innerEx = IllegalArgumentException("Invalid arg") + val ioEx = java.io.IOException("IO error", innerEx) + val runtimeEx = RuntimeException("Runtime wrapper", ioEx) + throw CloudPluginException(runtimeEx) + }, + ) + fail("Expected exception") + } catch (e: CloudPluginException) { + // Expected - network error path, no auth trigger + } + + assertEquals(2, actionCallCount.get()) + assertEquals(0, authTriggerCalled.get()) // Auth should NOT be called + } + + // ========== Tests for Different Return Types ========== + + /** + * Test returning Int type. + */ + @Test + fun testReturnTypeInt() = + runTest { + val result: Int = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { 42 }, + ) + + assertEquals(42, result) + } + + /** + * Test returning List type. + */ + @Test + fun testReturnTypeList() = + runTest { + val result: List = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { listOf("a", "b", "c") }, + ) + + assertEquals(listOf("a", "b", "c"), result) + } + + /** + * Test returning nullable type with null value. + */ + @Test + fun testReturnTypeNullable() = + runTest { + val result: String? = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { null }, + ) + + assertEquals(null, result) + } + + /** + * Test returning Unit type. + */ + @Test + fun testReturnTypeUnit() = + runTest { + var sideEffect = false + + val result: Unit = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { + sideEffect = true + }, + ) + + assertEquals(Unit, result) + assertTrue(sideEffect) + } + + /** + * Test returning data class. + */ + @Test + fun testReturnTypeDataClass() = + runTest { + data class TestData(val id: Int, val name: String) + + val result: TestData = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { TestData(1, "test") }, + ) + + assertEquals(TestData(1, "test"), result) + } + + // ========== Tests for Sequential Calls ========== + + /** + * Test sequential calls where first fails and second succeeds. + */ + @Test + fun testSequentialCalls() = + runTest { + val globalCallCount = AtomicInteger(0) + + // First call - fails on first attempt, succeeds after retry + val result1 = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { + val count = globalCallCount.incrementAndGet() + if (count == 1) { + throw createProtocolException() + } + "result1" + }, + ) + + // Second call - immediate success + val result2 = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { + globalCallCount.incrementAndGet() + "result2" + }, + ) + + assertEquals("result1", result1) + assertEquals("result2", result2) + assertEquals(3, globalCallCount.get()) // 2 for first call, 1 for second + } + + /** + * Test sequential calls with different open modes. + */ + @Test + fun testSequentialCallsDifferentModes() = + runTest { + val modesUsed = mutableListOf() + + for (mode in listOf(OpenMode.DROPBOX, OpenMode.GDRIVE, OpenMode.ONEDRIVE)) { + retryOnUnauthorized( + openMode = mode, + maxRetries = 1, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + modesUsed.add(openMode) + return true + } + }, + refreshAction = { }, + action = { + if (modesUsed.lastOrNull() != mode) { + throw createProtocolException() + } + "success-$mode" + }, + ) + } + + assertEquals(listOf(OpenMode.DROPBOX, OpenMode.GDRIVE, OpenMode.ONEDRIVE), modesUsed) + } + + // ========== Tests for Mixed Exception Types ========== + + /** + * Test mixed exception sequence: network error then protocol error. + */ + @Test + fun testMixedExceptionSequence() = + runTest { + val actionCallCount = AtomicInteger(0) + val refreshCallCount = AtomicInteger(0) + + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 4, + trigger = createMockAuthTrigger(true), + refreshAction = { refreshCallCount.incrementAndGet() }, + action = { + val count = actionCallCount.incrementAndGet() + when (count) { + 1 -> throw CloudPluginException(RuntimeException("Network error")) // Network error + 2 -> throw createProtocolException() // Protocol error - triggers refresh + 3 -> throw CloudPluginException(java.io.IOException("Timeout")) // Network error + 4 -> throw createProtocolException() // Protocol error at maxRetries - triggers auth + else -> "success" + } + }, + ) + + assertEquals("success", result) + assertEquals(5, actionCallCount.get()) + // Refresh called for attempt 2 (protocol exception before max retries) + assertEquals(1, refreshCallCount.get()) + } + + // ========== Tests for Action Success After Failures ========== + + /** + * Test that action succeeds immediately after token refresh (not at maxRetries). + */ + @Test + fun testSuccessAfterRefresh() = + runTest { + val actionCallCount = AtomicInteger(0) + val refreshCallCount = AtomicInteger(0) + val authCallCount = AtomicInteger(0) + + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 5, + trigger = + object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + authCallCount.incrementAndGet() + return true + } + }, + refreshAction = { refreshCallCount.incrementAndGet() }, + action = { + val count = actionCallCount.incrementAndGet() + // Fail on first attempt, succeed on second (after refresh) + if (count == 1) { + throw createProtocolException() + } + "success after refresh" + }, + ) + + assertEquals("success after refresh", result) + assertEquals(2, actionCallCount.get()) + assertEquals(1, refreshCallCount.get()) + assertEquals(0, authCallCount.get()) // Auth never called since we succeeded before maxRetries + } + + // ========== Tests for Concurrent Failures ========== + + /** + * Test concurrent calls all experiencing failures then success. + */ + @Test + fun testConcurrentCallsWithFailures() = + runTest { + val totalCalls = 3 + val successCount = AtomicInteger(0) + val callAttempts = (1..totalCalls).map { AtomicInteger(0) } + + val results = + (0 until totalCalls).map { index -> + async(Dispatchers.IO) { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { + val attempt = callAttempts[index].incrementAndGet() + if (attempt == 1) { + throw createProtocolException() + } + successCount.incrementAndGet() + "result-$index" + }, + ) + } + }.awaitAll() + + assertEquals(totalCalls, results.size) + assertEquals(totalCalls, successCount.get()) + callAttempts.forEach { assertEquals(2, it.get()) } + } + + // ========== Tests for runBlocking Scenarios ========== + + /** + * Test nested runBlocking calls (simulating complex Java interop). + */ + @Test + fun testNestedRunBlocking() { + val outerResult = + runBlocking { + val innerResult = + withContext(Dispatchers.IO) { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { "inner" }, + ) + } + "outer-$innerResult" + } + + assertEquals("outer-inner", outerResult) + } + + /** + * Test from different dispatcher contexts. + */ + @Test + fun testDifferentDispatcherContexts() = + runTest { + // Default dispatcher + val result1 = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { "default" }, + ) + + // IO dispatcher + val result2 = + withContext(Dispatchers.IO) { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { "io" }, + ) + } + + // Limited parallelism + val result3 = + withContext(Dispatchers.IO.limitedParallelism(1)) { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { "limited" }, + ) + } + + assertEquals("default", result1) + assertEquals("io", result2) + assertEquals("limited", result3) + } + + // ========== Tests for Edge Cases ========== + + /** + * Test with very high maxRetries value. + */ + @Test + fun testHighMaxRetries() = + runTest { + val actionCallCount = AtomicInteger(0) + + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 100, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { + val count = actionCallCount.incrementAndGet() + if (count < 50) { + throw createProtocolException() + } + "success at 50" + }, + ) + + assertEquals("success at 50", result) + assertEquals(50, actionCallCount.get()) + } + + /** + * Test action that returns after some computation. + */ + @Test + fun testActionWithComputation() = + runTest { + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { + // Simulate some computation + var sum = 0 + for (i in 1..100) { + sum += i + } + sum + }, + ) + + assertEquals(5050, result) // Sum of 1 to 100 + } + + /** + * Test action that suspends. + */ + @Test + fun testActionWithSuspension() = + runTest { + val actionCallCount = AtomicInteger(0) + + val result = + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 2, + trigger = createMockAuthTrigger(true), + refreshAction = { }, + action = { + actionCallCount.incrementAndGet() + kotlinx.coroutines.delay(10) // Suspend + "suspended result" + }, + ) + + assertEquals("suspended result", result) + assertEquals(1, actionCallCount.get()) + } + + /** + * Test rapid sequential retries with exponential backoff. + * Note: runTest uses virtual time, so we just verify the action count. + */ + @Test + fun testNetworkErrorRetriesWithBackoff() = + runTest { + val actionCallCount = AtomicInteger(0) + + try { + retryOnUnauthorized( + openMode = testOpenMode, + maxRetries = 3, + trigger = createMockAuthTrigger(true), + refreshAction = { fail("Should not refresh for network error") }, + action = { + actionCallCount.incrementAndGet() + throw CloudPluginException(RuntimeException("Quick failure")) + }, + ) + fail("Expected exception") + } catch (e: CloudPluginException) { + // Expected - verify the exception message + assertEquals("Quick failure", e.cause?.message) + } + + assertEquals(3, actionCallCount.get()) + } + + // Helper methods + + // ...existing code... + + private fun createMockAuthTrigger(shouldSucceed: Boolean): AuthTrigger { + return object : AuthTrigger { + override fun triggerAuthBlocking(openMode: OpenMode): Boolean { + return shouldSucceed + } + } + } + + private fun createProtocolException(): CloudPluginException { + val protocolException = ProtocolException("401 Unauthorized") + return CloudPluginException(protocolException) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/utils/smb/AbstractSubnetDiscoverDevicesStrategyTests.kt b/app/src/test/java/com/amaze/filemanager/utils/smb/AbstractSubnetDiscoverDevicesStrategyTests.kt index d937bc0c06..655694f528 100644 --- a/app/src/test/java/com/amaze/filemanager/utils/smb/AbstractSubnetDiscoverDevicesStrategyTests.kt +++ b/app/src/test/java/com/amaze/filemanager/utils/smb/AbstractSubnetDiscoverDevicesStrategyTests.kt @@ -21,7 +21,7 @@ package com.amaze.filemanager.utils.smb import android.os.Build.VERSION_CODES -import android.os.Build.VERSION_CODES.LOLLIPOP +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import androidx.test.ext.junit.runners.AndroidJUnit4 import com.amaze.filemanager.utils.NetworkUtil @@ -38,7 +38,7 @@ import java.net.InetAddress * Base class for [SmbDeviceScannerObservable.DiscoverDeviceStrategy] tests. */ @RunWith(AndroidJUnit4::class) -@Config(sdk = [LOLLIPOP, P, VERSION_CODES.R]) +@Config(sdk = [O, P, VERSION_CODES.R]) abstract class AbstractSubnetDiscoverDevicesStrategyTests { /** * Post test cleanup. diff --git a/app/src/testPlay/java/com/amaze/filemanager/filesystem/compressed/extractcontents/MultipartRarExtractorTest.kt b/app/src/testPlay/java/com/amaze/filemanager/filesystem/compressed/extractcontents/MultipartRarExtractorTest.kt index e2538e9cf8..02e485249f 100644 --- a/app/src/testPlay/java/com/amaze/filemanager/filesystem/compressed/extractcontents/MultipartRarExtractorTest.kt +++ b/app/src/testPlay/java/com/amaze/filemanager/filesystem/compressed/extractcontents/MultipartRarExtractorTest.kt @@ -44,7 +44,7 @@ import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) @Config( shadows = [ShadowMultiDex::class], - sdk = [Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.P], + sdk = [Build.VERSION_CODES.O, Build.VERSION_CODES.P], ) class MultipartRarExtractorTest { private val callback = diff --git a/app/src/testPlayRelease/java/com/amaze/filemanager/utils/PackageInstallValidationTest.kt b/app/src/testPlayRelease/java/com/amaze/filemanager/utils/PackageInstallValidationTest.kt index cf91812023..2e67f47fc9 100644 --- a/app/src/testPlayRelease/java/com/amaze/filemanager/utils/PackageInstallValidationTest.kt +++ b/app/src/testPlayRelease/java/com/amaze/filemanager/utils/PackageInstallValidationTest.kt @@ -25,6 +25,7 @@ import android.content.Context import android.content.pm.PackageInfo import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES.N +import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.P import android.os.storage.StorageManager import androidx.lifecycle.Lifecycle @@ -64,7 +65,7 @@ import java.util.concurrent.TimeUnit @SuppressLint("SdCardPath") @RunWith(AndroidJUnit4::class) @Config( - sdk = [P], + sdk = [O, P], shadows = [ShadowPackageManager::class, ShadowMultiDex::class, ShadowTabHandler::class], ) class PackageInstallValidationTest { diff --git a/build.gradle b/build.gradle index 379e3c33b2..0ee4edc882 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { plugins { id "com.diffplug.spotless" version "6.25.0" - id "com.google.devtools.ksp" version "1.9.25-1.0.20" apply false + id "com.google.devtools.ksp" version "2.1.20-2.0.1" apply false } allprojects { diff --git a/file_operations/build.gradle b/file_operations/build.gradle index 5400d64458..d8719104ca 100644 --- a/file_operations/build.gradle +++ b/file_operations/build.gradle @@ -122,7 +122,7 @@ dependencies { testImplementation libs.apache.sshd testImplementation libs.awaitility testImplementation libs.jsoup - kspTest libs.auto.service +// kspTest libs.auto.service androidTestImplementation libs.junit //tests the app logic androidTestImplementation libs.androidX.test.expresso diff --git a/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/CloudPluginException.java b/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/CloudPluginException.java deleted file mode 100644 index e95ede156f..0000000000 --- a/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/CloudPluginException.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.fileoperations.exceptions; - -/** Created by vishal on 18/4/17. */ -public class CloudPluginException extends Exception {} diff --git a/app/src/fdroid/java/com/cloudrail/si/services/Dropbox.java b/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/CloudPluginException.kt similarity index 73% rename from app/src/fdroid/java/com/cloudrail/si/services/Dropbox.java rename to file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/CloudPluginException.kt index 76d55c771e..6a4b95428a 100644 --- a/app/src/fdroid/java/com/cloudrail/si/services/Dropbox.java +++ b/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/CloudPluginException.kt @@ -17,16 +17,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +package com.amaze.filemanager.fileoperations.exceptions -package com.cloudrail.si.services; +/** Created by vishal on 18/4/17. */ +class CloudPluginException : Exception { + constructor() -import com.cloudrail.si.interfaces.CloudStorage; + constructor(message: String?) : super(message) -import android.content.Context; + constructor(message: String?, cause: Throwable?) : super(message, cause) -public class Dropbox extends CloudStorage { - - public Dropbox(Context unused1, String unused2, String unused3, String unused4, String unused5) { - super(); - } + constructor(cause: Throwable?) : super(cause) } diff --git a/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/ShellNotRunningException.java b/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/ShellNotRunningException.kt similarity index 83% rename from file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/ShellNotRunningException.java rename to file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/ShellNotRunningException.kt index 5b9d77253c..b9f48a8b45 100644 --- a/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/ShellNotRunningException.java +++ b/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/ShellNotRunningException.kt @@ -17,12 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +package com.amaze.filemanager.fileoperations.exceptions -package com.amaze.filemanager.fileoperations.exceptions; - -/** Created by vishal on 24/12/16. Exception thrown when root is */ -public class ShellNotRunningException extends Exception { - public ShellNotRunningException() { - super("Shell stopped running!"); - } -} +/** Created by vishal on 24/12/16. Exception thrown when root is */ +class ShellNotRunningException : Exception("Shell stopped running!") diff --git a/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/StreamNotFoundException.java b/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/StreamNotFoundException.java deleted file mode 100644 index b6cfb7e035..0000000000 --- a/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/StreamNotFoundException.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2014-2024 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.fileoperations.exceptions; - -/** - * Created by vishal on 21/1/17. - * - *

Exception thrown when we can't get stream after trying any specific methods - */ -public class StreamNotFoundException extends Exception { - private static final String MESSAGE = "Can't get stream"; - - public StreamNotFoundException() { - super(MESSAGE); - } - - public StreamNotFoundException(String message) { - super(message); - } - - public StreamNotFoundException(String message, Throwable cause) { - super(message, cause); - } - - public StreamNotFoundException(Throwable cause) { - super(MESSAGE, cause); - } -} diff --git a/app/src/fdroid/java/com/cloudrail/si/types/CloudMetaData.java b/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/StreamNotFoundException.kt similarity index 63% rename from app/src/fdroid/java/com/cloudrail/si/types/CloudMetaData.java rename to file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/StreamNotFoundException.kt index 4fa5b4e799..13784b1611 100644 --- a/app/src/fdroid/java/com/cloudrail/si/types/CloudMetaData.java +++ b/file_operations/src/main/java/com/amaze/filemanager/fileoperations/exceptions/StreamNotFoundException.kt @@ -17,31 +17,24 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +package com.amaze.filemanager.fileoperations.exceptions -package com.cloudrail.si.types; - -public class CloudMetaData { - public String getPath() { - return ""; - } - - public String getName() { - return ""; - } +/** + * Created by vishal on 21/1/17. + * + * + * Exception thrown when we can't get stream after trying any specific methods + */ +class StreamNotFoundException : Exception { + constructor() : super(MESSAGE) - public Long getModifiedAt() { - return 0L; - } + constructor(message: String?) : super(message) - public long getSize() { - return 0; - } + constructor(message: String?, cause: Throwable?) : super(message, cause) - public boolean getFolder() { - return false; - } + constructor(cause: Throwable?) : super(MESSAGE, cause) - public String getImageMetaData() { - return ""; - } + companion object { + private const val MESSAGE = "Can't get stream" + } } diff --git a/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/cloud/CloudStreamSourceTest.java b/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/cloud/CloudStreamSourceTest.java index c6d3bcee74..ca02f90435 100644 --- a/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/cloud/CloudStreamSourceTest.java +++ b/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/cloud/CloudStreamSourceTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.fileoperations.filesystem.cloud; -import static android.os.Build.VERSION_CODES.KITKAT; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -50,7 +50,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class}, - sdk = {KITKAT, P}) + sdk = {O, P}) public class CloudStreamSourceTest { private CloudStreamSource cs; private String testFilePath; diff --git a/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/smbstreamer/StreamSourceTest.java b/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/smbstreamer/StreamSourceTest.java index 9415dcf1f6..f34c10c321 100644 --- a/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/smbstreamer/StreamSourceTest.java +++ b/file_operations/src/test/java/com/amaze/filemanager/fileoperations/filesystem/smbstreamer/StreamSourceTest.java @@ -20,6 +20,7 @@ package com.amaze.filemanager.fileoperations.filesystem.smbstreamer; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -50,7 +51,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class, ShadowSmbFile.class}, - sdk = {P}) + sdk = {O, P}) public class StreamSourceTest { private SmbFile file; private StreamSource ss; diff --git a/gradle.properties b/gradle.properties index e9994ab628..d1c3d6b7eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,3 +28,4 @@ org.gradle.parallel=true android.disableResourceValidation=true # for macs, omit for other operating systems # org.gradle.java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home +android.includeDependencyInfoInApks=false \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e475d17e45..349c7ea33b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,10 @@ [versions] compileSdk = "34" -minSdk = "21" +jdk9Deps = "1.0" +jdk9DepsVersion = "1.0" +minSdk = "26" targetSdk = "35" -kotlin = "1.9.25" +kotlin = "2.1.20" ndk = "28.2.13676358" #r28c jacocoAndroid = "0.2.1" @@ -30,14 +32,17 @@ androidXPalette = "1.0.0" androidXCardView = "1.0.0" androidXConstraintLayout = "1.1.3" androidXBiometric = "1.1.0" +androidXSecurity = "1.0.0" +androidXBrowserCustomTabs = "1.5.0" androidXTest = "1.6.1" androidXTestRunner = "1.6.2" androidXTestExt = "1.2.1" androidXArchCoreTest = "2.2.0" androidXMultidex = "2.0.1" autoService = "1.1.1" -kotlinxCoroutineTest = "1.7.3" -kotlinStdlibJdk8 = "1.9.20" +kotlinxCoroutines = "1.10.2" +kotlinxCoroutineTest = "1.10.2" +kotlinStdlibJdk8 = "2.1.20" uiAutomator = "2.3.0" junit = "4.13.2" slf4j = "2.0.13" @@ -47,7 +52,6 @@ mockito = "4.11.0" mockitoInline = "4.11.0" mockitoKotlin = "4.1.0" androidBilling = "7.1.1" -cloudrailSiAndroid = "2.22.4" junrar = "7.4.0" vectordrawableAnimated = "1.2.0" xz = "1.9" @@ -73,11 +77,15 @@ mpAndroidChart = "v3.0.2" concurrentTrees = "2.6.1" aboutLibraries = "6.1.1" amazeTrashBin = "1.0.10" +retrofit = "2.9.0" +jackson = "2.15.2" rustAndroidGradle = "0.9.6" [libraries] gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } jacoco-android-plugin = { module = "com.mxalbert.gradle:jacoco-android", version.ref = "jacocoAndroid" } +#jdk9-deps = { module = "com.github.pengrad:jdk9-deps", version.ref = "jdk9Deps" } +jdk9-deps = { module = "com.github.pengrad:jdk9-deps", version.ref = "jdk9DepsVersion" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } rust-android-gradle = { module = "org.mozilla.rust-android-gradle.rust-android:org.mozilla.rust-android-gradle.rust-android.gradle.plugin", version.ref = "rustAndroidGradle" } @@ -96,6 +104,8 @@ androidX-palette = { module = "androidx.palette:palette-ktx", version.ref = "and androidX-preference = { module = "androidx.preference:preference-ktx", version.ref = "androidXPref" } androidX-vectordrawable-animated = { module = "androidx.vectordrawable:vectordrawable-animated", version.ref = "vectordrawableAnimated" } androidX-biometric = { module = "androidx.biometric:biometric", version.ref = "androidXBiometric" } +androidX-security = { module = "androidx.security:security-crypto", version.ref = "androidXSecurity" } +androidX-browser-customtabs = { module = "androidx.browser:browser", version.ref = "androidXBrowserCustomTabs" } androidX-test-core = { module = "androidx.test:core", version.ref = "androidXTest" } androidX-test-runner = { module = "androidx.test:runner", version.ref = "androidXTestRunner" } @@ -116,16 +126,18 @@ apache-ftpserver-core = { module = "org.apache.ftpserver:ftpserver-core", versio google-play-billing = { module = "com.android.billingclient:billing", version.ref = "androidBilling" } -cloudrail-si-android = { module = "com.cloudrail:cloudrail-si-android", version.ref = "cloudrailSiAndroid" } - eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" } concurrent-trees = { module = "com.github.npgall:concurrent-trees", version.ref = "concurrentTrees" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlinStdlibJdk8" } -kotlin-coroutine-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutineTest" } +kotlin-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "kotlinxCoroutines" } +kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +kotlin-coroutines-rxjava2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "kotlinxCoroutines" } +kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutineTest" } libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" } libsu-io = { module = "com.github.topjohnwu.libsu:io", version.ref = "libsu" } @@ -152,6 +164,7 @@ jcifs-ng = { module = "eu.agno3.jcifs:jcifs-ng", version.ref = "jcifs" } speedDial = { module = "com.leinardi.android:speed-dial", version.ref = "fabSpeedDial" } sshj = { module = "com.hierynomus:sshj", version.ref = "sshj" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okHttp" } +okhttp-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okHttp" } junit = { module = "junit:junit", version.ref = "junit" } awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" } @@ -178,3 +191,7 @@ acra-core = { module = "ch.acra:acra-core", version.ref = "acraCore" } leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" } amaze-trashBin = { module = "com.github.TeamAmaze:AmazeTrashBin", version.ref = "amazeTrashBin" } + +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofit-jacksonConverter = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" } +jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } \ No newline at end of file diff --git a/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java b/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java index c1f8919b50..6122ec648b 100644 --- a/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java +++ b/testShared/src/test/java/com/amaze/filemanager/test/ShadowPasswordUtilTest.java @@ -20,7 +20,7 @@ package com.amaze.filemanager.test; -import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; import static android.os.Build.VERSION_CODES.P; import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; @@ -56,7 +56,7 @@ @RunWith(AndroidJUnit4.class) @Config( shadows = {ShadowMultiDex.class, ShadowPasswordUtil.class}, - sdk = {LOLLIPOP, P, Build.VERSION_CODES.R}) + sdk = {O, P, Build.VERSION_CODES.R}) public class ShadowPasswordUtilTest { @Before