From 2bcc5632cc213beeb78349dd3c64305870feb92c Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Thu, 25 Dec 2025 17:31:09 +0500 Subject: [PATCH 01/37] Added new menu item --- app/src/main/res/menu/menu_mimetypes.xml | 5 +++++ app/src/main/res/values/strings.xml | 1 + 2 files changed, 6 insertions(+) diff --git a/app/src/main/res/menu/menu_mimetypes.xml b/app/src/main/res/menu/menu_mimetypes.xml index c043d04d9..7849d08e5 100644 --- a/app/src/main/res/menu/menu_mimetypes.xml +++ b/app/src/main/res/menu/menu_mimetypes.xml @@ -24,6 +24,11 @@ android:icon="@drawable/ic_change_view_vector" android:title="@string/change_view_type" app:showAsAction="ifRoom" /> + Others free Total storage: %s + Cloud Connection Enable root access From 59869f840097a19d772fbbe2807bd2a9055658f8 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sun, 1 Mar 2026 20:47:20 +0500 Subject: [PATCH 02/37] Added new cloud activity for SMB and WebDav connection --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 5 +++++ .../filemanager/activities/CloudActivity.kt | 15 +++++++++++++++ .../filemanager/activities/MainActivity.kt | 4 ++++ app/src/main/res/layout/cloud_activity.xml | 6 ++++++ app/src/main/res/menu/menu.xml | 5 +++++ gradle/libs.versions.toml | 2 ++ 7 files changed, 38 insertions(+) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt create mode 100644 app/src/main/res/layout/cloud_activity.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c35cf4ccc..8c69122e0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -142,5 +142,6 @@ dependencies { implementation(libs.gestureviews) implementation(libs.autofittextview) implementation(libs.zip4j) + implementation(libs.jcifs.ng) detektPlugins(libs.compose.detekt) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2684edd7f..578fc4161 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -77,6 +77,11 @@ android:configChanges="orientation" android:exported="false" android:parentActivityName=".activities.MainActivity" /> + launchMoreAppsFromUsIntent() R.id.settings -> launchSettings() R.id.about -> launchAbout() + R.id.cloud -> launchCloudActivity() else -> return@setOnMenuItemClickListener false } return@setOnMenuItemClickListener true @@ -260,6 +261,9 @@ class MainActivity : SimpleActivity() { } } + private fun launchCloudActivity(){ + startActivity(Intent(applicationContext, CloudActivity::class.java)) + } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString(PICKED_PATH, getItemsFragment()?.currentPath ?: "") diff --git a/app/src/main/res/layout/cloud_activity.xml b/app/src/main/res/layout/cloud_activity.xml new file mode 100644 index 000000000..f9504c9a4 --- /dev/null +++ b/app/src/main/res/layout/cloud_activity.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/menu/menu.xml b/app/src/main/res/menu/menu.xml index fbe716715..9e2098336 100644 --- a/app/src/main/res/menu/menu.xml +++ b/app/src/main/res/menu/menu.xml @@ -23,6 +23,11 @@ android:icon="@drawable/ic_star_outline_vector" android:title="@string/add_to_favorites" app:showAsAction="ifRoom" /> + Date: Wed, 18 Mar 2026 21:47:51 +0500 Subject: [PATCH 03/37] Added smb --- app/src/main/AndroidManifest.xml | 4 +- .../filemanager/activities/CloudActivity.kt | 66 +++++++++++++ .../filemanager/dialogs/ConnectionDialog.kt | 23 +++++ app/src/main/res/layout/cloud_activity.xml | 47 +++++++++- .../main/res/layout/dialog_add_connection.xml | 93 +++++++++++++++++++ gradle.properties | 1 + 6 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt create mode 100644 app/src/main/res/layout/dialog_add_connection.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 578fc4161..d381da2eb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,9 @@ - + + + diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 512ac4497..dda236572 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -1,8 +1,23 @@ package org.fossify.filemanager.activities import android.os.Bundle +import android.util.Log +import android.widget.Toast +import jcifs.CIFSContext +import jcifs.Configuration +import jcifs.config.PropertyConfiguration +import jcifs.context.BaseContext +import jcifs.smb.NtlmPasswordAuthenticator +import jcifs.smb.SmbFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.fossify.commons.extensions.toast import org.fossify.commons.extensions.viewBinding +import org.fossify.commons.helpers.NavigationIcon import org.fossify.filemanager.databinding.CloudActivityBinding +import org.fossify.filemanager.dialogs.ConnectionDialog + class CloudActivity: SimpleActivity() { private val binding by viewBinding(CloudActivityBinding::inflate) @@ -10,6 +25,57 @@ class CloudActivity: SimpleActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) + binding.apply { + setupMaterialScrollListener(binding.cloudNestedScrollview, binding.cloudAppbar) + } + setupToolBar() + registerAddConnectionListener() + } + + + + private fun setupToolBar(){ + setupTopAppBar(binding.cloudAppbar, NavigationIcon.Arrow) + } + + private fun showConnectionDialog(){ + ConnectionDialog(this@CloudActivity){host,user,password,shared-> + listFiles(host,user,password,shared) + } + } + + private fun registerAddConnectionListener(){ + binding.addButton.setOnClickListener { + showConnectionDialog() + } + } + + private fun listFiles(host: String, user: String, password: String,shared: String){ + CoroutineScope(Dispatchers.IO).launch { + try { + val config: Configuration = PropertyConfiguration(System.getProperties()) + val context: CIFSContext = BaseContext(config) + val auth = NtlmPasswordAuthenticator( + "", + user, + password + ) + val authContext = context.withCredentials(auth) + val smbUrl = "smb://$host/$shared" + + val dir = SmbFile(smbUrl, authContext) + + val files = dir.listFiles() + + for (file in files) { + Log.d("Loading Files", file.name) + } + + } catch (e: Exception) { + Log.e("File Load Failed",e.toString()) + toast(e.message.toString(), Toast.LENGTH_LONG) + } + } } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt new file mode 100644 index 000000000..24ecbfedb --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -0,0 +1,23 @@ +package org.fossify.filemanager.dialogs + +import org.fossify.commons.activities.BaseSimpleActivity +import org.fossify.commons.extensions.getAlertDialogBuilder +import org.fossify.commons.extensions.setupDialogStuff +import org.fossify.commons.extensions.value +import org.fossify.filemanager.R +import org.fossify.filemanager.databinding.DialogAddConnectionBinding +import org.fossify.filemanager.databinding.DialogChangeViewTypeBinding + +class ConnectionDialog(val activity: BaseSimpleActivity, dispatch:(String, String, String, String)-> Unit) { + private var binding: DialogAddConnectionBinding + + init { + binding = DialogAddConnectionBinding.inflate(activity.layoutInflater) + activity.getAlertDialogBuilder() + .setPositiveButton(R.string.ok) { dialog, which -> dispatch(binding.hostEt.value,binding.userEt.value,binding.passwordEt.value,binding.sharedPathEt.value) } + .setNegativeButton(R.string.cancel, null) + .apply { + activity.setupDialogStuff(binding.root, this) + } + } +} diff --git a/app/src/main/res/layout/cloud_activity.xml b/app/src/main/res/layout/cloud_activity.xml index f9504c9a4..df8ece541 100644 --- a/app/src/main/res/layout/cloud_activity.xml +++ b/app/src/main/res/layout/cloud_activity.xml @@ -1,6 +1,49 @@ - - + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_add_connection.xml b/app/src/main/res/layout/dialog_add_connection.xml new file mode 100644 index 000000000..2b40b173b --- /dev/null +++ b/app/src/main/res/layout/dialog_add_connection.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties index 29e44085a..f1552a7ef 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,3 +23,4 @@ org.gradle.jvmargs=-Xmx4g VERSION_NAME=1.5.0 VERSION_CODE=11 APP_ID=org.fossify.filemanager +org.gradle.java.home=C:\\Users\\ba269\\.jdks\\ms-17.0.17 From b68ebbdafefd4c8f7d22a8fb04120629b27c864f Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Mon, 30 Mar 2026 23:21:59 +0500 Subject: [PATCH 04/37] Integrated smb --- app/build.gradle.kts | 6 ++ .../kotlin/org/fossify/filemanager/App.kt | 4 +- .../filemanager/activities/CloudActivity.kt | 89 ++++++++++++++++--- .../filemanager/activities/MainActivity.kt | 39 +++++--- .../adapters/ConnectionItemsAdapter.kt | 80 +++++++++++++++++ .../filemanager/dao/NetworkConnectionDao.kt | 18 ++++ .../database/NetworkConnectionDatabase.kt | 11 +++ .../dependencies/AppComposition.kt | 32 +++++++ .../filemanager/dialogs/ConnectionDialog.kt | 4 +- .../entity/NetworkConnectionEntity.kt | 15 ++++ .../filemanager/enums/ConnectionTypes.kt | 6 ++ .../factory/NetworkBrowserViewModelFactory.kt | 13 +++ .../filemanager/fileSystems/FileHelpers.kt | 36 ++++++++ .../filemanager/fileSystems/MimeTypes.kt | 17 ++++ .../filemanager/fragments/ItemsFragment.kt | 44 +++++++-- .../fossify/filemanager/helpers/Constants.kt | 5 ++ .../NetworkConnectionRepositoryApi.kt | 10 +++ .../NetworkConnectionRepositoryDb.kt | 11 +++ .../mapper/NetworkConnectionMapper.kt | 43 +++++++++ .../fossify/filemanager/models/ListItem.kt | 2 +- .../filemanager/models/NetworkConnection.kt | 10 +++ .../NetworkConnectionRepositoryApiImpl.kt | 48 ++++++++++ .../NetworkConnectionRepositoryDbImpl.kt | 26 ++++++ .../viewmodels/NetworkBrowserViewModel.kt | 46 ++++++++++ app/src/main/res/layout/cloud_activity.xml | 21 +++-- .../main/res/layout/dialog_add_connection.xml | 14 +++ .../res/layout/item_network_connection.xml | 46 ++++++++++ build.gradle.kts | 1 + gradle/libs.versions.toml | 2 + 29 files changed, 655 insertions(+), 44 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/database/NetworkConnectionDatabase.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/factory/NetworkBrowserViewModelFactory.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/fileSystems/MimeTypes.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt create mode 100644 app/src/main/res/layout/item_network_connection.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8c69122e0..943786978 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.android) alias(libs.plugins.kotlinAndroid) alias(libs.plugins.detekt) + id("com.google.devtools.ksp") } val keystorePropertiesFile: File = rootProject.file("keystore.properties") @@ -144,4 +145,9 @@ dependencies { implementation(libs.zip4j) implementation(libs.jcifs.ng) detektPlugins(libs.compose.detekt) + + val roomVersion = "2.7.0-alpha11" + implementation("androidx.room:room-runtime:$roomVersion") + ksp("androidx.room:room-compiler:$roomVersion") + implementation (libs.androidx.room.ktx) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/App.kt b/app/src/main/kotlin/org/fossify/filemanager/App.kt index 499db7a17..41a366f8a 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/App.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/App.kt @@ -2,12 +2,14 @@ package org.fossify.filemanager import com.github.ajalt.reprint.core.Reprint import org.fossify.commons.FossifyApp +import org.fossify.filemanager.dependencies.AppComposition class App : FossifyApp() { + lateinit var appComposition: AppComposition override val isAppLockFeatureAvailable = true - override fun onCreate() { super.onCreate() Reprint.initialize(this) + appComposition = AppComposition(this) } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index dda236572..810f6f0de 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -1,8 +1,11 @@ package org.fossify.filemanager.activities +import android.content.Intent import android.os.Bundle import android.util.Log import android.widget.Toast +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import jcifs.CIFSContext import jcifs.Configuration import jcifs.config.PropertyConfiguration @@ -11,46 +14,63 @@ import jcifs.smb.NtlmPasswordAuthenticator import jcifs.smb.SmbFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.fossify.commons.extensions.toast import org.fossify.commons.extensions.viewBinding import org.fossify.commons.helpers.NavigationIcon +import org.fossify.filemanager.App +import org.fossify.filemanager.adapters.ConnectionItemsAdapter +import org.fossify.filemanager.adapters.DecompressItemsAdapter import org.fossify.filemanager.databinding.CloudActivityBinding import org.fossify.filemanager.dialogs.ConnectionDialog +import org.fossify.filemanager.enums.ConnectionTypes +import org.fossify.filemanager.helpers.NETWORK_PATH +import org.fossify.filemanager.helpers.PATH +import org.fossify.filemanager.models.ListItem +import org.fossify.filemanager.models.NetworkConnection +import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel -class CloudActivity: SimpleActivity() { +class CloudActivity : SimpleActivity() { private val binding by viewBinding(CloudActivityBinding::inflate) + private lateinit var viewModel: NetworkBrowserViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) binding.apply { - setupMaterialScrollListener(binding.cloudNestedScrollview, binding.cloudAppbar) + setupMaterialScrollListener(binding.connectionsList, binding.cloudAppbar) } setupToolBar() registerAddConnectionListener() - } + val composition = (application as App).appComposition + val factory = composition.provideNetworkBrowserViewModelFactory() + viewModel = ViewModelProvider(this, factory) + .get(NetworkBrowserViewModel::class.java) + getAllSavedNetworks() + } - private fun setupToolBar(){ + private fun setupToolBar() { setupTopAppBar(binding.cloudAppbar, NavigationIcon.Arrow) } - private fun showConnectionDialog(){ - ConnectionDialog(this@CloudActivity){host,user,password,shared-> - listFiles(host,user,password,shared) + private fun showConnectionDialog() { + ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName -> + listFiles(host, user, password, shared, displayName) } } - private fun registerAddConnectionListener(){ + private fun registerAddConnectionListener() { binding.addButton.setOnClickListener { showConnectionDialog() } } - private fun listFiles(host: String, user: String, password: String,shared: String){ + private fun listFiles(host: String, user: String, password: String, shared: String, displayName: String) { CoroutineScope(Dispatchers.IO).launch { try { val config: Configuration = PropertyConfiguration(System.getProperties()) @@ -64,7 +84,18 @@ class CloudActivity: SimpleActivity() { val smbUrl = "smb://$host/$shared" val dir = SmbFile(smbUrl, authContext) - + Log.d("Display Name", displayName) + if (dir.exists()) + viewModel.saveNetwork( + NetworkConnection( + host = host, + username = user, + password = password, + sharedPath = shared, + connectionType = ConnectionTypes.SMB.type, + displayName = displayName + ) + ) val files = dir.listFiles() for (file in files) { @@ -72,10 +103,46 @@ class CloudActivity: SimpleActivity() { } } catch (e: Exception) { - Log.e("File Load Failed",e.toString()) + Log.e("File Load Failed", e.toString()) toast(e.message.toString(), Toast.LENGTH_LONG) } + } } + private fun getAllSavedNetworks() { + viewModel.getAllSavedNetworks() + collectSavedNetworks() + } + + private fun collectSavedNetworks() { + lifecycleScope.launch { + viewModel.savedNetworks.collectLatest { + if (it.isNotEmpty()) + updateAdapter(it.toMutableList()) + } + } + } + + private fun updateAdapter(listItems: MutableList) { + ConnectionItemsAdapter(this, listItems, binding.connectionsList) { item -> + viewModel.verifyNetwork(item as NetworkConnection) + lifecycleScope.launch { + viewModel.verifyNetwork.collectLatest { value -> + if (value) { + val path = "${item.host.trimEnd('/')}/${item.sharedPath.trimStart('/')}" + startActivity(Intent(this@CloudActivity, MainActivity::class.java).apply { + putExtra(PATH, path) + putExtra(NETWORK_PATH, true) + }) + } else { + Toast.makeText(this@CloudActivity, "Connection failed", Toast.LENGTH_SHORT).show() + } + } + } + + }.apply { + binding.connectionsList.adapter = this + } + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt index 71be1cfc8..24f388b3d 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt @@ -7,6 +7,7 @@ import android.graphics.drawable.Drawable import android.media.RingtoneManager import android.os.Bundle import android.os.Handler +import android.util.Log import android.widget.ImageView import android.widget.TextView import androidx.viewpager.widget.ViewPager @@ -21,7 +22,6 @@ import org.fossify.commons.extensions.getBottomNavigationBackgroundColor import org.fossify.commons.extensions.getColoredDrawableWithColor import org.fossify.commons.extensions.getFilePublicUri import org.fossify.commons.extensions.getMimeType -import org.fossify.commons.extensions.getProperBackgroundColor import org.fossify.commons.extensions.getProperTextColor import org.fossify.commons.extensions.getRealPathFromURI import org.fossify.commons.extensions.getStorageDirectories @@ -70,13 +70,15 @@ import org.fossify.filemanager.fragments.MyViewPagerFragment import org.fossify.filemanager.fragments.RecentsFragment import org.fossify.filemanager.fragments.StorageFragment import org.fossify.filemanager.helpers.MAX_COLUMN_COUNT +import org.fossify.filemanager.helpers.NETWORK_PATH +import org.fossify.filemanager.helpers.PATH import org.fossify.filemanager.helpers.RootHelpers import org.fossify.filemanager.interfaces.ItemOperationsListener import java.io.File class MainActivity : SimpleActivity() { override var isSearchBarEnabled = true - + companion object { private const val BACK_PRESS_TIMEOUT = 5000 private const val PICKED_PATH = "picked_path" @@ -115,13 +117,20 @@ class MainActivity : SimpleActivity() { if (savedInstanceState == null) { config.temporarilyShowHidden = false initFragments() - tryInitFileManager() + val data = getIntentDataIfAny() + tryInitFileManager(data.first,data.second) checkWhatsNewDialog() checkIfRootAvailable() checkInvalidFavorites() } } + private fun getIntentDataIfAny(): Pair { + val path = intent.getStringExtra(PATH) + val isNetworkPath = intent.getBooleanExtra(NETWORK_PATH, false) + return Pair(path ?: "",isNetworkPath) + } + override fun onResume() { super.onResume() if (mStoredShowTabs != config.showTabs) { @@ -261,9 +270,10 @@ class MainActivity : SimpleActivity() { } } - private fun launchCloudActivity(){ + private fun launchCloudActivity() { startActivity(Intent(applicationContext, CloudActivity::class.java)) } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString(PICKED_PATH, getItemsFragment()?.currentPath ?: "") @@ -295,7 +305,7 @@ class MainActivity : SimpleActivity() { } } - private fun tryInitFileManager() { + private fun tryInitFileManager(path: String, isNetworkPath : Boolean = false) { val hadPermission = hasStoragePermission() handleStoragePermission { checkOTGPath() @@ -305,7 +315,7 @@ class MainActivity : SimpleActivity() { } binding.mainViewPager.onGlobalLayout { - initFileManager(!hadPermission) + initFileManager(!hadPermission,path,isNetworkPath) } } else { toast(R.string.no_storage_permissions) @@ -314,7 +324,8 @@ class MainActivity : SimpleActivity() { } } - private fun initFileManager(refreshRecents: Boolean) { + private fun initFileManager(refreshRecents: Boolean, path: String, isNetworkPath: Boolean) { + Log.d("File Path",path) if (intent.action == Intent.ACTION_VIEW && intent.data != null) { val data = intent.data if (data?.scheme == "file") { @@ -334,7 +345,7 @@ class MainActivity : SimpleActivity() { binding.mainViewPager.currentItem = 0 } else { - openPath(config.homeFolder) + openPath(if(path.isNotEmpty()) path else config.homeFolder,isNetworkPath = isNetworkPath) } if (refreshRecents) { @@ -371,8 +382,8 @@ class MainActivity : SimpleActivity() { binding.mainTabsHolder.removeAllTabs() val action = intent.action val isPickFileIntent = action == RingtoneManager.ACTION_RINGTONE_PICKER - || action == Intent.ACTION_GET_CONTENT - || action == Intent.ACTION_PICK + || action == Intent.ACTION_GET_CONTENT + || action == Intent.ACTION_PICK val isCreateDocumentIntent = action == Intent.ACTION_CREATE_DOCUMENT if (isPickFileIntent) { @@ -460,18 +471,18 @@ class MainActivity : SimpleActivity() { } } - private fun openPath(path: String, forceRefresh: Boolean = false) { + private fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false) { var newPath = path val file = File(path) if (config.OTGPath.isNotEmpty() && config.OTGPath == path.trimEnd('/')) { newPath = path - } else if (file.exists() && !file.isDirectory) { + } else if (file.exists() && !file.isDirectory && !isNetworkPath) { newPath = file.parent - } else if (!file.exists() && !isPathOnOTG(newPath)) { + } else if (!file.exists() && !isPathOnOTG(newPath) && !isNetworkPath) { newPath = internalStoragePath } - getItemsFragment()?.openPath(newPath, forceRefresh) + getItemsFragment()?.openPath(newPath, forceRefresh, isNetworkPath) } private fun goHome() { diff --git a/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt b/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt new file mode 100644 index 000000000..3a104d844 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt @@ -0,0 +1,80 @@ +package org.fossify.filemanager.adapters + +import android.util.TypedValue +import android.view.Menu +import android.view.View +import android.view.ViewGroup +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.bumptech.glide.request.RequestOptions +import org.fossify.commons.adapters.MyRecyclerViewAdapter +import org.fossify.commons.views.MyRecyclerView +import org.fossify.filemanager.activities.SimpleActivity +import org.fossify.filemanager.databinding.ItemDecompressionListFileDirBinding +import org.fossify.filemanager.databinding.ItemNetworkConnectionBinding +import org.fossify.filemanager.models.ListItem +import org.fossify.filemanager.models.NetworkConnection +import java.util.Locale + +class ConnectionItemsAdapter(activity: SimpleActivity, var listItems: MutableList, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit) : + MyRecyclerViewAdapter(activity, recyclerView, itemClick) { + override fun getActionMenuId(): Int { + TODO("Not yet implemented") + } + + override fun prepareActionMode(menu: Menu) { + TODO("Not yet implemented") + } + + override fun actionItemPressed(id: Int) { + TODO("Not yet implemented") + } + + override fun getSelectableItemCount(): Int { + TODO("Not yet implemented") + } + + override fun getIsItemSelectable(position: Int): Boolean { + TODO("Not yet implemented") + } + + override fun getItemSelectionKey(position: Int): Int? { + TODO("Not yet implemented") + } + + override fun getItemKeyPosition(key: Int): Int { + TODO("Not yet implemented") + } + + override fun onActionModeCreated() { + TODO("Not yet implemented") + } + + override fun onActionModeDestroyed() { + TODO("Not yet implemented") + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return createViewHolder(ItemNetworkConnectionBinding.inflate(layoutInflater, parent, false).root) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val fileDirItem = listItems[position] + holder.bindView(fileDirItem, true, false) { itemView, layoutPosition -> + setupView(itemView, fileDirItem) + } + bindViewHolder(holder) + } + + override fun getItemCount() = listItems.size + + private fun setupView(view: View, listItem: NetworkConnection) { + ItemNetworkConnectionBinding.bind(view).apply { + tvHost.text = listItem.host + tvType.text = listItem.connectionType + tvDisplayName.text = listItem.displayName + tvSharedPath.text = listItem.sharedPath + } + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt b/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt new file mode 100644 index 000000000..f76273641 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt @@ -0,0 +1,18 @@ +package org.fossify.filemanager.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow +import org.fossify.filemanager.entity.NetworkConnectionEntity + +@Dao +interface NetworkConnectionDao { + @Query("SELECT * FROM network_connections") + fun getAll(): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(connection: NetworkConnectionEntity) +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/database/NetworkConnectionDatabase.kt b/app/src/main/kotlin/org/fossify/filemanager/database/NetworkConnectionDatabase.kt new file mode 100644 index 000000000..a7b6ccaf0 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/database/NetworkConnectionDatabase.kt @@ -0,0 +1,11 @@ +package org.fossify.filemanager.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import org.fossify.filemanager.dao.NetworkConnectionDao +import org.fossify.filemanager.entity.NetworkConnectionEntity + +@Database(entities = [NetworkConnectionEntity::class], version = 1) +abstract class NetworkConnectionDatabase : RoomDatabase() { + abstract fun networkConnectionDao(): NetworkConnectionDao +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt b/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt new file mode 100644 index 000000000..a2c63c98e --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt @@ -0,0 +1,32 @@ +package org.fossify.filemanager.dependencies + +import android.content.Context +import androidx.room.Room +import org.fossify.filemanager.database.NetworkConnectionDatabase +import org.fossify.filemanager.factory.NetworkBrowserViewModelFactory +import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb +import org.fossify.filemanager.repository.NetworkConnectionRepositoryApiImpl +import org.fossify.filemanager.repository.NetworkConnectionRepositoryDbImpl + +class AppComposition (private val context: Context) { + + private fun createDataBase(context: Context): NetworkConnectionDatabase { + return Room.databaseBuilder(context.applicationContext, NetworkConnectionDatabase::class.java,"app-db").build() + } + + private val database by lazy { + createDataBase(context) + } + + val networkDbRepository by lazy { + NetworkConnectionRepositoryDbImpl(database.networkConnectionDao()) + } + + val networkApiRepository by lazy { + NetworkConnectionRepositoryApiImpl() + } + + fun provideNetworkBrowserViewModelFactory(): NetworkBrowserViewModelFactory{ + return NetworkBrowserViewModelFactory(networkDbRepository,networkApiRepository) + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 24ecbfedb..41fcd4f3a 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -8,13 +8,13 @@ import org.fossify.filemanager.R import org.fossify.filemanager.databinding.DialogAddConnectionBinding import org.fossify.filemanager.databinding.DialogChangeViewTypeBinding -class ConnectionDialog(val activity: BaseSimpleActivity, dispatch:(String, String, String, String)-> Unit) { +class ConnectionDialog(val activity: BaseSimpleActivity, dispatch:(String, String, String, String, String)-> Unit) { private var binding: DialogAddConnectionBinding init { binding = DialogAddConnectionBinding.inflate(activity.layoutInflater) activity.getAlertDialogBuilder() - .setPositiveButton(R.string.ok) { dialog, which -> dispatch(binding.hostEt.value,binding.userEt.value,binding.passwordEt.value,binding.sharedPathEt.value) } + .setPositiveButton(R.string.ok) { _,_ -> dispatch(binding.hostEt.value,binding.userEt.value,binding.passwordEt.value,binding.sharedPathEt.value,binding.displayEt.value) } .setNegativeButton(R.string.cancel, null) .apply { activity.setupDialogStuff(binding.root, this) diff --git a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt new file mode 100644 index 000000000..f5f573bc5 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt @@ -0,0 +1,15 @@ +package org.fossify.filemanager.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey +@Entity(tableName = "network_connections") +data class NetworkConnectionEntity( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val host: String, + val port: Int = 445, + val username: String?, + val password: String?, + val displayName: String, + val connectionType: String, + val sharedPath: String +) diff --git a/app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt b/app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt new file mode 100644 index 000000000..e681b9385 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt @@ -0,0 +1,6 @@ +package org.fossify.filemanager.enums + +enum class ConnectionTypes(val type: String) { + SMB("SMB"), + WebDav("WebDav") +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/factory/NetworkBrowserViewModelFactory.kt b/app/src/main/kotlin/org/fossify/filemanager/factory/NetworkBrowserViewModelFactory.kt new file mode 100644 index 000000000..4a45bbf43 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/factory/NetworkBrowserViewModelFactory.kt @@ -0,0 +1,13 @@ +package org.fossify.filemanager.factory + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi +import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb +import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel + +class NetworkBrowserViewModelFactory(private val networkConnectionRepositoryDb: NetworkConnectionRepositoryDb, private val networkConnectionRepositoryApi: NetworkConnectionRepositoryApi): ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return NetworkBrowserViewModel(networkConnectionRepositoryDb,networkConnectionRepositoryApi) as T + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt new file mode 100644 index 000000000..403e3c79d --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -0,0 +1,36 @@ +package org.fossify.filemanager.fileSystems + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.util.Log +import androidx.core.net.toUri +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.fossify.filemanager.models.ListItem +import java.io.File + +object FileHelpers { + val URL: String = "http://127.0.0.1:7871/" + fun launchSMB(item: ListItem, context: Context) { + try { + CoroutineScope(Dispatchers.Main).launch { + val uri = "${URL}${item.parent}${(item.path.toUri()).path}".toUri() + kotlinx . coroutines . delay (50) + val i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) + val packageManager: PackageManager = context.packageManager + val resInfos = packageManager.queryIntentActivities(i, 0) + if (resInfos.size > 0){ context.startActivity(i) } } } + catch(exp: Exception) { + Log.e("Activity Launch Failed", exp.toString()) + } + } + } + + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/MimeTypes.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/MimeTypes.kt new file mode 100644 index 000000000..8dec85d78 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/MimeTypes.kt @@ -0,0 +1,17 @@ +package org.fossify.filemanager.fileSystems + +import android.webkit.MimeTypeMap +import java.util.Locale.getDefault + +object MimeTypes { + fun getMimeTypes(path: String?): String? { + return getFileExtension(path) + } + + private fun getFileExtension(path: String?): String? { + var extension: String? = path?.substring(path.lastIndexOf(".") + 1)?.lowercase(getDefault()) + val mime = MimeTypeMap.getSingleton() + extension = mime.getMimeTypeFromExtension(extension) + return extension + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index d7589648c..b8be123dd 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.os.Parcelable import android.util.AttributeSet +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.dialogs.StoragePickerDialog @@ -13,6 +14,7 @@ import org.fossify.commons.models.FileDirItem import org.fossify.commons.views.Breadcrumbs import org.fossify.commons.views.MyGridLayoutManager import org.fossify.commons.views.MyRecyclerView +import org.fossify.filemanager.App import org.fossify.filemanager.R import org.fossify.filemanager.activities.MainActivity import org.fossify.filemanager.activities.SimpleActivity @@ -21,15 +23,19 @@ import org.fossify.filemanager.databinding.ItemsFragmentBinding import org.fossify.filemanager.dialogs.CreateNewItemDialog import org.fossify.filemanager.extensions.config import org.fossify.filemanager.extensions.isPathOnRoot +import org.fossify.filemanager.fileSystems.FileHelpers import org.fossify.filemanager.helpers.MAX_COLUMN_COUNT import org.fossify.filemanager.helpers.RootHelpers import org.fossify.filemanager.interfaces.ItemOperationsListener +import org.fossify.filemanager.mapper.toFileItem import org.fossify.filemanager.models.ListItem +import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel import java.io.File class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), ItemOperationsListener, Breadcrumbs.BreadcrumbsListener { + private lateinit var viewModel: NetworkBrowserViewModel private var showHidden = false private var lastSearchedText = "" private var scrollStates = HashMap() @@ -59,6 +65,16 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } } + setUpViewModel() + } + } + + private fun setUpViewModel() { + val composition = (activity?.application as App).appComposition + val factory = composition.provideNetworkBrowserViewModelFactory() + activity?.let { + viewModel = ViewModelProvider(it, factory) + .get(NetworkBrowserViewModel::class.java) } } @@ -99,7 +115,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF getRecyclerAdapter()?.finishActMode() } - fun openPath(path: String, forceRefresh: Boolean = false) { + fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false) { if ((activity as? BaseSimpleActivity)?.isAskingPermissions == true) { return } @@ -113,12 +129,17 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF currentPath = realPath showHidden = context!!.config.shouldShowHidden() showProgressBar() - getItems(currentPath) { originalPath, listItems -> + getItems(currentPath, isNetworkPath) { originalPath, listItems -> if (currentPath != originalPath) { return@getItems } FileDirItem.sorting = context!!.config.getFolderSorting(currentPath) + if(isNetworkPath){ + listItems.forEach { + it.parent = originalPath.substringAfter('/') + } + } listItems.sort() if (context!!.config.getFolderViewType(currentPath) == VIEW_TYPE_GRID && listItems.none { it.isSectionTitle }) { @@ -134,7 +155,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF itemsIgnoringSearch = listItems activity?.runOnUiThread { (activity as? MainActivity)?.refreshMenuItems() - addItems(listItems, forceRefresh) + addItems(listItems, forceRefresh,isNetworkPath) if (context != null && currentViewType != context!!.config.getFolderViewType(currentPath)) { setupLayoutManager() } @@ -143,7 +164,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } - private fun addItems(items: ArrayList, forceRefresh: Boolean = false) { + private fun addItems(items: ArrayList, forceRefresh: Boolean = false,isNetworkPath: Boolean = false) { activity?.runOnUiThread { binding.itemsSwipeRefresh.isRefreshing = false binding.breadcrumbs.setBreadcrumb(currentPath) @@ -157,7 +178,12 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } ItemsAdapter(activity as SimpleActivity, storedItems, this, binding.itemsList, isPickMultipleIntent, binding.itemsSwipeRefresh) { - if ((it as? ListItem)?.isSectionTitle == true) { + if(isNetworkPath){ + (it as? ListItem)?.let { item -> + FileHelpers.launchSMB(item, this@ItemsFragment.context) + } + } + else if ((it as? ListItem)?.isSectionTitle == true) { openDirectory(it.mPath) searchClosed() } else { @@ -181,11 +207,15 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF private fun getRecyclerLayoutManager() = (binding.itemsList.layoutManager as MyGridLayoutManager) @SuppressLint("NewApi") - private fun getItems(path: String, callback: (originalPath: String, items: ArrayList) -> Unit) { + private fun getItems(path: String, isNetworkPath: Boolean = false, callback: (originalPath: String, items: ArrayList) -> Unit) { ensureBackgroundThread { if (activity?.isDestroyed == false && activity?.isFinishing == false) { val config = context!!.config - if (context.isRestrictedSAFOnlyRoot(path)) { + if (isNetworkPath) { + val fileItems = viewModel.getFilesFromNetworkPath() + val items = fileItems.map { it -> it.toFileItem() } + callback(path, getListItemsFromFileDirItems(ArrayList(items.toList()))) + } else if (context.isRestrictedSAFOnlyRoot(path)) { activity?.runOnUiThread { hideProgressBar() } activity?.handleAndroidSAFDialog(path, openInSystemAppAllowed = true) { if (!it) { diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt index 6df79baf1..e116f199e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt @@ -44,6 +44,11 @@ const val SHOW_MIMETYPE = "show_mimetype" const val VOLUME_NAME = "volume_name" const val PRIMARY_VOLUME_NAME = "external_primary" +const val PATH = "path" + +const val NETWORK_PATH = "network_path" + + // what else should we count as an audio except "audio/*" mimetype val extraAudioMimeTypes = arrayListOf("application/ogg") val extraDocumentMimeTypes = arrayListOf( diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt new file mode 100644 index 000000000..fa0ebfa85 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt @@ -0,0 +1,10 @@ +package org.fossify.filemanager.interfaces + +import jcifs.smb.SmbFile +import org.fossify.filemanager.models.NetworkConnection + +interface NetworkConnectionRepositoryApi { + suspend fun verifyConnection(connection: NetworkConnection): Boolean + + fun getFilesFromNetworkPath(): Array +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt new file mode 100644 index 000000000..cac65cf6b --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt @@ -0,0 +1,11 @@ +package org.fossify.filemanager.interfaces + +import kotlinx.coroutines.flow.Flow +import org.fossify.filemanager.entity.NetworkConnectionEntity +import org.fossify.filemanager.models.NetworkConnection + +interface NetworkConnectionRepositoryDb { + suspend fun saveConnection(connection: NetworkConnection); + suspend fun getAllSavedConnections(): Flow> + suspend fun deleteConnection() +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt new file mode 100644 index 000000000..a78d84a40 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -0,0 +1,43 @@ +package org.fossify.filemanager.mapper + +import android.util.Log +import jcifs.smb.SmbFile +import org.fossify.commons.models.FileDirItem +import org.fossify.filemanager.entity.NetworkConnectionEntity +import org.fossify.filemanager.models.NetworkConnection + +fun NetworkConnectionEntity.toDomain(): NetworkConnection { + return NetworkConnection( + host = host, + port = port, + username = username, + password = password, + displayName = displayName, + connectionType = connectionType, + sharedPath = sharedPath + ) +} + +fun NetworkConnection.toEntity(): NetworkConnectionEntity { + return NetworkConnectionEntity( + host = host, + port = port, + username = username, + password = password, + displayName = displayName, + connectionType = connectionType, + sharedPath = sharedPath + ) +} + +fun SmbFile.toFileItem(): FileDirItem { + return FileDirItem( + path = this.path, + name = this.name.trimEnd('/'), + isDirectory = this.isDirectory, + size = if (!this.isDirectory) this.length() else 0L, + modified = this.lastModified(), + children = 0, + mediaStoreId = 0L + ) +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/ListItem.kt b/app/src/main/kotlin/org/fossify/filemanager/models/ListItem.kt index 8bf14583a..9b5573559 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/models/ListItem.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/models/ListItem.kt @@ -5,5 +5,5 @@ import org.fossify.commons.models.FileDirItem // isSectionTitle is used only at search results for showing the current folders path data class ListItem( val mPath: String, val mName: String = "", var mIsDirectory: Boolean = false, var mChildren: Int = 0, var mSize: Long = 0L, var mModified: Long = 0L, - var isSectionTitle: Boolean, val isGridTypeDivider: Boolean + var isSectionTitle: Boolean, val isGridTypeDivider: Boolean, var parent: String = "" ) : FileDirItem(mPath, mName, mIsDirectory, mChildren, mSize, mModified) diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt new file mode 100644 index 000000000..0419bc9b5 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt @@ -0,0 +1,10 @@ +package org.fossify.filemanager.models + +import org.fossify.filemanager.entity.NetworkConnectionEntity + +data class NetworkConnection( + val host: String, val port: Int = 445, val username: String?, + val password: String?, val displayName: String,val connectionType: String, val sharedPath: String +) + + diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt new file mode 100644 index 000000000..1a4c3de53 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt @@ -0,0 +1,48 @@ +package org.fossify.filemanager.repository + +import android.util.Log +import jcifs.CIFSContext +import jcifs.Configuration +import jcifs.config.PropertyConfiguration +import jcifs.context.BaseContext +import jcifs.smb.NtlmPasswordAuthenticator +import jcifs.smb.SmbFile +import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi +import org.fossify.filemanager.models.NetworkConnection +import java.util.Properties + +class NetworkConnectionRepositoryApiImpl: NetworkConnectionRepositoryApi { + private lateinit var dir: SmbFile + private val defaultProperties: Properties = + Properties().apply { + setProperty("jcifs.resolveOrder", "BCAST") + setProperty("jcifs.smb.client.responseTimeout", "30000") + setProperty("jcifs.netbios.retryTimeout", "5000") + setProperty("jcifs.netbios.cachePolicy", "-1") + } + override suspend fun verifyConnection(connection: NetworkConnection): Boolean { + try { + val config: Configuration = PropertyConfiguration(System.getProperties()) + val p = Properties(defaultProperties) + var context: CIFSContext = BaseContext(PropertyConfiguration(p)) + val auth = NtlmPasswordAuthenticator( + "", + connection.username, + connection.password + ) + val authContext = context.withCredentials(auth) + val smbUrl = "smb://${connection.host}/${connection.sharedPath}" + dir = SmbFile(smbUrl, authContext) + return dir.exists() + } + catch (exp: Exception){ + Log.e("Exception",exp.toString()) + } + return false + } + + override fun getFilesFromNetworkPath(): Array { + val files = dir.listFiles() + return files + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt new file mode 100644 index 000000000..20125f51e --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt @@ -0,0 +1,26 @@ +package org.fossify.filemanager.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import org.fossify.filemanager.dao.NetworkConnectionDao +import org.fossify.filemanager.database.NetworkConnectionDatabase +import org.fossify.filemanager.entity.NetworkConnectionEntity +import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb +import org.fossify.filemanager.mapper.toDomain +import org.fossify.filemanager.mapper.toEntity +import org.fossify.filemanager.models.NetworkConnection + +class NetworkConnectionRepositoryDbImpl(private val dao: NetworkConnectionDao): NetworkConnectionRepositoryDb { + override suspend fun saveConnection(connection: NetworkConnection) { + dao.insert(connection.toEntity()) + } + + override suspend fun getAllSavedConnections(): Flow> { + return dao.getAll().map { value -> value.map { entity -> entity.toDomain() } } + } + + override suspend fun deleteConnection() { + TODO("Not yet implemented") + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt new file mode 100644 index 000000000..e1fbcc275 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -0,0 +1,46 @@ +package org.fossify.filemanager.viewmodels + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import jcifs.smb.NtlmPasswordAuthenticator +import jcifs.smb.SmbFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi +import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb +import org.fossify.filemanager.models.NetworkConnection + +class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkConnectionRepositoryDb, private val networkConnectionRepositoryApi: NetworkConnectionRepositoryApi): ViewModel() { + + val savedNetworks = MutableStateFlow>(emptyList()) + val verifyNetwork = MutableSharedFlow() + + fun saveNetwork(networkConnection: NetworkConnection){ + viewModelScope.launch(Dispatchers.IO) { + networkConnectionRepository.saveConnection(networkConnection) + } + } + + fun getAllSavedNetworks(){ + viewModelScope.launch(Dispatchers.IO){ + val connections = networkConnectionRepository.getAllSavedConnections().collectLatest { value -> + savedNetworks.emit(value) + } + } + } + + fun verifyNetwork(connection: NetworkConnection){ + viewModelScope.launch(Dispatchers.IO) { + val value = networkConnectionRepositoryApi.verifyConnection(connection) + verifyNetwork.emit(value) + } + } + + fun getFilesFromNetworkPath():Array{ + return networkConnectionRepositoryApi.getFilesFromNetworkPath() + } +} diff --git a/app/src/main/res/layout/cloud_activity.xml b/app/src/main/res/layout/cloud_activity.xml index df8ece541..de5833795 100644 --- a/app/src/main/res/layout/cloud_activity.xml +++ b/app/src/main/res/layout/cloud_activity.xml @@ -30,20 +30,25 @@ - - + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingTop="@dimen/small_margin" + android:scrollbars="none" + app:layoutManager="org.fossify.commons.views.MyLinearLayoutManager" /> + + + - diff --git a/app/src/main/res/layout/dialog_add_connection.xml b/app/src/main/res/layout/dialog_add_connection.xml index 2b40b173b..65e13c880 100644 --- a/app/src/main/res/layout/dialog_add_connection.xml +++ b/app/src/main/res/layout/dialog_add_connection.xml @@ -48,6 +48,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/build.gradle.kts b/build.gradle.kts index 883ff3df8..bec4ede3d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { alias(libs.plugins.android).apply(false) alias(libs.plugins.kotlinAndroid).apply(false) alias(libs.plugins.detekt).apply(false) + id("com.google.devtools.ksp") version "2.2.0-2.0.2" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 056175599..ac5ad4fa8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ commons = "5.12.0" #Other autofittextview = "0.2.1" gestureviews = "2.8.3" +roomVersion = "2.8.4" rootshell = "bc7e5d398e" roottools = "965c154e20" zip4j = "2.11.5" @@ -26,6 +27,7 @@ app-build-javaVersion = "VERSION_17" app-build-kotlinJVMTarget = "17" [libraries] #AndroidX +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" } androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "androidx-documentfile" } #Compose From 678528088306cefc35c09da79e09689c27dd61ef Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Tue, 31 Mar 2026 23:33:32 +0500 Subject: [PATCH 05/37] added nanohttpd server --- app/build.gradle.kts | 2 + .../filemanager/activities/CloudActivity.kt | 7 ++ .../filemanager/fileSystems/FileHelpers.kt | 13 ++-- .../filemanager/fileSystems/HttpServer.kt | 71 +++++++++++++++++++ .../filemanager/fragments/ItemsFragment.kt | 2 +- .../NetworkConnectionRepositoryApi.kt | 2 + .../NetworkConnectionRepositoryApiImpl.kt | 4 +- .../viewmodels/NetworkBrowserViewModel.kt | 2 + 8 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 943786978..b9a08ad6a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -150,4 +150,6 @@ dependencies { implementation("androidx.room:room-runtime:$roomVersion") ksp("androidx.room:room-compiler:$roomVersion") implementation (libs.androidx.room.ktx) + //noinspection UseTomlInstead + implementation("org.nanohttpd:nanohttpd:2.3.1") } diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 810f6f0de..fe39f4a19 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -26,6 +26,7 @@ import org.fossify.filemanager.adapters.DecompressItemsAdapter import org.fossify.filemanager.databinding.CloudActivityBinding import org.fossify.filemanager.dialogs.ConnectionDialog import org.fossify.filemanager.enums.ConnectionTypes +import org.fossify.filemanager.fileSystems.HttpServer import org.fossify.filemanager.helpers.NETWORK_PATH import org.fossify.filemanager.helpers.PATH import org.fossify.filemanager.models.ListItem @@ -131,6 +132,7 @@ class CloudActivity : SimpleActivity() { viewModel.verifyNetwork.collectLatest { value -> if (value) { val path = "${item.host.trimEnd('/')}/${item.sharedPath.trimStart('/')}" + startServer(item) startActivity(Intent(this@CloudActivity, MainActivity::class.java).apply { putExtra(PATH, path) putExtra(NETWORK_PATH, true) @@ -145,4 +147,9 @@ class CloudActivity : SimpleActivity() { binding.connectionsList.adapter = this } } + + private fun startServer(connection:NetworkConnection){ + val https = HttpServer(7871,connection.host) + https.start() + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt index 403e3c79d..9ab00a56e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -7,6 +7,7 @@ import android.content.pm.PackageManager import android.net.Uri import android.util.Log import androidx.core.net.toUri +import jcifs.smb.SmbFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -15,22 +16,22 @@ import java.io.File object FileHelpers { val URL: String = "http://127.0.0.1:7871/" - fun launchSMB(item: ListItem, context: Context) { + fun launchSMB(item: ListItem, context: Context, smb: SmbFile) { try { CoroutineScope(Dispatchers.Main).launch { val uri = "${URL}${item.parent}${(item.path.toUri()).path}".toUri() - kotlinx . coroutines . delay (50) val i = Intent(Intent.ACTION_VIEW) + Log.d("FileName",uri.toString()) i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) val packageManager: PackageManager = context.packageManager val resInfos = packageManager.queryIntentActivities(i, 0) - if (resInfos.size > 0){ context.startActivity(i) } } } - catch(exp: Exception) { - Log.e("Activity Launch Failed", exp.toString()) + if (resInfos.size > 0) { + context.startActivity(i) } } + } catch (exp: Exception) { + Log.e("Activity Launch Failed", exp.toString()) } - } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt new file mode 100644 index 000000000..14a93cd31 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -0,0 +1,71 @@ +package org.fossify.filemanager.fileSystems + +import fi.iki.elonen.NanoHTTPD +import java.io.FileInputStream + +import jcifs.smb.SmbFile +import jcifs.smb.SmbFileInputStream + +class HttpServer(private val port: Int, private val serverIp: String) : NanoHTTPD(port) { + override fun serve(session: IHTTPSession): Response { + val uri = session.uri + + val smbUrl = "smb://${serverIp}${uri}" + val file = SmbFile(smbUrl) + + if (!file.exists()) { + return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "File not found") + } + + val fileLength = file.length() + val rangeHeader = session.headers["range"] + + return handleRangeRequest(file, rangeHeader, fileLength) + } + + private fun handleRangeRequest( + file: SmbFile, + rangeHeader: String?, + fileLength: Long + ): Response { + + var start: Long = 0 + var end = fileLength - 1 + + if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { + val ranges = rangeHeader.substring(6).split("-") + try { + if (ranges[0].isNotEmpty()) start = ranges[0].toLong() + if (ranges.size > 1 && ranges[1].isNotEmpty()) end = ranges[1].toLong() + } catch (e: NumberFormatException) {} + } + + if (start >= fileLength) { + return newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, "text/plain", "") + } + + val contentLength = end - start + 1 + + val inputStream = SmbFileInputStream(file) + var remaining = start + while (remaining > 0) { + val skipped = inputStream.skip(remaining) + if (skipped <= 0) break + remaining -= skipped + } + + return newFixedLengthResponse( + Response.Status.PARTIAL_CONTENT, + MimeTypes.getMimeTypes(file.path), + inputStream, + contentLength + ).apply { + addHeader("Accept-Ranges", "bytes") + addHeader("Content-Length", contentLength.toString()) + addHeader("Content-Range", "bytes $start-$end/$fileLength") + addHeader("Connection", "keep-alive") + } + } + + +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index b8be123dd..74939c43c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -180,7 +180,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF ItemsAdapter(activity as SimpleActivity, storedItems, this, binding.itemsList, isPickMultipleIntent, binding.itemsSwipeRefresh) { if(isNetworkPath){ (it as? ListItem)?.let { item -> - FileHelpers.launchSMB(item, this@ItemsFragment.context) + FileHelpers.launchSMB(item, this@ItemsFragment.context,viewModel.getMainSmb()) } } else if ((it as? ListItem)?.isSectionTitle == true) { diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt index fa0ebfa85..089e8998f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt @@ -7,4 +7,6 @@ interface NetworkConnectionRepositoryApi { suspend fun verifyConnection(connection: NetworkConnection): Boolean fun getFilesFromNetworkPath(): Array + + fun getMainSmbFile(): SmbFile } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt index 1a4c3de53..7537637fc 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt @@ -12,7 +12,7 @@ import org.fossify.filemanager.models.NetworkConnection import java.util.Properties class NetworkConnectionRepositoryApiImpl: NetworkConnectionRepositoryApi { - private lateinit var dir: SmbFile + lateinit var dir: SmbFile private val defaultProperties: Properties = Properties().apply { setProperty("jcifs.resolveOrder", "BCAST") @@ -45,4 +45,6 @@ class NetworkConnectionRepositoryApiImpl: NetworkConnectionRepositoryApi { val files = dir.listFiles() return files } + + override fun getMainSmbFile(): SmbFile = dir } diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index e1fbcc275..0e8cf4005 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -43,4 +43,6 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo fun getFilesFromNetworkPath():Array{ return networkConnectionRepositoryApi.getFilesFromNetworkPath() } + + fun getMainSmb(): SmbFile = networkConnectionRepositoryApi.getMainSmbFile() } From 117c374934eff02fe0a6a1ac861ce8cca52f53db Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sun, 5 Apr 2026 23:00:51 +0500 Subject: [PATCH 06/37] integrated davx5 and some little webdav --- app/build.gradle.kts | 2 +- app/src/main/AndroidManifest.xml | 5 +- .../filemanager/activities/CloudActivity.kt | 160 +++++++++++------- .../filemanager/activities/MainActivity.kt | 29 ++-- .../filemanager/dialogs/ConnectionDialog.kt | 40 ++++- .../entity/NetworkConnectionEntity.kt | 3 +- .../filemanager/enums/ConnectionTypes.kt | 11 +- .../fossify/filemanager/enums/Protocols.kt | 8 + .../filemanager/fileSystems/FileHelpers.kt | 27 ++- .../filemanager/fileSystems/HttpServer.kt | 92 ++++++++-- .../filemanager/fragments/ItemsFragment.kt | 122 +++++++++---- .../fossify/filemanager/helpers/Constants.kt | 6 + .../fossify/filemanager/helpers/Helpers.kt | 30 ++++ .../NetworkConnectionRepositoryApi.kt | 10 ++ .../mapper/NetworkConnectionMapper.kt | 19 ++- .../filemanager/models/NetworkConnection.kt | 4 +- .../NetworkConnectionRepositoryApiImpl.kt | 38 +++++ .../viewmodels/NetworkBrowserViewModel.kt | 50 +++++- .../main/res/layout/dialog_add_connection.xml | 32 ++++ 19 files changed, 548 insertions(+), 140 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/enums/Protocols.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b9a08ad6a..1389e1863 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -152,4 +152,4 @@ dependencies { implementation (libs.androidx.room.ktx) //noinspection UseTomlInstead implementation("org.nanohttpd:nanohttpd:2.3.1") -} + implementation("com.github.thegrizzlylabs:sardine-android:0.9")} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d381da2eb..5b958d75c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + @@ -26,7 +27,8 @@ android:label="@string/app_launcher_name" android:requestLegacyExternalStorage="true" android:roundIcon="@mipmap/ic_launcher" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + android:usesCleartextTraffic="true"> + diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index fe39f4a19..9ba0db870 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -2,34 +2,26 @@ package org.fossify.filemanager.activities import android.content.Intent import android.os.Bundle -import android.util.Log import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import jcifs.CIFSContext -import jcifs.Configuration -import jcifs.config.PropertyConfiguration -import jcifs.context.BaseContext -import jcifs.smb.NtlmPasswordAuthenticator -import jcifs.smb.SmbFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.fossify.commons.extensions.toast import org.fossify.commons.extensions.viewBinding import org.fossify.commons.helpers.NavigationIcon import org.fossify.filemanager.App import org.fossify.filemanager.adapters.ConnectionItemsAdapter -import org.fossify.filemanager.adapters.DecompressItemsAdapter import org.fossify.filemanager.databinding.CloudActivityBinding import org.fossify.filemanager.dialogs.ConnectionDialog import org.fossify.filemanager.enums.ConnectionTypes import org.fossify.filemanager.fileSystems.HttpServer -import org.fossify.filemanager.helpers.NETWORK_PATH +import org.fossify.filemanager.helpers.CONNECTION_TYPE import org.fossify.filemanager.helpers.PATH -import org.fossify.filemanager.models.ListItem +import org.fossify.filemanager.helpers.PORT_WEBDAV import org.fossify.filemanager.models.NetworkConnection import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel @@ -54,60 +46,85 @@ class CloudActivity : SimpleActivity() { getAllSavedNetworks() } + private val openDocumentTreeLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> + uri?.let { + contentResolver.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + + val storage = DocumentFile.fromTreeUri(this, it) + storage?.let { s -> + if (s.name != null && it.path != null) { + viewModel.saveNetwork( + NetworkConnection( + displayName = s.name!!, + sharedPath = s.uri.toString(), + connectionType = ConnectionTypes.ExternalStorage.toString() + ) + ) + } + } + } + } + + fun promptUserToSelectStorage() { + openDocumentTreeLauncher.launch(null) + } private fun setupToolBar() { setupTopAppBar(binding.cloudAppbar, NavigationIcon.Arrow) } private fun showConnectionDialog() { - ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName -> - listFiles(host, user, password, shared, displayName) + ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName,port, connection -> + saveNetwork(host, user, password, shared, displayName,port, connection) } } + private fun registerAddConnectionListener() { binding.addButton.setOnClickListener { showConnectionDialog() } } - private fun listFiles(host: String, user: String, password: String, shared: String, displayName: String) { + private fun saveNetwork(host: String, user: String, password: String, shared: String, displayName: String,port: Int, connectionType: ConnectionTypes) { CoroutineScope(Dispatchers.IO).launch { - try { - val config: Configuration = PropertyConfiguration(System.getProperties()) - val context: CIFSContext = BaseContext(config) - val auth = NtlmPasswordAuthenticator( - "", - user, - password + if (connectionType == ConnectionTypes.SMB) { + viewModel.authenticateAndSaveSMBNetwork( + NetworkConnection( + host = host, + username = user, + password = password, + sharedPath = shared, + connectionType = connectionType.toString(), + displayName = displayName + ) ) - val authContext = context.withCredentials(auth) - val smbUrl = "smb://$host/$shared" - - val dir = SmbFile(smbUrl, authContext) - Log.d("Display Name", displayName) - if (dir.exists()) - viewModel.saveNetwork( - NetworkConnection( - host = host, - username = user, - password = password, - sharedPath = shared, - connectionType = ConnectionTypes.SMB.type, - displayName = displayName + } + if (connectionType == ConnectionTypes.WebDav) { + val url = "http://${host}:8090/${shared}" + viewModel.connectAndAuthenticateWebDav(user, password, url) + viewModel.verifyWebDav.collectLatest { + if (it) { + viewModel.saveNetwork( + NetworkConnection( + host = host, + username = user, + password = password, + sharedPath = shared, + connectionType = connectionType.toString(), + displayName = displayName, + url = url, + port = port + ) ) - ) - val files = dir.listFiles() - - for (file in files) { - Log.d("Loading Files", file.name) + } } - - } catch (e: Exception) { - Log.e("File Load Failed", e.toString()) - toast(e.message.toString(), Toast.LENGTH_LONG) } - } } @@ -127,18 +144,34 @@ class CloudActivity : SimpleActivity() { private fun updateAdapter(listItems: MutableList) { ConnectionItemsAdapter(this, listItems, binding.connectionsList) { item -> - viewModel.verifyNetwork(item as NetworkConnection) - lifecycleScope.launch { - viewModel.verifyNetwork.collectLatest { value -> - if (value) { - val path = "${item.host.trimEnd('/')}/${item.sharedPath.trimStart('/')}" - startServer(item) - startActivity(Intent(this@CloudActivity, MainActivity::class.java).apply { - putExtra(PATH, path) - putExtra(NETWORK_PATH, true) - }) - } else { - Toast.makeText(this@CloudActivity, "Connection failed", Toast.LENGTH_SHORT).show() + val itm = item as NetworkConnection + if (itm.connectionType == ConnectionTypes.SMB.type) { + viewModel.verifyNetwork(itm) + lifecycleScope.launch { + viewModel.verifyNetwork.collectLatest { value -> + if (value) { + val path = "${item.host.trimEnd('/')}/${item.sharedPath.trimStart('/')}" + startServer(item, connectionType = ConnectionTypes.SMB, machinePort = itm.port) + launchMainActivity(ConnectionTypes.SMB, path) + } else { + Toast.makeText(this@CloudActivity, "Connection failed", Toast.LENGTH_SHORT).show() + } + } + } + } else if (itm.connectionType == ConnectionTypes.ExternalStorage.type) { + launchMainActivity(ConnectionTypes.ExternalStorage, itm.sharedPath) + } else if (itm.connectionType == ConnectionTypes.WebDav.type) { + itm.username?.let { username -> + itm.password?.let { password -> + viewModel.connectAndAuthenticateWebDav(username, password, itm.url) + lifecycleScope.launch { + viewModel.verifyWebDav.collectLatest { + if (it) { + startServer(item, PORT_WEBDAV, connectionType = ConnectionTypes.WebDav, machinePort = itm.port) + launchMainActivity(ConnectionTypes.WebDav, itm.url) + } + } + } } } } @@ -148,8 +181,15 @@ class CloudActivity : SimpleActivity() { } } - private fun startServer(connection:NetworkConnection){ - val https = HttpServer(7871,connection.host) + private fun launchMainActivity(connectionType: ConnectionTypes, path: String) { + startActivity(Intent(this@CloudActivity, MainActivity::class.java).apply { + putExtra(PATH, path) + putExtra(CONNECTION_TYPE, connectionType) + }) + } + + private fun startServer(connection: NetworkConnection, port: Int = 7871,connectionType: ConnectionTypes,machinePort: Int) { + val https = HttpServer(port, connection.host,connectionType,viewModel,machinePort) https.start() } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt index 24f388b3d..f423eedbb 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt @@ -63,12 +63,14 @@ import org.fossify.filemanager.databinding.ActivityMainBinding import org.fossify.filemanager.dialogs.ChangeSortingDialog import org.fossify.filemanager.dialogs.ChangeViewTypeDialog import org.fossify.filemanager.dialogs.InsertFilenameDialog +import org.fossify.filemanager.enums.ConnectionTypes import org.fossify.filemanager.extensions.config import org.fossify.filemanager.extensions.tryOpenPathIntent import org.fossify.filemanager.fragments.ItemsFragment import org.fossify.filemanager.fragments.MyViewPagerFragment import org.fossify.filemanager.fragments.RecentsFragment import org.fossify.filemanager.fragments.StorageFragment +import org.fossify.filemanager.helpers.CONNECTION_TYPE import org.fossify.filemanager.helpers.MAX_COLUMN_COUNT import org.fossify.filemanager.helpers.NETWORK_PATH import org.fossify.filemanager.helpers.PATH @@ -118,17 +120,18 @@ class MainActivity : SimpleActivity() { config.temporarilyShowHidden = false initFragments() val data = getIntentDataIfAny() - tryInitFileManager(data.first,data.second) + tryInitFileManager(data.first,data.second, connectionType = data.third) checkWhatsNewDialog() checkIfRootAvailable() checkInvalidFavorites() } } - private fun getIntentDataIfAny(): Pair { + private fun getIntentDataIfAny(): Triple { val path = intent.getStringExtra(PATH) val isNetworkPath = intent.getBooleanExtra(NETWORK_PATH, false) - return Pair(path ?: "",isNetworkPath) + val connectionType = intent.getSerializableExtra(CONNECTION_TYPE) as? ConnectionTypes ?: ConnectionTypes.Default + return Triple(path ?: "",isNetworkPath,connectionType) } override fun onResume() { @@ -305,7 +308,7 @@ class MainActivity : SimpleActivity() { } } - private fun tryInitFileManager(path: String, isNetworkPath : Boolean = false) { + private fun tryInitFileManager(path: String, isNetworkPath : Boolean = false, connectionType: ConnectionTypes = ConnectionTypes.Default) { val hadPermission = hasStoragePermission() handleStoragePermission { checkOTGPath() @@ -315,7 +318,7 @@ class MainActivity : SimpleActivity() { } binding.mainViewPager.onGlobalLayout { - initFileManager(!hadPermission,path,isNetworkPath) + initFileManager(!hadPermission,path,isNetworkPath,connectionType) } } else { toast(R.string.no_storage_permissions) @@ -324,18 +327,18 @@ class MainActivity : SimpleActivity() { } } - private fun initFileManager(refreshRecents: Boolean, path: String, isNetworkPath: Boolean) { + private fun initFileManager(refreshRecents: Boolean, path: String, isNetworkPath: Boolean, connectionType: ConnectionTypes) { Log.d("File Path",path) if (intent.action == Intent.ACTION_VIEW && intent.data != null) { val data = intent.data if (data?.scheme == "file") { - openPath(data.path!!) + openPath(data.path!!, connectionType = connectionType) } else { val path = getRealPathFromURI(data!!) if (path != null) { - openPath(path) + openPath(path, connectionType = connectionType) } else { - openPath(config.homeFolder) + openPath(config.homeFolder, connectionType = connectionType) } } @@ -345,7 +348,7 @@ class MainActivity : SimpleActivity() { binding.mainViewPager.currentItem = 0 } else { - openPath(if(path.isNotEmpty()) path else config.homeFolder,isNetworkPath = isNetworkPath) + openPath(if(path.isNotEmpty()) path else config.homeFolder,isNetworkPath = isNetworkPath, connectionType = connectionType) } if (refreshRecents) { @@ -471,18 +474,18 @@ class MainActivity : SimpleActivity() { } } - private fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false) { + private fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false,connectionType: ConnectionTypes = ConnectionTypes.Default) { var newPath = path val file = File(path) if (config.OTGPath.isNotEmpty() && config.OTGPath == path.trimEnd('/')) { newPath = path } else if (file.exists() && !file.isDirectory && !isNetworkPath) { newPath = file.parent - } else if (!file.exists() && !isPathOnOTG(newPath) && !isNetworkPath) { + } else if (!file.exists() && !isPathOnOTG(newPath) && connectionType.equals(ConnectionTypes.Default)) { newPath = internalStoragePath } - getItemsFragment()?.openPath(newPath, forceRefresh, isNetworkPath) + getItemsFragment()?.openPath(newPath, forceRefresh, isNetworkPath,connectionType=connectionType) } private fun goHome() { diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 41fcd4f3a..27b1eb20b 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -1,23 +1,59 @@ package org.fossify.filemanager.dialogs +import android.content.Intent +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.extensions.getAlertDialogBuilder import org.fossify.commons.extensions.setupDialogStuff import org.fossify.commons.extensions.value import org.fossify.filemanager.R +import org.fossify.filemanager.activities.CloudActivity import org.fossify.filemanager.databinding.DialogAddConnectionBinding import org.fossify.filemanager.databinding.DialogChangeViewTypeBinding +import org.fossify.filemanager.enums.ConnectionTypes -class ConnectionDialog(val activity: BaseSimpleActivity, dispatch:(String, String, String, String, String)-> Unit) { +class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Int, ConnectionTypes) -> Unit) { private var binding: DialogAddConnectionBinding + val items = listOf(ConnectionTypes.ExternalStorage.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type) init { binding = DialogAddConnectionBinding.inflate(activity.layoutInflater) activity.getAlertDialogBuilder() - .setPositiveButton(R.string.ok) { _,_ -> dispatch(binding.hostEt.value,binding.userEt.value,binding.passwordEt.value,binding.sharedPathEt.value,binding.displayEt.value) } + .setPositiveButton(R.string.ok) { _, _ -> + dispatch( + binding.hostEt.value, binding.userEt.value, binding.passwordEt.value, binding.sharedPathEt.value, binding.displayEt.value,binding.portEt.value.toIntOrNull() ?: 0, + ConnectionTypes.fromType(binding.dropdownMenu.value) + ) + } .setNegativeButton(R.string.cancel, null) .apply { activity.setupDialogStuff(binding.root, this) } + initializeDropDownList() + dropDownItemSelected() + } + + + private fun initializeDropDownList() { + val adapter = ArrayAdapter(activity, android.R.layout.simple_list_item_1, items) + binding.dropdownMenu.setAdapter(adapter) + } + + + private fun promptUserToSelectStorage() { + // This launches the system file picker + (activity as CloudActivity).promptUserToSelectStorage() + } + + private fun dropDownItemSelected() { + binding.dropdownMenu.setOnItemClickListener { parent, view, position, id -> + val selectedItem = parent.getItemAtPosition(position).toString() + if (selectedItem == ConnectionTypes.ExternalStorage.type) { + promptUserToSelectStorage() + } + Toast.makeText(activity, "Selected: $selectedItem", Toast.LENGTH_SHORT).show() + } } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt index f5f573bc5..2aa3662bc 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt @@ -11,5 +11,6 @@ data class NetworkConnectionEntity( val password: String?, val displayName: String, val connectionType: String, - val sharedPath: String + val sharedPath: String, + val url: String ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt b/app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt index e681b9385..d54a996f6 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt @@ -2,5 +2,14 @@ package org.fossify.filemanager.enums enum class ConnectionTypes(val type: String) { SMB("SMB"), - WebDav("WebDav") + WebDav("WebDav"), + ExternalStorage("External Storage"), + SFTP("SFTP"), + Default("Default"); + + companion object { + fun fromType(value: String): ConnectionTypes { + return entries.find { it.type == value } ?: Default + } + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/enums/Protocols.kt b/app/src/main/kotlin/org/fossify/filemanager/enums/Protocols.kt new file mode 100644 index 000000000..f5502cf74 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/enums/Protocols.kt @@ -0,0 +1,8 @@ +package org.fossify.filemanager.enums + +enum class Protocols { + HTTP, + HTTPS, + SMB, + SSH +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt index 9ab00a56e..b65940ba9 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -11,6 +11,8 @@ import jcifs.smb.SmbFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.fossify.filemanager.enums.ConnectionTypes +import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.models.ListItem import java.io.File @@ -18,11 +20,10 @@ object FileHelpers { val URL: String = "http://127.0.0.1:7871/" fun launchSMB(item: ListItem, context: Context, smb: SmbFile) { try { - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.IO).launch { val uri = "${URL}${item.parent}${(item.path.toUri()).path}".toUri() val i = Intent(Intent.ACTION_VIEW) - Log.d("FileName",uri.toString()) i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) val packageManager: PackageManager = context.packageManager val resInfos = packageManager.queryIntentActivities(i, 0) @@ -34,4 +35,26 @@ object FileHelpers { Log.e("Activity Launch Failed", exp.toString()) } } + + fun launchWebDav(connectionTypes: ConnectionTypes, item: ListItem, context: Context){ + try { + CoroutineScope(Dispatchers.IO).launch { + val port = Helpers.getPortForEachService(connectionTypes) + val uri = Helpers.createUrl(connectionTypes, item.mPath, item.parent,port).toUri() + val i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) + + Log.d("MimeType",MimeTypes.getMimeTypes(item.mPath).toString()) + val packageManager: PackageManager = context.packageManager + val resInfos = packageManager.queryIntentActivities(i, 0) + if (resInfos.size > 0) { + context.startActivity(i) + } + } + } + catch (exp: Exception){ + Log.e("Activity Launch Failed", exp.toString()) + } + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 14a93cd31..0e297a0ca 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -1,26 +1,41 @@ package org.fossify.filemanager.fileSystems +import com.thegrizzlylabs.sardineandroid.DavResource import fi.iki.elonen.NanoHTTPD -import java.io.FileInputStream import jcifs.smb.SmbFile import jcifs.smb.SmbFileInputStream +import org.fossify.filemanager.enums.ConnectionTypes +import org.fossify.filemanager.helpers.Helpers +import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel -class HttpServer(private val port: Int, private val serverIp: String) : NanoHTTPD(port) { +class HttpServer(private val port: Int, private val serverIp: String, private val connectionTypes: ConnectionTypes, private val viewModel: NetworkBrowserViewModel, private val machinePort: Int) : + NanoHTTPD(port) { + private lateinit var webDavFile: DavResource override fun serve(session: IHTTPSession): Response { val uri = session.uri - - val smbUrl = "smb://${serverIp}${uri}" - val file = SmbFile(smbUrl) - - if (!file.exists()) { - return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "File not found") + val rangeHeader = session.headers["range"] + if (connectionTypes.equals(ConnectionTypes.SMB)) { + val smbUrl = "smb://${serverIp}${uri}" + val file = SmbFile(smbUrl) + if (!file.exists()) { + return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "File not found") + } + return handleRangeRequest(file, rangeHeader, file.length()) } + val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri.toString(), port = machinePort) + cacheFileDetail(url) - val fileLength = file.length() - val rangeHeader = session.headers["range"] + return handleRangeRequestWebDav(rangeHeader, webDavFile.contentLength, uri = uri) + } - return handleRangeRequest(file, rangeHeader, fileLength) + private fun cacheFileDetail(uri: String){ + if(!::webDavFile.isInitialized && connectionTypes.equals(ConnectionTypes.WebDav)){ + val data = viewModel.listWebDavFileDetail(uri) + data?.let { + webDavFile = it + } + } } private fun handleRangeRequest( @@ -37,7 +52,8 @@ class HttpServer(private val port: Int, private val serverIp: String) : NanoHTTP try { if (ranges[0].isNotEmpty()) start = ranges[0].toLong() if (ranges.size > 1 && ranges[1].isNotEmpty()) end = ranges[1].toLong() - } catch (e: NumberFormatException) {} + } catch (e: NumberFormatException) { + } } if (start >= fileLength) { @@ -47,12 +63,12 @@ class HttpServer(private val port: Int, private val serverIp: String) : NanoHTTP val contentLength = end - start + 1 val inputStream = SmbFileInputStream(file) - var remaining = start - while (remaining > 0) { - val skipped = inputStream.skip(remaining) - if (skipped <= 0) break - remaining -= skipped - } + var remaining = start + while (remaining > 0) { + val skipped = inputStream.skip(remaining) + if (skipped <= 0) break + remaining -= skipped + } return newFixedLengthResponse( Response.Status.PARTIAL_CONTENT, @@ -68,4 +84,44 @@ class HttpServer(private val port: Int, private val serverIp: String) : NanoHTTP } + private fun handleRangeRequestWebDav(rangeHeader: String?, fileLength: Long, uri: String):Response { + var start: Long = 0 + var end = fileLength - 1 + val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri, port = machinePort) + if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { + val ranges = rangeHeader.substring(6).split("-") + try { + if (ranges[0].isNotEmpty()) start = ranges[0].toLong() + if (ranges.size > 1 && ranges[1].isNotEmpty()) end = ranges[1].toLong() + } catch (e: NumberFormatException) { + } + } + + if (start >= fileLength) { + return newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, "text/plain", "") + } + + val contentLength = end - start + 1 + + val inputStream = viewModel.listWebDavFileStream(url =url ) + var remaining = start + while (remaining > 0) { + val skipped = inputStream.skip(remaining) + if (skipped <= 0) break + remaining -= skipped + } + + return newFixedLengthResponse( + Response.Status.PARTIAL_CONTENT, + MimeTypes.getMimeTypes(webDavFile?.contentType), + inputStream, + contentLength + ).apply { + addHeader("Accept-Ranges", "bytes") + addHeader("Content-Length", contentLength.toString()) + addHeader("Content-Range", "bytes $start-$end/$fileLength") + addHeader("Connection", "keep-alive") + } + } + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index 74939c43c..7bdd0d41a 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -2,10 +2,16 @@ package org.fossify.filemanager.fragments import android.annotation.SuppressLint import android.content.Context +import android.net.Uri import android.os.Parcelable import android.util.AttributeSet +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.dialogs.StoragePickerDialog import org.fossify.commons.extensions.* @@ -21,6 +27,7 @@ import org.fossify.filemanager.activities.SimpleActivity import org.fossify.filemanager.adapters.ItemsAdapter import org.fossify.filemanager.databinding.ItemsFragmentBinding import org.fossify.filemanager.dialogs.CreateNewItemDialog +import org.fossify.filemanager.enums.ConnectionTypes import org.fossify.filemanager.extensions.config import org.fossify.filemanager.extensions.isPathOnRoot import org.fossify.filemanager.fileSystems.FileHelpers @@ -115,7 +122,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF getRecyclerAdapter()?.finishActMode() } - fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false) { + fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, connectionType: ConnectionTypes = ConnectionTypes.Default) { if ((activity as? BaseSimpleActivity)?.isAskingPermissions == true) { return } @@ -129,7 +136,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF currentPath = realPath showHidden = context!!.config.shouldShowHidden() showProgressBar() - getItems(currentPath, isNetworkPath) { originalPath, listItems -> + getItems(currentPath, isNetworkPath, connectionType = connectionType) { originalPath, listItems -> if (currentPath != originalPath) { return@getItems } @@ -155,16 +162,16 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF itemsIgnoringSearch = listItems activity?.runOnUiThread { (activity as? MainActivity)?.refreshMenuItems() - addItems(listItems, forceRefresh,isNetworkPath) + addItems(listItems, forceRefresh,isNetworkPath,connectionType) if (context != null && currentViewType != context!!.config.getFolderViewType(currentPath)) { - setupLayoutManager() + setupLayoutManager(connectionType) } hideProgressBar() } } } - private fun addItems(items: ArrayList, forceRefresh: Boolean = false,isNetworkPath: Boolean = false) { + private fun addItems(items: ArrayList, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, connectionType: ConnectionTypes) { activity?.runOnUiThread { binding.itemsSwipeRefresh.isRefreshing = false binding.breadcrumbs.setBreadcrumb(currentPath) @@ -178,11 +185,16 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } ItemsAdapter(activity as SimpleActivity, storedItems, this, binding.itemsList, isPickMultipleIntent, binding.itemsSwipeRefresh) { - if(isNetworkPath){ + if(connectionType == ConnectionTypes.SMB){ (it as? ListItem)?.let { item -> FileHelpers.launchSMB(item, this@ItemsFragment.context,viewModel.getMainSmb()) } } + else if(connectionType == ConnectionTypes.WebDav){ + (it as? ListItem)?.let { item -> + FileHelpers.launchWebDav(connectionType, context = this@ItemsFragment.context, item = item) + } + } else if ((it as? ListItem)?.isSectionTitle == true) { openDirectory(it.mPath) searchClosed() @@ -207,15 +219,29 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF private fun getRecyclerLayoutManager() = (binding.itemsList.layoutManager as MyGridLayoutManager) @SuppressLint("NewApi") - private fun getItems(path: String, isNetworkPath: Boolean = false, callback: (originalPath: String, items: ArrayList) -> Unit) { + private fun getItems(path: String, isNetworkPath: Boolean = false,connectionType: ConnectionTypes, callback: (originalPath: String, items: ArrayList) -> Unit) { ensureBackgroundThread { if (activity?.isDestroyed == false && activity?.isFinishing == false) { val config = context!!.config - if (isNetworkPath) { + + if (connectionType.equals(ConnectionTypes.SMB)) { val fileItems = viewModel.getFilesFromNetworkPath() val items = fileItems.map { it -> it.toFileItem() } callback(path, getListItemsFromFileDirItems(ArrayList(items.toList()))) - } else if (context.isRestrictedSAFOnlyRoot(path)) { + } + else if(connectionType.equals(ConnectionTypes.WebDav)){ + CoroutineScope(Dispatchers.IO).launch { + viewModel.listWebDavFiles(path) + viewModel.webDavFiles.collectLatest { + if(it.isNotEmpty()) { + val fileItems = it + val items = fileItems.map { it -> it.toFileItem() } + callback(path, getListItemsFromFileDirItems(ArrayList(items.toList()))) + } + } + } + } + else if (context.isRestrictedSAFOnlyRoot(path)) { activity?.runOnUiThread { hideProgressBar() } activity?.handleAndroidSAFDialog(path, openInSystemAppAllowed = true) { if (!it) { @@ -232,8 +258,8 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF context!!.getOTGItems(path, config.shouldShowHidden(), getProperFileSize) { callback(path, getListItemsFromFileDirItems(it)) } - } else if (!config.enableRootAccess || !context!!.isPathOnRoot(path)) { - getRegularItemsOf(path, callback) + } else if (!config.enableRootAccess || !context!!.isPathOnRoot(path) && (connectionType.equals(ConnectionTypes.ExternalStorage) || connectionType.equals(ConnectionTypes.Default))) { + getRegularItemsOf(path, callback,connectionType) } else { RootHelpers(activity!!).getFiles(path, callback) } @@ -241,42 +267,66 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } - private fun getRegularItemsOf(path: String, callback: (originalPath: String, items: ArrayList) -> Unit) { + private fun getRegularItemsOf(path: String, callback: (originalPath: String, items: ArrayList) -> Unit, connectionType: ConnectionTypes) { val items = ArrayList() - val files = File(path).listFiles()?.filterNotNull() - if (context == null || files == null) { - callback(path, items) - return + val getProperChildCount = context!!.config.getFolderViewType(currentPath) == VIEW_TYPE_LIST + + if(connectionType == ConnectionTypes.ExternalStorage){ + val uri = Uri.parse(path) + val docFile = DocumentFile.fromTreeUri(context, uri) + val files = docFile?.listFiles() + + files?.forEach { file -> + items.add( + ListItem( + mPath = file.uri.toString(), + mName = file.name ?: "", + mIsDirectory = file.isDirectory, + mChildren = if (file.isDirectory) 1 else 0, + mSize = if (file.isFile) file.length() else 0L, + mModified = file.lastModified(), + isSectionTitle = false, + isGridTypeDivider = false, + parent = path + ) + ) + } } + else { + val files = File(path).listFiles()?.filterNotNull() + if (context == null || files == null) { + callback(path, items) + return + } - val isSortingBySize = context!!.config.getFolderSorting(currentPath) and SORT_BY_SIZE != 0 - val getProperChildCount = context!!.config.getFolderViewType(currentPath) == VIEW_TYPE_LIST - val lastModifieds = context!!.getFolderLastModifieds(path) + val isSortingBySize = context!!.config.getFolderSorting(currentPath) and SORT_BY_SIZE != 0 + val lastModifieds = context!!.getFolderLastModifieds(path) - for (file in files) { - val listItem = getListItemFromFile(file, isSortingBySize, lastModifieds, false) - if (listItem != null) { - if (wantedMimeTypes.any { isProperMimeType(it, file.absolutePath, file.isDirectory) }) { - items.add(listItem) + for (file in files) { + val listItem = getListItemFromFile(file, isSortingBySize, lastModifieds, false) + if (listItem != null) { + if (wantedMimeTypes.any { isProperMimeType(it, file.absolutePath, file.isDirectory) }) { + items.add(listItem) + } } } } - // send out the initial item list asap, get proper child count asynchronously as it can be slow - callback(path, items) + // send out the initial item list asap, get proper child count asynchronously as it can be slow + callback(path, items) - if (getProperChildCount) { - items.filter { it.mIsDirectory }.forEach { - if (context != null) { - val childrenCount = it.getDirectChildrenCount(activity as BaseSimpleActivity, showHidden) - if (childrenCount != 0) { - activity?.runOnUiThread { - getRecyclerAdapter()?.updateChildCount(it.mPath, childrenCount) + if (getProperChildCount) { + items.filter { it.mIsDirectory }.forEach { + if (context != null) { + val childrenCount = it.getDirectChildrenCount(activity as BaseSimpleActivity, showHidden) + if (childrenCount != 0) { + activity?.runOnUiThread { + getRecyclerAdapter()?.updateChildCount(it.mPath, childrenCount) + } } } } } - } } private fun getListItemFromFile(file: File, isSortingBySize: Boolean, lastModifieds: HashMap, getProperChildCount: Boolean): ListItem? { @@ -460,7 +510,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF private fun getRecyclerAdapter() = binding.itemsList.adapter as? ItemsAdapter - private fun setupLayoutManager() { + private fun setupLayoutManager(connectionType: ConnectionTypes) { if (context!!.config.getFolderViewType(currentPath) == VIEW_TYPE_GRID) { currentViewType = VIEW_TYPE_GRID setupGridLayoutManager() @@ -471,7 +521,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF binding.itemsList.adapter = null initZoomListener() - addItems(storedItems, true) + addItems(storedItems, true, connectionType = connectionType) } private fun setupGridLayoutManager() { diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt index e116f199e..e52ef9a7f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt @@ -8,6 +8,11 @@ import org.fossify.filemanager.models.ListItem const val MAX_COLUMN_COUNT = 15 +//Ports + +const val PORT_SMB = 7871 +const val PORT_WEBDAV = 7890 + // shared preferences const val SHOW_HIDDEN = "show_hidden" const val PRESS_BACK_TWICE = "press_back_twice" @@ -48,6 +53,7 @@ const val PATH = "path" const val NETWORK_PATH = "network_path" +const val CONNECTION_TYPE = "connection_type" // what else should we count as an audio except "audio/*" mimetype val extraAudioMimeTypes = arrayListOf("application/ogg") diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt new file mode 100644 index 000000000..ddb3584a2 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -0,0 +1,30 @@ +package org.fossify.filemanager.helpers + +import org.fossify.filemanager.enums.ConnectionTypes +import org.fossify.filemanager.enums.Protocols +import java.nio.file.Path + +object Helpers { + val host: String = "127.0.0.1" + fun createUrl(connectionTypes: ConnectionTypes, path: String, server: String = "", port: Int): String{ + var protocol = Protocols.HTTP.toString().lowercase() + if(connectionTypes.equals(ConnectionTypes.WebDav)){ + protocol = Protocols.HTTP.toString().lowercase() + } + if(connectionTypes.equals(ConnectionTypes.SMB)){ + protocol = Protocols.SMB.toString().lowercase() + } + val url = "${protocol}://${if (server.isEmpty()) host else server }:${port}/${path}" + return url + } + + fun getPortForEachService(connectionTypes: ConnectionTypes): Int{ + if (connectionTypes.equals(ConnectionTypes.WebDav)){ + return PORT_WEBDAV + } + else if(connectionTypes.equals(ConnectionTypes.SMB)){ + return PORT_SMB + } + return PORT_SMB + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt index 089e8998f..530eb7d8f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt @@ -1,7 +1,9 @@ package org.fossify.filemanager.interfaces +import com.thegrizzlylabs.sardineandroid.DavResource import jcifs.smb.SmbFile import org.fossify.filemanager.models.NetworkConnection +import java.io.InputStream interface NetworkConnectionRepositoryApi { suspend fun verifyConnection(connection: NetworkConnection): Boolean @@ -9,4 +11,12 @@ interface NetworkConnectionRepositoryApi { fun getFilesFromNetworkPath(): Array fun getMainSmbFile(): SmbFile + + suspend fun connectAndVerifyWebDav(userName: String = "", password: String = "", url: String): Boolean + + suspend fun listAllFilesOnWebDav(url: String): List + + fun listWebDavFileInputStream(url: String): InputStream + + fun listWebDavFileDetail(url: String): DavResource? } diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index a78d84a40..146745a30 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -1,6 +1,7 @@ package org.fossify.filemanager.mapper import android.util.Log +import com.thegrizzlylabs.sardineandroid.DavResource import jcifs.smb.SmbFile import org.fossify.commons.models.FileDirItem import org.fossify.filemanager.entity.NetworkConnectionEntity @@ -14,7 +15,8 @@ fun NetworkConnectionEntity.toDomain(): NetworkConnection { password = password, displayName = displayName, connectionType = connectionType, - sharedPath = sharedPath + sharedPath = sharedPath, + url = url ) } @@ -26,7 +28,8 @@ fun NetworkConnection.toEntity(): NetworkConnectionEntity { password = password, displayName = displayName, connectionType = connectionType, - sharedPath = sharedPath + sharedPath = sharedPath, + url = url ) } @@ -41,3 +44,15 @@ fun SmbFile.toFileItem(): FileDirItem { mediaStoreId = 0L ) } + +fun DavResource.toFileItem(): FileDirItem { + return FileDirItem( + path = this.path, + name = this.name.trimEnd('/'), + isDirectory = this.isDirectory, + size = if (!this.isDirectory) (this.contentLength ?: 0L) else 0L, + modified = this.modified?.time ?: 0L, + children = 0, + mediaStoreId = 0L + ) +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt index 0419bc9b5..0db07f90d 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt @@ -3,8 +3,8 @@ package org.fossify.filemanager.models import org.fossify.filemanager.entity.NetworkConnectionEntity data class NetworkConnection( - val host: String, val port: Int = 445, val username: String?, - val password: String?, val displayName: String,val connectionType: String, val sharedPath: String + val host: String = "", val port: Int = 445, val username: String? = "", + val password: String? = "", val displayName: String = "",val connectionType: String, val sharedPath: String, val url: String = "" ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt index 7537637fc..cf232245b 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt @@ -1,6 +1,9 @@ package org.fossify.filemanager.repository import android.util.Log +import com.thegrizzlylabs.sardineandroid.DavResource +import com.thegrizzlylabs.sardineandroid.Sardine +import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine import jcifs.CIFSContext import jcifs.Configuration import jcifs.config.PropertyConfiguration @@ -9,10 +12,12 @@ import jcifs.smb.NtlmPasswordAuthenticator import jcifs.smb.SmbFile import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi import org.fossify.filemanager.models.NetworkConnection +import java.io.InputStream import java.util.Properties class NetworkConnectionRepositoryApiImpl: NetworkConnectionRepositoryApi { lateinit var dir: SmbFile + lateinit var sardine: Sardine private val defaultProperties: Properties = Properties().apply { setProperty("jcifs.resolveOrder", "BCAST") @@ -47,4 +52,37 @@ class NetworkConnectionRepositoryApiImpl: NetworkConnectionRepositoryApi { } override fun getMainSmbFile(): SmbFile = dir + + override suspend fun connectAndVerifyWebDav(userName: String, password: String, url: String): Boolean { + try { + sardine = OkHttpSardine() + sardine.setCredentials(userName,password) + return sardine.exists(url) + } + catch (exp: Exception){ + Log.d("WebDav",exp.toString()) + return false + } + } + + override suspend fun listAllFilesOnWebDav(url: String): List { + val resources = sardine.list(url) + return resources + } + + override fun listWebDavFileInputStream(url: String): InputStream { + val inputStream = sardine.get(url) + return inputStream + } + + override fun listWebDavFileDetail(url: String): DavResource? { + val resources = sardine.list(url) + + if (resources.isNotEmpty()) { + return resources[0] + } + return null + } + + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 0e8cf4005..b38a1ff41 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -1,8 +1,12 @@ package org.fossify.filemanager.viewmodels -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.thegrizzlylabs.sardineandroid.DavResource +import jcifs.CIFSContext +import jcifs.Configuration +import jcifs.config.PropertyConfiguration +import jcifs.context.BaseContext import jcifs.smb.NtlmPasswordAuthenticator import jcifs.smb.SmbFile import kotlinx.coroutines.Dispatchers @@ -13,18 +17,41 @@ import kotlinx.coroutines.launch import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb import org.fossify.filemanager.models.NetworkConnection +import java.io.InputStream class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkConnectionRepositoryDb, private val networkConnectionRepositoryApi: NetworkConnectionRepositoryApi): ViewModel() { val savedNetworks = MutableStateFlow>(emptyList()) val verifyNetwork = MutableSharedFlow() + val verifyWebDav = MutableSharedFlow() + + val webDavFiles = MutableStateFlow>(emptyList()) fun saveNetwork(networkConnection: NetworkConnection){ viewModelScope.launch(Dispatchers.IO) { networkConnectionRepository.saveConnection(networkConnection) } } + fun authenticateAndSaveSMBNetwork(networkConnection: NetworkConnection){ + viewModelScope.launch(Dispatchers.IO) { + val config: Configuration = PropertyConfiguration(System.getProperties()) + val context: CIFSContext = BaseContext(config) + val auth = NtlmPasswordAuthenticator( + "", + networkConnection.username, + networkConnection.password + ) + val authContext = context.withCredentials(auth) + val smbUrl = "smb://${networkConnection.host}/${networkConnection.sharedPath}" + + val dir = SmbFile(smbUrl, authContext) + if (dir.exists()) { + saveNetwork(networkConnection) + } + } + } + fun getAllSavedNetworks(){ viewModelScope.launch(Dispatchers.IO){ val connections = networkConnectionRepository.getAllSavedConnections().collectLatest { value -> @@ -45,4 +72,25 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo } fun getMainSmb(): SmbFile = networkConnectionRepositoryApi.getMainSmbFile() + + fun connectAndAuthenticateWebDav(userName: String = "", password: String = "", url: String){ + viewModelScope.launch(Dispatchers.IO) { + val result = networkConnectionRepositoryApi.connectAndVerifyWebDav(userName, password, url) + verifyWebDav.emit(result) + } + } + + fun listWebDavFiles(url: String){ + viewModelScope.launch(Dispatchers.IO) { + webDavFiles.emit(networkConnectionRepositoryApi.listAllFilesOnWebDav(url)) + } + } + + fun listWebDavFileStream(url: String): InputStream{ + return networkConnectionRepositoryApi.listWebDavFileInputStream(url) + } + + fun listWebDavFileDetail(url: String): DavResource?{ + return networkConnectionRepositoryApi.listWebDavFileDetail(url) + } } diff --git a/app/src/main/res/layout/dialog_add_connection.xml b/app/src/main/res/layout/dialog_add_connection.xml index 65e13c880..099e49b24 100644 --- a/app/src/main/res/layout/dialog_add_connection.xml +++ b/app/src/main/res/layout/dialog_add_connection.xml @@ -20,6 +20,24 @@ android:layout_height="wrap_content" android:layout_marginBottom="@dimen/medium_margin"> + + + + + + + + + + + + Date: Tue, 7 Apr 2026 01:38:10 +0500 Subject: [PATCH 07/37] implemented webdav --- .../filemanager/fileSystems/FileHelpers.kt | 2 +- .../filemanager/fileSystems/HttpServer.kt | 38 +++++++------------ .../filemanager/fragments/ItemsFragment.kt | 2 +- .../NetworkConnectionRepositoryApi.kt | 2 +- .../NetworkConnectionRepositoryApiImpl.kt | 7 ++-- .../viewmodels/NetworkBrowserViewModel.kt | 4 +- 6 files changed, 22 insertions(+), 33 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt index b65940ba9..f40ae87f7 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -40,7 +40,7 @@ object FileHelpers { try { CoroutineScope(Dispatchers.IO).launch { val port = Helpers.getPortForEachService(connectionTypes) - val uri = Helpers.createUrl(connectionTypes, item.mPath, item.parent,port).toUri() + val uri = Helpers.createUrl(connectionTypes, item.mPath, port = port).toUri() val i = Intent(Intent.ACTION_VIEW) i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 0e297a0ca..a36d35847 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -1,6 +1,5 @@ package org.fossify.filemanager.fileSystems -import com.thegrizzlylabs.sardineandroid.DavResource import fi.iki.elonen.NanoHTTPD import jcifs.smb.SmbFile @@ -9,14 +8,19 @@ import org.fossify.filemanager.enums.ConnectionTypes import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel -class HttpServer(private val port: Int, private val serverIp: String, private val connectionTypes: ConnectionTypes, private val viewModel: NetworkBrowserViewModel, private val machinePort: Int) : +class HttpServer( + private val port: Int, + private val serverIp: String, + private val connectionTypes: ConnectionTypes, + private val viewModel: NetworkBrowserViewModel, + private val machinePort: Int +) : NanoHTTPD(port) { - private lateinit var webDavFile: DavResource override fun serve(session: IHTTPSession): Response { val uri = session.uri val rangeHeader = session.headers["range"] if (connectionTypes.equals(ConnectionTypes.SMB)) { - val smbUrl = "smb://${serverIp}${uri}" + val smbUrl = "smb://${serverIp}/${uri}" val file = SmbFile(smbUrl) if (!file.exists()) { return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "File not found") @@ -24,19 +28,10 @@ class HttpServer(private val port: Int, private val serverIp: String, private va return handleRangeRequest(file, rangeHeader, file.length()) } val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri.toString(), port = machinePort) - cacheFileDetail(url) - - return handleRangeRequestWebDav(rangeHeader, webDavFile.contentLength, uri = uri) + val webDavFile = viewModel.listWebDavFileDetail(url) + return handleRangeRequestWebDav(rangeHeader, webDavFile?.contentLength!!, uri = uri,webDavFile.contentType) } - private fun cacheFileDetail(uri: String){ - if(!::webDavFile.isInitialized && connectionTypes.equals(ConnectionTypes.WebDav)){ - val data = viewModel.listWebDavFileDetail(uri) - data?.let { - webDavFile = it - } - } - } private fun handleRangeRequest( file: SmbFile, @@ -84,7 +79,7 @@ class HttpServer(private val port: Int, private val serverIp: String, private va } - private fun handleRangeRequestWebDav(rangeHeader: String?, fileLength: Long, uri: String):Response { + private fun handleRangeRequestWebDav(rangeHeader: String?, fileLength: Long, uri: String, contentType: String): Response { var start: Long = 0 var end = fileLength - 1 val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri, port = machinePort) @@ -96,24 +91,17 @@ class HttpServer(private val port: Int, private val serverIp: String, private va } catch (e: NumberFormatException) { } } - if (start >= fileLength) { return newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, "text/plain", "") } val contentLength = end - start + 1 - val inputStream = viewModel.listWebDavFileStream(url =url ) - var remaining = start - while (remaining > 0) { - val skipped = inputStream.skip(remaining) - if (skipped <= 0) break - remaining -= skipped - } + val inputStream = viewModel.listWebDavFileStream(url = url,start,end) return newFixedLengthResponse( Response.Status.PARTIAL_CONTENT, - MimeTypes.getMimeTypes(webDavFile?.contentType), + MimeTypes.getMimeTypes(contentType), inputStream, contentLength ).apply { diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index 7bdd0d41a..057805257 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -142,7 +142,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } FileDirItem.sorting = context!!.config.getFolderSorting(currentPath) - if(isNetworkPath){ + if(connectionType != ConnectionTypes.Default){ listItems.forEach { it.parent = originalPath.substringAfter('/') } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt index 530eb7d8f..84b94d372 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt @@ -16,7 +16,7 @@ interface NetworkConnectionRepositoryApi { suspend fun listAllFilesOnWebDav(url: String): List - fun listWebDavFileInputStream(url: String): InputStream + fun listWebDavFileInputStream(url: String,start: Long,end: Long): InputStream fun listWebDavFileDetail(url: String): DavResource? } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt index cf232245b..67efb8c41 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt @@ -70,9 +70,10 @@ class NetworkConnectionRepositoryApiImpl: NetworkConnectionRepositoryApi { return resources } - override fun listWebDavFileInputStream(url: String): InputStream { - val inputStream = sardine.get(url) - return inputStream + override fun listWebDavFileInputStream(url: String,start: Long,end: Long): InputStream { + val rangeHeader = "bytes=$start-$end" + val headers = mapOf("Range" to rangeHeader) + return sardine.get(url, headers) } override fun listWebDavFileDetail(url: String): DavResource? { diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index b38a1ff41..fed483ec0 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -86,8 +86,8 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo } } - fun listWebDavFileStream(url: String): InputStream{ - return networkConnectionRepositoryApi.listWebDavFileInputStream(url) + fun listWebDavFileStream(url: String,start: Long,end: Long): InputStream{ + return networkConnectionRepositoryApi.listWebDavFileInputStream(url,start,end) } fun listWebDavFileDetail(url: String): DavResource?{ From 089c17fe0f67e706d2ae19790e51d4281c9298a1 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Thu, 9 Apr 2026 01:59:46 +0500 Subject: [PATCH 08/37] added sftp --- .../filemanager/activities/CloudActivity.kt | 42 ++++++++++-- .../filemanager/dialogs/ConnectionDialog.kt | 2 +- .../filemanager/fileSystems/FileHelpers.kt | 22 +++++- .../filemanager/fileSystems/HttpServer.kt | 51 ++++++++++++-- .../filemanager/fragments/ItemsFragment.kt | 19 ++++++ .../fossify/filemanager/helpers/Constants.kt | 2 + .../fossify/filemanager/helpers/Helpers.kt | 7 +- .../NetworkConnectionRepositoryApi.kt | 12 ++++ .../mapper/NetworkConnectionMapper.kt | 17 +++++ .../filemanager/models/NetworkConnection.kt | 2 +- .../NetworkConnectionRepositoryApiImpl.kt | 68 ++++++++++++++++--- .../viewmodels/NetworkBrowserViewModel.kt | 30 ++++++++ 12 files changed, 247 insertions(+), 27 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 9ba0db870..9df783ca9 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -2,6 +2,7 @@ package org.fossify.filemanager.activities import android.content.Intent import android.os.Bundle +import android.util.Log import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.documentfile.provider.DocumentFile @@ -21,6 +22,7 @@ import org.fossify.filemanager.enums.ConnectionTypes import org.fossify.filemanager.fileSystems.HttpServer import org.fossify.filemanager.helpers.CONNECTION_TYPE import org.fossify.filemanager.helpers.PATH +import org.fossify.filemanager.helpers.PORT_SFTP import org.fossify.filemanager.helpers.PORT_WEBDAV import org.fossify.filemanager.models.NetworkConnection import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel @@ -79,8 +81,8 @@ class CloudActivity : SimpleActivity() { } private fun showConnectionDialog() { - ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName,port, connection -> - saveNetwork(host, user, password, shared, displayName,port, connection) + ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName, port, connection -> + saveNetwork(host, user, password, shared, displayName, port, connection) } } @@ -91,8 +93,8 @@ class CloudActivity : SimpleActivity() { } } - private fun saveNetwork(host: String, user: String, password: String, shared: String, displayName: String,port: Int, connectionType: ConnectionTypes) { - CoroutineScope(Dispatchers.IO).launch { + private fun saveNetwork(host: String, user: String, password: String, shared: String, displayName: String, port: Int, connectionType: ConnectionTypes) { + lifecycleScope.launch((Dispatchers.IO)) { if (connectionType == ConnectionTypes.SMB) { viewModel.authenticateAndSaveSMBNetwork( NetworkConnection( @@ -106,7 +108,7 @@ class CloudActivity : SimpleActivity() { ) } if (connectionType == ConnectionTypes.WebDav) { - val url = "http://${host}:8090/${shared}" + val url = "http://${host}:${port}/${shared}" viewModel.connectAndAuthenticateWebDav(user, password, url) viewModel.verifyWebDav.collectLatest { if (it) { @@ -125,6 +127,17 @@ class CloudActivity : SimpleActivity() { } } } + + if (connectionType == ConnectionTypes.SFTP) { + viewModel.connectSFTP(user, password, host, port) + viewModel.verifySFTP.collectLatest { + if (it) { + viewModel.saveNetwork( + NetworkConnection(host = host, username = user, password = password, connectionType = connectionType.toString(), port = port, displayName = displayName, url = viewModel.getSFTPConn().pwd()) + ) + } + } + } } } @@ -174,6 +187,21 @@ class CloudActivity : SimpleActivity() { } } } + } else if (item.connectionType == ConnectionTypes.SFTP.type) { + itm.username?.let { username -> + itm.password?.let { password -> + viewModel.connectSFTP(username, password, itm.host, itm.port) + lifecycleScope.launch(Dispatchers.IO) { + viewModel.verifySFTP.collectLatest { + if(it){ + startServer(item, PORT_SFTP, connectionType = ConnectionTypes.SFTP, machinePort = itm.port) + launchMainActivity(ConnectionTypes.SFTP,itm.url) + } + } + } + } + } + } }.apply { @@ -188,8 +216,8 @@ class CloudActivity : SimpleActivity() { }) } - private fun startServer(connection: NetworkConnection, port: Int = 7871,connectionType: ConnectionTypes,machinePort: Int) { - val https = HttpServer(port, connection.host,connectionType,viewModel,machinePort) + private fun startServer(connection: NetworkConnection, port: Int = 7871, connectionType: ConnectionTypes, machinePort: Int) { + val https = HttpServer(port, connection.host, connectionType, viewModel, machinePort) https.start() } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 27b1eb20b..f439febbd 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -16,7 +16,7 @@ import org.fossify.filemanager.enums.ConnectionTypes class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Int, ConnectionTypes) -> Unit) { private var binding: DialogAddConnectionBinding - val items = listOf(ConnectionTypes.ExternalStorage.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type) + val items = listOf(ConnectionTypes.ExternalStorage.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.SFTP.type) init { binding = DialogAddConnectionBinding.inflate(activity.layoutInflater) diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt index f40ae87f7..a7b422624 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -45,7 +45,27 @@ object FileHelpers { Intent(Intent.ACTION_VIEW) i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) - Log.d("MimeType",MimeTypes.getMimeTypes(item.mPath).toString()) + val packageManager: PackageManager = context.packageManager + val resInfos = packageManager.queryIntentActivities(i, 0) + if (resInfos.size > 0) { + context.startActivity(i) + } + } + } + catch (exp: Exception){ + Log.e("Activity Launch Failed", exp.toString()) + } + } + + fun launchSFTP(connectionTypes: ConnectionTypes,item: ListItem,context: Context){ + try{ + CoroutineScope(Dispatchers.IO).launch { + val port = Helpers.getPortForEachService(connectionTypes) + val uri = Helpers.createUrl(connectionTypes, item.mPath, port = port).toUri() + val i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) + val packageManager: PackageManager = context.packageManager val resInfos = packageManager.queryIntentActivities(i, 0) if (resInfos.size > 0) { diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index a36d35847..44bf66e79 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -27,9 +27,14 @@ class HttpServer( } return handleRangeRequest(file, rangeHeader, file.length()) } - val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri.toString(), port = machinePort) - val webDavFile = viewModel.listWebDavFileDetail(url) - return handleRangeRequestWebDav(rangeHeader, webDavFile?.contentLength!!, uri = uri,webDavFile.contentType) + else if(connectionTypes.equals(ConnectionTypes.WebDav)) { + val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri.toString(), port = machinePort) + val webDavFile = viewModel.listWebDavFileDetail(url) + return handleRangeRequestWebDav(rangeHeader, webDavFile?.contentLength!!, uri = uri, webDavFile.contentType) + } + val url = Helpers.createUrl(connectionTypes, server = serverIp, path = "", port = machinePort) + val sftFile = viewModel.listSFTPFileDetails(url) + return handleRangeRequestSFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } @@ -79,7 +84,7 @@ class HttpServer( } - private fun handleRangeRequestWebDav(rangeHeader: String?, fileLength: Long, uri: String, contentType: String): Response { + private fun handleRangeRequestWebDav(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String): Response { var start: Long = 0 var end = fileLength - 1 val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri, port = machinePort) @@ -112,4 +117,42 @@ class HttpServer( } } + private fun handleRangeRequestSFTPServer(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String): Response { + var start: Long = 0 + var end = fileLength - 1 + val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri, port = machinePort) + if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { + val ranges = rangeHeader.substring(6).split("-") + try { + if (ranges[0].isNotEmpty()) start = ranges[0].toLong() + if (ranges.size > 1 && ranges[1].isNotEmpty()) end = ranges[1].toLong() + } catch (e: NumberFormatException) { + } + } + if (start >= fileLength) { + return newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, "text/plain", "") + } + + val contentLength = end - start + 1 + + val inputStream = viewModel.getSFTPFileStream(url) + var remaining = start + while (remaining > 0) { + val skipped = inputStream.skip(remaining) + if (skipped <= 0) break + remaining -= skipped + } + return newFixedLengthResponse( + Response.Status.PARTIAL_CONTENT, + MimeTypes.getMimeTypes(contentType), + inputStream, + contentLength + ).apply { + addHeader("Accept-Ranges", "bytes") + addHeader("Content-Length", contentLength.toString()) + addHeader("Content-Range", "bytes $start-$end/$fileLength") + addHeader("Connection", "keep-alive") + } + } + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index 057805257..9592960f6 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -195,6 +195,12 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF FileHelpers.launchWebDav(connectionType, context = this@ItemsFragment.context, item = item) } } + + else if(connectionType == ConnectionTypes.SFTP){ + (it as? ListItem)?.let { item -> + FileHelpers.launchSFTP(connectionType, context = this@ItemsFragment.context, item = item) + } + } else if ((it as? ListItem)?.isSectionTitle == true) { openDirectory(it.mPath) searchClosed() @@ -241,6 +247,19 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } } + + else if (connectionType.equals(ConnectionTypes.SFTP)){ + CoroutineScope(Dispatchers.IO).launch { + viewModel.listAllSFTPFile(path) + viewModel.sftpFiles.collectLatest { + if(it.isNotEmpty()) { + val fileItems = it + val items = fileItems.map { it -> it.toFileItem(path) } + callback(path, getListItemsFromFileDirItems(ArrayList(items.toList()))) + } + } + } + } else if (context.isRestrictedSAFOnlyRoot(path)) { activity?.runOnUiThread { hideProgressBar() } activity?.handleAndroidSAFDialog(path, openInSystemAppAllowed = true) { diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt index e52ef9a7f..24ad6ff67 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt @@ -13,6 +13,8 @@ const val MAX_COLUMN_COUNT = 15 const val PORT_SMB = 7871 const val PORT_WEBDAV = 7890 +const val PORT_SFTP = 7860 + // shared preferences const val SHOW_HIDDEN = "show_hidden" const val PRESS_BACK_TWICE = "press_back_twice" diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt index ddb3584a2..49eb4dc74 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -6,12 +6,12 @@ import java.nio.file.Path object Helpers { val host: String = "127.0.0.1" - fun createUrl(connectionTypes: ConnectionTypes, path: String, server: String = "", port: Int): String{ + fun createUrl(connectionTypes: ConnectionTypes, path: String = "", server: String = "", port: Int): String{ var protocol = Protocols.HTTP.toString().lowercase() if(connectionTypes.equals(ConnectionTypes.WebDav)){ protocol = Protocols.HTTP.toString().lowercase() } - if(connectionTypes.equals(ConnectionTypes.SMB)){ + else if(connectionTypes.equals(ConnectionTypes.SMB)){ protocol = Protocols.SMB.toString().lowercase() } val url = "${protocol}://${if (server.isEmpty()) host else server }:${port}/${path}" @@ -25,6 +25,9 @@ object Helpers { else if(connectionTypes.equals(ConnectionTypes.SMB)){ return PORT_SMB } + else if(connectionTypes.equals(ConnectionTypes.SFTP)){ + return PORT_SFTP + } return PORT_SMB } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt index 84b94d372..c88c41ba6 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt @@ -1,5 +1,7 @@ package org.fossify.filemanager.interfaces +import com.jcraft.jsch.ChannelSftp +import com.jcraft.jsch.SftpATTRS import com.thegrizzlylabs.sardineandroid.DavResource import jcifs.smb.SmbFile import org.fossify.filemanager.models.NetworkConnection @@ -19,4 +21,14 @@ interface NetworkConnectionRepositoryApi { fun listWebDavFileInputStream(url: String,start: Long,end: Long): InputStream fun listWebDavFileDetail(url: String): DavResource? + + suspend fun connectToSftp(userName: String, password: String,server: String,port: Int): Boolean + + suspend fun listAllSFTPFiles(path: String):MutableList + + fun listSFTPFileDetails(path: String): SftpATTRS? + + fun listSFTPFileInputStream(url: String): InputStream + + fun getSFTPConn(): ChannelSftp } diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index 146745a30..5579cfd99 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -1,6 +1,7 @@ package org.fossify.filemanager.mapper import android.util.Log +import com.jcraft.jsch.ChannelSftp import com.thegrizzlylabs.sardineandroid.DavResource import jcifs.smb.SmbFile import org.fossify.commons.models.FileDirItem @@ -56,3 +57,19 @@ fun DavResource.toFileItem(): FileDirItem { mediaStoreId = 0L ) } + + +fun ChannelSftp.LsEntry.toFileItem(parentPath: String): FileDirItem { + val attrs = this.attrs + val cleanParent = parentPath.trimEnd('/') + + return FileDirItem( + path = "$cleanParent/${this.filename}", + name = this.filename.trimEnd('/'), + isDirectory = attrs.isDir, + size = if (!attrs.isDir) attrs.size else 0L, + modified = attrs.mTime.toLong() * 1000L, + children = 0, + mediaStoreId = 0L + ) +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt index 0db07f90d..f68f0b5ff 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt @@ -4,7 +4,7 @@ import org.fossify.filemanager.entity.NetworkConnectionEntity data class NetworkConnection( val host: String = "", val port: Int = 445, val username: String? = "", - val password: String? = "", val displayName: String = "",val connectionType: String, val sharedPath: String, val url: String = "" + val password: String? = "", val displayName: String = "",val connectionType: String, val sharedPath: String = "", val url: String = "" ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt index 67efb8c41..a9169b82f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt @@ -1,6 +1,10 @@ package org.fossify.filemanager.repository import android.util.Log +import com.jcraft.jsch.ChannelSftp +import com.jcraft.jsch.JSch +import com.jcraft.jsch.Session +import com.jcraft.jsch.SftpATTRS import com.thegrizzlylabs.sardineandroid.DavResource import com.thegrizzlylabs.sardineandroid.Sardine import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine @@ -15,9 +19,11 @@ import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream import java.util.Properties -class NetworkConnectionRepositoryApiImpl: NetworkConnectionRepositoryApi { +class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { lateinit var dir: SmbFile lateinit var sardine: Sardine + + lateinit var sftp: ChannelSftp private val defaultProperties: Properties = Properties().apply { setProperty("jcifs.resolveOrder", "BCAST") @@ -25,6 +31,7 @@ class NetworkConnectionRepositoryApiImpl: NetworkConnectionRepositoryApi { setProperty("jcifs.netbios.retryTimeout", "5000") setProperty("jcifs.netbios.cachePolicy", "-1") } + override suspend fun verifyConnection(connection: NetworkConnection): Boolean { try { val config: Configuration = PropertyConfiguration(System.getProperties()) @@ -39,9 +46,8 @@ class NetworkConnectionRepositoryApiImpl: NetworkConnectionRepositoryApi { val smbUrl = "smb://${connection.host}/${connection.sharedPath}" dir = SmbFile(smbUrl, authContext) return dir.exists() - } - catch (exp: Exception){ - Log.e("Exception",exp.toString()) + } catch (exp: Exception) { + Log.e("Exception", exp.toString()) } return false } @@ -56,21 +62,20 @@ class NetworkConnectionRepositoryApiImpl: NetworkConnectionRepositoryApi { override suspend fun connectAndVerifyWebDav(userName: String, password: String, url: String): Boolean { try { sardine = OkHttpSardine() - sardine.setCredentials(userName,password) + sardine.setCredentials(userName, password) return sardine.exists(url) - } - catch (exp: Exception){ - Log.d("WebDav",exp.toString()) + } catch (exp: Exception) { + Log.d("WebDav", exp.toString()) return false } } override suspend fun listAllFilesOnWebDav(url: String): List { - val resources = sardine.list(url) + val resources = sardine.list(url) return resources } - override fun listWebDavFileInputStream(url: String,start: Long,end: Long): InputStream { + override fun listWebDavFileInputStream(url: String, start: Long, end: Long): InputStream { val rangeHeader = "bytes=$start-$end" val headers = mapOf("Range" to rangeHeader) return sardine.get(url, headers) @@ -80,10 +85,51 @@ class NetworkConnectionRepositoryApiImpl: NetworkConnectionRepositoryApi { val resources = sardine.list(url) if (resources.isNotEmpty()) { - return resources[0] + return resources[0] } return null } + override suspend fun connectToSftp(userName: String, password: String, server: String, port: Int): Boolean { + try { + val jsch = JSch() + val session: Session = jsch.getSession(userName, server, port) + + session.setPassword(password) + val config = Properties() + config["StrictHostKeyChecking"] = "no" + session.setConfig(config) + + session.connect() + sftp = session.openChannel("sftp") as ChannelSftp + sftp.connect() + return true + + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + + override suspend fun listAllSFTPFiles(path: String): MutableList { + val currentPath = sftp.pwd() + val files = sftp.ls(currentPath) + val allFiles = mutableListOf() + for (item in files) { + val entry = item as ChannelSftp.LsEntry + allFiles.add(entry) + } + return allFiles + } + + override fun listSFTPFileDetails(path: String): SftpATTRS? { + val file = sftp.stat(path) + return file + } + + override fun listSFTPFileInputStream(url: String): InputStream { + return sftp.get(url) + } + override fun getSFTPConn() = sftp } diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index fed483ec0..1bbeed999 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -2,6 +2,8 @@ package org.fossify.filemanager.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.jcraft.jsch.ChannelSftp +import com.jcraft.jsch.SftpATTRS import com.thegrizzlylabs.sardineandroid.DavResource import jcifs.CIFSContext import jcifs.Configuration @@ -26,6 +28,10 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo val verifyWebDav = MutableSharedFlow() + val verifySFTP = MutableSharedFlow() + + val sftpFiles = MutableStateFlow>(emptyList()) + val webDavFiles = MutableStateFlow>(emptyList()) fun saveNetwork(networkConnection: NetworkConnection){ viewModelScope.launch(Dispatchers.IO) { @@ -73,6 +79,8 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo fun getMainSmb(): SmbFile = networkConnectionRepositoryApi.getMainSmbFile() + fun getSFTPConn():ChannelSftp = networkConnectionRepositoryApi.getSFTPConn() + fun connectAndAuthenticateWebDav(userName: String = "", password: String = "", url: String){ viewModelScope.launch(Dispatchers.IO) { val result = networkConnectionRepositoryApi.connectAndVerifyWebDav(userName, password, url) @@ -93,4 +101,26 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo fun listWebDavFileDetail(url: String): DavResource?{ return networkConnectionRepositoryApi.listWebDavFileDetail(url) } + + fun connectSFTP(userName: String, password: String,server: String,port: Int){ + viewModelScope.launch(Dispatchers.IO) { + val res = networkConnectionRepositoryApi.connectToSftp(userName,password,server,port) + verifySFTP.emit(res) + } + } + + fun listAllSFTPFile(path: String){ + viewModelScope.launch(Dispatchers.IO) { + val res = networkConnectionRepositoryApi.listAllSFTPFiles(path) + sftpFiles.emit(res) + } + } + + fun listSFTPFileDetails(path: String):SftpATTRS?{ + return networkConnectionRepositoryApi.listSFTPFileDetails(path) + } + + fun getSFTPFileStream(path: String): InputStream{ + return networkConnectionRepositoryApi.listSFTPFileInputStream(url = path) + } } From 10422d11efd264f209e94d4b146b61aaf599a5db Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Fri, 10 Apr 2026 02:51:16 +0500 Subject: [PATCH 09/37] some change for FileDirItem --- .../filemanager/activities/CloudActivity.kt | 2 +- .../filemanager/activities/MainActivity.kt | 2 +- .../filemanager/activities/ReadTextActivity.kt | 6 +++--- .../filemanager/dialogs/ChangeViewTypeDialog.kt | 2 +- .../filemanager/dialogs/ConnectionDialog.kt | 2 +- .../fossify/filemanager/enums/ConnectionTypes.kt | 15 --------------- .../filemanager/fileSystems/FileHelpers.kt | 2 +- .../fossify/filemanager/fileSystems/HttpServer.kt | 2 +- .../filemanager/fragments/ItemsFragment.kt | 10 +++++----- .../org/fossify/filemanager/helpers/Helpers.kt | 2 +- 10 files changed, 15 insertions(+), 30 deletions(-) delete mode 100644 app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 9df783ca9..23c08f299 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -12,13 +12,13 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.viewBinding import org.fossify.commons.helpers.NavigationIcon import org.fossify.filemanager.App import org.fossify.filemanager.adapters.ConnectionItemsAdapter import org.fossify.filemanager.databinding.CloudActivityBinding import org.fossify.filemanager.dialogs.ConnectionDialog -import org.fossify.filemanager.enums.ConnectionTypes import org.fossify.filemanager.fileSystems.HttpServer import org.fossify.filemanager.helpers.CONNECTION_TYPE import org.fossify.filemanager.helpers.PATH diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt index f423eedbb..1bf71eb9b 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt @@ -14,6 +14,7 @@ import androidx.viewpager.widget.ViewPager import com.stericson.RootTools.RootTools import me.grantland.widget.AutofitHelper import org.fossify.commons.dialogs.RadioGroupDialog +import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.appLaunched import org.fossify.commons.extensions.appLockManager import org.fossify.commons.extensions.beGoneIf @@ -63,7 +64,6 @@ import org.fossify.filemanager.databinding.ActivityMainBinding import org.fossify.filemanager.dialogs.ChangeSortingDialog import org.fossify.filemanager.dialogs.ChangeViewTypeDialog import org.fossify.filemanager.dialogs.InsertFilenameDialog -import org.fossify.filemanager.enums.ConnectionTypes import org.fossify.filemanager.extensions.config import org.fossify.filemanager.extensions.tryOpenPathIntent import org.fossify.filemanager.fragments.ItemsFragment diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/ReadTextActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/ReadTextActivity.kt index 9696e0650..be3a3f6d5 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/ReadTextActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/ReadTextActivity.kt @@ -158,7 +158,7 @@ class ReadTextActivity : SimpleActivity() { private fun openSearch() { isSearchActive = true - binding.searchWrapper.beVisible() +// binding.searchWrapper.beVisible() showKeyboard(searchQueryET) binding.readTextView.requestFocus() @@ -354,7 +354,7 @@ class ReadTextActivity : SimpleActivity() { false }) - binding.searchWrapper.setBackgroundColor(getProperPrimaryColor()) +// binding.searchWrapper.setBackgroundColor(getProperPrimaryColor()) val contrastColor = getProperPrimaryColor().getContrastColor() arrayListOf(searchPrevBtn, searchNextBtn, searchClearBtn).forEach { it.applyColorFilter(contrastColor) @@ -402,7 +402,7 @@ class ReadTextActivity : SimpleActivity() { private fun closeSearch() { searchQueryET.text?.clear() isSearchActive = false - binding.searchWrapper.beGone() +// binding.searchWrapper.beGone() hideKeyboard() } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ChangeViewTypeDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ChangeViewTypeDialog.kt index 0bac2f17f..063e85582 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ChangeViewTypeDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ChangeViewTypeDialog.kt @@ -25,7 +25,7 @@ class ChangeViewTypeDialog(val activity: BaseSimpleActivity, val path: String = changeViewTypeDialogRadio.check(viewToCheck) if (!showFolderCheck) { - useForThisFolderDivider.beGone() +// useForThisFolderDivider.beGone() changeViewTypeDialogUseForThisFolder.beGone() } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index f439febbd..255f3905e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -5,6 +5,7 @@ import android.widget.ArrayAdapter import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import org.fossify.commons.activities.BaseSimpleActivity +import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.getAlertDialogBuilder import org.fossify.commons.extensions.setupDialogStuff import org.fossify.commons.extensions.value @@ -12,7 +13,6 @@ import org.fossify.filemanager.R import org.fossify.filemanager.activities.CloudActivity import org.fossify.filemanager.databinding.DialogAddConnectionBinding import org.fossify.filemanager.databinding.DialogChangeViewTypeBinding -import org.fossify.filemanager.enums.ConnectionTypes class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Int, ConnectionTypes) -> Unit) { private var binding: DialogAddConnectionBinding diff --git a/app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt b/app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt deleted file mode 100644 index d54a996f6..000000000 --- a/app/src/main/kotlin/org/fossify/filemanager/enums/ConnectionTypes.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.fossify.filemanager.enums - -enum class ConnectionTypes(val type: String) { - SMB("SMB"), - WebDav("WebDav"), - ExternalStorage("External Storage"), - SFTP("SFTP"), - Default("Default"); - - companion object { - fun fromType(value: String): ConnectionTypes { - return entries.find { it.type == value } ?: Default - } - } -} diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt index a7b422624..2cda091b8 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -11,7 +11,7 @@ import jcifs.smb.SmbFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.fossify.filemanager.enums.ConnectionTypes +import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.models.ListItem import java.io.File diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 44bf66e79..bfbdf2ab9 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -4,7 +4,7 @@ import fi.iki.elonen.NanoHTTPD import jcifs.smb.SmbFile import jcifs.smb.SmbFileInputStream -import org.fossify.filemanager.enums.ConnectionTypes +import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index 9592960f6..793349e7f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.dialogs.StoragePickerDialog +import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.* import org.fossify.commons.helpers.* import org.fossify.commons.models.FileDirItem @@ -27,7 +28,6 @@ import org.fossify.filemanager.activities.SimpleActivity import org.fossify.filemanager.adapters.ItemsAdapter import org.fossify.filemanager.databinding.ItemsFragmentBinding import org.fossify.filemanager.dialogs.CreateNewItemDialog -import org.fossify.filemanager.enums.ConnectionTypes import org.fossify.filemanager.extensions.config import org.fossify.filemanager.extensions.isPathOnRoot import org.fossify.filemanager.fileSystems.FileHelpers @@ -122,7 +122,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF getRecyclerAdapter()?.finishActMode() } - fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, connectionType: ConnectionTypes = ConnectionTypes.Default) { + fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, connectionType: ConnectionTypes = ConnectionTypes.Default) { if ((activity as? BaseSimpleActivity)?.isAskingPermissions == true) { return } @@ -174,14 +174,14 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF private fun addItems(items: ArrayList, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, connectionType: ConnectionTypes) { activity?.runOnUiThread { binding.itemsSwipeRefresh.isRefreshing = false - binding.breadcrumbs.setBreadcrumb(currentPath) + binding.breadcrumbs.setBreadcrumb(currentPath,connectionType) if (!forceRefresh && items.hashCode() == storedItems.hashCode()) { return@runOnUiThread } storedItems = items if (binding.itemsList.adapter == null) { - binding.breadcrumbs.updateFontSize(context!!.getTextSize(), true) + binding.breadcrumbs.updateFontSize(context!!.getTextSize(), true,connectionType) } ItemsAdapter(activity as SimpleActivity, storedItems, this, binding.itemsList, isPickMultipleIntent, binding.itemsSwipeRefresh) { @@ -631,7 +631,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } else { val item = binding.breadcrumbs.getItem(id) - openPath(item.path) + openPath(item.path, connectionType = item.connectionType) } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt index 49eb4dc74..63a73ddf2 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -1,6 +1,6 @@ package org.fossify.filemanager.helpers -import org.fossify.filemanager.enums.ConnectionTypes +import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.enums.Protocols import java.nio.file.Path From d5183a89c9aae0c8d33fd6b5f5af94f569a6afc6 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sat, 11 Apr 2026 16:42:52 +0500 Subject: [PATCH 10/37] Implemented sftp and bug fixes for bread crumbs --- .../filemanager/activities/CloudActivity.kt | 21 ++++-- .../filemanager/fileSystems/HttpServer.kt | 16 ++--- .../filemanager/fragments/ItemsFragment.kt | 38 +++++----- .../NetworkConnectionRepositoryApi.kt | 16 +++-- .../mapper/NetworkConnectionMapper.kt | 16 ++--- .../NetworkConnectionRepositoryApiImpl.kt | 71 ++++++++++--------- .../viewmodels/NetworkBrowserViewModel.kt | 26 ++++--- 7 files changed, 116 insertions(+), 88 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 23c08f299..0c2a05fef 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -2,13 +2,11 @@ package org.fossify.filemanager.activities import android.content.Intent import android.os.Bundle -import android.util.Log import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -26,12 +24,27 @@ import org.fossify.filemanager.helpers.PORT_SFTP import org.fossify.filemanager.helpers.PORT_WEBDAV import org.fossify.filemanager.models.NetworkConnection import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel +import java.security.Security +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.security.Provider class CloudActivity : SimpleActivity() { private val binding by viewBinding(CloudActivityBinding::inflate) private lateinit var viewModel: NetworkBrowserViewModel + private fun setupBouncyCastle() { + val provider: Provider? = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) + if (provider == null) { + return + } + if (provider.javaClass.equals(BouncyCastleProvider::class.java)) { + return + } + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.insertProviderAt(BouncyCastleProvider(), 1) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) @@ -42,7 +55,7 @@ class CloudActivity : SimpleActivity() { registerAddConnectionListener() val composition = (application as App).appComposition val factory = composition.provideNetworkBrowserViewModelFactory() - + setupBouncyCastle() viewModel = ViewModelProvider(this, factory) .get(NetworkBrowserViewModel::class.java) getAllSavedNetworks() @@ -133,7 +146,7 @@ class CloudActivity : SimpleActivity() { viewModel.verifySFTP.collectLatest { if (it) { viewModel.saveNetwork( - NetworkConnection(host = host, username = user, password = password, connectionType = connectionType.toString(), port = port, displayName = displayName, url = viewModel.getSFTPConn().pwd()) + NetworkConnection(host = host, username = user, password = password, connectionType = connectionType.toString(), port = port, displayName = displayName, url = viewModel.getSFTPConn().canonicalize(".")) ) } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index bfbdf2ab9..4c1a6f705 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -7,6 +7,7 @@ import jcifs.smb.SmbFileInputStream import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel +import java.io.BufferedInputStream class HttpServer( private val port: Int, @@ -33,7 +34,7 @@ class HttpServer( return handleRangeRequestWebDav(rangeHeader, webDavFile?.contentLength!!, uri = uri, webDavFile.contentType) } val url = Helpers.createUrl(connectionTypes, server = serverIp, path = "", port = machinePort) - val sftFile = viewModel.listSFTPFileDetails(url) + val sftFile = viewModel.listSFTPFileDetails(uri) return handleRangeRequestSFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } @@ -118,7 +119,7 @@ class HttpServer( } private fun handleRangeRequestSFTPServer(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String): Response { - var start: Long = 0 + var start: Long = 0 var end = fileLength - 1 val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri, port = machinePort) if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { @@ -135,17 +136,12 @@ class HttpServer( val contentLength = end - start + 1 - val inputStream = viewModel.getSFTPFileStream(url) - var remaining = start - while (remaining > 0) { - val skipped = inputStream.skip(remaining) - if (skipped <= 0) break - remaining -= skipped - } + val inputStream = viewModel.getSFTPFileStream(uri,start) + val bufferedStream = BufferedInputStream(inputStream, 1024 * 1024) return newFixedLengthResponse( Response.Status.PARTIAL_CONTENT, MimeTypes.getMimeTypes(contentType), - inputStream, + bufferedStream, contentLength ).apply { addHeader("Accept-Ranges", "bytes") diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index 793349e7f..d4859cbfe 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -185,27 +185,27 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } ItemsAdapter(activity as SimpleActivity, storedItems, this, binding.itemsList, isPickMultipleIntent, binding.itemsSwipeRefresh) { - if(connectionType == ConnectionTypes.SMB){ - (it as? ListItem)?.let { item -> - FileHelpers.launchSMB(item, this@ItemsFragment.context,viewModel.getMainSmb()) - } - } - else if(connectionType == ConnectionTypes.WebDav){ - (it as? ListItem)?.let { item -> - FileHelpers.launchWebDav(connectionType, context = this@ItemsFragment.context, item = item) - } - } - else if(connectionType == ConnectionTypes.SFTP){ - (it as? ListItem)?.let { item -> - FileHelpers.launchSFTP(connectionType, context = this@ItemsFragment.context, item = item) + if((it as? ListItem)?.mIsDirectory == false) { + if (connectionType == ConnectionTypes.SMB) { + it?.let { item -> + FileHelpers.launchSMB(item, this@ItemsFragment.context, viewModel.getMainSmb()) + } + } else if (connectionType == ConnectionTypes.WebDav) { + it?.let { item -> + FileHelpers.launchWebDav(connectionType, context = this@ItemsFragment.context, item = item) + } + } else if (connectionType == ConnectionTypes.SFTP) { + it?.let { item -> + FileHelpers.launchSFTP(connectionType, context = this@ItemsFragment.context, item = item) + } } } else if ((it as? ListItem)?.isSectionTitle == true) { openDirectory(it.mPath) searchClosed() } else { - itemClicked(it as FileDirItem) + itemClicked(it as FileDirItem,connectionType) } }.apply { setupZoomListener(zoomListener) @@ -250,7 +250,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF else if (connectionType.equals(ConnectionTypes.SFTP)){ CoroutineScope(Dispatchers.IO).launch { - viewModel.listAllSFTPFile(path) + viewModel.listAllFilesSFTPRoot(path) viewModel.sftpFiles.collectLatest { if(it.isNotEmpty()) { val fileItems = it @@ -386,19 +386,19 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF return listItems } - private fun itemClicked(item: FileDirItem) { + private fun itemClicked(item: FileDirItem, connectionType: ConnectionTypes) { if (item.isDirectory) { - openDirectory(item.path) + openDirectory(item.path,connectionType) } else { clickedPath(item.path) } } - private fun openDirectory(path: String) { + private fun openDirectory(path: String, connectionType: ConnectionTypes = ConnectionTypes.Default) { (activity as? MainActivity)?.apply { openedDirectory() } - openPath(path) + openPath(path, connectionType = connectionType) } override fun searchQueryChanged(text: String) { diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt index c88c41ba6..2cc6c45da 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt @@ -1,9 +1,11 @@ package org.fossify.filemanager.interfaces -import com.jcraft.jsch.ChannelSftp -import com.jcraft.jsch.SftpATTRS + import com.thegrizzlylabs.sardineandroid.DavResource import jcifs.smb.SmbFile +import net.schmizz.sshj.sftp.FileAttributes +import net.schmizz.sshj.sftp.RemoteResourceInfo +import net.schmizz.sshj.sftp.SFTPClient import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream @@ -24,11 +26,13 @@ interface NetworkConnectionRepositoryApi { suspend fun connectToSftp(userName: String, password: String,server: String,port: Int): Boolean - suspend fun listAllSFTPFiles(path: String):MutableList + suspend fun listAllFilesSFTPRoot(path: String): List + + suspend fun listAllFilesSFTPPath(path: String):List - fun listSFTPFileDetails(path: String): SftpATTRS? + fun listSFTPFileDetails(path: String): FileAttributes? - fun listSFTPFileInputStream(url: String): InputStream + fun listSFTPFileInputStream(url: String,startByte: Long): InputStream - fun getSFTPConn(): ChannelSftp + fun getSFTPConn(): SFTPClient } diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index 5579cfd99..177c7881c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -1,9 +1,9 @@ package org.fossify.filemanager.mapper import android.util.Log -import com.jcraft.jsch.ChannelSftp import com.thegrizzlylabs.sardineandroid.DavResource import jcifs.smb.SmbFile +import net.schmizz.sshj.sftp.RemoteResourceInfo import org.fossify.commons.models.FileDirItem import org.fossify.filemanager.entity.NetworkConnectionEntity import org.fossify.filemanager.models.NetworkConnection @@ -59,16 +59,16 @@ fun DavResource.toFileItem(): FileDirItem { } -fun ChannelSftp.LsEntry.toFileItem(parentPath: String): FileDirItem { - val attrs = this.attrs +fun RemoteResourceInfo.toFileItem(parentPath: String): FileDirItem { + val attrs = this.attributes val cleanParent = parentPath.trimEnd('/') return FileDirItem( - path = "$cleanParent/${this.filename}", - name = this.filename.trimEnd('/'), - isDirectory = attrs.isDir, - size = if (!attrs.isDir) attrs.size else 0L, - modified = attrs.mTime.toLong() * 1000L, + path = "$cleanParent/${this.name}", + name = this.name, + isDirectory = this.isDirectory, + size = if (this.isRegularFile) attrs.size else 0L, + modified = attrs.mtime * 1000L, children = 0, mediaStoreId = 0L ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt index a9169b82f..ef6433d4c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt @@ -1,10 +1,6 @@ package org.fossify.filemanager.repository import android.util.Log -import com.jcraft.jsch.ChannelSftp -import com.jcraft.jsch.JSch -import com.jcraft.jsch.Session -import com.jcraft.jsch.SftpATTRS import com.thegrizzlylabs.sardineandroid.DavResource import com.thegrizzlylabs.sardineandroid.Sardine import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine @@ -14,16 +10,22 @@ import jcifs.config.PropertyConfiguration import jcifs.context.BaseContext import jcifs.smb.NtlmPasswordAuthenticator import jcifs.smb.SmbFile +import net.schmizz.sshj.SSHClient +import net.schmizz.sshj.sftp.FileAttributes +import net.schmizz.sshj.sftp.SFTPClient +import net.schmizz.sshj.transport.verification.PromiscuousVerifier import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream import java.util.Properties +import net.schmizz.sshj.sftp.RemoteResourceInfo class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { lateinit var dir: SmbFile lateinit var sardine: Sardine - - lateinit var sftp: ChannelSftp + private val sftpLock = Any() + private lateinit var ssh: SSHClient + private lateinit var sftp: SFTPClient private val defaultProperties: Properties = Properties().apply { setProperty("jcifs.resolveOrder", "BCAST") @@ -92,44 +94,49 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { override suspend fun connectToSftp(userName: String, password: String, server: String, port: Int): Boolean { try { - val jsch = JSch() - val session: Session = jsch.getSession(userName, server, port) - - session.setPassword(password) - val config = Properties() - config["StrictHostKeyChecking"] = "no" - session.setConfig(config) - - session.connect() - sftp = session.openChannel("sftp") as ChannelSftp - sftp.connect() + if (!::ssh.isInitialized || !ssh.isConnected || !ssh.isAuthenticated) { + ssh = SSHClient() + ssh.addHostKeyVerifier(PromiscuousVerifier()) + ssh.connect(server) + ssh.authPassword(userName, password) + sftp = ssh.newSFTPClient() + } return true - } catch (e: Exception) { e.printStackTrace() return false } } - override suspend fun listAllSFTPFiles(path: String): MutableList { - val currentPath = sftp.pwd() - val files = sftp.ls(currentPath) - val allFiles = mutableListOf() - for (item in files) { - val entry = item as ChannelSftp.LsEntry - allFiles.add(entry) - } - return allFiles + override suspend fun listAllFilesSFTPRoot(path: String): List { + val files = sftp.ls(path) + return files } - override fun listSFTPFileDetails(path: String): SftpATTRS? { - val file = sftp.stat(path) - return file + override suspend fun listAllFilesSFTPPath(path: String): List { + val files = sftp.ls(path) + return files } - override fun listSFTPFileInputStream(url: String): InputStream { - return sftp.get(url) + override fun listSFTPFileDetails(path: String): FileAttributes? { + synchronized(sftpLock) { + return try { + val myPath = path.replace("//", "/") + sftp.stat(myPath) + } catch (e: Exception) { + Log.e("SFTP", "Stat failed: ${e.message}") + null + } + } + } + + override fun listSFTPFileInputStream(url: String, startByte: Long): InputStream { + val myPath = url.replace("//", "/") + val remoteFile = sftp.open(myPath) + val inputStream = remoteFile.RemoteFileInputStream(startByte) + return inputStream } override fun getSFTPConn() = sftp + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 1bbeed999..2c986fab0 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -2,8 +2,6 @@ package org.fossify.filemanager.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.jcraft.jsch.ChannelSftp -import com.jcraft.jsch.SftpATTRS import com.thegrizzlylabs.sardineandroid.DavResource import jcifs.CIFSContext import jcifs.Configuration @@ -16,6 +14,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import net.schmizz.sshj.sftp.FileAttributes +import net.schmizz.sshj.sftp.RemoteResourceInfo +import net.schmizz.sshj.sftp.SFTPClient import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb import org.fossify.filemanager.models.NetworkConnection @@ -30,7 +31,7 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo val verifySFTP = MutableSharedFlow() - val sftpFiles = MutableStateFlow>(emptyList()) + val sftpFiles = MutableStateFlow>(emptyList()) val webDavFiles = MutableStateFlow>(emptyList()) fun saveNetwork(networkConnection: NetworkConnection){ @@ -79,7 +80,7 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo fun getMainSmb(): SmbFile = networkConnectionRepositoryApi.getMainSmbFile() - fun getSFTPConn():ChannelSftp = networkConnectionRepositoryApi.getSFTPConn() + fun getSFTPConn(): SFTPClient = networkConnectionRepositoryApi.getSFTPConn() fun connectAndAuthenticateWebDav(userName: String = "", password: String = "", url: String){ viewModelScope.launch(Dispatchers.IO) { @@ -109,18 +110,25 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo } } - fun listAllSFTPFile(path: String){ + fun listAllFilesSFTPRoot(path: String){ viewModelScope.launch(Dispatchers.IO) { - val res = networkConnectionRepositoryApi.listAllSFTPFiles(path) + val res = networkConnectionRepositoryApi.listAllFilesSFTPRoot(path) sftpFiles.emit(res) } } - fun listSFTPFileDetails(path: String):SftpATTRS?{ + fun listAllFilesSFTPPath(path: String){ + viewModelScope.launch(Dispatchers.IO) { + val res = networkConnectionRepositoryApi.listAllFilesSFTPRoot(path) + sftpFiles.emit(res) + } + } + + fun listSFTPFileDetails(path: String): FileAttributes?{ return networkConnectionRepositoryApi.listSFTPFileDetails(path) } - fun getSFTPFileStream(path: String): InputStream{ - return networkConnectionRepositoryApi.listSFTPFileInputStream(url = path) + fun getSFTPFileStream(path: String,startByte: Long): InputStream{ + return networkConnectionRepositoryApi.listSFTPFileInputStream(url = path,startByte) } } From ab36c341e3505e7d0769a2e29db0b4f45fc7359d Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sun, 12 Apr 2026 02:07:00 +0500 Subject: [PATCH 11/37] Integrated ftp --- .../filemanager/activities/CloudActivity.kt | 31 +++++++++++++- .../filemanager/dialogs/ConnectionDialog.kt | 2 +- .../filemanager/fileSystems/HttpServer.kt | 40 ++++++++++++++++++- .../filemanager/fragments/ItemsFragment.kt | 13 ++++++ .../fossify/filemanager/helpers/Constants.kt | 2 + .../NetworkConnectionRepositoryApi.kt | 8 ++++ .../mapper/NetworkConnectionMapper.kt | 14 +++++++ .../NetworkConnectionRepositoryApiImpl.kt | 28 +++++++++++++ .../viewmodels/NetworkBrowserViewModel.kt | 22 ++++++++++ 9 files changed, 156 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 0c2a05fef..7861fdc2c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -26,6 +26,7 @@ import org.fossify.filemanager.models.NetworkConnection import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel import java.security.Security import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.fossify.filemanager.helpers.PORT_FTP import java.security.Provider @@ -120,7 +121,7 @@ class CloudActivity : SimpleActivity() { ) ) } - if (connectionType == ConnectionTypes.WebDav) { + else if (connectionType == ConnectionTypes.WebDav) { val url = "http://${host}:${port}/${shared}" viewModel.connectAndAuthenticateWebDav(user, password, url) viewModel.verifyWebDav.collectLatest { @@ -141,7 +142,7 @@ class CloudActivity : SimpleActivity() { } } - if (connectionType == ConnectionTypes.SFTP) { + else if (connectionType == ConnectionTypes.SFTP) { viewModel.connectSFTP(user, password, host, port) viewModel.verifySFTP.collectLatest { if (it) { @@ -151,6 +152,16 @@ class CloudActivity : SimpleActivity() { } } } + else if (connectionType == ConnectionTypes.FTP) { + viewModel.connectFTP(user, password, host, port) + viewModel.verifyFTP.collectLatest { + if (it) { + viewModel.saveNetwork( + NetworkConnection(host = host, username = user, password = password, connectionType = connectionType.toString(), port = port, displayName = displayName, url = viewModel.getFTP().printWorkingDirectory()) + ) + } + } + } } } @@ -217,6 +228,22 @@ class CloudActivity : SimpleActivity() { } + else if (item.connectionType == ConnectionTypes.FTP.type){ + itm.username?.let { username -> + itm.password?.let { password -> + viewModel.connectFTP(username,password,itm.host,itm.port) + lifecycleScope.launch(Dispatchers.IO) { + viewModel.verifyFTP.collectLatest { + if(it){ + startServer(item,PORT_FTP, connectionType = ConnectionTypes.FTP, machinePort = itm.port) + launchMainActivity(ConnectionTypes.FTP,itm.url) + } + } + } + } + } + } + }.apply { binding.connectionsList.adapter = this } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 255f3905e..8d5407917 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -16,7 +16,7 @@ import org.fossify.filemanager.databinding.DialogChangeViewTypeBinding class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Int, ConnectionTypes) -> Unit) { private var binding: DialogAddConnectionBinding - val items = listOf(ConnectionTypes.ExternalStorage.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.SFTP.type) + val items = listOf(ConnectionTypes.ExternalStorage.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.FTP.type) init { binding = DialogAddConnectionBinding.inflate(activity.layoutInflater) diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 4c1a6f705..402b596bf 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -33,9 +33,14 @@ class HttpServer( val webDavFile = viewModel.listWebDavFileDetail(url) return handleRangeRequestWebDav(rangeHeader, webDavFile?.contentLength!!, uri = uri, webDavFile.contentType) } + else if (connectionTypes.equals(ConnectionTypes.SFTP)) { + val url = Helpers.createUrl(connectionTypes, server = serverIp, path = "", port = machinePort) + val sftFile = viewModel.listSFTPFileDetails(uri) + return handleRangeRequestSFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) + } val url = Helpers.createUrl(connectionTypes, server = serverIp, path = "", port = machinePort) val sftFile = viewModel.listSFTPFileDetails(uri) - return handleRangeRequestSFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) + return handleRangeRequestFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } @@ -151,4 +156,37 @@ class HttpServer( } } + private fun handleRangeRequestFTPServer(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String): Response { + var start: Long = 0 + var end = fileLength - 1 + val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri, port = machinePort) + if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { + val ranges = rangeHeader.substring(6).split("-") + try { + if (ranges[0].isNotEmpty()) start = ranges[0].toLong() + if (ranges.size > 1 && ranges[1].isNotEmpty()) end = ranges[1].toLong() + } catch (e: NumberFormatException) { + } + } + if (start >= fileLength) { + return newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, "text/plain", "") + } + + val contentLength = end - start + 1 + + val inputStream = viewModel.getSFTPFileStream(uri,start) + val bufferedStream = BufferedInputStream(inputStream, 1024 * 1024) + return newFixedLengthResponse( + Response.Status.PARTIAL_CONTENT, + MimeTypes.getMimeTypes(contentType), + bufferedStream, + contentLength + ).apply { + addHeader("Accept-Ranges", "bytes") + addHeader("Content-Length", contentLength.toString()) + addHeader("Content-Range", "bytes $start-$end/$fileLength") + addHeader("Connection", "keep-alive") + } + } + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index d4859cbfe..512ef3640 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -260,6 +260,19 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } } + + else if (connectionType.equals(ConnectionTypes.FTP)){ + CoroutineScope(Dispatchers.IO).launch { + viewModel.listAllFTPFiles(path) + viewModel.ftpFiles.collectLatest { + if(it.isNotEmpty()) { + val fileItems = it + val items = fileItems.map { it -> it.toFileItem(path) } + callback(path, getListItemsFromFileDirItems(ArrayList(items.toList()))) + } + } + } + } else if (context.isRestrictedSAFOnlyRoot(path)) { activity?.runOnUiThread { hideProgressBar() } activity?.handleAndroidSAFDialog(path, openInSystemAppAllowed = true) { diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt index 24ad6ff67..0d574a891 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt @@ -15,6 +15,8 @@ const val PORT_WEBDAV = 7890 const val PORT_SFTP = 7860 +const val PORT_FTP = 7850 + // shared preferences const val SHOW_HIDDEN = "show_hidden" const val PRESS_BACK_TWICE = "press_back_twice" diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt index 2cc6c45da..e1baaefa9 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt @@ -6,6 +6,8 @@ import jcifs.smb.SmbFile import net.schmizz.sshj.sftp.FileAttributes import net.schmizz.sshj.sftp.RemoteResourceInfo import net.schmizz.sshj.sftp.SFTPClient +import org.apache.commons.net.ftp.FTPClient +import org.apache.commons.net.ftp.FTPFile import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream @@ -35,4 +37,10 @@ interface NetworkConnectionRepositoryApi { fun listSFTPFileInputStream(url: String,startByte: Long): InputStream fun getSFTPConn(): SFTPClient + + suspend fun connectToFTP(userName: String, password: String,server: String,port: Int): Boolean + + suspend fun listAllFTPFiles(path: String): List + + fun getFTPConn(): FTPClient } diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index 177c7881c..151fbaf10 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -4,6 +4,7 @@ import android.util.Log import com.thegrizzlylabs.sardineandroid.DavResource import jcifs.smb.SmbFile import net.schmizz.sshj.sftp.RemoteResourceInfo +import org.apache.commons.net.ftp.FTPFile import org.fossify.commons.models.FileDirItem import org.fossify.filemanager.entity.NetworkConnectionEntity import org.fossify.filemanager.models.NetworkConnection @@ -73,3 +74,16 @@ fun RemoteResourceInfo.toFileItem(parentPath: String): FileDirItem { mediaStoreId = 0L ) } + +fun FTPFile.toFileItem(parentPath: String): FileDirItem { + val cleanParent = parentPath.trimEnd('/') + return FileDirItem( + path = "$cleanParent/${this.name}", + name = this.name, + isDirectory = this.isDirectory, + size = if (this.isFile) this.size else 0L, + modified = this.timestamp?.timeInMillis ?: 0L, + children = 0, + mediaStoreId = 0L + ) +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt index ef6433d4c..de9175c0f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt @@ -19,6 +19,8 @@ import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream import java.util.Properties import net.schmizz.sshj.sftp.RemoteResourceInfo +import org.apache.commons.net.ftp.FTPClient +import org.apache.commons.net.ftp.FTPFile class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { lateinit var dir: SmbFile @@ -26,6 +28,8 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { private val sftpLock = Any() private lateinit var ssh: SSHClient private lateinit var sftp: SFTPClient + + private lateinit var ftp: FTPClient private val defaultProperties: Properties = Properties().apply { setProperty("jcifs.resolveOrder", "BCAST") @@ -139,4 +143,28 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { override fun getSFTPConn() = sftp + override suspend fun connectToFTP(userName: String, password: String, server: String, port: Int): Boolean { + try { + ftp = FTPClient() + ftp.connect(server, port) + val loginSuccess = ftp.login(userName, password) + if (!loginSuccess) { + return false + } + ftp.enterLocalPassiveMode() + return true + } + catch (exp: Exception){ + return false + } + } + + override suspend fun listAllFTPFiles(path: String): List { + ftp.changeWorkingDirectory(path) + val files: Array = ftp.listFiles() + return files.toList() + } + + override fun getFTPConn(): FTPClient = ftp + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 2c986fab0..1f2262caf 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.launch import net.schmizz.sshj.sftp.FileAttributes import net.schmizz.sshj.sftp.RemoteResourceInfo import net.schmizz.sshj.sftp.SFTPClient +import org.apache.commons.net.ftp.FTPFile import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb import org.fossify.filemanager.models.NetworkConnection @@ -30,10 +31,15 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo val verifyWebDav = MutableSharedFlow() val verifySFTP = MutableSharedFlow() + val verifyFTP = MutableSharedFlow() + val sftpFiles = MutableStateFlow>(emptyList()) val webDavFiles = MutableStateFlow>(emptyList()) + + val ftpFiles = MutableStateFlow>(emptyList()) + fun saveNetwork(networkConnection: NetworkConnection){ viewModelScope.launch(Dispatchers.IO) { networkConnectionRepository.saveConnection(networkConnection) @@ -131,4 +137,20 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo fun getSFTPFileStream(path: String,startByte: Long): InputStream{ return networkConnectionRepositoryApi.listSFTPFileInputStream(url = path,startByte) } + + fun connectFTP(userName: String, password: String,server: String,port: Int){ + viewModelScope.launch(Dispatchers.IO) { + val res = networkConnectionRepositoryApi.connectToFTP(userName,password,server,port) + verifyFTP.emit(res) + } + } + + fun getFTP() = networkConnectionRepositoryApi.getFTPConn() + + fun listAllFTPFiles(path: String){ + viewModelScope.launch(Dispatchers.IO) { + val res = networkConnectionRepositoryApi.listAllFTPFiles(path) + ftpFiles.emit(res) + } + } } From 519ec657a5f4fbf660bc7328f3b8e364181d03e6 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Mon, 13 Apr 2026 00:45:03 +0500 Subject: [PATCH 12/37] added ftp and saf --- app/src/main/AndroidManifest.xml | 11 ++++ .../filemanager/activities/CloudActivity.kt | 27 ++++++++- .../filemanager/dao/DocumentProviderDao.kt | 4 ++ .../filemanager/dialogs/ConnectionDialog.kt | 7 +-- .../ExternalStorageProvider.kt | 56 +++++++++++++++++++ .../entity/DocumentProviderEntity.kt | 4 ++ .../filemanager/fileSystems/FileHelpers.kt | 20 +++++++ .../filemanager/fileSystems/HttpServer.kt | 6 +- .../filemanager/fragments/ItemsFragment.kt | 10 +++- .../fossify/filemanager/helpers/Helpers.kt | 3 + .../NetworkConnectionRepositoryApi.kt | 4 ++ .../NetworkConnectionRepositoryApiImpl.kt | 34 ++++++++++- .../viewmodels/NetworkBrowserViewModel.kt | 4 ++ 13 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/documentProvider/ExternalStorageProvider.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5b958d75c..8812edeae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,17 @@ android:theme="@style/AppTheme" android:usesCleartextTraffic="true"> + + + + + + itm.password?.let { password -> diff --git a/app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt b/app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt new file mode 100644 index 000000000..f13ec8a38 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt @@ -0,0 +1,4 @@ +package org.fossify.filemanager.dao + +interface DocumentProviderDao { +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 8d5407917..ddf37d18e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -1,9 +1,7 @@ package org.fossify.filemanager.dialogs -import android.content.Intent import android.widget.ArrayAdapter import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.getAlertDialogBuilder @@ -12,11 +10,10 @@ import org.fossify.commons.extensions.value import org.fossify.filemanager.R import org.fossify.filemanager.activities.CloudActivity import org.fossify.filemanager.databinding.DialogAddConnectionBinding -import org.fossify.filemanager.databinding.DialogChangeViewTypeBinding class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Int, ConnectionTypes) -> Unit) { private var binding: DialogAddConnectionBinding - val items = listOf(ConnectionTypes.ExternalStorage.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.FTP.type) + val items = listOf(ConnectionTypes.DAVx5.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.FTP.type) init { binding = DialogAddConnectionBinding.inflate(activity.layoutInflater) @@ -50,7 +47,7 @@ class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, Stri private fun dropDownItemSelected() { binding.dropdownMenu.setOnItemClickListener { parent, view, position, id -> val selectedItem = parent.getItemAtPosition(position).toString() - if (selectedItem == ConnectionTypes.ExternalStorage.type) { + if (selectedItem == ConnectionTypes.DAVx5.type) { promptUserToSelectStorage() } Toast.makeText(activity, "Selected: $selectedItem", Toast.LENGTH_SHORT).show() diff --git a/app/src/main/kotlin/org/fossify/filemanager/documentProvider/ExternalStorageProvider.kt b/app/src/main/kotlin/org/fossify/filemanager/documentProvider/ExternalStorageProvider.kt new file mode 100644 index 000000000..39f339c2f --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/documentProvider/ExternalStorageProvider.kt @@ -0,0 +1,56 @@ +package org.fossify.filemanager.documentProvider + +import android.database.Cursor +import android.database.MatrixCursor +import android.os.CancellationSignal +import android.os.ParcelFileDescriptor +import android.provider.DocumentsContract +import android.provider.DocumentsProvider +import androidx.lifecycle.ViewModelProvider +import org.fossify.filemanager.App + +import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel + +class ExternalStorageProvider: DocumentsProvider() { + override fun openDocument(documentId: String?, mode: String?, signal: CancellationSignal?): ParcelFileDescriptor? { + TODO("Not yet implemented") + } + + override fun queryChildDocuments( + parentDocumentId: String?, + projection: Array?, + sortOrder: String? + ): Cursor? { + TODO("Not yet implemented") + } + + override fun queryDocument(documentId: String?, projection: Array?): Cursor? { + TODO("Not yet implemented") + } + + override fun queryRoots(projection: Array?): Cursor? { + val cursor = MatrixCursor( + arrayOf( + DocumentsContract.Root.COLUMN_ROOT_ID, + DocumentsContract.Root.COLUMN_TITLE, + DocumentsContract.Root.COLUMN_FLAGS + ) + ) + + val row = cursor.newRow() + row.add(DocumentsContract.Root.COLUMN_ROOT_ID, "root") + row.add(DocumentsContract.Root.COLUMN_TITLE, "My Remote Storage") + row.add(DocumentsContract.Root.COLUMN_FLAGS, + DocumentsContract.Root.FLAG_SUPPORTS_CREATE) + + return cursor + } + + override fun onCreate(): Boolean { + val app = context?.applicationContext as App + val composition = app.appComposition + val factory = composition.provideNetworkBrowserViewModelFactory() + return true + } + +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt b/app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt new file mode 100644 index 000000000..e3a6ce7fa --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt @@ -0,0 +1,4 @@ +package org.fossify.filemanager.entity + + +data class DocumentProviderEntity () diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt index 2cda091b8..cb6cd934c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -77,4 +77,24 @@ object FileHelpers { Log.e("Activity Launch Failed", exp.toString()) } } + fun launchFTP(connectionTypes: ConnectionTypes,item: ListItem,context: Context){ + try{ + CoroutineScope(Dispatchers.IO).launch { + val port = Helpers.getPortForEachService(connectionTypes) + val uri = Helpers.createUrl(connectionTypes, item.mPath, port = port).toUri() + val i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) + + val packageManager: PackageManager = context.packageManager + val resInfos = packageManager.queryIntentActivities(i, 0) + if (resInfos.size > 0) { + context.startActivity(i) + } + } + } + catch (exp: Exception){ + Log.e("Activity Launch Failed", exp.toString()) + } + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 402b596bf..3ca459910 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -39,7 +39,7 @@ class HttpServer( return handleRangeRequestSFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } val url = Helpers.createUrl(connectionTypes, server = serverIp, path = "", port = machinePort) - val sftFile = viewModel.listSFTPFileDetails(uri) + val sftFile = viewModel.getFTPFileDetail(uri) return handleRangeRequestFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } @@ -126,7 +126,6 @@ class HttpServer( private fun handleRangeRequestSFTPServer(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String): Response { var start: Long = 0 var end = fileLength - 1 - val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri, port = machinePort) if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { val ranges = rangeHeader.substring(6).split("-") try { @@ -159,7 +158,6 @@ class HttpServer( private fun handleRangeRequestFTPServer(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String): Response { var start: Long = 0 var end = fileLength - 1 - val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri, port = machinePort) if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { val ranges = rangeHeader.substring(6).split("-") try { @@ -174,7 +172,7 @@ class HttpServer( val contentLength = end - start + 1 - val inputStream = viewModel.getSFTPFileStream(uri,start) + val inputStream = viewModel.getFTPFileStream(uri,start) val bufferedStream = BufferedInputStream(inputStream, 1024 * 1024) return newFixedLengthResponse( Response.Status.PARTIAL_CONTENT, diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index 512ef3640..a0389abb7 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -5,6 +5,7 @@ import android.content.Context import android.net.Uri import android.os.Parcelable import android.util.AttributeSet +import androidx.activity.viewModels import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager @@ -200,6 +201,11 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF FileHelpers.launchSFTP(connectionType, context = this@ItemsFragment.context, item = item) } } + else if (connectionType == ConnectionTypes.FTP) { + it?.let { item -> + FileHelpers.launchFTP(connectionType, context = this@ItemsFragment.context, item = item) + } + } } else if ((it as? ListItem)?.isSectionTitle == true) { openDirectory(it.mPath) @@ -290,7 +296,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF context!!.getOTGItems(path, config.shouldShowHidden(), getProperFileSize) { callback(path, getListItemsFromFileDirItems(it)) } - } else if (!config.enableRootAccess || !context!!.isPathOnRoot(path) && (connectionType.equals(ConnectionTypes.ExternalStorage) || connectionType.equals(ConnectionTypes.Default))) { + } else if (!config.enableRootAccess || !context!!.isPathOnRoot(path) && (connectionType.equals(ConnectionTypes.DAVx5) || connectionType.equals(ConnectionTypes.Default))) { getRegularItemsOf(path, callback,connectionType) } else { RootHelpers(activity!!).getFiles(path, callback) @@ -303,7 +309,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF val items = ArrayList() val getProperChildCount = context!!.config.getFolderViewType(currentPath) == VIEW_TYPE_LIST - if(connectionType == ConnectionTypes.ExternalStorage){ + if(connectionType == ConnectionTypes.DAVx5){ val uri = Uri.parse(path) val docFile = DocumentFile.fromTreeUri(context, uri) val files = docFile?.listFiles() diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt index 63a73ddf2..8275d7ed8 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -28,6 +28,9 @@ object Helpers { else if(connectionTypes.equals(ConnectionTypes.SFTP)){ return PORT_SFTP } + else if(connectionTypes.equals(ConnectionTypes.FTP)){ + return PORT_FTP + } return PORT_SMB } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt index e1baaefa9..1e16fd1e2 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt @@ -42,5 +42,9 @@ interface NetworkConnectionRepositoryApi { suspend fun listAllFTPFiles(path: String): List + fun getFTPFileDetail(path: String): FTPFile? + + fun getFTPFileInputStream(path: String,start: Long): InputStream + fun getFTPConn(): FTPClient } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt index de9175c0f..22c7e5894 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt @@ -19,8 +19,11 @@ import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream import java.util.Properties import net.schmizz.sshj.sftp.RemoteResourceInfo +import org.apache.commons.net.ftp.FTP import org.apache.commons.net.ftp.FTPClient +import org.apache.commons.net.ftp.FTPCmd import org.apache.commons.net.ftp.FTPFile +import java.io.File class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { lateinit var dir: SmbFile @@ -28,8 +31,10 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { private val sftpLock = Any() private lateinit var ssh: SSHClient private lateinit var sftp: SFTPClient - + private lateinit var currentStream: InputStream private lateinit var ftp: FTPClient + private lateinit var ftpStream: FTPClient + private val defaultProperties: Properties = Properties().apply { setProperty("jcifs.resolveOrder", "BCAST") @@ -146,12 +151,18 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { override suspend fun connectToFTP(userName: String, password: String, server: String, port: Int): Boolean { try { ftp = FTPClient() + ftpStream = FTPClient() ftp.connect(server, port) + ftpStream.connect(server, port) + val loginSuccess = ftp.login(userName, password) + ftpStream.login(userName, password) + if (!loginSuccess) { return false } ftp.enterLocalPassiveMode() + ftpStream.enterLocalPassiveMode() return true } catch (exp: Exception){ @@ -165,6 +176,27 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { return files.toList() } + override fun getFTPFileDetail(path: String):FTPFile? { + val myPath = path.replace("//", "/") + if(ftp.hasFeature(FTPCmd.MLST)){ + val file = ftp.mlistFile(myPath) + return file + } + val mP = File(myPath) + val files = ftp.listFiles(mP.parent).firstOrNull { it != null && it.name == mP.name } + return files + } + + override fun getFTPFileInputStream(path: String,start: Long): InputStream { + if(::currentStream.isInitialized) + currentStream.close() + ftpStream.completePendingCommand() + ftpStream.setFileType(FTP.BINARY_FILE_TYPE) + ftpStream.restartOffset = start + currentStream = ftpStream.retrieveFileStream(path) + return currentStream + } + override fun getFTPConn(): FTPClient = ftp } diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 1f2262caf..3ee1506aa 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -153,4 +153,8 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo ftpFiles.emit(res) } } + + fun getFTPFileDetail(path: String) = networkConnectionRepositoryApi.getFTPFileDetail(path) + + fun getFTPFileStream(path: String,start: Long) = networkConnectionRepositoryApi.getFTPFileInputStream(path,start) } From 2d1863fbbbd749d81fdf5308442be3b159d874f6 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Tue, 14 Apr 2026 02:11:18 +0500 Subject: [PATCH 13/37] minor changes for saf --- .../filemanager/dao/DocumentProviderDao.kt | 14 +++++++++++ .../entity/DocumentProviderEntity.kt | 8 +++++- .../interfaces/ExternalStorageRepositoryDb.kt | 8 ++++++ .../ExternalStorageRepositoryDbImpl.kt | 25 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/interfaces/ExternalStorageRepositoryDb.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt diff --git a/app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt b/app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt index f13ec8a38..b935186ab 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt @@ -1,4 +1,18 @@ package org.fossify.filemanager.dao +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow +import org.fossify.filemanager.entity.DocumentProviderEntity +import org.fossify.filemanager.entity.NetworkConnectionEntity + +@Dao interface DocumentProviderDao { + @Query("SELECT * FROM document_provider_entity") + fun getAll(): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(doc: DocumentProviderEntity) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt b/app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt index e3a6ce7fa..bff5502e5 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt @@ -1,4 +1,10 @@ package org.fossify.filemanager.entity +import androidx.room.Entity +import androidx.room.PrimaryKey -data class DocumentProviderEntity () +@Entity(tableName = "document_provider_entity") +data class DocumentProviderEntity ( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val username: String +) diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/ExternalStorageRepositoryDb.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/ExternalStorageRepositoryDb.kt new file mode 100644 index 000000000..70fa798ae --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/ExternalStorageRepositoryDb.kt @@ -0,0 +1,8 @@ +package org.fossify.filemanager.interfaces + +import org.fossify.filemanager.entity.DocumentProviderEntity + +interface ExternalStorageRepositoryDb { + fun saveDocumentInfo(doc: DocumentProviderEntity) + fun getAllDocuments() +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt new file mode 100644 index 000000000..e89b3ec0f --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt @@ -0,0 +1,25 @@ +package org.fossify.filemanager.repository + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import org.fossify.filemanager.dao.DocumentProviderDao +import org.fossify.filemanager.dao.NetworkConnectionDao +import org.fossify.filemanager.entity.DocumentProviderEntity +import org.fossify.filemanager.interfaces.ExternalStorageRepositoryDb + +class ExternalStorageRepositoryDbImpl(private val dao: DocumentProviderDao): ExternalStorageRepositoryDb { + val allDocs = MutableStateFlow>() + override fun saveDocumentInfo(doc: DocumentProviderEntity) { + CoroutineScope(Dispatchers.IO).launch{ + dao.insert(doc) + } + } + + override fun getAllDocuments() { + CoroutineScope(Dispatchers.IO).launch{ + dao.getAll() + } + } +} From 3441c4eba687481853e0af262c4cbeb6dcf130f4 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Wed, 15 Apr 2026 23:55:55 +0500 Subject: [PATCH 14/37] some changes for webdav mount --- .../filemanager/activities/CloudActivity.kt | 8 +++++++- .../org/fossify/filemanager/database/Database.kt | 14 ++++++++++++++ .../database/NetworkConnectionDatabase.kt | 11 ----------- .../filemanager/dependencies/AppComposition.kt | 12 ++++++++---- .../fossify/filemanager/models/DocumentProvider.kt | 3 +++ .../repository/ExternalStorageRepositoryDbImpl.kt | 2 +- .../NetworkConnectionRepositoryDbImpl.kt | 3 --- 7 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/database/Database.kt delete mode 100644 app/src/main/kotlin/org/fossify/filemanager/database/NetworkConnectionDatabase.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/models/DocumentProvider.kt diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 929ed4ec7..de711118e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -27,7 +27,10 @@ import org.fossify.filemanager.models.NetworkConnection import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel import java.security.Security import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.fossify.filemanager.dependencies.AppComposition import org.fossify.filemanager.helpers.PORT_FTP +import org.fossify.filemanager.interfaces.ExternalStorageRepositoryDb +import org.fossify.filemanager.repository.ExternalStorageRepositoryDbImpl import java.security.Provider @@ -35,6 +38,8 @@ class CloudActivity : SimpleActivity() { private val binding by viewBinding(CloudActivityBinding::inflate) private lateinit var viewModel: NetworkBrowserViewModel + private lateinit var composition: AppComposition + private fun setupBouncyCastle() { val provider: Provider? = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) if (provider == null) { @@ -55,7 +60,7 @@ class CloudActivity : SimpleActivity() { } setupToolBar() registerAddConnectionListener() - val composition = (application as App).appComposition + composition = (application as App).appComposition val factory = composition.provideNetworkBrowserViewModelFactory() setupBouncyCastle() viewModel = ViewModelProvider(this, factory) @@ -166,6 +171,7 @@ class CloudActivity : SimpleActivity() { else if (connectionType == ConnectionTypes.WebDavMount) { val url = "http://${host}:${port}/${shared}" viewModel.connectAndAuthenticateWebDav(user, password, url) + composition.documentRepository.saveDocumentInfo() viewModel.verifyWebDav.collectLatest { if (it) { viewModel.saveNetwork( diff --git a/app/src/main/kotlin/org/fossify/filemanager/database/Database.kt b/app/src/main/kotlin/org/fossify/filemanager/database/Database.kt new file mode 100644 index 000000000..2a3252b7c --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/database/Database.kt @@ -0,0 +1,14 @@ +package org.fossify.filemanager.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import org.fossify.filemanager.dao.DocumentProviderDao +import org.fossify.filemanager.dao.NetworkConnectionDao +import org.fossify.filemanager.entity.DocumentProviderEntity +import org.fossify.filemanager.entity.NetworkConnectionEntity + +@Database(entities = [NetworkConnectionEntity::class, DocumentProviderEntity::class], version = 1) +abstract class Database : RoomDatabase() { + abstract fun networkConnectionDao(): NetworkConnectionDao + abstract fun docDao(): DocumentProviderDao +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/database/NetworkConnectionDatabase.kt b/app/src/main/kotlin/org/fossify/filemanager/database/NetworkConnectionDatabase.kt deleted file mode 100644 index a7b6ccaf0..000000000 --- a/app/src/main/kotlin/org/fossify/filemanager/database/NetworkConnectionDatabase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.fossify.filemanager.database - -import androidx.room.Database -import androidx.room.RoomDatabase -import org.fossify.filemanager.dao.NetworkConnectionDao -import org.fossify.filemanager.entity.NetworkConnectionEntity - -@Database(entities = [NetworkConnectionEntity::class], version = 1) -abstract class NetworkConnectionDatabase : RoomDatabase() { - abstract fun networkConnectionDao(): NetworkConnectionDao -} diff --git a/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt b/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt index a2c63c98e..7a75e7006 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt @@ -2,16 +2,16 @@ package org.fossify.filemanager.dependencies import android.content.Context import androidx.room.Room -import org.fossify.filemanager.database.NetworkConnectionDatabase +import org.fossify.filemanager.database.Database import org.fossify.filemanager.factory.NetworkBrowserViewModelFactory -import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb +import org.fossify.filemanager.repository.ExternalStorageRepositoryDbImpl import org.fossify.filemanager.repository.NetworkConnectionRepositoryApiImpl import org.fossify.filemanager.repository.NetworkConnectionRepositoryDbImpl class AppComposition (private val context: Context) { - private fun createDataBase(context: Context): NetworkConnectionDatabase { - return Room.databaseBuilder(context.applicationContext, NetworkConnectionDatabase::class.java,"app-db").build() + private fun createDataBase(context: Context): Database { + return Room.databaseBuilder(context.applicationContext, Database::class.java,"app-db").build() } private val database by lazy { @@ -22,6 +22,10 @@ class AppComposition (private val context: Context) { NetworkConnectionRepositoryDbImpl(database.networkConnectionDao()) } + val documentRepository by lazy { + ExternalStorageRepositoryDbImpl(database.docDao()) + } + val networkApiRepository by lazy { NetworkConnectionRepositoryApiImpl() } diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/DocumentProvider.kt b/app/src/main/kotlin/org/fossify/filemanager/models/DocumentProvider.kt new file mode 100644 index 000000000..5cf84281a --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/models/DocumentProvider.kt @@ -0,0 +1,3 @@ +package org.fossify.filemanager.models + +data class DocumentProvider() diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt index e89b3ec0f..18208b570 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt @@ -10,7 +10,7 @@ import org.fossify.filemanager.entity.DocumentProviderEntity import org.fossify.filemanager.interfaces.ExternalStorageRepositoryDb class ExternalStorageRepositoryDbImpl(private val dao: DocumentProviderDao): ExternalStorageRepositoryDb { - val allDocs = MutableStateFlow>() + val allDocs = MutableStateFlow>(emptyList()) override fun saveDocumentInfo(doc: DocumentProviderEntity) { CoroutineScope(Dispatchers.IO).launch{ dao.insert(doc) diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt index 20125f51e..c2f2b3780 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt @@ -1,11 +1,8 @@ package org.fossify.filemanager.repository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import org.fossify.filemanager.dao.NetworkConnectionDao -import org.fossify.filemanager.database.NetworkConnectionDatabase -import org.fossify.filemanager.entity.NetworkConnectionEntity import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb import org.fossify.filemanager.mapper.toDomain import org.fossify.filemanager.mapper.toEntity From c5c8878126f4f29bf1b31e23ba6bc444848bd926 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sun, 3 May 2026 19:41:33 +0500 Subject: [PATCH 15/37] Changes regarding https madde it stable --- app/src/main/AndroidManifest.xml | 12 -- .../kotlin/org/fossify/filemanager/App.kt | 5 +- .../filemanager/activities/CloudActivity.kt | 120 +++++++++----- .../filemanager/dao/DocumentProviderDao.kt | 18 --- .../filemanager/dao/NetworkConnectionDao.kt | 2 +- .../fossify/filemanager/database/Database.kt | 5 +- .../dependencies/AppComposition.kt | 9 +- .../filemanager/dialogs/ConnectionDialog.kt | 42 ++++- .../ExternalStorageProvider.kt | 56 ------- .../entity/DocumentProviderEntity.kt | 10 -- .../entity/NetworkConnectionEntity.kt | 2 +- .../filemanager/fileSystems/HttpServer.kt | 15 +- .../fossify/filemanager/helpers/Constants.kt | 3 + .../fossify/filemanager/helpers/Helpers.kt | 6 + .../interfaces/CertificateRepository.kt | 10 ++ .../interfaces/ExternalStorageRepositoryDb.kt | 8 - .../NetworkConnectionRepositoryApi.kt | 14 +- .../NetworkConnectionRepositoryDb.kt | 2 +- .../filemanager/keyStores/CertificateStore.kt | 46 ++++++ .../filemanager/models/DocumentProvider.kt | 3 - .../repository/CertificateRepositoryImpl.kt | 22 +++ .../ExternalStorageRepositoryDbImpl.kt | 25 --- .../NetworkConnectionRepositoryApiImpl.kt | 93 +++++++++-- .../NetworkConnectionRepositoryDbImpl.kt | 4 +- .../viewmodels/NetworkBrowserViewModel.kt | 15 +- .../main/res/layout/dialog_add_connection.xml | 49 +++++- gradle/libs.versions.toml | 150 ++++++++++++++++-- 27 files changed, 511 insertions(+), 235 deletions(-) delete mode 100644 app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt delete mode 100644 app/src/main/kotlin/org/fossify/filemanager/documentProvider/ExternalStorageProvider.kt delete mode 100644 app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/interfaces/CertificateRepository.kt delete mode 100644 app/src/main/kotlin/org/fossify/filemanager/interfaces/ExternalStorageRepositoryDb.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/keyStores/CertificateStore.kt delete mode 100644 app/src/main/kotlin/org/fossify/filemanager/models/DocumentProvider.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/repository/CertificateRepositoryImpl.kt delete mode 100644 app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8812edeae..16015c281 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,18 +29,6 @@ android:roundIcon="@mipmap/ic_launcher" android:theme="@style/AppTheme" android:usesCleartextTraffic="true"> - - - - - - - Unit)? = null + private lateinit var certificateRepository: CertificateRepository private lateinit var composition: AppComposition private fun setupBouncyCastle() { @@ -61,6 +67,7 @@ class CloudActivity : SimpleActivity() { setupToolBar() registerAddConnectionListener() composition = (application as App).appComposition + certificateRepository = composition.certificateRepository val factory = composition.provideNetworkBrowserViewModelFactory() setupBouncyCastle() viewModel = ViewModelProvider(this, factory) @@ -92,17 +99,36 @@ class CloudActivity : SimpleActivity() { } } + private val pickCert = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { + onCertPicked?.invoke(it) +// val stream = contentResolver.openInputStream(it) ?: return@let +// val res = viewModel.loadSSLCertificate(inputStream = stream) +// res.onSuccess { certificate -> +// viewModel.attachCertificate(certificate, "TestUser", "bilal786") +// }.onFailure { +// Log.d("WebDav HTTPS", it.localizedMessage) +// } + } + } + fun promptUserToSelectStorage() { openDocumentTreeLauncher.launch(null) } + + fun openFileLink(dispatch: (Uri) -> Unit) { + pickCert.launch("*/*") + onCertPicked = dispatch + } + private fun setupToolBar() { setupTopAppBar(binding.cloudAppbar, NavigationIcon.Arrow) } private fun showConnectionDialog() { - ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName, port, connection -> - saveNetwork(host, user, password, shared, displayName, port, connection) + ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName, certPath, port, connection, protocol -> + saveNetwork(host, user, password, shared, displayName, certPath, port, connection, protocol) } } @@ -113,7 +139,17 @@ class CloudActivity : SimpleActivity() { } } - private fun saveNetwork(host: String, user: String, password: String, shared: String, displayName: String, port: Int, connectionType: ConnectionTypes) { + private fun saveNetwork( + host: String, + user: String, + password: String, + shared: String, + displayName: String, + certUri: Uri?, + port: Int, + connectionType: ConnectionTypes, + protocol: Protocols + ) { lifecycleScope.launch((Dispatchers.IO)) { if (connectionType == ConnectionTypes.SMB) { viewModel.authenticateAndSaveSMBNetwork( @@ -126,10 +162,12 @@ class CloudActivity : SimpleActivity() { displayName = displayName ) ) - } - else if (connectionType == ConnectionTypes.WebDav) { - val url = "http://${host}:${port}/${shared}" - viewModel.connectAndAuthenticateWebDav(user, password, url) + } else if (connectionType == ConnectionTypes.WebDav) { + val url = Helpers.createProtocolPath(protocol, host, port, shared) + if (protocol == Protocols.HTTPS) { + saveCertificate(certUri,host) + } + viewModel.connectAndAuthenticateWebDav(user, password, url,host,protocol,this@CloudActivity) viewModel.verifyWebDav.collectLatest { if (it) { viewModel.saveNetwork( @@ -146,44 +184,36 @@ class CloudActivity : SimpleActivity() { ) } } - } - - else if (connectionType == ConnectionTypes.SFTP) { + } else if (connectionType == ConnectionTypes.SFTP) { viewModel.connectSFTP(user, password, host, port) viewModel.verifySFTP.collectLatest { if (it) { viewModel.saveNetwork( - NetworkConnection(host = host, username = user, password = password, connectionType = connectionType.toString(), port = port, displayName = displayName, url = viewModel.getSFTPConn().canonicalize(".")) + NetworkConnection( + host = host, + username = user, + password = password, + connectionType = connectionType.toString(), + port = port, + displayName = displayName, + url = viewModel.getSFTPConn().canonicalize(".") + ) ) } } - } - else if (connectionType == ConnectionTypes.FTP) { + } else if (connectionType == ConnectionTypes.FTP) { viewModel.connectFTP(user, password, host, port) viewModel.verifyFTP.collectLatest { - if (it) { - viewModel.saveNetwork( - NetworkConnection(host = host, username = user, password = password, connectionType = connectionType.toString(), port = port, displayName = displayName, url = viewModel.getFTP().printWorkingDirectory()) - ) - } - } - } - else if (connectionType == ConnectionTypes.WebDavMount) { - val url = "http://${host}:${port}/${shared}" - viewModel.connectAndAuthenticateWebDav(user, password, url) - composition.documentRepository.saveDocumentInfo() - viewModel.verifyWebDav.collectLatest { if (it) { viewModel.saveNetwork( NetworkConnection( host = host, username = user, password = password, - sharedPath = shared, connectionType = connectionType.toString(), + port = port, displayName = displayName, - url = url, - port = port + url = viewModel.getFTP().printWorkingDirectory() ) ) } @@ -227,7 +257,8 @@ class CloudActivity : SimpleActivity() { } else if (itm.connectionType == ConnectionTypes.WebDav.type) { itm.username?.let { username -> itm.password?.let { password -> - viewModel.connectAndAuthenticateWebDav(username, password, itm.url) + val protocol = itm.url.split(':')[0]; + viewModel.connectAndAuthenticateWebDav(username, password, itm.url,itm.host, Protocols.valueOf(protocol.uppercase(Locale.getDefault())), this) lifecycleScope.launch { viewModel.verifyWebDav.collectLatest { if (it) { @@ -244,26 +275,24 @@ class CloudActivity : SimpleActivity() { viewModel.connectSFTP(username, password, itm.host, itm.port) lifecycleScope.launch(Dispatchers.IO) { viewModel.verifySFTP.collectLatest { - if(it){ + if (it) { startServer(item, PORT_SFTP, connectionType = ConnectionTypes.SFTP, machinePort = itm.port) - launchMainActivity(ConnectionTypes.SFTP,itm.url) + launchMainActivity(ConnectionTypes.SFTP, itm.url) } } } } } - } - - else if (item.connectionType == ConnectionTypes.FTP.type){ + } else if (item.connectionType == ConnectionTypes.FTP.type) { itm.username?.let { username -> itm.password?.let { password -> - viewModel.connectFTP(username,password,itm.host,itm.port) + viewModel.connectFTP(username, password, itm.host, itm.port) lifecycleScope.launch(Dispatchers.IO) { viewModel.verifyFTP.collectLatest { - if(it){ - startServer(item,PORT_FTP, connectionType = ConnectionTypes.FTP, machinePort = itm.port) - launchMainActivity(ConnectionTypes.FTP,itm.url) + if (it) { + startServer(item, PORT_FTP, connectionType = ConnectionTypes.FTP, machinePort = itm.port) + launchMainActivity(ConnectionTypes.FTP, itm.url) } } } @@ -284,7 +313,14 @@ class CloudActivity : SimpleActivity() { } private fun startServer(connection: NetworkConnection, port: Int = 7871, connectionType: ConnectionTypes, machinePort: Int) { - val https = HttpServer(port, connection.host, connectionType, viewModel, machinePort) + val https = HttpServer(port, connection.host, connectionType, composition.networkApiRepository, machinePort) https.start() } + + private fun saveCertificate(uri: Uri?, name: String) { + uri?.let { + val cert = certificateRepository.loadCertificate(it, this) + certificateRepository.saveCertificate(name, cert, this) + } + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt b/app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt deleted file mode 100644 index b935186ab..000000000 --- a/app/src/main/kotlin/org/fossify/filemanager/dao/DocumentProviderDao.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.fossify.filemanager.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import kotlinx.coroutines.flow.Flow -import org.fossify.filemanager.entity.DocumentProviderEntity -import org.fossify.filemanager.entity.NetworkConnectionEntity - -@Dao -interface DocumentProviderDao { - @Query("SELECT * FROM document_provider_entity") - fun getAll(): Flow> - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(doc: DocumentProviderEntity) -} diff --git a/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt b/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt index f76273641..525fc022c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt @@ -14,5 +14,5 @@ interface NetworkConnectionDao { fun getAll(): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(connection: NetworkConnectionEntity) + suspend fun insert(connection: NetworkConnectionEntity): Long } diff --git a/app/src/main/kotlin/org/fossify/filemanager/database/Database.kt b/app/src/main/kotlin/org/fossify/filemanager/database/Database.kt index 2a3252b7c..f9126a848 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/database/Database.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/database/Database.kt @@ -2,13 +2,10 @@ package org.fossify.filemanager.database import androidx.room.Database import androidx.room.RoomDatabase -import org.fossify.filemanager.dao.DocumentProviderDao import org.fossify.filemanager.dao.NetworkConnectionDao -import org.fossify.filemanager.entity.DocumentProviderEntity import org.fossify.filemanager.entity.NetworkConnectionEntity -@Database(entities = [NetworkConnectionEntity::class, DocumentProviderEntity::class], version = 1) +@Database(entities = [NetworkConnectionEntity::class], version = 1) abstract class Database : RoomDatabase() { abstract fun networkConnectionDao(): NetworkConnectionDao - abstract fun docDao(): DocumentProviderDao } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt b/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt index 7a75e7006..01cc33073 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.room.Room import org.fossify.filemanager.database.Database import org.fossify.filemanager.factory.NetworkBrowserViewModelFactory -import org.fossify.filemanager.repository.ExternalStorageRepositoryDbImpl +import org.fossify.filemanager.repository.CertificateRepositoryImpl import org.fossify.filemanager.repository.NetworkConnectionRepositoryApiImpl import org.fossify.filemanager.repository.NetworkConnectionRepositoryDbImpl @@ -22,14 +22,15 @@ class AppComposition (private val context: Context) { NetworkConnectionRepositoryDbImpl(database.networkConnectionDao()) } - val documentRepository by lazy { - ExternalStorageRepositoryDbImpl(database.docDao()) - } val networkApiRepository by lazy { NetworkConnectionRepositoryApiImpl() } + val certificateRepository by lazy { + CertificateRepositoryImpl() + } + fun provideNetworkBrowserViewModelFactory(): NetworkBrowserViewModelFactory{ return NetworkBrowserViewModelFactory(networkDbRepository,networkApiRepository) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index ddf37d18e..322b83a56 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -1,7 +1,12 @@ package org.fossify.filemanager.dialogs +import android.net.Uri +import android.util.Log +import android.view.View import android.widget.ArrayAdapter import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.getAlertDialogBuilder @@ -10,18 +15,28 @@ import org.fossify.commons.extensions.value import org.fossify.filemanager.R import org.fossify.filemanager.activities.CloudActivity import org.fossify.filemanager.databinding.DialogAddConnectionBinding +import org.fossify.filemanager.enums.Protocols -class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Int, ConnectionTypes) -> Unit) { +class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Uri?, Int, ConnectionTypes, Protocols) -> Unit) { private var binding: DialogAddConnectionBinding val items = listOf(ConnectionTypes.DAVx5.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.FTP.type) + private var certUri: Uri? = null + val protocols = listOf(Protocols.HTTP, Protocols.HTTPS) init { binding = DialogAddConnectionBinding.inflate(activity.layoutInflater) activity.getAlertDialogBuilder() .setPositiveButton(R.string.ok) { _, _ -> dispatch( - binding.hostEt.value, binding.userEt.value, binding.passwordEt.value, binding.sharedPathEt.value, binding.displayEt.value,binding.portEt.value.toIntOrNull() ?: 0, - ConnectionTypes.fromType(binding.dropdownMenu.value) + binding.hostEt.value, + binding.userEt.value, + binding.passwordEt.value, + binding.sharedPathEt.value, + binding.displayEt.value, + certUri, + binding.portEt.value.toIntOrNull() ?: 0, + ConnectionTypes.fromType(binding.dropdownMenu.value), + Protocols.valueOf(binding.dropdownMenuProtocol.text.toString()) ) } .setNegativeButton(R.string.cancel, null) @@ -30,27 +45,46 @@ class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, Stri } initializeDropDownList() dropDownItemSelected() + attachCertBtnClickListener() } private fun initializeDropDownList() { val adapter = ArrayAdapter(activity, android.R.layout.simple_list_item_1, items) binding.dropdownMenu.setAdapter(adapter) + val protocolsAdapter = ArrayAdapter(activity,android.R.layout.simple_list_item_1, protocols) + binding.dropdownMenuProtocol.setAdapter(protocolsAdapter) } private fun promptUserToSelectStorage() { - // This launches the system file picker (activity as CloudActivity).promptUserToSelectStorage() } + private fun dropDownItemSelected() { binding.dropdownMenu.setOnItemClickListener { parent, view, position, id -> val selectedItem = parent.getItemAtPosition(position).toString() if (selectedItem == ConnectionTypes.DAVx5.type) { + binding.dropdownProtocol.visibility = View.GONE promptUserToSelectStorage() + } else if (selectedItem == ConnectionTypes.WebDav.type) { + binding.dropdownProtocol.visibility = View.VISIBLE + } else { + binding.dropdownProtocol.visibility = View.GONE } Toast.makeText(activity, "Selected: $selectedItem", Toast.LENGTH_SHORT).show() } } + + private fun attachCertBtnClickListener() { + binding.certAttachBtn.setOnClickListener { + (activity as CloudActivity).openFileLink{ + certUri = it + binding.certStatusTv.text = it.path + } + + } + } + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/documentProvider/ExternalStorageProvider.kt b/app/src/main/kotlin/org/fossify/filemanager/documentProvider/ExternalStorageProvider.kt deleted file mode 100644 index 39f339c2f..000000000 --- a/app/src/main/kotlin/org/fossify/filemanager/documentProvider/ExternalStorageProvider.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.fossify.filemanager.documentProvider - -import android.database.Cursor -import android.database.MatrixCursor -import android.os.CancellationSignal -import android.os.ParcelFileDescriptor -import android.provider.DocumentsContract -import android.provider.DocumentsProvider -import androidx.lifecycle.ViewModelProvider -import org.fossify.filemanager.App - -import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel - -class ExternalStorageProvider: DocumentsProvider() { - override fun openDocument(documentId: String?, mode: String?, signal: CancellationSignal?): ParcelFileDescriptor? { - TODO("Not yet implemented") - } - - override fun queryChildDocuments( - parentDocumentId: String?, - projection: Array?, - sortOrder: String? - ): Cursor? { - TODO("Not yet implemented") - } - - override fun queryDocument(documentId: String?, projection: Array?): Cursor? { - TODO("Not yet implemented") - } - - override fun queryRoots(projection: Array?): Cursor? { - val cursor = MatrixCursor( - arrayOf( - DocumentsContract.Root.COLUMN_ROOT_ID, - DocumentsContract.Root.COLUMN_TITLE, - DocumentsContract.Root.COLUMN_FLAGS - ) - ) - - val row = cursor.newRow() - row.add(DocumentsContract.Root.COLUMN_ROOT_ID, "root") - row.add(DocumentsContract.Root.COLUMN_TITLE, "My Remote Storage") - row.add(DocumentsContract.Root.COLUMN_FLAGS, - DocumentsContract.Root.FLAG_SUPPORTS_CREATE) - - return cursor - } - - override fun onCreate(): Boolean { - val app = context?.applicationContext as App - val composition = app.appComposition - val factory = composition.provideNetworkBrowserViewModelFactory() - return true - } - -} diff --git a/app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt b/app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt deleted file mode 100644 index bff5502e5..000000000 --- a/app/src/main/kotlin/org/fossify/filemanager/entity/DocumentProviderEntity.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.fossify.filemanager.entity - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "document_provider_entity") -data class DocumentProviderEntity ( - @PrimaryKey(autoGenerate = true) val id: Int = 0, - val username: String -) diff --git a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt index 2aa3662bc..706126498 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt @@ -4,7 +4,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "network_connections") data class NetworkConnectionEntity( - @PrimaryKey(autoGenerate = true) val id: Int = 0, + @PrimaryKey(autoGenerate = true) val id: Long = 0, val host: String, val port: Int = 445, val username: String?, diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 3ca459910..a20419bcc 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -6,6 +6,7 @@ import jcifs.smb.SmbFile import jcifs.smb.SmbFileInputStream import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.helpers.Helpers +import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel import java.io.BufferedInputStream @@ -13,7 +14,7 @@ class HttpServer( private val port: Int, private val serverIp: String, private val connectionTypes: ConnectionTypes, - private val viewModel: NetworkBrowserViewModel, + private val networkConnectionRepository: NetworkConnectionRepositoryApi, private val machinePort: Int ) : NanoHTTPD(port) { @@ -30,16 +31,16 @@ class HttpServer( } else if(connectionTypes.equals(ConnectionTypes.WebDav)) { val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri.toString(), port = machinePort) - val webDavFile = viewModel.listWebDavFileDetail(url) + val webDavFile = networkConnectionRepository.listWebDavFileDetail(url) return handleRangeRequestWebDav(rangeHeader, webDavFile?.contentLength!!, uri = uri, webDavFile.contentType) } else if (connectionTypes.equals(ConnectionTypes.SFTP)) { val url = Helpers.createUrl(connectionTypes, server = serverIp, path = "", port = machinePort) - val sftFile = viewModel.listSFTPFileDetails(uri) + val sftFile = networkConnectionRepository.listSFTPFileDetails(uri) return handleRangeRequestSFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } val url = Helpers.createUrl(connectionTypes, server = serverIp, path = "", port = machinePort) - val sftFile = viewModel.getFTPFileDetail(uri) + val sftFile = networkConnectionRepository.getFTPFileDetail(uri) return handleRangeRequestFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } @@ -108,7 +109,7 @@ class HttpServer( val contentLength = end - start + 1 - val inputStream = viewModel.listWebDavFileStream(url = url,start,end) + val inputStream = networkConnectionRepository.getWebDavFileInputStream(url = url,start,end) return newFixedLengthResponse( Response.Status.PARTIAL_CONTENT, @@ -140,7 +141,7 @@ class HttpServer( val contentLength = end - start + 1 - val inputStream = viewModel.getSFTPFileStream(uri,start) + val inputStream = networkConnectionRepository.getSFTPFileInputStream(uri,start) val bufferedStream = BufferedInputStream(inputStream, 1024 * 1024) return newFixedLengthResponse( Response.Status.PARTIAL_CONTENT, @@ -172,7 +173,7 @@ class HttpServer( val contentLength = end - start + 1 - val inputStream = viewModel.getFTPFileStream(uri,start) + val inputStream = networkConnectionRepository.getFTPFileInputStream(uri,start) val bufferedStream = BufferedInputStream(inputStream, 1024 * 1024) return newFixedLengthResponse( Response.Status.PARTIAL_CONTENT, diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt index 0d574a891..22c35d8e9 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt @@ -17,6 +17,9 @@ const val PORT_SFTP = 7860 const val PORT_FTP = 7850 +const val PORT_WEBDAV_MOUNT = 7840 + + // shared preferences const val SHOW_HIDDEN = "show_hidden" const val PRESS_BACK_TWICE = "press_back_twice" diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt index 8275d7ed8..c144b3929 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -3,6 +3,8 @@ package org.fossify.filemanager.helpers import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.enums.Protocols import java.nio.file.Path +import java.util.Locale +import java.util.Locale.getDefault object Helpers { val host: String = "127.0.0.1" @@ -33,4 +35,8 @@ object Helpers { } return PORT_SMB } + + fun createProtocolPath(protocol: Protocols, server: String, port:Int, path:String): String{ + return "${protocol.name.lowercase(getDefault())}://${server}:${port}/${path}" + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/CertificateRepository.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/CertificateRepository.kt new file mode 100644 index 000000000..b0f85a523 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/CertificateRepository.kt @@ -0,0 +1,10 @@ +package org.fossify.filemanager.interfaces + +import android.content.Context +import android.net.Uri +import java.security.cert.X509Certificate + +interface CertificateRepository { + fun loadCertificate(uri: Uri, context: Context): X509Certificate + fun saveCertificate(host: String, cert: X509Certificate,context: Context) +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/ExternalStorageRepositoryDb.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/ExternalStorageRepositoryDb.kt deleted file mode 100644 index 70fa798ae..000000000 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/ExternalStorageRepositoryDb.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.fossify.filemanager.interfaces - -import org.fossify.filemanager.entity.DocumentProviderEntity - -interface ExternalStorageRepositoryDb { - fun saveDocumentInfo(doc: DocumentProviderEntity) - fun getAllDocuments() -} diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt index 1e16fd1e2..0441388ca 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt @@ -1,6 +1,7 @@ package org.fossify.filemanager.interfaces +import android.content.Context import com.thegrizzlylabs.sardineandroid.DavResource import jcifs.smb.SmbFile import net.schmizz.sshj.sftp.FileAttributes @@ -8,8 +9,10 @@ import net.schmizz.sshj.sftp.RemoteResourceInfo import net.schmizz.sshj.sftp.SFTPClient import org.apache.commons.net.ftp.FTPClient import org.apache.commons.net.ftp.FTPFile +import org.fossify.filemanager.enums.Protocols import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream +import java.security.cert.X509Certificate interface NetworkConnectionRepositoryApi { suspend fun verifyConnection(connection: NetworkConnection): Boolean @@ -18,14 +21,19 @@ interface NetworkConnectionRepositoryApi { fun getMainSmbFile(): SmbFile - suspend fun connectAndVerifyWebDav(userName: String = "", password: String = "", url: String): Boolean + ///WebDav + + suspend fun connectAndVerifyWebDav(userName: String = "", password: String = "", url: String,host:String,protocols: Protocols, context: Context): Boolean suspend fun listAllFilesOnWebDav(url: String): List - fun listWebDavFileInputStream(url: String,start: Long,end: Long): InputStream + fun getWebDavFileInputStream(url: String, start: Long, end: Long): InputStream fun listWebDavFileDetail(url: String): DavResource? + fun loadCertificate(stream: InputStream):Result + //SFTP + suspend fun connectToSftp(userName: String, password: String,server: String,port: Int): Boolean suspend fun listAllFilesSFTPRoot(path: String): List @@ -34,7 +42,7 @@ interface NetworkConnectionRepositoryApi { fun listSFTPFileDetails(path: String): FileAttributes? - fun listSFTPFileInputStream(url: String,startByte: Long): InputStream + fun getSFTPFileInputStream(url: String, startByte: Long): InputStream fun getSFTPConn(): SFTPClient diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt index cac65cf6b..c2586b17c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt @@ -5,7 +5,7 @@ import org.fossify.filemanager.entity.NetworkConnectionEntity import org.fossify.filemanager.models.NetworkConnection interface NetworkConnectionRepositoryDb { - suspend fun saveConnection(connection: NetworkConnection); + suspend fun saveConnection(connection: NetworkConnection): Long; suspend fun getAllSavedConnections(): Flow> suspend fun deleteConnection() } diff --git a/app/src/main/kotlin/org/fossify/filemanager/keyStores/CertificateStore.kt b/app/src/main/kotlin/org/fossify/filemanager/keyStores/CertificateStore.kt new file mode 100644 index 000000000..ff19df764 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/keyStores/CertificateStore.kt @@ -0,0 +1,46 @@ +package org.fossify.filemanager.keyStores + +import android.content.Context +import java.io.FileNotFoundException +import java.security.KeyStore +import java.security.cert.X509Certificate + +object CertificateStore { + private const val KEYSTORE_FILE = "webdav_server_certs.keystore" + private const val KEYSTORE_PASSWORD = "webdav_ks_pass" + fun loadOrCreateKeyStore(context: Context): KeyStore { + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + return try { + val fis = context.openFileInput(KEYSTORE_FILE) + keyStore.load(fis, KEYSTORE_PASSWORD.toCharArray()) + keyStore + } catch (e: FileNotFoundException) { + keyStore.load(null, null) + keyStore + } + } + + fun loadCert(context: Context, host: String): X509Certificate { + val keyStore = loadOrCreateKeyStore(context) + return keyStore.getCertificate(host) as X509Certificate + } + + fun saveCert(context: Context, host: String, cert: X509Certificate) { + val keyStore = loadOrCreateKeyStore(context) + keyStore.setCertificateEntry(host, cert) + + val fos = context.openFileOutput(KEYSTORE_FILE, Context.MODE_PRIVATE) + keyStore.store(fos, KEYSTORE_PASSWORD.toCharArray()) + fos.close() + } + + fun removeCert(context: Context, host: String) { + val keyStore = loadOrCreateKeyStore(context) + if (keyStore.containsAlias(host)) { + keyStore.deleteEntry(host) + val fos = context.openFileOutput(KEYSTORE_FILE, Context.MODE_PRIVATE) + keyStore.store(fos, KEYSTORE_PASSWORD.toCharArray()) + fos.close() + } + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/DocumentProvider.kt b/app/src/main/kotlin/org/fossify/filemanager/models/DocumentProvider.kt deleted file mode 100644 index 5cf84281a..000000000 --- a/app/src/main/kotlin/org/fossify/filemanager/models/DocumentProvider.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.fossify.filemanager.models - -data class DocumentProvider() diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/CertificateRepositoryImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/CertificateRepositoryImpl.kt new file mode 100644 index 000000000..95282a144 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/CertificateRepositoryImpl.kt @@ -0,0 +1,22 @@ +package org.fossify.filemanager.repository + +import android.content.Context +import android.net.Uri +import org.fossify.filemanager.interfaces.CertificateRepository +import org.fossify.filemanager.keyStores.CertificateStore +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +class CertificateRepositoryImpl: CertificateRepository { + override fun loadCertificate(uri: Uri, context: Context): X509Certificate { + val inputStream = context.contentResolver.openInputStream(uri) + ?: throw IllegalArgumentException("Cannot open URI") + + val cf = CertificateFactory.getInstance("X.509") + return cf.generateCertificate(inputStream) as X509Certificate + } + + override fun saveCertificate(host: String, cert: X509Certificate, context: Context) { + CertificateStore.saveCert(context, host, cert) + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt deleted file mode 100644 index 18208b570..000000000 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/ExternalStorageRepositoryDbImpl.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.fossify.filemanager.repository - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import org.fossify.filemanager.dao.DocumentProviderDao -import org.fossify.filemanager.dao.NetworkConnectionDao -import org.fossify.filemanager.entity.DocumentProviderEntity -import org.fossify.filemanager.interfaces.ExternalStorageRepositoryDb - -class ExternalStorageRepositoryDbImpl(private val dao: DocumentProviderDao): ExternalStorageRepositoryDb { - val allDocs = MutableStateFlow>(emptyList()) - override fun saveDocumentInfo(doc: DocumentProviderEntity) { - CoroutineScope(Dispatchers.IO).launch{ - dao.insert(doc) - } - } - - override fun getAllDocuments() { - CoroutineScope(Dispatchers.IO).launch{ - dao.getAll() - } - } -} diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt index 22c7e5894..67f2de6f4 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt @@ -1,5 +1,7 @@ package org.fossify.filemanager.repository +import android.annotation.SuppressLint +import android.content.Context import android.util.Log import com.thegrizzlylabs.sardineandroid.DavResource import com.thegrizzlylabs.sardineandroid.Sardine @@ -19,11 +21,21 @@ import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream import java.util.Properties import net.schmizz.sshj.sftp.RemoteResourceInfo +import okhttp3.OkHttpClient import org.apache.commons.net.ftp.FTP import org.apache.commons.net.ftp.FTPClient import org.apache.commons.net.ftp.FTPCmd import org.apache.commons.net.ftp.FTPFile +import org.fossify.filemanager.enums.Protocols +import org.fossify.filemanager.keyStores.CertificateStore import java.io.File +import java.security.KeyStore +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { lateinit var dir: SmbFile @@ -70,9 +82,20 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { override fun getMainSmbFile(): SmbFile = dir - override suspend fun connectAndVerifyWebDav(userName: String, password: String, url: String): Boolean { + override suspend fun connectAndVerifyWebDav( + userName: String, + password: String, + url: String, + host: String, + protocols: Protocols, + context: Context + ): Boolean { try { - sardine = OkHttpSardine() + sardine = if (protocols == Protocols.HTTP) { + OkHttpSardine() + } else { + createHTTPSSardine(context,host) + } sardine.setCredentials(userName, password) return sardine.exists(url) } catch (exp: Exception) { @@ -86,7 +109,7 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { return resources } - override fun listWebDavFileInputStream(url: String, start: Long, end: Long): InputStream { + override fun getWebDavFileInputStream(url: String, start: Long, end: Long): InputStream { val rangeHeader = "bytes=$start-$end" val headers = mapOf("Range" to rangeHeader) return sardine.get(url, headers) @@ -101,6 +124,18 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { return null } + override fun loadCertificate(stream: InputStream): Result { + return try { + val cf = CertificateFactory.getInstance("X.509") + val cert = cf.generateCertificate(stream) as X509Certificate + cert.checkValidity() + Result.success(cert) + } catch (exp: Exception) { + Result.failure(Exception(exp.message)) + } + } + + override suspend fun connectToSftp(userName: String, password: String, server: String, port: Int): Boolean { try { if (!::ssh.isInitialized || !ssh.isConnected || !ssh.isAuthenticated) { @@ -139,7 +174,7 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { } } - override fun listSFTPFileInputStream(url: String, startByte: Long): InputStream { + override fun getSFTPFileInputStream(url: String, startByte: Long): InputStream { val myPath = url.replace("//", "/") val remoteFile = sftp.open(myPath) val inputStream = remoteFile.RemoteFileInputStream(startByte) @@ -164,8 +199,7 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { ftp.enterLocalPassiveMode() ftpStream.enterLocalPassiveMode() return true - } - catch (exp: Exception){ + } catch (exp: Exception) { return false } } @@ -176,9 +210,9 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { return files.toList() } - override fun getFTPFileDetail(path: String):FTPFile? { + override fun getFTPFileDetail(path: String): FTPFile? { val myPath = path.replace("//", "/") - if(ftp.hasFeature(FTPCmd.MLST)){ + if (ftp.hasFeature(FTPCmd.MLST)) { val file = ftp.mlistFile(myPath) return file } @@ -187,8 +221,8 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { return files } - override fun getFTPFileInputStream(path: String,start: Long): InputStream { - if(::currentStream.isInitialized) + override fun getFTPFileInputStream(path: String, start: Long): InputStream { + if (::currentStream.isInitialized) currentStream.close() ftpStream.completePendingCommand() ftpStream.setFileType(FTP.BINARY_FILE_TYPE) @@ -199,4 +233,43 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { override fun getFTPConn(): FTPClient = ftp + private fun createHTTPSSardine(context: Context, host: String): Sardine { + return buildSardineWithUserCert(context, host) + } + + private fun buildSardineWithUserCert( + context: Context, + host: String + ): Sardine { + val cert = CertificateStore.loadCert(context, host) + + val trustManager = @SuppressLint("CustomX509TrustManager") + object : X509TrustManager { + override fun getAcceptedIssuers(): Array = arrayOf(cert) + + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted(chain: Array, authType: String) { + } + + override fun checkServerTrusted(chain: Array, authType: String) { + if (!chain[0].encoded.contentEquals(cert.encoded)) { + throw CertificateException("Untrusted certificate") + } + } + } + + val sslContext = SSLContext.getInstance("TLS").apply { + init(null, arrayOf(trustManager), null) + } + + val okHttpClient = OkHttpClient.Builder() + .sslSocketFactory(sslContext.socketFactory, trustManager) + .hostnameVerifier { hostname, _ -> + hostname == host + } + .build() + + return OkHttpSardine(okHttpClient) + } + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt index c2f2b3780..aca12464f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt @@ -9,8 +9,8 @@ import org.fossify.filemanager.mapper.toEntity import org.fossify.filemanager.models.NetworkConnection class NetworkConnectionRepositoryDbImpl(private val dao: NetworkConnectionDao): NetworkConnectionRepositoryDb { - override suspend fun saveConnection(connection: NetworkConnection) { - dao.insert(connection.toEntity()) + override suspend fun saveConnection(connection: NetworkConnection): Long { + return dao.insert(connection.toEntity()) } override suspend fun getAllSavedConnections(): Flow> { diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 3ee1506aa..943d1ffad 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -1,5 +1,6 @@ package org.fossify.filemanager.viewmodels +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.thegrizzlylabs.sardineandroid.DavResource @@ -18,10 +19,12 @@ import net.schmizz.sshj.sftp.FileAttributes import net.schmizz.sshj.sftp.RemoteResourceInfo import net.schmizz.sshj.sftp.SFTPClient import org.apache.commons.net.ftp.FTPFile +import org.fossify.filemanager.enums.Protocols import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream +import java.security.cert.X509Certificate class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkConnectionRepositoryDb, private val networkConnectionRepositoryApi: NetworkConnectionRepositoryApi): ViewModel() { @@ -40,9 +43,11 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo val ftpFiles = MutableStateFlow>(emptyList()) + val connectionId = MutableSharedFlow() + fun saveNetwork(networkConnection: NetworkConnection){ viewModelScope.launch(Dispatchers.IO) { - networkConnectionRepository.saveConnection(networkConnection) + connectionId.emit(networkConnectionRepository.saveConnection(networkConnection)) } } @@ -88,9 +93,9 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo fun getSFTPConn(): SFTPClient = networkConnectionRepositoryApi.getSFTPConn() - fun connectAndAuthenticateWebDav(userName: String = "", password: String = "", url: String){ + fun connectAndAuthenticateWebDav(userName: String = "", password: String = "", url: String, host: String, protocol: Protocols, context: Context){ viewModelScope.launch(Dispatchers.IO) { - val result = networkConnectionRepositoryApi.connectAndVerifyWebDav(userName, password, url) + val result = networkConnectionRepositoryApi.connectAndVerifyWebDav(userName, password, url,host,protocol,context) verifyWebDav.emit(result) } } @@ -102,7 +107,7 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo } fun listWebDavFileStream(url: String,start: Long,end: Long): InputStream{ - return networkConnectionRepositoryApi.listWebDavFileInputStream(url,start,end) + return networkConnectionRepositoryApi.getWebDavFileInputStream(url,start,end) } fun listWebDavFileDetail(url: String): DavResource?{ @@ -135,7 +140,7 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo } fun getSFTPFileStream(path: String,startByte: Long): InputStream{ - return networkConnectionRepositoryApi.listSFTPFileInputStream(url = path,startByte) + return networkConnectionRepositoryApi.getSFTPFileInputStream(url = path,startByte) } fun connectFTP(userName: String, password: String,server: String,port: Int){ diff --git a/app/src/main/res/layout/dialog_add_connection.xml b/app/src/main/res/layout/dialog_add_connection.xml index 099e49b24..567d98fba 100644 --- a/app/src/main/res/layout/dialog_add_connection.xml +++ b/app/src/main/res/layout/dialog_add_connection.xml @@ -37,6 +37,24 @@ + + + + + + + android:hint="Port"> + + + + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac5ad4fa8..e1d90e41c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,47 +1,165 @@ [versions] -#jetbrains +# jetbrains & Kotlin kotlin = "2.2.21" -#Detekt +kotlin-immutable-collections = "0.4.0" +kotlinxSerializationJson = "1.10.0" +ksp = "2.2.0-2.0.2" + +# Detekt detekt = "1.23.8" detektCompose = "0.4.28" -#AndroidX -androidx-swiperefreshlayout = "1.2.0" + +# AndroidX +androidx-appcompat = "1.7.1" +androidx-biometricKtx = "1.4.0-alpha02" +androidx-constraintlayout = "2.2.1" +androidx-coreKtx = "1.18.0" +androidx-customView = "1.2.0" +androidx-customViewPooling = "1.1.0" androidx-documentfile = "1.1.0" -#Fossify +androidx-exifinterface = "1.4.2" +androidx-lifecycle = "2.10.0" +androidx-swiperefreshlayout = "1.2.0" + +# Compose +compose = "1.7.6" +composeActivity = "1.13.0" +composeMaterial3 = "1.4.0" + +# Material +material = "1.13.0" + +# Databases & Networking +room = "2.8.4" +jcifs = "2.1.10" +gson = "2.13.2" + +# Glide +glide = "5.0.5" +glideCompose = "4.14.0" + +# Helpers & Fossify commons = "5.12.0" -#Other autofittextview = "0.2.1" gestureviews = "2.8.3" -roomVersion = "2.8.4" rootshell = "bc7e5d398e" roottools = "965c154e20" zip4j = "2.11.5" -jcifs = "2.1.10" -#Gradle +patternLockView = "a90b0d4bf0" +reprint = "2cb206415d" +recyclerviewFastscroller = "5a95285b1f" +rtlViewpager = "2.0.2" +ezVcard = "0.12.2" +jodaTime = "2.14.1" + +# Gradle & Build gradlePlugins-agp = "8.11.1" -#build app-build-compileSDKVersion = "36" app-build-targetSDK = "36" app-build-minimumSDK = "26" app-build-javaVersion = "VERSION_17" app-build-kotlinJVMTarget = "17" + [libraries] -#AndroidX -androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" } -androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" } +# AndroidX Core +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "androidx-biometricKtx" } +androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-coreKtx" } +androidx-customView = { module = "androidx.customview:customview", version.ref = "androidx-customView" } +androidx-customViewPooling = { module = "androidx.customview:customview-poolingcontainer", version.ref = "androidx-customViewPooling" } androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "androidx-documentfile" } -#Compose +androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "androidx-exifinterface" } +androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" } + +# AndroidX Lifecycle +androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } +androidx-lifecycle-viewModel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } +androidx-lifecycle-viewModel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" } + +# Room +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } + +# Compose +compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } +compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial3" } +compose-material2 = { module = "androidx.compose.material:material", version.ref = "compose" } +compose-material-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } +compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose" } +compose-activity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" } +compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } +compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } +compose-uiTooling-debug = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } +compose-uiTooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } +compose-view-binding = { module = "androidx.compose.ui:ui-viewbinding", version.ref = "compose" } compose-detekt = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } -#Fossify + +# Glide +glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } +glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glideCompose" } +glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } + +# Material +material = { module = "com.google.android.material:material", version.ref = "material" } + +# Fossify & Others fossify-commons = { module = "org.fossify:commons", version.ref = "commons" } -#Other autofittextview = { module = "me.grantland:autofittextview", version.ref = "autofittextview" } gestureviews = { module = "com.alexvasilkov:gesture-views", version.ref = "gestureviews" } rootshell = { module = "com.github.naveensingh:RootShell", version.ref = "rootshell" } roottools = { module = "com.github.naveensingh:RootTools", version.ref = "roottools" } zip4j = { module = "net.lingala.zip4j:zip4j", version.ref = "zip4j" } jcifs-ng = { module = "eu.agno3.jcifs:jcifs-ng", version.ref = "jcifs" } +patternLockView = { module = "com.github.aritraroy:patternLockView", version.ref = "patternLockView" } +reprint = { module = "com.github.tibbi:reprint", version.ref = "reprint" } +recyclerView-fastScroller = { module = "com.github.tibbi:RecyclerView-FastScroller", version.ref = "recyclerviewFastscroller" } +rtl-viewpager = { module = "com.github.naveensingh:rtl-viewpager", version.ref = "rtlViewpager" } +ez-vcard = { module = "com.googlecode.ez-vcard:ez-vcard", version.ref = "ezVcard" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +joda-time = { module = "joda-time:joda-time", version.ref = "jodaTime" } +# Kotlin +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +kotlin-immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlin-immutable-collections" } + +[bundles] +compose = [ + "compose-activity", + "compose-animation", + "compose-foundation", + "compose-material-icons", + "compose-material3", + "compose-runtime", + "compose-ui", + "compose-uiTooling-preview", +] +compose-preview = [ + "androidx-customView", + "androidx-customViewPooling", + "compose-uiTooling-debug", +] +room = [ + "androidx-room-ktx", + "androidx-room-runtime", +] +lifecycle = [ + "androidx-lifecycle-compose", + "androidx-lifecycle-runtime", + "androidx-lifecycle-viewModel", + "androidx-lifecycle-viewModel-compose", +] + [plugins] android = { id = "com.android.application", version.ref = "gradlePlugins-agp" } +library = { id = "com.android.library", version.ref = "gradlePlugins-agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } + + From ec44bc37fe57f2dea74d29382806eb631edb34d5 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Mon, 4 May 2026 00:28:32 +0500 Subject: [PATCH 16/37] issues related to https streaming fixed --- .../fossify/filemanager/activities/CloudActivity.kt | 6 +++--- .../fossify/filemanager/fileSystems/HttpServer.kt | 12 +++++++----- .../org/fossify/filemanager/helpers/Helpers.kt | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index df383e4e7..4f98f5bf9 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -262,7 +262,7 @@ class CloudActivity : SimpleActivity() { lifecycleScope.launch { viewModel.verifyWebDav.collectLatest { if (it) { - startServer(item, PORT_WEBDAV, connectionType = ConnectionTypes.WebDav, machinePort = itm.port) + startServer(item, PORT_WEBDAV, connectionType = ConnectionTypes.WebDav, machinePort = itm.port, Protocols.valueOf(protocol.uppercase(Locale.getDefault()))) launchMainActivity(ConnectionTypes.WebDav, itm.url) } } @@ -312,8 +312,8 @@ class CloudActivity : SimpleActivity() { }) } - private fun startServer(connection: NetworkConnection, port: Int = 7871, connectionType: ConnectionTypes, machinePort: Int) { - val https = HttpServer(port, connection.host, connectionType, composition.networkApiRepository, machinePort) + private fun startServer(connection: NetworkConnection, port: Int = 7871, connectionType: ConnectionTypes, machinePort: Int,protocol: Protocols = Protocols.HTTP) { + val https = HttpServer(port, connection.host, connectionType, composition.networkApiRepository, machinePort,protocol) https.start() } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index a20419bcc..365f5efb7 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -5,6 +5,7 @@ import fi.iki.elonen.NanoHTTPD import jcifs.smb.SmbFile import jcifs.smb.SmbFileInputStream import org.fossify.commons.enums.ConnectionTypes +import org.fossify.filemanager.enums.Protocols import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel @@ -15,7 +16,8 @@ class HttpServer( private val serverIp: String, private val connectionTypes: ConnectionTypes, private val networkConnectionRepository: NetworkConnectionRepositoryApi, - private val machinePort: Int + private val machinePort: Int, + private val protocols: Protocols = Protocols.HTTP ) : NanoHTTPD(port) { override fun serve(session: IHTTPSession): Response { @@ -30,9 +32,9 @@ class HttpServer( return handleRangeRequest(file, rangeHeader, file.length()) } else if(connectionTypes.equals(ConnectionTypes.WebDav)) { - val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri.toString(), port = machinePort) + val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri.toString(), port = machinePort, protocols = protocols) val webDavFile = networkConnectionRepository.listWebDavFileDetail(url) - return handleRangeRequestWebDav(rangeHeader, webDavFile?.contentLength!!, uri = uri, webDavFile.contentType) + return handleRangeRequestWebDav(rangeHeader, webDavFile?.contentLength!!, uri = uri, webDavFile.contentType,protocols) } else if (connectionTypes.equals(ConnectionTypes.SFTP)) { val url = Helpers.createUrl(connectionTypes, server = serverIp, path = "", port = machinePort) @@ -91,10 +93,10 @@ class HttpServer( } - private fun handleRangeRequestWebDav(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String): Response { + private fun handleRangeRequestWebDav(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String,protocol: Protocols = Protocols.HTTP): Response { var start: Long = 0 var end = fileLength - 1 - val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri, port = machinePort) + val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri, port = machinePort, protocols = protocol) if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { val ranges = rangeHeader.substring(6).split("-") try { diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt index c144b3929..65a1ee9b9 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -8,10 +8,10 @@ import java.util.Locale.getDefault object Helpers { val host: String = "127.0.0.1" - fun createUrl(connectionTypes: ConnectionTypes, path: String = "", server: String = "", port: Int): String{ + fun createUrl(connectionTypes: ConnectionTypes, path: String = "", server: String = "", port: Int,protocols: Protocols = Protocols.HTTP): String{ var protocol = Protocols.HTTP.toString().lowercase() if(connectionTypes.equals(ConnectionTypes.WebDav)){ - protocol = Protocols.HTTP.toString().lowercase() + protocol = protocols.name.lowercase() } else if(connectionTypes.equals(ConnectionTypes.SMB)){ protocol = Protocols.SMB.toString().lowercase() From 4f9b0a352225c911181296290ac56a4580a4fad2 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sat, 9 May 2026 23:12:34 +0500 Subject: [PATCH 17/37] Code refactoring and fixed bug for port already in use --- app/build.gradle.kts | 25 +- .../filemanager/activities/CloudActivity.kt | 361 ++++++++++++------ .../filemanager/dialogs/ConnectionDialog.kt | 7 +- .../fossify/filemanager/enums/Protocols.kt | 4 +- .../fossify/filemanager/helpers/Helpers.kt | 2 +- .../filemanager/models/ConnectionResult.kt | 3 + .../viewmodels/NetworkBrowserViewModel.kt | 104 +++-- build.gradle.kts | 1 + settings.gradle.kts | 4 + 9 files changed, 321 insertions(+), 190 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1389e1863..06c2e3d65 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,7 +25,11 @@ fun hasSigningVars(): Boolean { android { compileSdk = project.libs.versions.app.build.compileSDKVersion.get().toInt() - + packaging { + resources { + excludes += listOf("META-INF/**") + } + } defaultConfig { applicationId = project.property("APP_ID").toString() minSdk = project.libs.versions.app.build.minimumSDK.get().toInt() @@ -135,7 +139,7 @@ detekt { } dependencies { - implementation(libs.fossify.commons) + api(project(":commons")) implementation(libs.androidx.documentfile) implementation(libs.androidx.swiperefreshlayout) implementation(libs.roottools) @@ -143,13 +147,24 @@ dependencies { implementation(libs.gestureviews) implementation(libs.autofittextview) implementation(libs.zip4j) - implementation(libs.jcifs.ng) + implementation(libs.jcifs.ng){ + exclude(group = "org.bouncycastle", module = "bcprov-jdk18on") + exclude(group = "org.bouncycastle", module = "bcpkix-jdk18on") + } detektPlugins(libs.compose.detekt) val roomVersion = "2.7.0-alpha11" implementation("androidx.room:room-runtime:$roomVersion") ksp("androidx.room:room-compiler:$roomVersion") implementation (libs.androidx.room.ktx) - //noinspection UseTomlInstead implementation("org.nanohttpd:nanohttpd:2.3.1") - implementation("com.github.thegrizzlylabs:sardine-android:0.9")} + implementation("com.github.thegrizzlylabs:sardine-android:0.9") + implementation("com.hierynomus:sshj:0.40.0") { + exclude(group = "org.bouncycastle", module = "bcprov-jdk18on") + exclude(group = "org.bouncycastle", module = "bcpkix-jdk18on") + } + val bouncy_castle_version = "1.81" + implementation("org.bouncycastle:bcprov-jdk15to18:$bouncy_castle_version") + implementation("org.bouncycastle:bcpkix-jdk15to18:${bouncy_castle_version}") + implementation("commons-net:commons-net:3.10.0") +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 4f98f5bf9..901549f7d 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -1,16 +1,16 @@ package org.fossify.filemanager.activities -import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle -import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.ui.text.toUpperCase import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.fossify.commons.enums.ConnectionTypes @@ -45,6 +45,9 @@ class CloudActivity : SimpleActivity() { private lateinit var certificateRepository: CertificateRepository private lateinit var composition: AppComposition + private var networkJob: Job? = null + + private var https: HttpServer? = null private fun setupBouncyCastle() { val provider: Provider? = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) @@ -66,13 +69,22 @@ class CloudActivity : SimpleActivity() { } setupToolBar() registerAddConnectionListener() + initializeCompositionAndViewModel() + startConnectionsCollection() + setupBouncyCastle() + getAllSavedNetworks() + } + + private val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + stopServer() + } + + private fun initializeCompositionAndViewModel() { composition = (application as App).appComposition certificateRepository = composition.certificateRepository val factory = composition.provideNetworkBrowserViewModelFactory() - setupBouncyCastle() viewModel = ViewModelProvider(this, factory) .get(NetworkBrowserViewModel::class.java) - getAllSavedNetworks() } private val openDocumentTreeLauncher = @@ -102,13 +114,6 @@ class CloudActivity : SimpleActivity() { private val pickCert = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> uri?.let { onCertPicked?.invoke(it) -// val stream = contentResolver.openInputStream(it) ?: return@let -// val res = viewModel.loadSSLCertificate(inputStream = stream) -// res.onSuccess { certificate -> -// viewModel.attachCertificate(certificate, "TestUser", "bilal786") -// }.onFailure { -// Log.d("WebDav HTTPS", it.localizedMessage) -// } } } @@ -148,11 +153,11 @@ class CloudActivity : SimpleActivity() { certUri: Uri?, port: Int, connectionType: ConnectionTypes, - protocol: Protocols + protocol: Protocols? ) { lifecycleScope.launch((Dispatchers.IO)) { if (connectionType == ConnectionTypes.SMB) { - viewModel.authenticateAndSaveSMBNetwork( + viewModel.verifyNetwork( NetworkConnection( host = host, username = user, @@ -160,64 +165,41 @@ class CloudActivity : SimpleActivity() { sharedPath = shared, connectionType = connectionType.toString(), displayName = displayName - ) + ), true ) } else if (connectionType == ConnectionTypes.WebDav) { - val url = Helpers.createProtocolPath(protocol, host, port, shared) if (protocol == Protocols.HTTPS) { - saveCertificate(certUri,host) - } - viewModel.connectAndAuthenticateWebDav(user, password, url,host,protocol,this@CloudActivity) - viewModel.verifyWebDav.collectLatest { - if (it) { - viewModel.saveNetwork( - NetworkConnection( - host = host, - username = user, - password = password, - sharedPath = shared, - connectionType = connectionType.toString(), - displayName = displayName, - url = url, - port = port - ) - ) - } + saveCertificate(certUri, host) } + val network = NetworkConnection( + host = host, + username = user, + password = password, + connectionType = connectionType.toString(), + port = port, + displayName = displayName, + ) + viewModel.connectAndAuthenticateWebDav(network, protocol!!, true, this@CloudActivity) } else if (connectionType == ConnectionTypes.SFTP) { - viewModel.connectSFTP(user, password, host, port) - viewModel.verifySFTP.collectLatest { - if (it) { - viewModel.saveNetwork( - NetworkConnection( - host = host, - username = user, - password = password, - connectionType = connectionType.toString(), - port = port, - displayName = displayName, - url = viewModel.getSFTPConn().canonicalize(".") - ) - ) - } - } + val network = NetworkConnection( + host = host, + username = user, + password = password, + connectionType = connectionType.toString(), + port = port, + displayName = displayName, + ) + viewModel.connectSFTP(network, true) } else if (connectionType == ConnectionTypes.FTP) { - viewModel.connectFTP(user, password, host, port) - viewModel.verifyFTP.collectLatest { - if (it) { - viewModel.saveNetwork( - NetworkConnection( - host = host, - username = user, - password = password, - connectionType = connectionType.toString(), - port = port, - displayName = displayName, - url = viewModel.getFTP().printWorkingDirectory() - ) - ) - } - } + viewModel.connectFTP( + NetworkConnection( + username = user, + password = password, + host = host, + port = port, + connectionType = ConnectionTypes.FTP.type + ), true + ) } } } @@ -236,86 +218,215 @@ class CloudActivity : SimpleActivity() { } } + private fun handleConnection(item: NetworkConnection, connectionType: ConnectionTypes) { + when (connectionType) { + ConnectionTypes.SMB -> { + viewModel.verifyNetwork(item, false) + } + + ConnectionTypes.DAVx5 -> { + launchMainActivity(ConnectionTypes.DAVx5, item.sharedPath) + } + + ConnectionTypes.WebDav -> { + val protocol = item.url.split(':')[0]; + viewModel.connectAndAuthenticateWebDav( + item, + Protocols.valueOf(protocol.uppercase(Locale.getDefault())), + false, + this@CloudActivity + + ) + } + + ConnectionTypes.SFTP -> { + viewModel.connectSFTP(item, false) + } + + ConnectionTypes.FTP -> { + viewModel.connectFTP(item, false) + } + + else -> Unit + } + } + private fun updateAdapter(listItems: MutableList) { ConnectionItemsAdapter(this, listItems, binding.connectionsList) { item -> - val itm = item as NetworkConnection - if (itm.connectionType == ConnectionTypes.SMB.type) { - viewModel.verifyNetwork(itm) - lifecycleScope.launch { - viewModel.verifyNetwork.collectLatest { value -> - if (value) { - val path = "${item.host.trimEnd('/')}/${item.sharedPath.trimStart('/')}" - startServer(item, connectionType = ConnectionTypes.SMB, machinePort = itm.port) - launchMainActivity(ConnectionTypes.SMB, path) + lifecycleScope.launch { + val itm = item as NetworkConnection + if (itm.connectionType == ConnectionTypes.SMB.type) { + handleConnection(itm, ConnectionTypes.SMB) + } else if (itm.connectionType == ConnectionTypes.DAVx5.type) { + handleConnection(itm, ConnectionTypes.DAVx5) + } else if (itm.connectionType == ConnectionTypes.WebDav.type) { + handleConnection(itm, ConnectionTypes.WebDav) + } else if (item.connectionType == ConnectionTypes.SFTP.type) { + handleConnection(itm, ConnectionTypes.SFTP) + } else if (item.connectionType == ConnectionTypes.FTP.type) { + handleConnection(itm, ConnectionTypes.FTP) + } + + } + }.apply { + binding.connectionsList.adapter = this + } + } + + private fun launchMainActivity(connectionType: ConnectionTypes, path: String) { + val intent = Intent(this@CloudActivity, MainActivity::class.java).apply { + putExtra(PATH, path) + putExtra(CONNECTION_TYPE, connectionType) + } + launcher.launch(intent) + } + + private fun startServer( + connection: NetworkConnection, + port: Int = 7871, + connectionType: ConnectionTypes, + machinePort: Int, + protocol: Protocols = Protocols.HTTP + ) { + https = HttpServer(port, connection.host, connectionType, composition.networkApiRepository, machinePort, protocol) + https?.start() + networkJob?.cancel() + } + + private fun stopServer() { + https?.stop() + https = null + } + + private fun startConnectionsCollection() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + handleResponse(ConnectionTypes.SMB) + } + launch { + handleResponse(ConnectionTypes.WebDav) + } + + launch { + handleResponse(ConnectionTypes.SFTP) + } + + launch { + handleResponse(ConnectionTypes.FTP) + } + } + } + } + + private suspend fun handleResponse(connectionType: ConnectionTypes) { + when (connectionType) { + ConnectionTypes.WebDav -> { + viewModel.verifyWebDav.collectLatest { + if (it.success) { + if (!it.saveInfo) { + val protocol = it.item.url.split(':')[0]; + startServer( + it.item, + PORT_WEBDAV, + connectionType = ConnectionTypes.WebDav, + machinePort = it.item.port, + Protocols.valueOf(protocol.uppercase(Locale.getDefault())) + ) + launchMainActivity(ConnectionTypes.WebDav, it.item.url) } else { - Toast.makeText(this@CloudActivity, "Connection failed", Toast.LENGTH_SHORT).show() + viewModel.saveNetwork( + NetworkConnection( + host = it.item.host, + username = it.item.username, + password = it.item.password, + sharedPath = it.item.sharedPath, + connectionType = connectionType.toString(), + displayName = it.item.displayName, + url = it.item.url, + port = it.item.port + ) + ) } } + } - } else if (itm.connectionType == ConnectionTypes.DAVx5.type) { - launchMainActivity(ConnectionTypes.DAVx5, itm.sharedPath) - } else if (itm.connectionType == ConnectionTypes.WebDav.type) { - itm.username?.let { username -> - itm.password?.let { password -> - val protocol = itm.url.split(':')[0]; - viewModel.connectAndAuthenticateWebDav(username, password, itm.url,itm.host, Protocols.valueOf(protocol.uppercase(Locale.getDefault())), this) - lifecycleScope.launch { - viewModel.verifyWebDav.collectLatest { - if (it) { - startServer(item, PORT_WEBDAV, connectionType = ConnectionTypes.WebDav, machinePort = itm.port, Protocols.valueOf(protocol.uppercase(Locale.getDefault()))) - launchMainActivity(ConnectionTypes.WebDav, itm.url) - } - } + } + + ConnectionTypes.SMB -> { + viewModel.verifyNetwork.collectLatest { + if (it.success) { + if (!it.saveInfo) { + val path = "${it.item.host.trimEnd('/')}/${it.item.sharedPath.trimStart('/')}" + startServer(it.item, connectionType = ConnectionTypes.SMB, machinePort = it.item.port) + launchMainActivity(ConnectionTypes.SMB, path) + } else { + viewModel.saveNetwork( + NetworkConnection( + host = it.item.host, + username = it.item.username, + password = it.item.password, + connectionType = connectionType.toString(), + displayName = it.item.displayName, + sharedPath = it.item.sharedPath + ) + ) } } } - } else if (item.connectionType == ConnectionTypes.SFTP.type) { - itm.username?.let { username -> - itm.password?.let { password -> - viewModel.connectSFTP(username, password, itm.host, itm.port) - lifecycleScope.launch(Dispatchers.IO) { - viewModel.verifySFTP.collectLatest { - if (it) { - startServer(item, PORT_SFTP, connectionType = ConnectionTypes.SFTP, machinePort = itm.port) - launchMainActivity(ConnectionTypes.SFTP, itm.url) - } - } + } + + ConnectionTypes.SFTP -> { + viewModel.verifySFTP.collectLatest { + if (it.success) { + if (!it.saveInfo) { + startServer(it.item, PORT_SFTP, connectionType = ConnectionTypes.SFTP, machinePort = it.item.port) + launchMainActivity(ConnectionTypes.SFTP, it.item.url) + } else { + viewModel.saveNetwork( + NetworkConnection( + host = it.item.host, + username = it.item.username, + password = it.item.password, + connectionType = connectionType.toString(), + port = it.item.port, + displayName = it.item.displayName, + url = viewModel.getSFTPConn().canonicalize(".") + ) + ) } } } + } - } else if (item.connectionType == ConnectionTypes.FTP.type) { - itm.username?.let { username -> - itm.password?.let { password -> - viewModel.connectFTP(username, password, itm.host, itm.port) - lifecycleScope.launch(Dispatchers.IO) { - viewModel.verifyFTP.collectLatest { - if (it) { - startServer(item, PORT_FTP, connectionType = ConnectionTypes.FTP, machinePort = itm.port) - launchMainActivity(ConnectionTypes.FTP, itm.url) - } - } + ConnectionTypes.FTP -> { + viewModel.verifyFTP.collectLatest { + if (it.success) { + if (!it.saveInfo) { + startServer(it.item, PORT_FTP, connectionType = ConnectionTypes.FTP, machinePort = it.item.port) + launchMainActivity(ConnectionTypes.FTP, it.item.url) + } else { + viewModel.saveNetwork( + NetworkConnection( + host = it.item.host, + username = it.item.username, + password = it.item.password, + connectionType = connectionType.toString(), + port = it.item.port, + displayName = it.item.displayName, + url = viewModel.getFTP().printWorkingDirectory() + ) + ) } } } } - }.apply { - binding.connectionsList.adapter = this + ConnectionTypes.DAVx5 -> Unit + else -> Unit } } - private fun launchMainActivity(connectionType: ConnectionTypes, path: String) { - startActivity(Intent(this@CloudActivity, MainActivity::class.java).apply { - putExtra(PATH, path) - putExtra(CONNECTION_TYPE, connectionType) - }) - } - - private fun startServer(connection: NetworkConnection, port: Int = 7871, connectionType: ConnectionTypes, machinePort: Int,protocol: Protocols = Protocols.HTTP) { - val https = HttpServer(port, connection.host, connectionType, composition.networkApiRepository, machinePort,protocol) - https.start() - } private fun saveCertificate(uri: Uri?, name: String) { uri?.let { diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 322b83a56..0855b543e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -17,7 +17,7 @@ import org.fossify.filemanager.activities.CloudActivity import org.fossify.filemanager.databinding.DialogAddConnectionBinding import org.fossify.filemanager.enums.Protocols -class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Uri?, Int, ConnectionTypes, Protocols) -> Unit) { +class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Uri?, Int, ConnectionTypes, Protocols?) -> Unit) { private var binding: DialogAddConnectionBinding val items = listOf(ConnectionTypes.DAVx5.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.FTP.type) private var certUri: Uri? = null @@ -36,7 +36,10 @@ class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, Stri certUri, binding.portEt.value.toIntOrNull() ?: 0, ConnectionTypes.fromType(binding.dropdownMenu.value), - Protocols.valueOf(binding.dropdownMenuProtocol.text.toString()) + binding.dropdownMenuProtocol.text + ?.toString() + ?.takeIf { it.isNotBlank() } + ?.let { Protocols.valueOf(it) } ) } .setNegativeButton(R.string.cancel, null) diff --git a/app/src/main/kotlin/org/fossify/filemanager/enums/Protocols.kt b/app/src/main/kotlin/org/fossify/filemanager/enums/Protocols.kt index f5502cf74..403296895 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/enums/Protocols.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/enums/Protocols.kt @@ -2,7 +2,5 @@ package org.fossify.filemanager.enums enum class Protocols { HTTP, - HTTPS, - SMB, - SSH + HTTPS } diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt index 65a1ee9b9..a6bada789 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -14,7 +14,7 @@ object Helpers { protocol = protocols.name.lowercase() } else if(connectionTypes.equals(ConnectionTypes.SMB)){ - protocol = Protocols.SMB.toString().lowercase() + protocol = ConnectionTypes.SMB.toString().lowercase() } val url = "${protocol}://${if (server.isEmpty()) host else server }:${port}/${path}" return url diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt b/app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt new file mode 100644 index 000000000..7dd1181c1 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt @@ -0,0 +1,3 @@ +package org.fossify.filemanager.models + +data class ConnectionResult(val item: NetworkConnection,val success: Boolean,val saveInfo: Boolean = true) diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 943d1ffad..a105e2530 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -22,19 +22,22 @@ import org.apache.commons.net.ftp.FTPFile import org.fossify.filemanager.enums.Protocols import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb +import org.fossify.filemanager.models.ConnectionResult import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream -import java.security.cert.X509Certificate -class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkConnectionRepositoryDb, private val networkConnectionRepositoryApi: NetworkConnectionRepositoryApi): ViewModel() { +class NetworkBrowserViewModel( + private val networkConnectionRepository: NetworkConnectionRepositoryDb, + private val networkConnectionRepositoryApi: NetworkConnectionRepositoryApi +) : ViewModel() { val savedNetworks = MutableStateFlow>(emptyList()) - val verifyNetwork = MutableSharedFlow() + val verifyNetwork = MutableSharedFlow() - val verifyWebDav = MutableSharedFlow() + val verifyWebDav = MutableSharedFlow() - val verifySFTP = MutableSharedFlow() - val verifyFTP = MutableSharedFlow() + val verifySFTP = MutableSharedFlow() + val verifyFTP = MutableSharedFlow() val sftpFiles = MutableStateFlow>(emptyList()) @@ -43,116 +46,109 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo val ftpFiles = MutableStateFlow>(emptyList()) - val connectionId = MutableSharedFlow() - fun saveNetwork(networkConnection: NetworkConnection){ + fun saveNetwork(networkConnection: NetworkConnection) { viewModelScope.launch(Dispatchers.IO) { - connectionId.emit(networkConnectionRepository.saveConnection(networkConnection)) + networkConnectionRepository.saveConnection(networkConnection) } } - fun authenticateAndSaveSMBNetwork(networkConnection: NetworkConnection){ + fun getAllSavedNetworks() { viewModelScope.launch(Dispatchers.IO) { - val config: Configuration = PropertyConfiguration(System.getProperties()) - val context: CIFSContext = BaseContext(config) - val auth = NtlmPasswordAuthenticator( - "", - networkConnection.username, - networkConnection.password - ) - val authContext = context.withCredentials(auth) - val smbUrl = "smb://${networkConnection.host}/${networkConnection.sharedPath}" - - val dir = SmbFile(smbUrl, authContext) - if (dir.exists()) { - saveNetwork(networkConnection) - } - } - } - - fun getAllSavedNetworks(){ - viewModelScope.launch(Dispatchers.IO){ val connections = networkConnectionRepository.getAllSavedConnections().collectLatest { value -> savedNetworks.emit(value) } } } - fun verifyNetwork(connection: NetworkConnection){ + fun verifyNetwork(connection: NetworkConnection,saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { val value = networkConnectionRepositoryApi.verifyConnection(connection) - verifyNetwork.emit(value) + verifyNetwork.emit(ConnectionResult(connection, value, saveInfo = saveInfo)) } } - fun getFilesFromNetworkPath():Array{ - return networkConnectionRepositoryApi.getFilesFromNetworkPath() + fun getFilesFromNetworkPath(): Array { + return networkConnectionRepositoryApi.getFilesFromNetworkPath() } fun getMainSmb(): SmbFile = networkConnectionRepositoryApi.getMainSmbFile() fun getSFTPConn(): SFTPClient = networkConnectionRepositoryApi.getSFTPConn() - fun connectAndAuthenticateWebDav(userName: String = "", password: String = "", url: String, host: String, protocol: Protocols, context: Context){ + fun connectAndAuthenticateWebDav(connection: NetworkConnection, protocol: Protocols,saveInfo: Boolean, context: Context) { viewModelScope.launch(Dispatchers.IO) { - val result = networkConnectionRepositoryApi.connectAndVerifyWebDav(userName, password, url,host,protocol,context) - verifyWebDav.emit(result) + connection.username?.let { username -> + connection.password?.let { password -> + val result = networkConnectionRepositoryApi.connectAndVerifyWebDav(username, password, connection.url, connection.host, protocol, context) + verifyWebDav.emit(ConnectionResult(connection, result,saveInfo)) + } + } } } - fun listWebDavFiles(url: String){ + fun listWebDavFiles(url: String) { viewModelScope.launch(Dispatchers.IO) { - webDavFiles.emit(networkConnectionRepositoryApi.listAllFilesOnWebDav(url)) + webDavFiles.emit(networkConnectionRepositoryApi.listAllFilesOnWebDav(url)) } } - fun listWebDavFileStream(url: String,start: Long,end: Long): InputStream{ - return networkConnectionRepositoryApi.getWebDavFileInputStream(url,start,end) + fun listWebDavFileStream(url: String, start: Long, end: Long): InputStream { + return networkConnectionRepositoryApi.getWebDavFileInputStream(url, start, end) } - fun listWebDavFileDetail(url: String): DavResource?{ + fun listWebDavFileDetail(url: String): DavResource? { return networkConnectionRepositoryApi.listWebDavFileDetail(url) } - fun connectSFTP(userName: String, password: String,server: String,port: Int){ + fun connectSFTP(connection: NetworkConnection,saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { - val res = networkConnectionRepositoryApi.connectToSftp(userName,password,server,port) - verifySFTP.emit(res) + connection.username?.let { username -> + connection.password?.let { password -> + val res = networkConnectionRepositoryApi.connectToSftp(username, password, connection.host, connection.port) + verifySFTP.emit(ConnectionResult(connection, res,saveInfo)) + } + } + } } - fun listAllFilesSFTPRoot(path: String){ + fun listAllFilesSFTPRoot(path: String) { viewModelScope.launch(Dispatchers.IO) { val res = networkConnectionRepositoryApi.listAllFilesSFTPRoot(path) sftpFiles.emit(res) } } - fun listAllFilesSFTPPath(path: String){ + fun listAllFilesSFTPPath(path: String) { viewModelScope.launch(Dispatchers.IO) { val res = networkConnectionRepositoryApi.listAllFilesSFTPRoot(path) sftpFiles.emit(res) } } - fun listSFTPFileDetails(path: String): FileAttributes?{ + fun listSFTPFileDetails(path: String): FileAttributes? { return networkConnectionRepositoryApi.listSFTPFileDetails(path) } - fun getSFTPFileStream(path: String,startByte: Long): InputStream{ - return networkConnectionRepositoryApi.getSFTPFileInputStream(url = path,startByte) + fun getSFTPFileStream(path: String, startByte: Long): InputStream { + return networkConnectionRepositoryApi.getSFTPFileInputStream(url = path, startByte) } - fun connectFTP(userName: String, password: String,server: String,port: Int){ + fun connectFTP(connection: NetworkConnection,saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { - val res = networkConnectionRepositoryApi.connectToFTP(userName,password,server,port) - verifyFTP.emit(res) + connection.username?.let { username -> + connection.password?.let { password -> + val res = networkConnectionRepositoryApi.connectToFTP(username, password, connection.host, connection.port) + verifyFTP.emit(ConnectionResult(connection, res,saveInfo)) + } + } } } fun getFTP() = networkConnectionRepositoryApi.getFTPConn() - fun listAllFTPFiles(path: String){ + fun listAllFTPFiles(path: String) { viewModelScope.launch(Dispatchers.IO) { val res = networkConnectionRepositoryApi.listAllFTPFiles(path) ftpFiles.emit(res) @@ -161,5 +157,5 @@ class NetworkBrowserViewModel(private val networkConnectionRepository: NetworkCo fun getFTPFileDetail(path: String) = networkConnectionRepositoryApi.getFTPFileDetail(path) - fun getFTPFileStream(path: String,start: Long) = networkConnectionRepositoryApi.getFTPFileInputStream(path,start) + fun getFTPFileStream(path: String, start: Long) = networkConnectionRepositoryApi.getFTPFileInputStream(path, start) } diff --git a/build.gradle.kts b/build.gradle.kts index bec4ede3d..91f6f0867 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,5 @@ plugins { alias(libs.plugins.kotlinAndroid).apply(false) alias(libs.plugins.detekt).apply(false) id("com.google.devtools.ksp") version "2.2.0-2.0.2" + } diff --git a/settings.gradle.kts b/settings.gradle.kts index 94d40dbf9..4320caaa4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,3 +15,7 @@ dependencyResolutionManagement { } } include(":app") +include(":commons") + +// Point specifically to the 'commons' subfolder +project(":commons").projectDir = file("../Fossify/Common/commons") From b8404b206f3fb5f983766ab90dd0b8fc54e313e0 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sun, 10 May 2026 21:08:25 +0500 Subject: [PATCH 18/37] Added anonymous login for smb protocol --- .../filemanager/activities/CloudActivity.kt | 62 +++++++++---------- .../adapters/ConnectionItemsAdapter.kt | 2 +- .../filemanager/dialogs/ConnectionDialog.kt | 44 +++++++++++-- .../entity/NetworkConnectionEntity.kt | 5 +- .../filemanager/enums/Authentication.kt | 6 ++ .../filemanager/fileSystems/FileHelpers.kt | 9 +-- .../filemanager/fileSystems/HttpServer.kt | 9 ++- .../fossify/filemanager/helpers/Helpers.kt | 15 +++-- .../NetworkConnectionRepositoryApi.kt | 6 +- .../mapper/NetworkConnectionMapper.kt | 12 ++-- .../filemanager/models/NetworkConnection.kt | 4 +- .../NetworkConnectionRepositoryApiImpl.kt | 60 +++++++++--------- .../viewmodels/NetworkBrowserViewModel.kt | 36 ++++------- .../main/res/layout/dialog_add_connection.xml | 18 +++++- 14 files changed, 172 insertions(+), 116 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/enums/Authentication.kt diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 901549f7d..90e3b676d 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -30,6 +30,7 @@ import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel import java.security.Security import org.bouncycastle.jce.provider.BouncyCastleProvider import org.fossify.filemanager.dependencies.AppComposition +import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.enums.Protocols import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.helpers.PORT_FTP @@ -45,8 +46,6 @@ class CloudActivity : SimpleActivity() { private lateinit var certificateRepository: CertificateRepository private lateinit var composition: AppComposition - private var networkJob: Job? = null - private var https: HttpServer? = null private fun setupBouncyCastle() { @@ -103,7 +102,7 @@ class CloudActivity : SimpleActivity() { NetworkConnection( displayName = s.name!!, sharedPath = s.uri.toString(), - connectionType = ConnectionTypes.DAVx5.toString() + connectionType = ConnectionTypes.DAVx5 ) ) } @@ -132,8 +131,8 @@ class CloudActivity : SimpleActivity() { } private fun showConnectionDialog() { - ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName, certPath, port, connection, protocol -> - saveNetwork(host, user, password, shared, displayName, certPath, port, connection, protocol) + ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName, certPath, port, connection, protocol, auth -> + saveNetwork(host, user, password, shared, displayName, certPath, port, connection, protocol, auth) } } @@ -153,7 +152,8 @@ class CloudActivity : SimpleActivity() { certUri: Uri?, port: Int, connectionType: ConnectionTypes, - protocol: Protocols? + protocol: Protocols?, + authentication: Authentication ) { lifecycleScope.launch((Dispatchers.IO)) { if (connectionType == ConnectionTypes.SMB) { @@ -163,21 +163,25 @@ class CloudActivity : SimpleActivity() { username = user, password = password, sharedPath = shared, - connectionType = connectionType.toString(), - displayName = displayName + connectionType = connectionType, + displayName = displayName, + authentication = authentication ), true ) } else if (connectionType == ConnectionTypes.WebDav) { if (protocol == Protocols.HTTPS) { saveCertificate(certUri, host) } + val url = Helpers.createProtocolPath(protocol,host,port,shared) val network = NetworkConnection( host = host, username = user, password = password, - connectionType = connectionType.toString(), + connectionType = connectionType, port = port, displayName = displayName, + authentication = authentication, + url = url ) viewModel.connectAndAuthenticateWebDav(network, protocol!!, true, this@CloudActivity) } else if (connectionType == ConnectionTypes.SFTP) { @@ -185,9 +189,10 @@ class CloudActivity : SimpleActivity() { host = host, username = user, password = password, - connectionType = connectionType.toString(), + connectionType = connectionType, port = port, displayName = displayName, + authentication = authentication ) viewModel.connectSFTP(network, true) } else if (connectionType == ConnectionTypes.FTP) { @@ -197,7 +202,8 @@ class CloudActivity : SimpleActivity() { password = password, host = host, port = port, - connectionType = ConnectionTypes.FTP.type + connectionType = ConnectionTypes.FTP, + authentication = authentication ), true ) } @@ -255,18 +261,7 @@ class CloudActivity : SimpleActivity() { ConnectionItemsAdapter(this, listItems, binding.connectionsList) { item -> lifecycleScope.launch { val itm = item as NetworkConnection - if (itm.connectionType == ConnectionTypes.SMB.type) { - handleConnection(itm, ConnectionTypes.SMB) - } else if (itm.connectionType == ConnectionTypes.DAVx5.type) { - handleConnection(itm, ConnectionTypes.DAVx5) - } else if (itm.connectionType == ConnectionTypes.WebDav.type) { - handleConnection(itm, ConnectionTypes.WebDav) - } else if (item.connectionType == ConnectionTypes.SFTP.type) { - handleConnection(itm, ConnectionTypes.SFTP) - } else if (item.connectionType == ConnectionTypes.FTP.type) { - handleConnection(itm, ConnectionTypes.FTP) - } - + handleConnection(itm,itm.connectionType) } }.apply { binding.connectionsList.adapter = this @@ -290,7 +285,6 @@ class CloudActivity : SimpleActivity() { ) { https = HttpServer(port, connection.host, connectionType, composition.networkApiRepository, machinePort, protocol) https?.start() - networkJob?.cancel() } private fun stopServer() { @@ -341,10 +335,11 @@ class CloudActivity : SimpleActivity() { username = it.item.username, password = it.item.password, sharedPath = it.item.sharedPath, - connectionType = connectionType.toString(), + connectionType = connectionType, displayName = it.item.displayName, url = it.item.url, - port = it.item.port + port = it.item.port, + authentication = it.item.authentication ) ) } @@ -366,9 +361,10 @@ class CloudActivity : SimpleActivity() { host = it.item.host, username = it.item.username, password = it.item.password, - connectionType = connectionType.toString(), + connectionType = connectionType, displayName = it.item.displayName, - sharedPath = it.item.sharedPath + sharedPath = it.item.sharedPath, + authentication = it.item.authentication ) ) } @@ -388,10 +384,11 @@ class CloudActivity : SimpleActivity() { host = it.item.host, username = it.item.username, password = it.item.password, - connectionType = connectionType.toString(), + connectionType = connectionType, port = it.item.port, displayName = it.item.displayName, - url = viewModel.getSFTPConn().canonicalize(".") + url = viewModel.getSFTPConn().canonicalize("."), + authentication = it.item.authentication ) ) } @@ -411,10 +408,11 @@ class CloudActivity : SimpleActivity() { host = it.item.host, username = it.item.username, password = it.item.password, - connectionType = connectionType.toString(), + connectionType = connectionType, port = it.item.port, displayName = it.item.displayName, - url = viewModel.getFTP().printWorkingDirectory() + url = viewModel.getFTP().printWorkingDirectory(), + authentication = it.item.authentication ) ) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt b/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt index 3a104d844..3905acd06 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt @@ -72,7 +72,7 @@ class ConnectionItemsAdapter(activity: SimpleActivity, var listItems: MutableLis private fun setupView(view: View, listItem: NetworkConnection) { ItemNetworkConnectionBinding.bind(view).apply { tvHost.text = listItem.host - tvType.text = listItem.connectionType + tvType.text = listItem.connectionType.toString() tvDisplayName.text = listItem.displayName tvSharedPath.text = listItem.sharedPath } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 0855b543e..0d245cd35 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -1,12 +1,9 @@ package org.fossify.filemanager.dialogs import android.net.Uri -import android.util.Log import android.view.View import android.widget.ArrayAdapter import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.getAlertDialogBuilder @@ -15,14 +12,17 @@ import org.fossify.commons.extensions.value import org.fossify.filemanager.R import org.fossify.filemanager.activities.CloudActivity import org.fossify.filemanager.databinding.DialogAddConnectionBinding +import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.enums.Protocols -class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Uri?, Int, ConnectionTypes, Protocols?) -> Unit) { +class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Uri?, Int, ConnectionTypes, Protocols?, Authentication) -> Unit) { private var binding: DialogAddConnectionBinding val items = listOf(ConnectionTypes.DAVx5.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.FTP.type) private var certUri: Uri? = null val protocols = listOf(Protocols.HTTP, Protocols.HTTPS) + val authentications = listOf(Authentication.Password, Authentication.Anonymous) + init { binding = DialogAddConnectionBinding.inflate(activity.layoutInflater) activity.getAlertDialogBuilder() @@ -39,7 +39,8 @@ class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, Stri binding.dropdownMenuProtocol.text ?.toString() ?.takeIf { it.isNotBlank() } - ?.let { Protocols.valueOf(it) } + ?.let { Protocols.valueOf(it) }, + Authentication.valueOf(binding.authDropDownMenu.text.toString()) ) } .setNegativeButton(R.string.cancel, null) @@ -48,17 +49,50 @@ class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, Stri } initializeDropDownList() dropDownItemSelected() + registerAuthClickListener() attachCertBtnClickListener() } private fun initializeDropDownList() { + initializeConnectionsDropDown() + initializeAuthDropdown() + initializeProtocolDropDown() + } + + private fun initializeConnectionsDropDown(){ val adapter = ArrayAdapter(activity, android.R.layout.simple_list_item_1, items) binding.dropdownMenu.setAdapter(adapter) + binding.dropdownMenu.setText(items[1],false) + } + + private fun initializeProtocolDropDown(){ val protocolsAdapter = ArrayAdapter(activity,android.R.layout.simple_list_item_1, protocols) binding.dropdownMenuProtocol.setAdapter(protocolsAdapter) + binding.dropdownMenuProtocol.setText(protocols[0].toString(),false) + } + private fun initializeAuthDropdown(){ + val authAdapter = ArrayAdapter(activity,android.R.layout.simple_list_item_1, authentications) + binding.authDropDownMenu.setAdapter(authAdapter) + binding.authDropDownMenu.setText(authentications[0].toString(),false) } + private fun registerAuthClickListener(){ + binding.authDropDownMenu.setOnItemClickListener { parent, view, position, id -> + val selectedItem = parent.getItemAtPosition(position).toString() + if(Authentication.valueOf(selectedItem) == Authentication.Anonymous){ + toggleCredentialsVisibility(View.GONE) + } + else{ + toggleCredentialsVisibility(View.VISIBLE) + } + } + } + + private fun toggleCredentialsVisibility(visibility: Int){ + binding.userTf.visibility = visibility + binding.passwordTf.visibility = visibility + } private fun promptUserToSelectStorage() { (activity as CloudActivity).promptUserToSelectStorage() diff --git a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt index 706126498..0fcb98d4e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt @@ -2,6 +2,8 @@ package org.fossify.filemanager.entity import androidx.room.Entity import androidx.room.PrimaryKey +import org.fossify.filemanager.enums.Authentication + @Entity(tableName = "network_connections") data class NetworkConnectionEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, @@ -12,5 +14,6 @@ data class NetworkConnectionEntity( val displayName: String, val connectionType: String, val sharedPath: String, - val url: String + val url: String, + val authentication: String ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/enums/Authentication.kt b/app/src/main/kotlin/org/fossify/filemanager/enums/Authentication.kt new file mode 100644 index 000000000..f9d2526ea --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/enums/Authentication.kt @@ -0,0 +1,6 @@ +package org.fossify.filemanager.enums + +enum class Authentication { + Password, + Anonymous +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt index cb6cd934c..ffe71366f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -1,10 +1,8 @@ package org.fossify.filemanager.fileSystems -import android.app.Activity import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.net.Uri import android.util.Log import androidx.core.net.toUri import jcifs.smb.SmbFile @@ -14,7 +12,6 @@ import kotlinx.coroutines.launch import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.models.ListItem -import java.io.File object FileHelpers { val URL: String = "http://127.0.0.1:7871/" @@ -40,7 +37,7 @@ object FileHelpers { try { CoroutineScope(Dispatchers.IO).launch { val port = Helpers.getPortForEachService(connectionTypes) - val uri = Helpers.createUrl(connectionTypes, item.mPath, port = port).toUri() + val uri = Helpers.createNanoHttpdUrl(connectionTypes, item.mPath, port = port).toUri() val i = Intent(Intent.ACTION_VIEW) i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) @@ -61,7 +58,7 @@ object FileHelpers { try{ CoroutineScope(Dispatchers.IO).launch { val port = Helpers.getPortForEachService(connectionTypes) - val uri = Helpers.createUrl(connectionTypes, item.mPath, port = port).toUri() + val uri = Helpers.createNanoHttpdUrl(connectionTypes, item.mPath, port = port).toUri() val i = Intent(Intent.ACTION_VIEW) i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) @@ -81,7 +78,7 @@ object FileHelpers { try{ CoroutineScope(Dispatchers.IO).launch { val port = Helpers.getPortForEachService(connectionTypes) - val uri = Helpers.createUrl(connectionTypes, item.mPath, port = port).toUri() + val uri = Helpers.createNanoHttpdUrl(connectionTypes, item.mPath, port = port).toUri() val i = Intent(Intent.ACTION_VIEW) i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 365f5efb7..f93076bd7 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -8,7 +8,6 @@ import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.enums.Protocols import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi -import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel import java.io.BufferedInputStream class HttpServer( @@ -32,16 +31,16 @@ class HttpServer( return handleRangeRequest(file, rangeHeader, file.length()) } else if(connectionTypes.equals(ConnectionTypes.WebDav)) { - val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri.toString(), port = machinePort, protocols = protocols) + val url = Helpers.createNanoHttpdUrl(connectionTypes, server = serverIp, path = uri.toString(), port = machinePort, protocols = protocols) val webDavFile = networkConnectionRepository.listWebDavFileDetail(url) return handleRangeRequestWebDav(rangeHeader, webDavFile?.contentLength!!, uri = uri, webDavFile.contentType,protocols) } else if (connectionTypes.equals(ConnectionTypes.SFTP)) { - val url = Helpers.createUrl(connectionTypes, server = serverIp, path = "", port = machinePort) + val url = Helpers.createNanoHttpdUrl(connectionTypes, server = serverIp, path = "", port = machinePort) val sftFile = networkConnectionRepository.listSFTPFileDetails(uri) return handleRangeRequestSFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } - val url = Helpers.createUrl(connectionTypes, server = serverIp, path = "", port = machinePort) + val url = Helpers.createNanoHttpdUrl(connectionTypes, server = serverIp, path = "", port = machinePort) val sftFile = networkConnectionRepository.getFTPFileDetail(uri) return handleRangeRequestFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } @@ -96,7 +95,7 @@ class HttpServer( private fun handleRangeRequestWebDav(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String,protocol: Protocols = Protocols.HTTP): Response { var start: Long = 0 var end = fileLength - 1 - val url = Helpers.createUrl(connectionTypes, server = serverIp, path = uri, port = machinePort, protocols = protocol) + val url = Helpers.createNanoHttpdUrl(connectionTypes, server = serverIp, path = uri, port = machinePort, protocols = protocol) if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { val ranges = rangeHeader.substring(6).split("-") try { diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt index a6bada789..c13327826 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -2,13 +2,11 @@ package org.fossify.filemanager.helpers import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.enums.Protocols -import java.nio.file.Path -import java.util.Locale import java.util.Locale.getDefault object Helpers { val host: String = "127.0.0.1" - fun createUrl(connectionTypes: ConnectionTypes, path: String = "", server: String = "", port: Int,protocols: Protocols = Protocols.HTTP): String{ + fun createNanoHttpdUrl(connectionTypes: ConnectionTypes, path: String = "", server: String = "", port: Int, protocols: Protocols = Protocols.HTTP): String{ var protocol = Protocols.HTTP.toString().lowercase() if(connectionTypes.equals(ConnectionTypes.WebDav)){ protocol = protocols.name.lowercase() @@ -20,6 +18,13 @@ object Helpers { return url } + fun createUrl(connectionTypes: ConnectionTypes,path: String,server: String,port: Int = 0): String{ + if(connectionTypes == ConnectionTypes.SMB){ + return "${connectionTypes.toString().lowercase()}://${server}/${path}" + } + return "" + } + fun getPortForEachService(connectionTypes: ConnectionTypes): Int{ if (connectionTypes.equals(ConnectionTypes.WebDav)){ return PORT_WEBDAV @@ -36,7 +41,7 @@ object Helpers { return PORT_SMB } - fun createProtocolPath(protocol: Protocols, server: String, port:Int, path:String): String{ - return "${protocol.name.lowercase(getDefault())}://${server}:${port}/${path}" + fun createProtocolPath(protocol: Protocols?, server: String, port:Int, path:String): String{ + return "${protocol?.name?.lowercase(getDefault())}://${server}:${port}/${path}" } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt index 0441388ca..c0d317ad7 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt @@ -23,7 +23,7 @@ interface NetworkConnectionRepositoryApi { ///WebDav - suspend fun connectAndVerifyWebDav(userName: String = "", password: String = "", url: String,host:String,protocols: Protocols, context: Context): Boolean + suspend fun connectAndVerifyWebDav(connection: NetworkConnection,protocols: Protocols, context: Context): Boolean suspend fun listAllFilesOnWebDav(url: String): List @@ -34,7 +34,7 @@ interface NetworkConnectionRepositoryApi { fun loadCertificate(stream: InputStream):Result //SFTP - suspend fun connectToSftp(userName: String, password: String,server: String,port: Int): Boolean + suspend fun connectToSftp(connection: NetworkConnection): Boolean suspend fun listAllFilesSFTPRoot(path: String): List @@ -46,7 +46,7 @@ interface NetworkConnectionRepositoryApi { fun getSFTPConn(): SFTPClient - suspend fun connectToFTP(userName: String, password: String,server: String,port: Int): Boolean + suspend fun connectToFTP(connection: NetworkConnection): Boolean suspend fun listAllFTPFiles(path: String): List diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index 151fbaf10..023f92422 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -5,8 +5,10 @@ import com.thegrizzlylabs.sardineandroid.DavResource import jcifs.smb.SmbFile import net.schmizz.sshj.sftp.RemoteResourceInfo import org.apache.commons.net.ftp.FTPFile +import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.models.FileDirItem import org.fossify.filemanager.entity.NetworkConnectionEntity +import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.models.NetworkConnection fun NetworkConnectionEntity.toDomain(): NetworkConnection { @@ -16,9 +18,10 @@ fun NetworkConnectionEntity.toDomain(): NetworkConnection { username = username, password = password, displayName = displayName, - connectionType = connectionType, + connectionType = ConnectionTypes.valueOf(connectionType), sharedPath = sharedPath, - url = url + url = url, + authentication = Authentication.valueOf(authentication) ) } @@ -29,9 +32,10 @@ fun NetworkConnection.toEntity(): NetworkConnectionEntity { username = username, password = password, displayName = displayName, - connectionType = connectionType, + connectionType = connectionType.toString(), sharedPath = sharedPath, - url = url + url = url, + authentication = authentication.toString() ) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt index f68f0b5ff..44c288418 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt @@ -1,10 +1,12 @@ package org.fossify.filemanager.models +import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.entity.NetworkConnectionEntity +import org.fossify.filemanager.enums.Authentication data class NetworkConnection( val host: String = "", val port: Int = 445, val username: String? = "", - val password: String? = "", val displayName: String = "",val connectionType: String, val sharedPath: String = "", val url: String = "" + val password: String? = "", val displayName: String = "",val connectionType: ConnectionTypes, val sharedPath: String = "", val url: String = "",val authentication: Authentication = Authentication.Password ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt index 67f2de6f4..94cfaf358 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt @@ -7,7 +7,6 @@ import com.thegrizzlylabs.sardineandroid.DavResource import com.thegrizzlylabs.sardineandroid.Sardine import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine import jcifs.CIFSContext -import jcifs.Configuration import jcifs.config.PropertyConfiguration import jcifs.context.BaseContext import jcifs.smb.NtlmPasswordAuthenticator @@ -26,15 +25,16 @@ import org.apache.commons.net.ftp.FTP import org.apache.commons.net.ftp.FTPClient import org.apache.commons.net.ftp.FTPCmd import org.apache.commons.net.ftp.FTPFile +import org.fossify.commons.enums.ConnectionTypes +import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.enums.Protocols +import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.keyStores.CertificateStore import java.io.File -import java.security.KeyStore import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { @@ -57,16 +57,20 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { override suspend fun verifyConnection(connection: NetworkConnection): Boolean { try { - val config: Configuration = PropertyConfiguration(System.getProperties()) val p = Properties(defaultProperties) - var context: CIFSContext = BaseContext(PropertyConfiguration(p)) - val auth = NtlmPasswordAuthenticator( - "", - connection.username, - connection.password - ) - val authContext = context.withCredentials(auth) - val smbUrl = "smb://${connection.host}/${connection.sharedPath}" + val context: CIFSContext = BaseContext(PropertyConfiguration(p)) + var authContext: CIFSContext? = null + if (connection.authentication == Authentication.Password) { + val auth = NtlmPasswordAuthenticator( + "", + connection.username, + connection.password + ) + authContext = context.withCredentials(auth) + } else if (connection.authentication == Authentication.Anonymous) { + authContext = context.withGuestCrendentials() + } + val smbUrl = Helpers.createUrl(ConnectionTypes.SMB, connection.sharedPath, connection.host) dir = SmbFile(smbUrl, authContext) return dir.exists() } catch (exp: Exception) { @@ -83,10 +87,7 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { override fun getMainSmbFile(): SmbFile = dir override suspend fun connectAndVerifyWebDav( - userName: String, - password: String, - url: String, - host: String, + connection: NetworkConnection, protocols: Protocols, context: Context ): Boolean { @@ -94,10 +95,13 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { sardine = if (protocols == Protocols.HTTP) { OkHttpSardine() } else { - createHTTPSSardine(context,host) + createHTTPSSardine(context,connection.host) + } + if(connection.authentication == Authentication.Anonymous){ + return sardine.exists(connection.url) } - sardine.setCredentials(userName, password) - return sardine.exists(url) + sardine.setCredentials(connection.username, connection.password) + return sardine.exists(connection.url) } catch (exp: Exception) { Log.d("WebDav", exp.toString()) return false @@ -105,7 +109,7 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { } override suspend fun listAllFilesOnWebDav(url: String): List { - val resources = sardine.list(url) + val resources = sardine.list("http://192.168.18.86:8090/WebDav/") return resources } @@ -136,13 +140,13 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { } - override suspend fun connectToSftp(userName: String, password: String, server: String, port: Int): Boolean { + override suspend fun connectToSftp(connection: NetworkConnection): Boolean { try { if (!::ssh.isInitialized || !ssh.isConnected || !ssh.isAuthenticated) { ssh = SSHClient() ssh.addHostKeyVerifier(PromiscuousVerifier()) - ssh.connect(server) - ssh.authPassword(userName, password) + ssh.connect(connection.host) + ssh.authPassword(connection.username, connection.password) sftp = ssh.newSFTPClient() } return true @@ -183,15 +187,15 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { override fun getSFTPConn() = sftp - override suspend fun connectToFTP(userName: String, password: String, server: String, port: Int): Boolean { + override suspend fun connectToFTP(connection: NetworkConnection): Boolean { try { ftp = FTPClient() ftpStream = FTPClient() - ftp.connect(server, port) - ftpStream.connect(server, port) + ftp.connect(connection.host, connection.port) + ftpStream.connect(connection.host, connection.port) - val loginSuccess = ftp.login(userName, password) - ftpStream.login(userName, password) + val loginSuccess = ftp.login(connection.username, connection.password) + ftpStream.login(connection.username,connection.password) if (!loginSuccess) { return false diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index a105e2530..0e81df20a 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -49,7 +49,7 @@ class NetworkBrowserViewModel( fun saveNetwork(networkConnection: NetworkConnection) { viewModelScope.launch(Dispatchers.IO) { - networkConnectionRepository.saveConnection(networkConnection) + networkConnectionRepository.saveConnection(networkConnection) } } @@ -61,7 +61,7 @@ class NetworkBrowserViewModel( } } - fun verifyNetwork(connection: NetworkConnection,saveInfo: Boolean) { + fun verifyNetwork(connection: NetworkConnection, saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { val value = networkConnectionRepositoryApi.verifyConnection(connection) verifyNetwork.emit(ConnectionResult(connection, value, saveInfo = saveInfo)) @@ -76,14 +76,10 @@ class NetworkBrowserViewModel( fun getSFTPConn(): SFTPClient = networkConnectionRepositoryApi.getSFTPConn() - fun connectAndAuthenticateWebDav(connection: NetworkConnection, protocol: Protocols,saveInfo: Boolean, context: Context) { + fun connectAndAuthenticateWebDav(connection: NetworkConnection, protocol: Protocols, saveInfo: Boolean, context: Context) { viewModelScope.launch(Dispatchers.IO) { - connection.username?.let { username -> - connection.password?.let { password -> - val result = networkConnectionRepositoryApi.connectAndVerifyWebDav(username, password, connection.url, connection.host, protocol, context) - verifyWebDav.emit(ConnectionResult(connection, result,saveInfo)) - } - } + val result = networkConnectionRepositoryApi.connectAndVerifyWebDav(connection, protocol, context) + verifyWebDav.emit(ConnectionResult(connection, result, saveInfo)) } } @@ -101,15 +97,10 @@ class NetworkBrowserViewModel( return networkConnectionRepositoryApi.listWebDavFileDetail(url) } - fun connectSFTP(connection: NetworkConnection,saveInfo: Boolean) { + fun connectSFTP(connection: NetworkConnection, saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { - connection.username?.let { username -> - connection.password?.let { password -> - val res = networkConnectionRepositoryApi.connectToSftp(username, password, connection.host, connection.port) - verifySFTP.emit(ConnectionResult(connection, res,saveInfo)) - } - } - + val res = networkConnectionRepositoryApi.connectToSftp(connection) + verifySFTP.emit(ConnectionResult(connection, res, saveInfo)) } } @@ -135,14 +126,11 @@ class NetworkBrowserViewModel( return networkConnectionRepositoryApi.getSFTPFileInputStream(url = path, startByte) } - fun connectFTP(connection: NetworkConnection,saveInfo: Boolean) { + fun connectFTP(connection: NetworkConnection, saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { - connection.username?.let { username -> - connection.password?.let { password -> - val res = networkConnectionRepositoryApi.connectToFTP(username, password, connection.host, connection.port) - verifyFTP.emit(ConnectionResult(connection, res,saveInfo)) - } - } + val res = networkConnectionRepositoryApi.connectToFTP(connection) + verifyFTP.emit(ConnectionResult(connection, res, saveInfo)) + } } diff --git a/app/src/main/res/layout/dialog_add_connection.xml b/app/src/main/res/layout/dialog_add_connection.xml index 567d98fba..720821278 100644 --- a/app/src/main/res/layout/dialog_add_connection.xml +++ b/app/src/main/res/layout/dialog_add_connection.xml @@ -65,8 +65,24 @@ + + + + + + + android:inputType="none" + android:focusable="false" + android:clickable="true"/> From 438dee75cd6cfcd1f5de83e141c7f2329c8b619b Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Mon, 11 May 2026 00:56:53 +0500 Subject: [PATCH 19/37] Code fixes and refactoring --- .../filemanager/activities/CloudActivity.kt | 87 ++++---- .../filemanager/dialogs/ConnectionDialog.kt | 86 ++++++-- .../fossify/filemanager/helpers/Constants.kt | 7 + .../fossify/filemanager/helpers/Helpers.kt | 2 +- .../NetworkConnectionRepositoryApiImpl.kt | 19 +- .../main/res/layout/dialog_add_connection.xml | 200 +++++++++--------- 6 files changed, 223 insertions(+), 178 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 90e3b676d..4bacade5f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -155,58 +155,57 @@ class CloudActivity : SimpleActivity() { protocol: Protocols?, authentication: Authentication ) { - lifecycleScope.launch((Dispatchers.IO)) { - if (connectionType == ConnectionTypes.SMB) { - viewModel.verifyNetwork( - NetworkConnection( - host = host, - username = user, - password = password, - sharedPath = shared, - connectionType = connectionType, - displayName = displayName, - authentication = authentication - ), true - ) - } else if (connectionType == ConnectionTypes.WebDav) { - if (protocol == Protocols.HTTPS) { - saveCertificate(certUri, host) - } - val url = Helpers.createProtocolPath(protocol,host,port,shared) - val network = NetworkConnection( + if (connectionType == ConnectionTypes.SMB) { + viewModel.verifyNetwork( + NetworkConnection( host = host, username = user, password = password, + sharedPath = shared, connectionType = connectionType, - port = port, displayName = displayName, authentication = authentication, - url = url - ) - viewModel.connectAndAuthenticateWebDav(network, protocol!!, true, this@CloudActivity) - } else if (connectionType == ConnectionTypes.SFTP) { - val network = NetworkConnection( - host = host, + port = port + ), true + ) + } else if (connectionType == ConnectionTypes.WebDav) { + if (protocol == Protocols.HTTPS) { + saveCertificate(certUri, host) + } + val url = Helpers.createProtocolPath(protocol, host, port, shared) + val network = NetworkConnection( + host = host, + username = user, + password = password, + connectionType = connectionType, + port = port, + displayName = displayName, + authentication = authentication, + url = url + ) + viewModel.connectAndAuthenticateWebDav(network, protocol!!, true, this@CloudActivity) + } else if (connectionType == ConnectionTypes.SFTP) { + val network = NetworkConnection( + host = host, + username = user, + password = password, + connectionType = connectionType, + port = port, + displayName = displayName, + authentication = authentication + ) + viewModel.connectSFTP(network, true) + } else if (connectionType == ConnectionTypes.FTP) { + viewModel.connectFTP( + NetworkConnection( username = user, password = password, - connectionType = connectionType, + host = host, port = port, - displayName = displayName, + connectionType = ConnectionTypes.FTP, authentication = authentication - ) - viewModel.connectSFTP(network, true) - } else if (connectionType == ConnectionTypes.FTP) { - viewModel.connectFTP( - NetworkConnection( - username = user, - password = password, - host = host, - port = port, - connectionType = ConnectionTypes.FTP, - authentication = authentication - ), true - ) - } + ), true + ) } } @@ -261,7 +260,7 @@ class CloudActivity : SimpleActivity() { ConnectionItemsAdapter(this, listItems, binding.connectionsList) { item -> lifecycleScope.launch { val itm = item as NetworkConnection - handleConnection(itm,itm.connectionType) + handleConnection(itm, itm.connectionType) } }.apply { binding.connectionsList.adapter = this @@ -387,7 +386,7 @@ class CloudActivity : SimpleActivity() { connectionType = connectionType, port = it.item.port, displayName = it.item.displayName, - url = viewModel.getSFTPConn().canonicalize("."), + url = "/", authentication = it.item.authentication ) ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 0d245cd35..8a3461068 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -14,8 +14,17 @@ import org.fossify.filemanager.activities.CloudActivity import org.fossify.filemanager.databinding.DialogAddConnectionBinding import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.enums.Protocols - -class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, String, String, String, String, Uri?, Int, ConnectionTypes, Protocols?, Authentication) -> Unit) { +import org.fossify.filemanager.helpers.DEFAULT_FTP_PORT +import org.fossify.filemanager.helpers.DEFAULT_SFTP_PORT +import org.fossify.filemanager.helpers.DEFAULT_SMB_PORT +import org.fossify.filemanager.helpers.DEFAULT_WEBDAV_HTTPS_PORT +import org.fossify.filemanager.helpers.DEFAULT_WEBDAV_HTTP_PORT +import org.fossify.filemanager.helpers.PORT_SMB + +class ConnectionDialog( + val activity: BaseSimpleActivity, + dispatch: (String, String, String, String, String, Uri?, Int, ConnectionTypes, Protocols?, Authentication) -> Unit +) { private var binding: DialogAddConnectionBinding val items = listOf(ConnectionTypes.DAVx5.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.FTP.type) private var certUri: Uri? = null @@ -47,10 +56,11 @@ class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, Stri .apply { activity.setupDialogStuff(binding.root, this) } - initializeDropDownList() dropDownItemSelected() + initializeDropDownList() registerAuthClickListener() attachCertBtnClickListener() + dropDownMenuProtocolItemClickListener() } @@ -60,36 +70,36 @@ class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, Stri initializeProtocolDropDown() } - private fun initializeConnectionsDropDown(){ + private fun initializeConnectionsDropDown() { val adapter = ArrayAdapter(activity, android.R.layout.simple_list_item_1, items) binding.dropdownMenu.setAdapter(adapter) - binding.dropdownMenu.setText(items[1],false) + binding.dropdownMenu.setText(items[1], false) } - private fun initializeProtocolDropDown(){ - val protocolsAdapter = ArrayAdapter(activity,android.R.layout.simple_list_item_1, protocols) + private fun initializeProtocolDropDown() { + val protocolsAdapter = ArrayAdapter(activity, android.R.layout.simple_list_item_1, protocols) binding.dropdownMenuProtocol.setAdapter(protocolsAdapter) - binding.dropdownMenuProtocol.setText(protocols[0].toString(),false) + binding.dropdownMenuProtocol.setText(protocols[0].toString(), false) } - private fun initializeAuthDropdown(){ - val authAdapter = ArrayAdapter(activity,android.R.layout.simple_list_item_1, authentications) + + private fun initializeAuthDropdown() { + val authAdapter = ArrayAdapter(activity, android.R.layout.simple_list_item_1, authentications) binding.authDropDownMenu.setAdapter(authAdapter) - binding.authDropDownMenu.setText(authentications[0].toString(),false) + binding.authDropDownMenu.setText(authentications[0].toString(), false) } - private fun registerAuthClickListener(){ + private fun registerAuthClickListener() { binding.authDropDownMenu.setOnItemClickListener { parent, view, position, id -> val selectedItem = parent.getItemAtPosition(position).toString() - if(Authentication.valueOf(selectedItem) == Authentication.Anonymous){ + if (Authentication.valueOf(selectedItem) == Authentication.Anonymous) { toggleCredentialsVisibility(View.GONE) - } - else{ + } else { toggleCredentialsVisibility(View.VISIBLE) } } } - private fun toggleCredentialsVisibility(visibility: Int){ + private fun toggleCredentialsVisibility(visibility: Int) { binding.userTf.visibility = visibility binding.passwordTf.visibility = visibility } @@ -102,21 +112,57 @@ class ConnectionDialog(val activity: BaseSimpleActivity, dispatch: (String, Stri private fun dropDownItemSelected() { binding.dropdownMenu.setOnItemClickListener { parent, view, position, id -> val selectedItem = parent.getItemAtPosition(position).toString() + togglePortValue(ConnectionTypes.valueOf(selectedItem), if(binding.dropdownMenuProtocol.value != "") Protocols.valueOf(binding.dropdownMenuProtocol.value) else Protocols.HTTP) if (selectedItem == ConnectionTypes.DAVx5.type) { - binding.dropdownProtocol.visibility = View.GONE + binding.allFieldsExceptConnection.visibility = View.GONE promptUserToSelectStorage() } else if (selectedItem == ConnectionTypes.WebDav.type) { + binding.allFieldsExceptConnection.visibility = View.VISIBLE binding.dropdownProtocol.visibility = View.VISIBLE - } else { + binding.authDropDownLayout.visibility = View.GONE + } + else if(selectedItem == ConnectionTypes.FTP.type || selectedItem == ConnectionTypes.SFTP.type || selectedItem == ConnectionTypes.SMB.type){ + binding.allFieldsExceptConnection.visibility = View.VISIBLE + binding.authDropDownLayout.visibility = View.VISIBLE binding.dropdownProtocol.visibility = View.GONE + binding.certRow.visibility = View.GONE + } + } + } + + private fun dropDownMenuProtocolItemClickListener() { + binding.dropdownMenuProtocol.setOnItemClickListener { parent, view, position, id -> + val selectedItem = parent.getItemAtPosition(position).toString() + val item = Protocols.valueOf(selectedItem) + if (item == Protocols.HTTP) { + binding.certRow.visibility = View.GONE + } else { + binding.certRow.visibility = View.VISIBLE + } + togglePortValue(ConnectionTypes.WebDav, item) + } + } + + private fun togglePortValue(connectionTypes: ConnectionTypes,protocols: Protocols){ + when(connectionTypes){ + ConnectionTypes.SMB -> binding.portEt.setText(DEFAULT_SMB_PORT.toString()) + ConnectionTypes.FTP -> binding.portEt.setText(DEFAULT_FTP_PORT.toString()) + ConnectionTypes.SFTP -> binding.portEt.setText(DEFAULT_SFTP_PORT.toString()) + ConnectionTypes.WebDav -> { + if(protocols == Protocols.HTTP){ + binding.portEt.setText(DEFAULT_WEBDAV_HTTP_PORT.toString()) + } + else{ + binding.portEt.setText(DEFAULT_WEBDAV_HTTPS_PORT.toString()) + } } - Toast.makeText(activity, "Selected: $selectedItem", Toast.LENGTH_SHORT).show() + else -> Unit } } private fun attachCertBtnClickListener() { binding.certAttachBtn.setOnClickListener { - (activity as CloudActivity).openFileLink{ + (activity as CloudActivity).openFileLink { certUri = it binding.certStatusTv.text = it.path } diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt index 22c35d8e9..e52c2df62 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt @@ -20,6 +20,13 @@ const val PORT_FTP = 7850 const val PORT_WEBDAV_MOUNT = 7840 +const val DEFAULT_SMB_PORT = 445 +const val DEFAULT_FTP_PORT = 21 +const val DEFAULT_SFTP_PORT = 22 +const val DEFAULT_WEBDAV_HTTP_PORT = 80 +const val DEFAULT_WEBDAV_HTTPS_PORT = 443 + + // shared preferences const val SHOW_HIDDEN = "show_hidden" const val PRESS_BACK_TWICE = "press_back_twice" diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt index c13327826..87a1b5881 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -20,7 +20,7 @@ object Helpers { fun createUrl(connectionTypes: ConnectionTypes,path: String,server: String,port: Int = 0): String{ if(connectionTypes == ConnectionTypes.SMB){ - return "${connectionTypes.toString().lowercase()}://${server}/${path}" + return "${connectionTypes.toString().lowercase()}://${server}:${port}/${path}" } return "" } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt index 94cfaf358..542f2d630 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt @@ -38,7 +38,7 @@ import javax.net.ssl.SSLContext import javax.net.ssl.X509TrustManager class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { - lateinit var dir: SmbFile + lateinit var smbClient: SmbFile lateinit var sardine: Sardine private val sftpLock = Any() private lateinit var ssh: SSHClient @@ -70,9 +70,9 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { } else if (connection.authentication == Authentication.Anonymous) { authContext = context.withGuestCrendentials() } - val smbUrl = Helpers.createUrl(ConnectionTypes.SMB, connection.sharedPath, connection.host) - dir = SmbFile(smbUrl, authContext) - return dir.exists() + val smbUrl = Helpers.createUrl(ConnectionTypes.SMB, connection.sharedPath, connection.host,connection.port) + smbClient = SmbFile(smbUrl, authContext) + return smbClient.exists() } catch (exp: Exception) { Log.e("Exception", exp.toString()) } @@ -80,11 +80,11 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { } override fun getFilesFromNetworkPath(): Array { - val files = dir.listFiles() + val files = smbClient.listFiles() return files } - override fun getMainSmbFile(): SmbFile = dir + override fun getMainSmbFile(): SmbFile = smbClient override suspend fun connectAndVerifyWebDav( connection: NetworkConnection, @@ -97,9 +97,6 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { } else { createHTTPSSardine(context,connection.host) } - if(connection.authentication == Authentication.Anonymous){ - return sardine.exists(connection.url) - } sardine.setCredentials(connection.username, connection.password) return sardine.exists(connection.url) } catch (exp: Exception) { @@ -109,7 +106,7 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { } override suspend fun listAllFilesOnWebDav(url: String): List { - val resources = sardine.list("http://192.168.18.86:8090/WebDav/") + val resources = sardine.list(url) return resources } @@ -145,7 +142,7 @@ class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { if (!::ssh.isInitialized || !ssh.isConnected || !ssh.isAuthenticated) { ssh = SSHClient() ssh.addHostKeyVerifier(PromiscuousVerifier()) - ssh.connect(connection.host) + ssh.connect(connection.host,connection.port) ssh.authPassword(connection.username, connection.password) sftp = ssh.newSFTPClient() } diff --git a/app/src/main/res/layout/dialog_add_connection.xml b/app/src/main/res/layout/dialog_add_connection.xml index 720821278..1b0dff1b2 100644 --- a/app/src/main/res/layout/dialog_add_connection.xml +++ b/app/src/main/res/layout/dialog_add_connection.xml @@ -1,7 +1,7 @@ + android:layout_marginBottom="@dimen/medium_margin" + android:orientation="vertical"> + android:inputType="none" /> - + android:layout_marginBottom="@dimen/medium_margin" + android:orientation="vertical" + android:visibility="visible"> - + android:hint="Select Protocol" + android:visibility="gone"> - + + - - + android:layout_height="wrap_content" + android:hint="Host"> - + - + - + android:hint="Authentication" + > - + - + - + android:hint="Username"> - + - + - + android:hint="Password"> - + - + - + android:hint="Display Name"> - - + - + + + android:hint="Shared Path"> - - + - + + + android:hint="Port"> - + + + + + android:orientation="horizontal" + android:visibility="gone"> + android:padding="@dimen/medium_margin" + android:src="@drawable/baseline_attach_file_24" /> - - - - From 14a765204b316056bb99b28ffff574535fd623fa Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sun, 17 May 2026 14:01:00 +0500 Subject: [PATCH 20/37] Code refactoring andd also fixed issues for private key usage in sftp --- .../filemanager/activities/CloudActivity.kt | 32 +- .../dependencies/AppComposition.kt | 24 +- .../filemanager/dialogs/ConnectionDialog.kt | 68 ++++- .../entity/NetworkConnectionEntity.kt | 4 +- .../filemanager/enums/Authentication.kt | 4 +- .../factory/NetworkBrowserViewModelFactory.kt | 9 +- .../filemanager/fileSystems/HttpServer.kt | 17 +- .../fossify/filemanager/interfaces/FTPApi.kt | 18 ++ .../NetworkConnectionRepositoryApi.kt | 58 ---- .../fossify/filemanager/interfaces/SFTPApi.kt | 21 ++ .../fossify/filemanager/interfaces/SMBApi.kt | 12 + .../filemanager/interfaces/WebDavApi.kt | 17 ++ .../mapper/NetworkConnectionMapper.kt | 8 +- .../filemanager/models/NetworkConnection.kt | 14 +- .../filemanager/repository/FTPApiImpl.kt | 66 +++++ .../NetworkConnectionRepositoryApiImpl.kt | 276 ------------------ .../filemanager/repository/SFTPApiImpl.kt | 95 ++++++ .../filemanager/repository/SMBApiImpl.kt | 56 ++++ .../filemanager/repository/WebDavApiImpl.kt | 99 +++++++ .../viewmodels/NetworkBrowserViewModel.kt | 77 ++--- .../main/res/layout/dialog_add_connection.xml | 37 ++- 21 files changed, 605 insertions(+), 407 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt delete mode 100644 app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt delete mode 100644 app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt create mode 100644 app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 4bacade5f..42233cec9 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -9,8 +9,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.fossify.commons.enums.ConnectionTypes @@ -43,6 +41,7 @@ class CloudActivity : SimpleActivity() { private val binding by viewBinding(CloudActivityBinding::inflate) private lateinit var viewModel: NetworkBrowserViewModel private var onCertPicked: ((Uri) -> Unit)? = null + private var onPrivateKeyPicked: ((Uri) -> Unit)? = null private lateinit var certificateRepository: CertificateRepository private lateinit var composition: AppComposition @@ -116,23 +115,34 @@ class CloudActivity : SimpleActivity() { } } + private val pickPrivateKey = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { + onPrivateKeyPicked?.invoke(it) + } + } + fun promptUserToSelectStorage() { openDocumentTreeLauncher.launch(null) } - fun openFileLink(dispatch: (Uri) -> Unit) { + fun openFileLinkForCert(dispatch: (Uri) -> Unit) { pickCert.launch("*/*") onCertPicked = dispatch } + fun openFileLinkForPrivateKey(dispatch: (Uri) -> Unit) { + pickPrivateKey.launch("*/*") + onPrivateKeyPicked = dispatch + } + private fun setupToolBar() { setupTopAppBar(binding.cloudAppbar, NavigationIcon.Arrow) } private fun showConnectionDialog() { - ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName, certPath, port, connection, protocol, auth -> - saveNetwork(host, user, password, shared, displayName, certPath, port, connection, protocol, auth) + ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName, certPath,privateKeyText,privateKeyPass, port, connection, protocol, auth -> + saveNetwork(host, user, password, shared, displayName,privateKeyText ,privateKeyPass,certPath, port, connection, protocol, auth) } } @@ -149,6 +159,8 @@ class CloudActivity : SimpleActivity() { password: String, shared: String, displayName: String, + privateKeyText: String, + privateKeyPass: String, certUri: Uri?, port: Int, connectionType: ConnectionTypes, @@ -192,7 +204,9 @@ class CloudActivity : SimpleActivity() { connectionType = connectionType, port = port, displayName = displayName, - authentication = authentication + authentication = authentication, + privateKeyText = privateKeyText, + privateKeyPass = privateKeyPass ) viewModel.connectSFTP(network, true) } else if (connectionType == ConnectionTypes.FTP) { @@ -282,7 +296,7 @@ class CloudActivity : SimpleActivity() { machinePort: Int, protocol: Protocols = Protocols.HTTP ) { - https = HttpServer(port, connection.host, connectionType, composition.networkApiRepository, machinePort, protocol) + https = HttpServer(port, connection.host, connectionType, composition, machinePort, protocol) https?.start() } @@ -387,7 +401,9 @@ class CloudActivity : SimpleActivity() { port = it.item.port, displayName = it.item.displayName, url = "/", - authentication = it.item.authentication + authentication = it.item.authentication, + privateKeyText = it.item.privateKeyText, + privateKeyPass = it.item.privateKeyPass ) ) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt b/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt index 01cc33073..0f3a0135c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt @@ -5,8 +5,11 @@ import androidx.room.Room import org.fossify.filemanager.database.Database import org.fossify.filemanager.factory.NetworkBrowserViewModelFactory import org.fossify.filemanager.repository.CertificateRepositoryImpl -import org.fossify.filemanager.repository.NetworkConnectionRepositoryApiImpl +import org.fossify.filemanager.repository.FTPApiImpl import org.fossify.filemanager.repository.NetworkConnectionRepositoryDbImpl +import org.fossify.filemanager.repository.SFTPApiImpl +import org.fossify.filemanager.repository.SMBApiImpl +import org.fossify.filemanager.repository.WebDavApiImpl class AppComposition (private val context: Context) { @@ -23,15 +26,28 @@ class AppComposition (private val context: Context) { } - val networkApiRepository by lazy { - NetworkConnectionRepositoryApiImpl() + val webDavApiRepository by lazy { + WebDavApiImpl() } + val smbApiRepository by lazy { + SMBApiImpl() + } + + val sftpApiRepository by lazy { + SFTPApiImpl() + } + + val ftpApiRepository by lazy { + FTPApiImpl() + } + + val certificateRepository by lazy { CertificateRepositoryImpl() } fun provideNetworkBrowserViewModelFactory(): NetworkBrowserViewModelFactory{ - return NetworkBrowserViewModelFactory(networkDbRepository,networkApiRepository) + return NetworkBrowserViewModelFactory(networkDbRepository,webDavApiRepository,sftpApiRepository,ftpApiRepository,smbApiRepository) } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 8a3461068..0bcdc02f8 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -1,9 +1,9 @@ package org.fossify.filemanager.dialogs import android.net.Uri +import android.util.Log import android.view.View import android.widget.ArrayAdapter -import android.widget.Toast import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.getAlertDialogBuilder @@ -19,18 +19,22 @@ import org.fossify.filemanager.helpers.DEFAULT_SFTP_PORT import org.fossify.filemanager.helpers.DEFAULT_SMB_PORT import org.fossify.filemanager.helpers.DEFAULT_WEBDAV_HTTPS_PORT import org.fossify.filemanager.helpers.DEFAULT_WEBDAV_HTTP_PORT -import org.fossify.filemanager.helpers.PORT_SMB +import java.io.File class ConnectionDialog( val activity: BaseSimpleActivity, - dispatch: (String, String, String, String, String, Uri?, Int, ConnectionTypes, Protocols?, Authentication) -> Unit + dispatch: (String, String, String, String, String, Uri?, String,String, Int, ConnectionTypes, Protocols?, Authentication) -> Unit ) { private var binding: DialogAddConnectionBinding val items = listOf(ConnectionTypes.DAVx5.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.FTP.type) private var certUri: Uri? = null + private var privateKeyUri: Uri? = null + val protocols = listOf(Protocols.HTTP, Protocols.HTTPS) val authentications = listOf(Authentication.Password, Authentication.Anonymous) + val sftpAuthentications = listOf(Authentication.Password, Authentication.PrivateKey) + init { binding = DialogAddConnectionBinding.inflate(activity.layoutInflater) @@ -43,6 +47,8 @@ class ConnectionDialog( binding.sharedPathEt.value, binding.displayEt.value, certUri, + binding.privateKeyEt.value.trimIndent(), + binding.privateKeyPassEt.value, binding.portEt.value.toIntOrNull() ?: 0, ConnectionTypes.fromType(binding.dropdownMenu.value), binding.dropdownMenuProtocol.text @@ -60,6 +66,7 @@ class ConnectionDialog( initializeDropDownList() registerAuthClickListener() attachCertBtnClickListener() + attachPrivateKeyBtnClickListener() dropDownMenuProtocolItemClickListener() } @@ -93,12 +100,30 @@ class ConnectionDialog( val selectedItem = parent.getItemAtPosition(position).toString() if (Authentication.valueOf(selectedItem) == Authentication.Anonymous) { toggleCredentialsVisibility(View.GONE) - } else { + toggleSFTPAuthVisibility(View.GONE) + } + else if(Authentication.valueOf(selectedItem) == Authentication.PrivateKey && binding.dropdownMenu.value == ConnectionTypes.SFTP.type){ + binding.userTf.visibility = View.VISIBLE + binding.passwordTf.visibility = View.GONE + toggleSFTPAuthVisibility(View.VISIBLE) + } + else if(Authentication.valueOf(selectedItem) == Authentication.Password && binding.dropdownMenu.value == ConnectionTypes.SFTP.type){ + binding.userTf.visibility = View.VISIBLE + binding.passwordTf.visibility = View.VISIBLE + toggleSFTPAuthVisibility(View.GONE) + } + else { toggleCredentialsVisibility(View.VISIBLE) + toggleSFTPAuthVisibility(View.GONE) } } } + private fun toggleSFTPAuthVisibility(visibility: Int) { + binding.privateKeyTf.visibility = visibility + binding.privateKeyPassTf.visibility = visibility + } + private fun toggleCredentialsVisibility(visibility: Int) { binding.userTf.visibility = visibility binding.passwordTf.visibility = visibility @@ -120,12 +145,29 @@ class ConnectionDialog( binding.allFieldsExceptConnection.visibility = View.VISIBLE binding.dropdownProtocol.visibility = View.VISIBLE binding.authDropDownLayout.visibility = View.GONE + toggleSFTPAuthVisibility(View.GONE) + toggleCredentialsVisibility(View.VISIBLE) } - else if(selectedItem == ConnectionTypes.FTP.type || selectedItem == ConnectionTypes.SFTP.type || selectedItem == ConnectionTypes.SMB.type){ + else if(selectedItem == ConnectionTypes.SMB.type){ + binding.dropdownProtocol.visibility = View.GONE + binding.certRow.visibility = View.GONE + binding.allFieldsExceptConnection.visibility = View.VISIBLE + binding.authDropDownLayout.visibility = View.VISIBLE + toggleSFTPAuthVisibility(View.GONE) + binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, authentications)) + } + else if(selectedItem == ConnectionTypes.FTP.type || selectedItem == ConnectionTypes.SFTP.type){ binding.allFieldsExceptConnection.visibility = View.VISIBLE binding.authDropDownLayout.visibility = View.VISIBLE binding.dropdownProtocol.visibility = View.GONE binding.certRow.visibility = View.GONE + if(Authentication.valueOf(binding.authDropDownMenu.value) == Authentication.Password && selectedItem == ConnectionTypes.SFTP.type){ + toggleSFTPAuthVisibility(View.GONE) + } + else{ + toggleSFTPAuthVisibility(View.VISIBLE) + } + binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, sftpAuthentications)) } } } @@ -162,7 +204,7 @@ class ConnectionDialog( private fun attachCertBtnClickListener() { binding.certAttachBtn.setOnClickListener { - (activity as CloudActivity).openFileLink { + (activity as CloudActivity).openFileLinkForCert { certUri = it binding.certStatusTv.text = it.path } @@ -170,4 +212,18 @@ class ConnectionDialog( } } + private fun attachPrivateKeyBtnClickListener() { + binding.privateKeyTf.setEndIconOnClickListener { + (activity as CloudActivity).openFileLinkForPrivateKey{ + privateKeyUri = it + privateKeyUri?.let {path-> + val inputStream = activity.contentResolver.openInputStream(path) + val keyText = inputStream?.bufferedReader().use { it?.readText() } ?: "" + binding.privateKeyEt.setText(keyText) + } + + } + } + } + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt index 0fcb98d4e..d301037fe 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt @@ -15,5 +15,7 @@ data class NetworkConnectionEntity( val connectionType: String, val sharedPath: String, val url: String, - val authentication: String + val authentication: String, + val privateKey: String = "", + val privateKeyPass: String = "" ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/enums/Authentication.kt b/app/src/main/kotlin/org/fossify/filemanager/enums/Authentication.kt index f9d2526ea..9d1c2609d 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/enums/Authentication.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/enums/Authentication.kt @@ -2,5 +2,7 @@ package org.fossify.filemanager.enums enum class Authentication { Password, - Anonymous + Anonymous, + PrivateKey } + diff --git a/app/src/main/kotlin/org/fossify/filemanager/factory/NetworkBrowserViewModelFactory.kt b/app/src/main/kotlin/org/fossify/filemanager/factory/NetworkBrowserViewModelFactory.kt index 4a45bbf43..3969dfdd5 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/factory/NetworkBrowserViewModelFactory.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/factory/NetworkBrowserViewModelFactory.kt @@ -2,12 +2,15 @@ package org.fossify.filemanager.factory import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi +import org.fossify.filemanager.interfaces.FTPApi import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb +import org.fossify.filemanager.interfaces.SFTPApi +import org.fossify.filemanager.interfaces.SMBApi +import org.fossify.filemanager.interfaces.WebDavApi import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel -class NetworkBrowserViewModelFactory(private val networkConnectionRepositoryDb: NetworkConnectionRepositoryDb, private val networkConnectionRepositoryApi: NetworkConnectionRepositoryApi): ViewModelProvider.Factory { +class NetworkBrowserViewModelFactory(private val networkConnectionRepositoryDb: NetworkConnectionRepositoryDb, private val webDavApi: WebDavApi,private val sftpApi: SFTPApi, private val ftpApi: FTPApi,private val smbApi: SMBApi): ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return NetworkBrowserViewModel(networkConnectionRepositoryDb,networkConnectionRepositoryApi) as T + return NetworkBrowserViewModel(networkConnectionRepositoryDb,webDavApi,ftpApi,sftpApi,smbApi) as T } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index f93076bd7..37c635f86 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -5,16 +5,17 @@ import fi.iki.elonen.NanoHTTPD import jcifs.smb.SmbFile import jcifs.smb.SmbFileInputStream import org.fossify.commons.enums.ConnectionTypes +import org.fossify.filemanager.dependencies.AppComposition import org.fossify.filemanager.enums.Protocols +import org.fossify.filemanager.helpers.DEFAULT_SMB_PORT import org.fossify.filemanager.helpers.Helpers -import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi import java.io.BufferedInputStream class HttpServer( private val port: Int, private val serverIp: String, private val connectionTypes: ConnectionTypes, - private val networkConnectionRepository: NetworkConnectionRepositoryApi, + private val composition: AppComposition, private val machinePort: Int, private val protocols: Protocols = Protocols.HTTP ) : @@ -32,16 +33,16 @@ class HttpServer( } else if(connectionTypes.equals(ConnectionTypes.WebDav)) { val url = Helpers.createNanoHttpdUrl(connectionTypes, server = serverIp, path = uri.toString(), port = machinePort, protocols = protocols) - val webDavFile = networkConnectionRepository.listWebDavFileDetail(url) + val webDavFile = composition.webDavApiRepository.listWebDavFileDetail(url) return handleRangeRequestWebDav(rangeHeader, webDavFile?.contentLength!!, uri = uri, webDavFile.contentType,protocols) } else if (connectionTypes.equals(ConnectionTypes.SFTP)) { val url = Helpers.createNanoHttpdUrl(connectionTypes, server = serverIp, path = "", port = machinePort) - val sftFile = networkConnectionRepository.listSFTPFileDetails(uri) + val sftFile = composition.sftpApiRepository.listSFTPFileDetails(uri) return handleRangeRequestSFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } val url = Helpers.createNanoHttpdUrl(connectionTypes, server = serverIp, path = "", port = machinePort) - val sftFile = networkConnectionRepository.getFTPFileDetail(uri) + val sftFile = composition.ftpApiRepository.getFTPFileDetail(uri) return handleRangeRequestFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } @@ -110,7 +111,7 @@ class HttpServer( val contentLength = end - start + 1 - val inputStream = networkConnectionRepository.getWebDavFileInputStream(url = url,start,end) + val inputStream = composition.webDavApiRepository.getWebDavFileInputStream(url = url,start,end) return newFixedLengthResponse( Response.Status.PARTIAL_CONTENT, @@ -142,7 +143,7 @@ class HttpServer( val contentLength = end - start + 1 - val inputStream = networkConnectionRepository.getSFTPFileInputStream(uri,start) + val inputStream = composition.sftpApiRepository.getSFTPFileInputStream(uri,start) val bufferedStream = BufferedInputStream(inputStream, 1024 * 1024) return newFixedLengthResponse( Response.Status.PARTIAL_CONTENT, @@ -174,7 +175,7 @@ class HttpServer( val contentLength = end - start + 1 - val inputStream = networkConnectionRepository.getFTPFileInputStream(uri,start) + val inputStream = composition.ftpApiRepository.getFTPFileInputStream(uri,start) val bufferedStream = BufferedInputStream(inputStream, 1024 * 1024) return newFixedLengthResponse( Response.Status.PARTIAL_CONTENT, diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt new file mode 100644 index 000000000..e3116619c --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt @@ -0,0 +1,18 @@ +package org.fossify.filemanager.interfaces + +import org.apache.commons.net.ftp.FTPClient +import org.apache.commons.net.ftp.FTPFile +import org.fossify.filemanager.models.NetworkConnection +import java.io.InputStream + +interface FTPApi { + suspend fun connectToFTP(connection: NetworkConnection): Boolean + + suspend fun listAllFTPFiles(path: String): List + + fun getFTPFileDetail(path: String): FTPFile? + + fun getFTPFileInputStream(path: String,start: Long): InputStream + + fun getFTPConn(): FTPClient +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt deleted file mode 100644 index c0d317ad7..000000000 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryApi.kt +++ /dev/null @@ -1,58 +0,0 @@ -package org.fossify.filemanager.interfaces - - -import android.content.Context -import com.thegrizzlylabs.sardineandroid.DavResource -import jcifs.smb.SmbFile -import net.schmizz.sshj.sftp.FileAttributes -import net.schmizz.sshj.sftp.RemoteResourceInfo -import net.schmizz.sshj.sftp.SFTPClient -import org.apache.commons.net.ftp.FTPClient -import org.apache.commons.net.ftp.FTPFile -import org.fossify.filemanager.enums.Protocols -import org.fossify.filemanager.models.NetworkConnection -import java.io.InputStream -import java.security.cert.X509Certificate - -interface NetworkConnectionRepositoryApi { - suspend fun verifyConnection(connection: NetworkConnection): Boolean - - fun getFilesFromNetworkPath(): Array - - fun getMainSmbFile(): SmbFile - - ///WebDav - - suspend fun connectAndVerifyWebDav(connection: NetworkConnection,protocols: Protocols, context: Context): Boolean - - suspend fun listAllFilesOnWebDav(url: String): List - - fun getWebDavFileInputStream(url: String, start: Long, end: Long): InputStream - - fun listWebDavFileDetail(url: String): DavResource? - - fun loadCertificate(stream: InputStream):Result - //SFTP - - suspend fun connectToSftp(connection: NetworkConnection): Boolean - - suspend fun listAllFilesSFTPRoot(path: String): List - - suspend fun listAllFilesSFTPPath(path: String):List - - fun listSFTPFileDetails(path: String): FileAttributes? - - fun getSFTPFileInputStream(url: String, startByte: Long): InputStream - - fun getSFTPConn(): SFTPClient - - suspend fun connectToFTP(connection: NetworkConnection): Boolean - - suspend fun listAllFTPFiles(path: String): List - - fun getFTPFileDetail(path: String): FTPFile? - - fun getFTPFileInputStream(path: String,start: Long): InputStream - - fun getFTPConn(): FTPClient -} diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt new file mode 100644 index 000000000..86735513b --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt @@ -0,0 +1,21 @@ +package org.fossify.filemanager.interfaces + +import net.schmizz.sshj.sftp.FileAttributes +import net.schmizz.sshj.sftp.RemoteResourceInfo +import net.schmizz.sshj.sftp.SFTPClient +import org.fossify.filemanager.models.NetworkConnection +import java.io.InputStream + +interface SFTPApi { + suspend fun connectToSftp(connection: NetworkConnection): Boolean + + suspend fun listAllFilesSFTPRoot(path: String): List + + suspend fun listAllFilesSFTPPath(path: String):List + + fun listSFTPFileDetails(path: String): FileAttributes? + + fun getSFTPFileInputStream(url: String, startByte: Long): InputStream + + fun getSFTPConn(): SFTPClient +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt new file mode 100644 index 000000000..9e44aab5d --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt @@ -0,0 +1,12 @@ +package org.fossify.filemanager.interfaces + +import jcifs.smb.SmbFile +import org.fossify.filemanager.models.NetworkConnection + +interface SMBApi { + suspend fun verifyConnection(connection: NetworkConnection): Boolean + + fun getFilesFromNetworkPath(): Array + + fun getMainSmbFile(): SmbFile +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt new file mode 100644 index 000000000..7040d9c7b --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt @@ -0,0 +1,17 @@ +package org.fossify.filemanager.interfaces + +import android.content.Context +import com.thegrizzlylabs.sardineandroid.DavResource +import org.fossify.filemanager.enums.Protocols +import org.fossify.filemanager.models.NetworkConnection +import java.io.InputStream + +interface WebDavApi { + suspend fun connectAndVerifyWebDav(connection: NetworkConnection,protocols: Protocols, context: Context): Boolean + + suspend fun listAllFilesOnWebDav(url: String): List + + fun getWebDavFileInputStream(url: String, start: Long, end: Long): InputStream + + fun listWebDavFileDetail(url: String): DavResource? +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index 023f92422..51340952b 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -21,7 +21,9 @@ fun NetworkConnectionEntity.toDomain(): NetworkConnection { connectionType = ConnectionTypes.valueOf(connectionType), sharedPath = sharedPath, url = url, - authentication = Authentication.valueOf(authentication) + authentication = Authentication.valueOf(authentication), + privateKeyText = privateKey, + privateKeyPass = privateKeyPass ) } @@ -35,7 +37,9 @@ fun NetworkConnection.toEntity(): NetworkConnectionEntity { connectionType = connectionType.toString(), sharedPath = sharedPath, url = url, - authentication = authentication.toString() + authentication = authentication.toString(), + privateKey = privateKeyText, + privateKeyPass = privateKeyPass ) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt index 44c288418..2786fef6a 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt @@ -1,12 +1,20 @@ package org.fossify.filemanager.models import org.fossify.commons.enums.ConnectionTypes -import org.fossify.filemanager.entity.NetworkConnectionEntity import org.fossify.filemanager.enums.Authentication data class NetworkConnection( - val host: String = "", val port: Int = 445, val username: String? = "", - val password: String? = "", val displayName: String = "",val connectionType: ConnectionTypes, val sharedPath: String = "", val url: String = "",val authentication: Authentication = Authentication.Password + val host: String = "", + val port: Int = 445, + val username: String? = "", + val password: String? = "", + val displayName: String = "", + val connectionType: ConnectionTypes, + val sharedPath: String = "", + val url: String = "", + val privateKeyText: String = "", + val privateKeyPass: String = "", + val authentication: Authentication = Authentication.Password ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt new file mode 100644 index 000000000..350fad0dd --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt @@ -0,0 +1,66 @@ +package org.fossify.filemanager.repository + +import org.apache.commons.net.ftp.FTP +import org.apache.commons.net.ftp.FTPClient +import org.apache.commons.net.ftp.FTPCmd +import org.apache.commons.net.ftp.FTPFile +import org.fossify.filemanager.interfaces.FTPApi +import org.fossify.filemanager.models.NetworkConnection +import java.io.File +import java.io.InputStream + +class FTPApiImpl: FTPApi { + private lateinit var currentStream: InputStream + private lateinit var ftp: FTPClient + private lateinit var ftpStream: FTPClient + override suspend fun connectToFTP(connection: NetworkConnection): Boolean { + try { + ftp = FTPClient() + ftpStream = FTPClient() + ftp.connect(connection.host, connection.port) + ftpStream.connect(connection.host, connection.port) + + val loginSuccess = ftp.login(connection.username, connection.password) + ftpStream.login(connection.username,connection.password) + + if (!loginSuccess) { + return false + } + ftp.enterLocalPassiveMode() + ftpStream.enterLocalPassiveMode() + return true + } catch (exp: Exception) { + return false + } + } + + override suspend fun listAllFTPFiles(path: String): List { + ftp.changeWorkingDirectory(path) + val files: Array = ftp.listFiles() + return files.toList() + } + + override fun getFTPFileDetail(path: String): FTPFile? { + val myPath = path.replace("//", "/") + if (ftp.hasFeature(FTPCmd.MLST)) { + val file = ftp.mlistFile(myPath) + return file + } + val mP = File(myPath) + val files = ftp.listFiles(mP.parent).firstOrNull { it != null && it.name == mP.name } + return files + } + + override fun getFTPFileInputStream(path: String, start: Long): InputStream { + if (::currentStream.isInitialized) + currentStream.close() + ftpStream.completePendingCommand() + ftpStream.setFileType(FTP.BINARY_FILE_TYPE) + ftpStream.restartOffset = start + currentStream = ftpStream.retrieveFileStream(path) + return currentStream + } + + override fun getFTPConn(): FTPClient = ftp + +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt deleted file mode 100644 index 542f2d630..000000000 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryApiImpl.kt +++ /dev/null @@ -1,276 +0,0 @@ -package org.fossify.filemanager.repository - -import android.annotation.SuppressLint -import android.content.Context -import android.util.Log -import com.thegrizzlylabs.sardineandroid.DavResource -import com.thegrizzlylabs.sardineandroid.Sardine -import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine -import jcifs.CIFSContext -import jcifs.config.PropertyConfiguration -import jcifs.context.BaseContext -import jcifs.smb.NtlmPasswordAuthenticator -import jcifs.smb.SmbFile -import net.schmizz.sshj.SSHClient -import net.schmizz.sshj.sftp.FileAttributes -import net.schmizz.sshj.sftp.SFTPClient -import net.schmizz.sshj.transport.verification.PromiscuousVerifier -import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi -import org.fossify.filemanager.models.NetworkConnection -import java.io.InputStream -import java.util.Properties -import net.schmizz.sshj.sftp.RemoteResourceInfo -import okhttp3.OkHttpClient -import org.apache.commons.net.ftp.FTP -import org.apache.commons.net.ftp.FTPClient -import org.apache.commons.net.ftp.FTPCmd -import org.apache.commons.net.ftp.FTPFile -import org.fossify.commons.enums.ConnectionTypes -import org.fossify.filemanager.enums.Authentication -import org.fossify.filemanager.enums.Protocols -import org.fossify.filemanager.helpers.Helpers -import org.fossify.filemanager.keyStores.CertificateStore -import java.io.File -import java.security.cert.CertificateException -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import javax.net.ssl.SSLContext -import javax.net.ssl.X509TrustManager - -class NetworkConnectionRepositoryApiImpl : NetworkConnectionRepositoryApi { - lateinit var smbClient: SmbFile - lateinit var sardine: Sardine - private val sftpLock = Any() - private lateinit var ssh: SSHClient - private lateinit var sftp: SFTPClient - private lateinit var currentStream: InputStream - private lateinit var ftp: FTPClient - private lateinit var ftpStream: FTPClient - - private val defaultProperties: Properties = - Properties().apply { - setProperty("jcifs.resolveOrder", "BCAST") - setProperty("jcifs.smb.client.responseTimeout", "30000") - setProperty("jcifs.netbios.retryTimeout", "5000") - setProperty("jcifs.netbios.cachePolicy", "-1") - } - - override suspend fun verifyConnection(connection: NetworkConnection): Boolean { - try { - val p = Properties(defaultProperties) - val context: CIFSContext = BaseContext(PropertyConfiguration(p)) - var authContext: CIFSContext? = null - if (connection.authentication == Authentication.Password) { - val auth = NtlmPasswordAuthenticator( - "", - connection.username, - connection.password - ) - authContext = context.withCredentials(auth) - } else if (connection.authentication == Authentication.Anonymous) { - authContext = context.withGuestCrendentials() - } - val smbUrl = Helpers.createUrl(ConnectionTypes.SMB, connection.sharedPath, connection.host,connection.port) - smbClient = SmbFile(smbUrl, authContext) - return smbClient.exists() - } catch (exp: Exception) { - Log.e("Exception", exp.toString()) - } - return false - } - - override fun getFilesFromNetworkPath(): Array { - val files = smbClient.listFiles() - return files - } - - override fun getMainSmbFile(): SmbFile = smbClient - - override suspend fun connectAndVerifyWebDav( - connection: NetworkConnection, - protocols: Protocols, - context: Context - ): Boolean { - try { - sardine = if (protocols == Protocols.HTTP) { - OkHttpSardine() - } else { - createHTTPSSardine(context,connection.host) - } - sardine.setCredentials(connection.username, connection.password) - return sardine.exists(connection.url) - } catch (exp: Exception) { - Log.d("WebDav", exp.toString()) - return false - } - } - - override suspend fun listAllFilesOnWebDav(url: String): List { - val resources = sardine.list(url) - return resources - } - - override fun getWebDavFileInputStream(url: String, start: Long, end: Long): InputStream { - val rangeHeader = "bytes=$start-$end" - val headers = mapOf("Range" to rangeHeader) - return sardine.get(url, headers) - } - - override fun listWebDavFileDetail(url: String): DavResource? { - val resources = sardine.list(url) - - if (resources.isNotEmpty()) { - return resources[0] - } - return null - } - - override fun loadCertificate(stream: InputStream): Result { - return try { - val cf = CertificateFactory.getInstance("X.509") - val cert = cf.generateCertificate(stream) as X509Certificate - cert.checkValidity() - Result.success(cert) - } catch (exp: Exception) { - Result.failure(Exception(exp.message)) - } - } - - - override suspend fun connectToSftp(connection: NetworkConnection): Boolean { - try { - if (!::ssh.isInitialized || !ssh.isConnected || !ssh.isAuthenticated) { - ssh = SSHClient() - ssh.addHostKeyVerifier(PromiscuousVerifier()) - ssh.connect(connection.host,connection.port) - ssh.authPassword(connection.username, connection.password) - sftp = ssh.newSFTPClient() - } - return true - } catch (e: Exception) { - e.printStackTrace() - return false - } - } - - override suspend fun listAllFilesSFTPRoot(path: String): List { - val files = sftp.ls(path) - return files - } - - override suspend fun listAllFilesSFTPPath(path: String): List { - val files = sftp.ls(path) - return files - } - - override fun listSFTPFileDetails(path: String): FileAttributes? { - synchronized(sftpLock) { - return try { - val myPath = path.replace("//", "/") - sftp.stat(myPath) - } catch (e: Exception) { - Log.e("SFTP", "Stat failed: ${e.message}") - null - } - } - } - - override fun getSFTPFileInputStream(url: String, startByte: Long): InputStream { - val myPath = url.replace("//", "/") - val remoteFile = sftp.open(myPath) - val inputStream = remoteFile.RemoteFileInputStream(startByte) - return inputStream - } - - override fun getSFTPConn() = sftp - - override suspend fun connectToFTP(connection: NetworkConnection): Boolean { - try { - ftp = FTPClient() - ftpStream = FTPClient() - ftp.connect(connection.host, connection.port) - ftpStream.connect(connection.host, connection.port) - - val loginSuccess = ftp.login(connection.username, connection.password) - ftpStream.login(connection.username,connection.password) - - if (!loginSuccess) { - return false - } - ftp.enterLocalPassiveMode() - ftpStream.enterLocalPassiveMode() - return true - } catch (exp: Exception) { - return false - } - } - - override suspend fun listAllFTPFiles(path: String): List { - ftp.changeWorkingDirectory(path) - val files: Array = ftp.listFiles() - return files.toList() - } - - override fun getFTPFileDetail(path: String): FTPFile? { - val myPath = path.replace("//", "/") - if (ftp.hasFeature(FTPCmd.MLST)) { - val file = ftp.mlistFile(myPath) - return file - } - val mP = File(myPath) - val files = ftp.listFiles(mP.parent).firstOrNull { it != null && it.name == mP.name } - return files - } - - override fun getFTPFileInputStream(path: String, start: Long): InputStream { - if (::currentStream.isInitialized) - currentStream.close() - ftpStream.completePendingCommand() - ftpStream.setFileType(FTP.BINARY_FILE_TYPE) - ftpStream.restartOffset = start - currentStream = ftpStream.retrieveFileStream(path) - return currentStream - } - - override fun getFTPConn(): FTPClient = ftp - - private fun createHTTPSSardine(context: Context, host: String): Sardine { - return buildSardineWithUserCert(context, host) - } - - private fun buildSardineWithUserCert( - context: Context, - host: String - ): Sardine { - val cert = CertificateStore.loadCert(context, host) - - val trustManager = @SuppressLint("CustomX509TrustManager") - object : X509TrustManager { - override fun getAcceptedIssuers(): Array = arrayOf(cert) - - @SuppressLint("TrustAllX509TrustManager") - override fun checkClientTrusted(chain: Array, authType: String) { - } - - override fun checkServerTrusted(chain: Array, authType: String) { - if (!chain[0].encoded.contentEquals(cert.encoded)) { - throw CertificateException("Untrusted certificate") - } - } - } - - val sslContext = SSLContext.getInstance("TLS").apply { - init(null, arrayOf(trustManager), null) - } - - val okHttpClient = OkHttpClient.Builder() - .sslSocketFactory(sslContext.socketFactory, trustManager) - .hostnameVerifier { hostname, _ -> - hostname == host - } - .build() - - return OkHttpSardine(okHttpClient) - } - -} diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt new file mode 100644 index 000000000..c4103358d --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt @@ -0,0 +1,95 @@ +package org.fossify.filemanager.repository + +import android.util.Log +import net.schmizz.sshj.DefaultConfig +import net.schmizz.sshj.SSHClient +import net.schmizz.sshj.common.Factory +import net.schmizz.sshj.sftp.FileAttributes +import net.schmizz.sshj.sftp.RemoteResourceInfo +import net.schmizz.sshj.sftp.SFTPClient +import net.schmizz.sshj.transport.verification.PromiscuousVerifier +import net.schmizz.sshj.userauth.keyprovider.KeyProvider +import net.schmizz.sshj.userauth.keyprovider.KeyProviderUtil +import net.schmizz.sshj.userauth.method.AuthPublickey +import net.schmizz.sshj.userauth.password.PasswordUtils +import org.fossify.filemanager.enums.Authentication +import org.fossify.filemanager.interfaces.SFTPApi +import org.fossify.filemanager.models.NetworkConnection +import java.io.IOException +import java.io.InputStream + +class SFTPApiImpl: SFTPApi { + private lateinit var ssh: SSHClient + private lateinit var sftp: SFTPClient + private val sftpLock = Any() + + override suspend fun connectToSftp(connection: NetworkConnection): Boolean { + try { + if (!::ssh.isInitialized || !ssh.isConnected || !ssh.isAuthenticated) { + ssh = SSHClient() + ssh.addHostKeyVerifier(PromiscuousVerifier()) + ssh.connect(connection.host,connection.port) + if(connection.authentication == Authentication.PrivateKey){ + ssh.auth(connection.username, AuthPublickey(createKeyProvider(connection.privateKeyText,connection.privateKeyPass.takeIf { it.isNotBlank() }))) + } + else{ + ssh.authPassword(connection.username, connection.password) + } + sftp = ssh.newSFTPClient() + } + return true + } catch (e: Exception) { + Log.e("SFTP", "Connect failed: ${e.message}") + e.printStackTrace() + return false + } + } + + override suspend fun listAllFilesSFTPRoot(path: String): List { + val files = sftp.ls(path) + return files + } + + override suspend fun listAllFilesSFTPPath(path: String): List { + val files = sftp.ls(path) + return files + } + + override fun listSFTPFileDetails(path: String): FileAttributes? { + synchronized(sftpLock) { + return try { + val myPath = path.replace("//", "/") + sftp.stat(myPath) + } catch (e: Exception) { + Log.e("SFTP", "Stat failed: ${e.message}") + null + } + } + } + + override fun getSFTPFileInputStream(url: String, startByte: Long): InputStream { + val myPath = url.replace("//", "/") + val remoteFile = sftp.open(myPath) + val inputStream = remoteFile.RemoteFileInputStream(startByte) + return inputStream + } + + override fun getSFTPConn() = sftp + + private val KEY_PROVIDER_FACTORIES = DefaultConfig().fileKeyProviderFactories + + + private fun createKeyProvider( + privateKey: String, + privateKeyPassword: String? + ): KeyProvider { + val format = KeyProviderUtil.detectKeyFileFormat(privateKey, false) + val keyProvider = Factory.Named.Util.create(KEY_PROVIDER_FACTORIES, format.toString()) + ?: throw IOException("No key provider factory found for $format") + keyProvider.init( + privateKey, null, + privateKeyPassword?.let { PasswordUtils.createOneOff(it.toCharArray()) } + ) + return keyProvider + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt new file mode 100644 index 000000000..279c059f0 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt @@ -0,0 +1,56 @@ +package org.fossify.filemanager.repository + +import android.util.Log +import jcifs.CIFSContext +import jcifs.config.PropertyConfiguration +import jcifs.context.BaseContext +import jcifs.smb.NtlmPasswordAuthenticator +import jcifs.smb.SmbFile +import org.fossify.commons.enums.ConnectionTypes +import org.fossify.filemanager.enums.Authentication +import org.fossify.filemanager.helpers.Helpers +import org.fossify.filemanager.interfaces.SMBApi +import org.fossify.filemanager.models.NetworkConnection +import java.util.Properties + +class SMBApiImpl : SMBApi { + lateinit var smbClient: SmbFile + + private val defaultProperties: Properties = + Properties().apply { + setProperty("jcifs.resolveOrder", "BCAST") + setProperty("jcifs.smb.client.responseTimeout", "30000") + setProperty("jcifs.netbios.retryTimeout", "5000") + setProperty("jcifs.netbios.cachePolicy", "-1") + } + override suspend fun verifyConnection(connection: NetworkConnection): Boolean { + try { + val p = Properties(defaultProperties) + val context: CIFSContext = BaseContext(PropertyConfiguration(p)) + var authContext: CIFSContext? = null + if (connection.authentication == Authentication.Password) { + val auth = NtlmPasswordAuthenticator( + "", + connection.username, + connection.password + ) + authContext = context.withCredentials(auth) + } else if (connection.authentication == Authentication.Anonymous) { + authContext = context.withGuestCrendentials() + } + val smbUrl = Helpers.createUrl(ConnectionTypes.SMB, connection.sharedPath, connection.host, connection.port) + smbClient = SmbFile(smbUrl, authContext) + return smbClient.exists() + } catch (exp: Exception) { + Log.e("Exception", exp.toString()) + } + return false + } + + override fun getFilesFromNetworkPath(): Array { + val files = smbClient.listFiles() + return files + } + + override fun getMainSmbFile(): SmbFile = smbClient +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt new file mode 100644 index 000000000..cbfb8e786 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt @@ -0,0 +1,99 @@ +package org.fossify.filemanager.repository + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import com.thegrizzlylabs.sardineandroid.DavResource +import com.thegrizzlylabs.sardineandroid.Sardine +import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine +import okhttp3.OkHttpClient +import org.fossify.filemanager.enums.Protocols +import org.fossify.filemanager.interfaces.WebDavApi +import org.fossify.filemanager.keyStores.CertificateStore +import org.fossify.filemanager.models.NetworkConnection +import java.io.InputStream +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.SSLContext +import javax.net.ssl.X509TrustManager + +class WebDavApiImpl: WebDavApi { + lateinit var sardine: Sardine + override suspend fun connectAndVerifyWebDav( + connection: NetworkConnection, + protocols: Protocols, + context: Context + ): Boolean { + try { + sardine = if (protocols == Protocols.HTTP) { + OkHttpSardine() + } else { + createHTTPSSardine(context,connection.host) + } + sardine.setCredentials(connection.username, connection.password) + return sardine.exists(connection.url) + } catch (exp: Exception) { + Log.d("WebDav", exp.toString()) + return false + } + } + + override suspend fun listAllFilesOnWebDav(url: String): List { + val resources = sardine.list(url) + return resources + } + + override fun getWebDavFileInputStream(url: String, start: Long, end: Long): InputStream { + val rangeHeader = "bytes=$start-$end" + val headers = mapOf("Range" to rangeHeader) + return sardine.get(url, headers) + } + + override fun listWebDavFileDetail(url: String): DavResource? { + val resources = sardine.list(url) + + if (resources.isNotEmpty()) { + return resources[0] + } + return null + } + + private fun createHTTPSSardine(context: Context, host: String): Sardine { + return buildSardineWithUserCert(context, host) + } + + private fun buildSardineWithUserCert( + context: Context, + host: String + ): Sardine { + val cert = CertificateStore.loadCert(context, host) + + val trustManager = @SuppressLint("CustomX509TrustManager") + object : X509TrustManager { + override fun getAcceptedIssuers(): Array = arrayOf(cert) + + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted(chain: Array, authType: String) { + } + + override fun checkServerTrusted(chain: Array, authType: String) { + if (!chain[0].encoded.contentEquals(cert.encoded)) { + throw CertificateException("Untrusted certificate") + } + } + } + + val sslContext = SSLContext.getInstance("TLS").apply { + init(null, arrayOf(trustManager), null) + } + + val okHttpClient = OkHttpClient.Builder() + .sslSocketFactory(sslContext.socketFactory, trustManager) + .hostnameVerifier { hostname, _ -> + hostname == host + } + .build() + + return OkHttpSardine(okHttpClient) + } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 0e81df20a..d5a07c4ed 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -20,15 +20,21 @@ import net.schmizz.sshj.sftp.RemoteResourceInfo import net.schmizz.sshj.sftp.SFTPClient import org.apache.commons.net.ftp.FTPFile import org.fossify.filemanager.enums.Protocols -import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryApi +import org.fossify.filemanager.interfaces.FTPApi import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb +import org.fossify.filemanager.interfaces.SFTPApi +import org.fossify.filemanager.interfaces.SMBApi +import org.fossify.filemanager.interfaces.WebDavApi import org.fossify.filemanager.models.ConnectionResult import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream class NetworkBrowserViewModel( private val networkConnectionRepository: NetworkConnectionRepositoryDb, - private val networkConnectionRepositoryApi: NetworkConnectionRepositoryApi + private val webDavApi: WebDavApi, + private val ftpApi: FTPApi, + private val sftpApi: SFTPApi, + private val smbApi: SMBApi ) : ViewModel() { val savedNetworks = MutableStateFlow>(emptyList()) @@ -63,87 +69,86 @@ class NetworkBrowserViewModel( fun verifyNetwork(connection: NetworkConnection, saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { - val value = networkConnectionRepositoryApi.verifyConnection(connection) + val value = smbApi.verifyConnection(connection) verifyNetwork.emit(ConnectionResult(connection, value, saveInfo = saveInfo)) } } fun getFilesFromNetworkPath(): Array { - return networkConnectionRepositoryApi.getFilesFromNetworkPath() + return smbApi.getFilesFromNetworkPath() } - fun getMainSmb(): SmbFile = networkConnectionRepositoryApi.getMainSmbFile() + fun getMainSmb(): SmbFile = smbApi.getMainSmbFile() - fun getSFTPConn(): SFTPClient = networkConnectionRepositoryApi.getSFTPConn() + fun getSFTPConn(): SFTPClient = sftpApi.getSFTPConn() fun connectAndAuthenticateWebDav(connection: NetworkConnection, protocol: Protocols, saveInfo: Boolean, context: Context) { viewModelScope.launch(Dispatchers.IO) { - val result = networkConnectionRepositoryApi.connectAndVerifyWebDav(connection, protocol, context) + val result = webDavApi.connectAndVerifyWebDav(connection, protocol, context) verifyWebDav.emit(ConnectionResult(connection, result, saveInfo)) } } fun listWebDavFiles(url: String) { viewModelScope.launch(Dispatchers.IO) { - webDavFiles.emit(networkConnectionRepositoryApi.listAllFilesOnWebDav(url)) + webDavFiles.emit(webDavApi.listAllFilesOnWebDav(url)) } } - fun listWebDavFileStream(url: String, start: Long, end: Long): InputStream { - return networkConnectionRepositoryApi.getWebDavFileInputStream(url, start, end) - } +// fun listWebDavFileStream(url: String, start: Long, end: Long): InputStream { +// return webDavApi.getWebDavFileInputStream(url, start, end) +// } - fun listWebDavFileDetail(url: String): DavResource? { - return networkConnectionRepositoryApi.listWebDavFileDetail(url) - } +// fun listWebDavFileDetail(url: String): DavResource? { +// return webDavApi.listWebDavFileDetail(url) +// } fun connectSFTP(connection: NetworkConnection, saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { - val res = networkConnectionRepositoryApi.connectToSftp(connection) + val res = sftpApi.connectToSftp(connection) verifySFTP.emit(ConnectionResult(connection, res, saveInfo)) } } fun listAllFilesSFTPRoot(path: String) { viewModelScope.launch(Dispatchers.IO) { - val res = networkConnectionRepositoryApi.listAllFilesSFTPRoot(path) + val res = sftpApi.listAllFilesSFTPRoot(path) sftpFiles.emit(res) } } - fun listAllFilesSFTPPath(path: String) { - viewModelScope.launch(Dispatchers.IO) { - val res = networkConnectionRepositoryApi.listAllFilesSFTPRoot(path) - sftpFiles.emit(res) - } - } - - fun listSFTPFileDetails(path: String): FileAttributes? { - return networkConnectionRepositoryApi.listSFTPFileDetails(path) - } - - fun getSFTPFileStream(path: String, startByte: Long): InputStream { - return networkConnectionRepositoryApi.getSFTPFileInputStream(url = path, startByte) - } +// fun listAllFilesSFTPPath(path: String) { +// viewModelScope.launch(Dispatchers.IO) { +// val res = sftpApi.listAllFilesSFTPRoot(path) +// sftpFiles.emit(res) +// } +// } +// +// fun listSFTPFileDetails(path: String): FileAttributes? { +// return sftpApi.listSFTPFileDetails(path) +// } +// +// fun getSFTPFileStream(path: String, startByte: Long): InputStream { +// return sftpApi.getSFTPFileInputStream(url = path, startByte) +// } fun connectFTP(connection: NetworkConnection, saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { - val res = networkConnectionRepositoryApi.connectToFTP(connection) + val res = ftpApi.connectToFTP(connection) verifyFTP.emit(ConnectionResult(connection, res, saveInfo)) - } } - fun getFTP() = networkConnectionRepositoryApi.getFTPConn() + fun getFTP() = ftpApi.getFTPConn() fun listAllFTPFiles(path: String) { viewModelScope.launch(Dispatchers.IO) { - val res = networkConnectionRepositoryApi.listAllFTPFiles(path) + val res = ftpApi.listAllFTPFiles(path) ftpFiles.emit(res) } } - fun getFTPFileDetail(path: String) = networkConnectionRepositoryApi.getFTPFileDetail(path) + fun getFTPFileDetail(path: String) = ftpApi.getFTPFileDetail(path) - fun getFTPFileStream(path: String, start: Long) = networkConnectionRepositoryApi.getFTPFileInputStream(path, start) + fun getFTPFileStream(path: String, start: Long) = ftpApi.getFTPFileInputStream(path, start) } diff --git a/app/src/main/res/layout/dialog_add_connection.xml b/app/src/main/res/layout/dialog_add_connection.xml index 1b0dff1b2..a16628174 100644 --- a/app/src/main/res/layout/dialog_add_connection.xml +++ b/app/src/main/res/layout/dialog_add_connection.xml @@ -1,9 +1,13 @@ - + + + + + + + + + + + + + + From 2d22f28a4486dd20d7fee5b94227334f0482754d Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sun, 17 May 2026 23:00:07 +0500 Subject: [PATCH 21/37] Exception handling and code refactoring --- .../filemanager/activities/CloudActivity.kt | 29 +- .../filemanager/fileSystems/HttpServer.kt | 263 +++++++++--------- .../filemanager/fragments/ItemsFragment.kt | 212 ++++++++------ .../fossify/filemanager/interfaces/FTPApi.kt | 9 +- .../fossify/filemanager/interfaces/SFTPApi.kt | 11 +- .../fossify/filemanager/interfaces/SMBApi.kt | 5 +- .../filemanager/interfaces/WebDavApi.kt | 9 +- .../fossify/filemanager/models/ApiResponse.kt | 3 + .../filemanager/models/ConnectionResult.kt | 2 +- .../filemanager/repository/FTPApiImpl.kt | 67 +++-- .../filemanager/repository/SFTPApiImpl.kt | 58 ++-- .../filemanager/repository/SMBApiImpl.kt | 21 +- .../filemanager/repository/WebDavApiImpl.kt | 50 ++-- .../viewmodels/NetworkBrowserViewModel.kt | 17 +- 14 files changed, 430 insertions(+), 326 deletions(-) create mode 100644 app/src/main/kotlin/org/fossify/filemanager/models/ApiResponse.kt diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 42233cec9..a6aa01280 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -3,6 +3,7 @@ package org.fossify.filemanager.activities import android.content.Intent import android.net.Uri import android.os.Bundle +import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.Lifecycle @@ -27,6 +28,7 @@ import org.fossify.filemanager.models.NetworkConnection import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel import java.security.Security import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.fossify.commons.extensions.toast import org.fossify.filemanager.dependencies.AppComposition import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.enums.Protocols @@ -141,8 +143,8 @@ class CloudActivity : SimpleActivity() { } private fun showConnectionDialog() { - ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName, certPath,privateKeyText,privateKeyPass, port, connection, protocol, auth -> - saveNetwork(host, user, password, shared, displayName,privateKeyText ,privateKeyPass,certPath, port, connection, protocol, auth) + ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName, certPath, privateKeyText, privateKeyPass, port, connection, protocol, auth -> + saveNetwork(host, user, password, shared, displayName, privateKeyText, privateKeyPass, certPath, port, connection, protocol, auth) } } @@ -296,7 +298,11 @@ class CloudActivity : SimpleActivity() { machinePort: Int, protocol: Protocols = Protocols.HTTP ) { - https = HttpServer(port, connection.host, connectionType, composition, machinePort, protocol) + https = HttpServer(port, connection.host, connectionType, composition, machinePort, protocol) { + it?.message?.let { exp -> + toast(exp, Toast.LENGTH_LONG) + } + } https?.start() } @@ -356,8 +362,11 @@ class CloudActivity : SimpleActivity() { ) ) } + } else { + it.exception?.let { exception -> + toast(exception.message.toString()) + } } - } } @@ -381,6 +390,10 @@ class CloudActivity : SimpleActivity() { ) ) } + } else { + it.exception?.let { exception -> + toast(exception.message.toString()) + } } } } @@ -407,6 +420,10 @@ class CloudActivity : SimpleActivity() { ) ) } + } else { + it.exception?.let { exception -> + toast(exception.message.toString()) + } } } } @@ -431,6 +448,10 @@ class CloudActivity : SimpleActivity() { ) ) } + } else { + it.exception?.let { exception -> + toast(exception.message.toString()) + } } } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 37c635f86..a5c35cb9c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -7,118 +7,134 @@ import jcifs.smb.SmbFileInputStream import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.dependencies.AppComposition import org.fossify.filemanager.enums.Protocols -import org.fossify.filemanager.helpers.DEFAULT_SMB_PORT import org.fossify.filemanager.helpers.Helpers +import org.fossify.filemanager.models.ApiResponse import java.io.BufferedInputStream +import java.io.InputStream class HttpServer( private val port: Int, private val serverIp: String, - private val connectionTypes: ConnectionTypes, + private val connectionType: ConnectionTypes, private val composition: AppComposition, private val machinePort: Int, - private val protocols: Protocols = Protocols.HTTP -) : - NanoHTTPD(port) { + private val protocol: Protocols = Protocols.HTTP, + private val dispatchException: (Exception) -> Unit +) : NanoHTTPD(port) { + override fun serve(session: IHTTPSession): Response { val uri = session.uri val rangeHeader = session.headers["range"] - if (connectionTypes.equals(ConnectionTypes.SMB)) { - val smbUrl = "smb://${serverIp}/${uri}" - val file = SmbFile(smbUrl) - if (!file.exists()) { - return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "File not found") - } - return handleRangeRequest(file, rangeHeader, file.length()) + + return when (connectionType) { + ConnectionTypes.SMB -> handleSmb(uri, rangeHeader) + ConnectionTypes.WebDav -> handleWebDav(uri, rangeHeader) + ConnectionTypes.SFTP -> handleSftp(uri, rangeHeader) + else -> handleFtp(uri, rangeHeader) } - else if(connectionTypes.equals(ConnectionTypes.WebDav)) { - val url = Helpers.createNanoHttpdUrl(connectionTypes, server = serverIp, path = uri.toString(), port = machinePort, protocols = protocols) - val webDavFile = composition.webDavApiRepository.listWebDavFileDetail(url) - return handleRangeRequestWebDav(rangeHeader, webDavFile?.contentLength!!, uri = uri, webDavFile.contentType,protocols) + } + + private fun handleSmb(uri: String, rangeHeader: String?): Response { + val file = SmbFile("smb://$serverIp/$uri") + if (!file.exists()) return notFound() + + val fileLength = file.length() + val (start, end) = parseRange(rangeHeader, fileLength) + ?: return rangeNotSatisfiable() + + val stream = SmbFileInputStream(file).also { it.skipFully(start) } + return try { + buildResponse(stream, start, end, fileLength, MimeTypes.getMimeTypes(file.path)) } - else if (connectionTypes.equals(ConnectionTypes.SFTP)) { - val url = Helpers.createNanoHttpdUrl(connectionTypes, server = serverIp, path = "", port = machinePort) - val sftFile = composition.sftpApiRepository.listSFTPFileDetails(uri) - return handleRangeRequestSFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) + catch (exp: Exception){ + dispatchException.invoke(exp) + notFound() } - val url = Helpers.createNanoHttpdUrl(connectionTypes, server = serverIp, path = "", port = machinePort) - val sftFile = composition.ftpApiRepository.getFTPFileDetail(uri) - return handleRangeRequestFTPServer(rangeHeader, sftFile?.size!!, uri = uri, url) } + private fun handleWebDav(uri: String, rangeHeader: String?): Response { + val url = buildUrl(uri) + val apiResponse = composition.webDavApiRepository.listWebDavFileDetail(url) + return handleResponse(apiResponse) { + val file = apiResponse.response ?: return@handleResponse notFound() + val (start, end) = parseRange(rangeHeader, file.contentLength) + ?: return@handleResponse rangeNotSatisfiable() + + val streamResponse = composition.webDavApiRepository.getWebDavFileInputStream(url, start, end) + val stream = handleStream(streamResponse) { + BufferedInputStream( + streamResponse.response, + BUFFER_SIZE + ) + } + buildResponse(stream, start, end, file.contentLength, MimeTypes.getMimeTypes(uri)) + } + } - private fun handleRangeRequest( - file: SmbFile, - rangeHeader: String?, - fileLength: Long - ): Response { + private fun handleSftp(uri: String, rangeHeader: String?): Response { + val apiResponse = composition.sftpApiRepository.listSFTPFileDetails(uri) - var start: Long = 0 - var end = fileLength - 1 + return handleResponse(apiResponse) { + val file = apiResponse.response ?: return@handleResponse notFound() + val (start, end) = parseRange(rangeHeader, file.size) + ?: return@handleResponse rangeNotSatisfiable() - if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { - val ranges = rangeHeader.substring(6).split("-") - try { - if (ranges[0].isNotEmpty()) start = ranges[0].toLong() - if (ranges.size > 1 && ranges[1].isNotEmpty()) end = ranges[1].toLong() - } catch (e: NumberFormatException) { + val streamResponse = composition.sftpApiRepository.getSFTPFileInputStream(uri, start) + val stream = handleStream(streamResponse) { + BufferedInputStream( + streamResponse.response, + BUFFER_SIZE + ) } + buildResponse(stream, start, end, file.size, MimeTypes.getMimeTypes(uri)) } + } - if (start >= fileLength) { - return newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, "text/plain", "") - } + private fun handleFtp(uri: String, rangeHeader: String?): Response { + val apiResponse = composition.ftpApiRepository.getFTPFileDetail(uri) - val contentLength = end - start + 1 + return handleResponse(apiResponse) { + val file = apiResponse.response ?: return@handleResponse notFound() - val inputStream = SmbFileInputStream(file) - var remaining = start - while (remaining > 0) { - val skipped = inputStream.skip(remaining) - if (skipped <= 0) break - remaining -= skipped - } + val (start, end) = parseRange(rangeHeader, file.size) + ?: return@handleResponse rangeNotSatisfiable() - return newFixedLengthResponse( - Response.Status.PARTIAL_CONTENT, - MimeTypes.getMimeTypes(file.path), - inputStream, - contentLength - ).apply { - addHeader("Accept-Ranges", "bytes") - addHeader("Content-Length", contentLength.toString()) - addHeader("Content-Range", "bytes $start-$end/$fileLength") - addHeader("Connection", "keep-alive") + val streamResponse = composition.ftpApiRepository.getFTPFileInputStream(uri, start) + val stream = handleStream(streamResponse) { + BufferedInputStream( + streamResponse.response, + BUFFER_SIZE + ) + } + buildResponse(stream, start, end, file.size, MimeTypes.getMimeTypes(uri)) } } - - private fun handleRangeRequestWebDav(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String,protocol: Protocols = Protocols.HTTP): Response { - var start: Long = 0 + private fun parseRange(header: String?, fileLength: Long): Pair? { + var start = 0L var end = fileLength - 1 - val url = Helpers.createNanoHttpdUrl(connectionTypes, server = serverIp, path = uri, port = machinePort, protocols = protocol) - if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { - val ranges = rangeHeader.substring(6).split("-") + + if (header != null && header.startsWith("bytes=")) { + val parts = header.removePrefix("bytes=").split("-") try { - if (ranges[0].isNotEmpty()) start = ranges[0].toLong() - if (ranges.size > 1 && ranges[1].isNotEmpty()) end = ranges[1].toLong() - } catch (e: NumberFormatException) { + if (parts[0].isNotEmpty()) start = parts[0].toLong() + if (parts.size > 1 && parts[1].isNotEmpty()) end = parts[1].toLong() + } catch (_: NumberFormatException) { } } - if (start >= fileLength) { - return newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, "text/plain", "") - } - val contentLength = end - start + 1 - - val inputStream = composition.webDavApiRepository.getWebDavFileInputStream(url = url,start,end) + return if (start >= fileLength) null else start to end + } - return newFixedLengthResponse( - Response.Status.PARTIAL_CONTENT, - MimeTypes.getMimeTypes(contentType), - inputStream, - contentLength - ).apply { + private fun buildResponse( + stream: InputStream?, + start: Long, + end: Long, + fileLength: Long, + mimeType: String? + ): Response { + val contentLength = end - start + 1 + return newFixedLengthResponse(Response.Status.PARTIAL_CONTENT, mimeType, stream, contentLength).apply { addHeader("Accept-Ranges", "bytes") addHeader("Content-Length", contentLength.toString()) addHeader("Content-Range", "bytes $start-$end/$fileLength") @@ -126,68 +142,51 @@ class HttpServer( } } - private fun handleRangeRequestSFTPServer(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String): Response { - var start: Long = 0 - var end = fileLength - 1 - if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { - val ranges = rangeHeader.substring(6).split("-") - try { - if (ranges[0].isNotEmpty()) start = ranges[0].toLong() - if (ranges.size > 1 && ranges[1].isNotEmpty()) end = ranges[1].toLong() - } catch (e: NumberFormatException) { - } - } - if (start >= fileLength) { - return newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, "text/plain", "") - } + private fun buildUrl(path: String) = + Helpers.createNanoHttpdUrl(connectionType, server = serverIp, path = path, port = machinePort, protocols = protocol) - val contentLength = end - start + 1 - val inputStream = composition.sftpApiRepository.getSFTPFileInputStream(uri,start) - val bufferedStream = BufferedInputStream(inputStream, 1024 * 1024) - return newFixedLengthResponse( - Response.Status.PARTIAL_CONTENT, - MimeTypes.getMimeTypes(contentType), - bufferedStream, - contentLength - ).apply { - addHeader("Accept-Ranges", "bytes") - addHeader("Content-Length", contentLength.toString()) - addHeader("Content-Range", "bytes $start-$end/$fileLength") - addHeader("Connection", "keep-alive") + private fun notFound() = + newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "File not found") + + private fun rangeNotSatisfiable() = + newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "") + + private fun serverError(message: String?) = + newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, message ?: "Server error") + + + private fun InputStream.skipFully(n: Long) { + var remaining = n + while (remaining > 0) { + val skipped = skip(remaining) + if (skipped <= 0) break + remaining -= skipped } } - private fun handleRangeRequestFTPServer(rangeHeader: String?, fileLength: Long, uri: String = "", contentType: String): Response { - var start: Long = 0 - var end = fileLength - 1 - if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { - val ranges = rangeHeader.substring(6).split("-") - try { - if (ranges[0].isNotEmpty()) start = ranges[0].toLong() - if (ranges.size > 1 && ranges[1].isNotEmpty()) end = ranges[1].toLong() - } catch (e: NumberFormatException) { - } - } - if (start >= fileLength) { - return newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, "text/plain", "") + private fun handleResponse( + apiResponse: ApiResponse, + callBack: () -> Response + ): Response { + return if (apiResponse.exception != null) { + dispatchException.invoke(apiResponse.exception) + serverError(apiResponse.exception.message) + } else { + callBack.invoke() } + } - val contentLength = end - start + 1 - - val inputStream = composition.ftpApiRepository.getFTPFileInputStream(uri,start) - val bufferedStream = BufferedInputStream(inputStream, 1024 * 1024) - return newFixedLengthResponse( - Response.Status.PARTIAL_CONTENT, - MimeTypes.getMimeTypes(contentType), - bufferedStream, - contentLength - ).apply { - addHeader("Accept-Ranges", "bytes") - addHeader("Content-Length", contentLength.toString()) - addHeader("Content-Range", "bytes $start-$end/$fileLength") - addHeader("Connection", "keep-alive") + private fun handleStream(apiResponse: ApiResponse, callBack: () -> InputStream): InputStream? { + return if (apiResponse.exception != null) { + dispatchException.invoke(apiResponse.exception) + return null + } else { + callBack.invoke() } } + companion object { + private const val BUFFER_SIZE = 1024 * 1024 + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index a0389abb7..538e6f6de 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -5,14 +5,19 @@ import android.content.Context import android.net.Uri import android.os.Parcelable import android.util.AttributeSet +import android.widget.Toast import androidx.activity.viewModels import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager +import com.thegrizzlylabs.sardineandroid.DavResource +import jcifs.smb.SmbFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import net.schmizz.sshj.sftp.RemoteResourceInfo +import org.apache.commons.net.ftp.FTPFile import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.dialogs.StoragePickerDialog import org.fossify.commons.enums.ConnectionTypes @@ -36,6 +41,7 @@ import org.fossify.filemanager.helpers.MAX_COLUMN_COUNT import org.fossify.filemanager.helpers.RootHelpers import org.fossify.filemanager.interfaces.ItemOperationsListener import org.fossify.filemanager.mapper.toFileItem +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.ListItem import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel import java.io.File @@ -123,7 +129,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF getRecyclerAdapter()?.finishActMode() } - fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, connectionType: ConnectionTypes = ConnectionTypes.Default) { + fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, connectionType: ConnectionTypes = ConnectionTypes.Default) { if ((activity as? BaseSimpleActivity)?.isAskingPermissions == true) { return } @@ -143,7 +149,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } FileDirItem.sorting = context!!.config.getFolderSorting(currentPath) - if(connectionType != ConnectionTypes.Default){ + if (connectionType != ConnectionTypes.Default) { listItems.forEach { it.parent = originalPath.substringAfter('/') } @@ -163,7 +169,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF itemsIgnoringSearch = listItems activity?.runOnUiThread { (activity as? MainActivity)?.refreshMenuItems() - addItems(listItems, forceRefresh,isNetworkPath,connectionType) + addItems(listItems, forceRefresh, isNetworkPath, connectionType) if (context != null && currentViewType != context!!.config.getFolderViewType(currentPath)) { setupLayoutManager(connectionType) } @@ -175,19 +181,19 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF private fun addItems(items: ArrayList, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, connectionType: ConnectionTypes) { activity?.runOnUiThread { binding.itemsSwipeRefresh.isRefreshing = false - binding.breadcrumbs.setBreadcrumb(currentPath,connectionType) + binding.breadcrumbs.setBreadcrumb(currentPath, connectionType) if (!forceRefresh && items.hashCode() == storedItems.hashCode()) { return@runOnUiThread } storedItems = items if (binding.itemsList.adapter == null) { - binding.breadcrumbs.updateFontSize(context!!.getTextSize(), true,connectionType) + binding.breadcrumbs.updateFontSize(context!!.getTextSize(), true, connectionType) } ItemsAdapter(activity as SimpleActivity, storedItems, this, binding.itemsList, isPickMultipleIntent, binding.itemsSwipeRefresh) { - if((it as? ListItem)?.mIsDirectory == false) { + if ((it as? ListItem)?.mIsDirectory == false) { if (connectionType == ConnectionTypes.SMB) { it?.let { item -> FileHelpers.launchSMB(item, this@ItemsFragment.context, viewModel.getMainSmb()) @@ -200,18 +206,16 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF it?.let { item -> FileHelpers.launchSFTP(connectionType, context = this@ItemsFragment.context, item = item) } - } - else if (connectionType == ConnectionTypes.FTP) { + } else if (connectionType == ConnectionTypes.FTP) { it?.let { item -> FileHelpers.launchFTP(connectionType, context = this@ItemsFragment.context, item = item) } } - } - else if ((it as? ListItem)?.isSectionTitle == true) { + } else if ((it as? ListItem)?.isSectionTitle == true) { openDirectory(it.mPath) searchClosed() } else { - itemClicked(it as FileDirItem,connectionType) + itemClicked(it as FileDirItem, connectionType) } }.apply { setupZoomListener(zoomListener) @@ -231,55 +235,41 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF private fun getRecyclerLayoutManager() = (binding.itemsList.layoutManager as MyGridLayoutManager) @SuppressLint("NewApi") - private fun getItems(path: String, isNetworkPath: Boolean = false,connectionType: ConnectionTypes, callback: (originalPath: String, items: ArrayList) -> Unit) { + private fun getItems( + path: String, + isNetworkPath: Boolean = false, + connectionType: ConnectionTypes, + callback: (originalPath: String, items: ArrayList) -> Unit + ) { ensureBackgroundThread { if (activity?.isDestroyed == false && activity?.isFinishing == false) { val config = context!!.config - if (connectionType.equals(ConnectionTypes.SMB)) { val fileItems = viewModel.getFilesFromNetworkPath() - val items = fileItems.map { it -> it.toFileItem() } - callback(path, getListItemsFromFileDirItems(ArrayList(items.toList()))) - } - else if(connectionType.equals(ConnectionTypes.WebDav)){ + handleApiResponse(fileItems, path, connectionType, callback) + } else if (connectionType.equals(ConnectionTypes.WebDav)) { CoroutineScope(Dispatchers.IO).launch { viewModel.listWebDavFiles(path) viewModel.webDavFiles.collectLatest { - if(it.isNotEmpty()) { - val fileItems = it - val items = fileItems.map { it -> it.toFileItem() } - callback(path, getListItemsFromFileDirItems(ArrayList(items.toList()))) - } + handleApiResponse(it, path, connectionType, callback) } } - } - else if (connectionType.equals(ConnectionTypes.SFTP)){ + } else if (connectionType.equals(ConnectionTypes.SFTP)) { CoroutineScope(Dispatchers.IO).launch { viewModel.listAllFilesSFTPRoot(path) - viewModel.sftpFiles.collectLatest { - if(it.isNotEmpty()) { - val fileItems = it - val items = fileItems.map { it -> it.toFileItem(path) } - callback(path, getListItemsFromFileDirItems(ArrayList(items.toList()))) - } + viewModel.sftpFiles.collectLatest { + handleApiResponse(it, path, connectionType, callback) } } - } - - else if (connectionType.equals(ConnectionTypes.FTP)){ + } else if (connectionType.equals(ConnectionTypes.FTP)) { CoroutineScope(Dispatchers.IO).launch { viewModel.listAllFTPFiles(path) viewModel.ftpFiles.collectLatest { - if(it.isNotEmpty()) { - val fileItems = it - val items = fileItems.map { it -> it.toFileItem(path) } - callback(path, getListItemsFromFileDirItems(ArrayList(items.toList()))) - } + handleApiResponse(it, path, connectionType, callback) } } - } - else if (context.isRestrictedSAFOnlyRoot(path)) { + } else if (context.isRestrictedSAFOnlyRoot(path)) { activity?.runOnUiThread { hideProgressBar() } activity?.handleAndroidSAFDialog(path, openInSystemAppAllowed = true) { if (!it) { @@ -296,8 +286,11 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF context!!.getOTGItems(path, config.shouldShowHidden(), getProperFileSize) { callback(path, getListItemsFromFileDirItems(it)) } - } else if (!config.enableRootAccess || !context!!.isPathOnRoot(path) && (connectionType.equals(ConnectionTypes.DAVx5) || connectionType.equals(ConnectionTypes.Default))) { - getRegularItemsOf(path, callback,connectionType) + } else if (!config.enableRootAccess || !context!!.isPathOnRoot(path) && (connectionType.equals(ConnectionTypes.DAVx5) || connectionType.equals( + ConnectionTypes.Default + )) + ) { + getRegularItemsOf(path, callback, connectionType) } else { RootHelpers(activity!!).getFiles(path, callback) } @@ -309,7 +302,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF val items = ArrayList() val getProperChildCount = context!!.config.getFolderViewType(currentPath) == VIEW_TYPE_LIST - if(connectionType == ConnectionTypes.DAVx5){ + if (connectionType == ConnectionTypes.DAVx5) { val uri = Uri.parse(path) val docFile = DocumentFile.fromTreeUri(context, uri) val files = docFile?.listFiles() @@ -329,8 +322,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF ) ) } - } - else { + } else { val files = File(path).listFiles()?.filterNotNull() if (context == null || files == null) { callback(path, items) @@ -350,21 +342,21 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } - // send out the initial item list asap, get proper child count asynchronously as it can be slow - callback(path, items) + // send out the initial item list asap, get proper child count asynchronously as it can be slow + callback(path, items) - if (getProperChildCount) { - items.filter { it.mIsDirectory }.forEach { - if (context != null) { - val childrenCount = it.getDirectChildrenCount(activity as BaseSimpleActivity, showHidden) - if (childrenCount != 0) { - activity?.runOnUiThread { - getRecyclerAdapter()?.updateChildCount(it.mPath, childrenCount) - } + if (getProperChildCount) { + items.filter { it.mIsDirectory }.forEach { + if (context != null) { + val childrenCount = it.getDirectChildrenCount(activity as BaseSimpleActivity, showHidden) + if (childrenCount != 0) { + activity?.runOnUiThread { + getRecyclerAdapter()?.updateChildCount(it.mPath, childrenCount) } } } } + } } private fun getListItemFromFile(file: File, isSortingBySize: Boolean, lastModifieds: HashMap, getProperChildCount: Boolean): ListItem? { @@ -407,7 +399,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF private fun itemClicked(item: FileDirItem, connectionType: ConnectionTypes) { if (item.isDirectory) { - openDirectory(item.path,connectionType) + openDirectory(item.path, connectionType) } else { clickedPath(item.path) } @@ -620,50 +612,90 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } - override fun columnCountChanged() { - (binding.itemsList.layoutManager as MyGridLayoutManager).spanCount = context!!.config.fileColumnCnt - (activity as? MainActivity)?.refreshMenuItems() - getRecyclerAdapter()?.apply { - notifyItemRangeChanged(0, listItems.size) + private fun handleApiResponse( + apiResponse: ApiResponse?, + path: String, + connectionType: ConnectionTypes, + callback: (originalPath: String, items: ArrayList) -> Unit + ) { + if (apiResponse?.exception != null) { + apiResponse.exception.message?.let { exp -> + activity?.toast(exp) + } + } else { + if (connectionType == ConnectionTypes.SMB) { + apiResponse?.response?.let { item -> + val fileItems = item as Array + val items = fileItems.map { it -> it.toFileItem() } + callback(path, getListItemsFromFileDirItems(ArrayList(items?.toList()))) + } + } else if (connectionType == ConnectionTypes.WebDav) { + apiResponse?.response?.let { item -> + val fileItems = item as List + val items = fileItems.map { it -> it.toFileItem() } + callback(path, getListItemsFromFileDirItems(ArrayList(items?.toList()))) + } + } else if (connectionType == ConnectionTypes.SFTP) { + apiResponse?.response?.let { item -> + val fileItems = item as List + val items = fileItems?.map { it -> it.toFileItem(path) } + callback(path, getListItemsFromFileDirItems(ArrayList(items?.toList()))) + } + + } else if (connectionType == ConnectionTypes.FTP) { + apiResponse?.response?.let { item -> + val fileItems = item as List + val items = fileItems?.map { it -> it.toFileItem(path) } + callback(path, getListItemsFromFileDirItems(ArrayList(items?.toList()))) + } + } } } - fun showProgressBar() { - binding.progressBar.show() - } + override fun columnCountChanged() { + (binding.itemsList.layoutManager as MyGridLayoutManager).spanCount = context!!.config.fileColumnCnt + (activity as? MainActivity)?.refreshMenuItems() + getRecyclerAdapter()?.apply { + notifyItemRangeChanged(0, listItems.size) + } + } - private fun hideProgressBar() { - binding.progressBar.hide() - } + fun showProgressBar() { + binding.progressBar.show() + } - fun getBreadcrumbs() = binding.breadcrumbs + private fun hideProgressBar() { + binding.progressBar.hide() + } - override fun toggleFilenameVisibility() { - getRecyclerAdapter()?.updateDisplayFilenamesInGrid() - } + fun getBreadcrumbs() = binding.breadcrumbs - override fun breadcrumbClicked(id: Int) { - if (id == 0) { - StoragePickerDialog(activity as SimpleActivity, currentPath, context!!.config.enableRootAccess, true) { - getRecyclerAdapter()?.finishActMode() - openPath(it) + override fun toggleFilenameVisibility() { + getRecyclerAdapter()?.updateDisplayFilenamesInGrid() + } + + override fun breadcrumbClicked(id: Int) { + if (id == 0) { + StoragePickerDialog(activity as SimpleActivity, currentPath, context!!.config.enableRootAccess, true) { + getRecyclerAdapter()?.finishActMode() + openPath(it) + } + } else { + val item = binding.breadcrumbs.getItem(id) + openPath(item.path, connectionType = item.connectionType) } - } else { - val item = binding.breadcrumbs.getItem(id) - openPath(item.path, connectionType = item.connectionType) } - } - override fun refreshFragment() { - openPath(currentPath) - } + override fun refreshFragment() { + openPath(currentPath) + } - override fun deleteFiles(files: ArrayList) { - val hasFolder = files.any { it.isDirectory } - handleFileDeleting(files, hasFolder) - } + override fun deleteFiles(files: ArrayList) { + val hasFolder = files.any { it.isDirectory } + handleFileDeleting(files, hasFolder) + } - override fun selectedPaths(paths: ArrayList) { - (activity as MainActivity).pickedPaths(paths) + override fun selectedPaths(paths: ArrayList) { + (activity as MainActivity).pickedPaths(paths) + } } -} diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt index e3116619c..c04a9aee1 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt @@ -2,17 +2,18 @@ package org.fossify.filemanager.interfaces import org.apache.commons.net.ftp.FTPClient import org.apache.commons.net.ftp.FTPFile +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream interface FTPApi { - suspend fun connectToFTP(connection: NetworkConnection): Boolean + suspend fun connectToFTP(connection: NetworkConnection): Pair - suspend fun listAllFTPFiles(path: String): List + suspend fun listAllFTPFiles(path: String): ApiResponse> - fun getFTPFileDetail(path: String): FTPFile? + fun getFTPFileDetail(path: String): ApiResponse - fun getFTPFileInputStream(path: String,start: Long): InputStream + fun getFTPFileInputStream(path: String,start: Long): ApiResponse fun getFTPConn(): FTPClient } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt index 86735513b..5713d78bd 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt @@ -3,19 +3,18 @@ package org.fossify.filemanager.interfaces import net.schmizz.sshj.sftp.FileAttributes import net.schmizz.sshj.sftp.RemoteResourceInfo import net.schmizz.sshj.sftp.SFTPClient +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream interface SFTPApi { - suspend fun connectToSftp(connection: NetworkConnection): Boolean + suspend fun connectToSftp(connection: NetworkConnection): Pair - suspend fun listAllFilesSFTPRoot(path: String): List + suspend fun listAllFilesSFTPRoot(path: String): ApiResponse> - suspend fun listAllFilesSFTPPath(path: String):List + fun listSFTPFileDetails(path: String): ApiResponse - fun listSFTPFileDetails(path: String): FileAttributes? - - fun getSFTPFileInputStream(url: String, startByte: Long): InputStream + fun getSFTPFileInputStream(url: String, startByte: Long): ApiResponse fun getSFTPConn(): SFTPClient } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt index 9e44aab5d..f219eb668 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt @@ -1,12 +1,13 @@ package org.fossify.filemanager.interfaces import jcifs.smb.SmbFile +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection interface SMBApi { - suspend fun verifyConnection(connection: NetworkConnection): Boolean + suspend fun verifyConnection(connection: NetworkConnection): Pair - fun getFilesFromNetworkPath(): Array + fun getFilesFromNetworkPath(): ApiResponse> fun getMainSmbFile(): SmbFile } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt index 7040d9c7b..9811e2dce 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt @@ -3,15 +3,16 @@ package org.fossify.filemanager.interfaces import android.content.Context import com.thegrizzlylabs.sardineandroid.DavResource import org.fossify.filemanager.enums.Protocols +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream interface WebDavApi { - suspend fun connectAndVerifyWebDav(connection: NetworkConnection,protocols: Protocols, context: Context): Boolean + suspend fun connectAndVerifyWebDav(connection: NetworkConnection,protocols: Protocols, context: Context): Pair - suspend fun listAllFilesOnWebDav(url: String): List + suspend fun listAllFilesOnWebDav(url: String): ApiResponse> - fun getWebDavFileInputStream(url: String, start: Long, end: Long): InputStream + fun getWebDavFileInputStream(url: String, start: Long, end: Long): ApiResponse - fun listWebDavFileDetail(url: String): DavResource? + fun listWebDavFileDetail(url: String): ApiResponse } diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/ApiResponse.kt b/app/src/main/kotlin/org/fossify/filemanager/models/ApiResponse.kt new file mode 100644 index 000000000..f221963cc --- /dev/null +++ b/app/src/main/kotlin/org/fossify/filemanager/models/ApiResponse.kt @@ -0,0 +1,3 @@ +package org.fossify.filemanager.models + +data class ApiResponse(val response:T?,val exception: Exception?) diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt b/app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt index 7dd1181c1..ea94b2689 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt @@ -1,3 +1,3 @@ package org.fossify.filemanager.models -data class ConnectionResult(val item: NetworkConnection,val success: Boolean,val saveInfo: Boolean = true) +data class ConnectionResult(val item: NetworkConnection,val success: Boolean,val saveInfo: Boolean = true,val exception: Exception? = null) diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt index 350fad0dd..06fd49da0 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt @@ -5,6 +5,7 @@ import org.apache.commons.net.ftp.FTPClient import org.apache.commons.net.ftp.FTPCmd import org.apache.commons.net.ftp.FTPFile import org.fossify.filemanager.interfaces.FTPApi +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection import java.io.File import java.io.InputStream @@ -13,8 +14,8 @@ class FTPApiImpl: FTPApi { private lateinit var currentStream: InputStream private lateinit var ftp: FTPClient private lateinit var ftpStream: FTPClient - override suspend fun connectToFTP(connection: NetworkConnection): Boolean { - try { + override suspend fun connectToFTP(connection: NetworkConnection): Pair { + return try { ftp = FTPClient() ftpStream = FTPClient() ftp.connect(connection.host, connection.port) @@ -24,41 +25,57 @@ class FTPApiImpl: FTPApi { ftpStream.login(connection.username,connection.password) if (!loginSuccess) { - return false + Pair(false, Exception("Login failed")) } ftp.enterLocalPassiveMode() ftpStream.enterLocalPassiveMode() - return true + Pair(true, null) } catch (exp: Exception) { - return false + Pair(false, exp) } } - override suspend fun listAllFTPFiles(path: String): List { - ftp.changeWorkingDirectory(path) - val files: Array = ftp.listFiles() - return files.toList() + override suspend fun listAllFTPFiles(path: String): ApiResponse> { + return try { + ftp.changeWorkingDirectory(path) + val files: Array = ftp.listFiles() + val theFiles = files.toList() + ApiResponse(theFiles,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) + } } - override fun getFTPFileDetail(path: String): FTPFile? { - val myPath = path.replace("//", "/") - if (ftp.hasFeature(FTPCmd.MLST)) { - val file = ftp.mlistFile(myPath) - return file + override fun getFTPFileDetail(path: String): ApiResponse { + return try { + val myPath = path.replace("//", "/") + if (ftp.hasFeature(FTPCmd.MLST)) { + val file = ftp.mlistFile(myPath) + ApiResponse(file,null) + } + val mP = File(myPath) + val files = ftp.listFiles(mP.parent).firstOrNull { it != null && it.name == mP.name } + ApiResponse(files,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) } - val mP = File(myPath) - val files = ftp.listFiles(mP.parent).firstOrNull { it != null && it.name == mP.name } - return files } - override fun getFTPFileInputStream(path: String, start: Long): InputStream { - if (::currentStream.isInitialized) - currentStream.close() - ftpStream.completePendingCommand() - ftpStream.setFileType(FTP.BINARY_FILE_TYPE) - ftpStream.restartOffset = start - currentStream = ftpStream.retrieveFileStream(path) - return currentStream + override fun getFTPFileInputStream(path: String, start: Long): ApiResponse { + return try { + if (::currentStream.isInitialized) + currentStream.close() + ftpStream.completePendingCommand() + ftpStream.setFileType(FTP.BINARY_FILE_TYPE) + ftpStream.restartOffset = start + currentStream = ftpStream.retrieveFileStream(path) + ApiResponse(currentStream,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) + } } override fun getFTPConn(): FTPClient = ftp diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt index c4103358d..3059dd78b 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt @@ -14,6 +14,7 @@ import net.schmizz.sshj.userauth.method.AuthPublickey import net.schmizz.sshj.userauth.password.PasswordUtils import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.interfaces.SFTPApi +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection import java.io.IOException import java.io.InputStream @@ -23,8 +24,8 @@ class SFTPApiImpl: SFTPApi { private lateinit var sftp: SFTPClient private val sftpLock = Any() - override suspend fun connectToSftp(connection: NetworkConnection): Boolean { - try { + override suspend fun connectToSftp(connection: NetworkConnection): Pair { + return try { if (!::ssh.isInitialized || !ssh.isConnected || !ssh.isAuthenticated) { ssh = SSHClient() ssh.addHostKeyVerifier(PromiscuousVerifier()) @@ -37,41 +38,46 @@ class SFTPApiImpl: SFTPApi { } sftp = ssh.newSFTPClient() } - return true + Pair(true,null) } catch (e: Exception) { - Log.e("SFTP", "Connect failed: ${e.message}") e.printStackTrace() - return false + Pair(false,e) } } - override suspend fun listAllFilesSFTPRoot(path: String): List { - val files = sftp.ls(path) - return files + override suspend fun listAllFilesSFTPRoot(path: String): ApiResponse> { + return try { + val files = sftp.ls(path) + ApiResponse(files,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) + } } - override suspend fun listAllFilesSFTPPath(path: String): List { - val files = sftp.ls(path) - return files - } + override fun listSFTPFileDetails(path: String): ApiResponse { - override fun listSFTPFileDetails(path: String): FileAttributes? { - synchronized(sftpLock) { - return try { - val myPath = path.replace("//", "/") - sftp.stat(myPath) - } catch (e: Exception) { - Log.e("SFTP", "Stat failed: ${e.message}") - null - } + return try { + val myPath = path.replace("//", "/") + val attributes = sftp.stat(myPath) + ApiResponse(attributes,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) } } - override fun getSFTPFileInputStream(url: String, startByte: Long): InputStream { - val myPath = url.replace("//", "/") - val remoteFile = sftp.open(myPath) - val inputStream = remoteFile.RemoteFileInputStream(startByte) - return inputStream + override fun getSFTPFileInputStream(url: String, startByte: Long):ApiResponse { + return try { + val myPath = url.replace("//", "/") + val remoteFile = sftp.open(myPath) + val inputStream = remoteFile.RemoteFileInputStream(startByte) + ApiResponse(inputStream,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) + } + } override fun getSFTPConn() = sftp diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt index 279c059f0..2c3ad5baa 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt @@ -10,6 +10,7 @@ import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.interfaces.SMBApi +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection import java.util.Properties @@ -23,8 +24,8 @@ class SMBApiImpl : SMBApi { setProperty("jcifs.netbios.retryTimeout", "5000") setProperty("jcifs.netbios.cachePolicy", "-1") } - override suspend fun verifyConnection(connection: NetworkConnection): Boolean { - try { + override suspend fun verifyConnection(connection: NetworkConnection): Pair { + return try { val p = Properties(defaultProperties) val context: CIFSContext = BaseContext(PropertyConfiguration(p)) var authContext: CIFSContext? = null @@ -40,16 +41,20 @@ class SMBApiImpl : SMBApi { } val smbUrl = Helpers.createUrl(ConnectionTypes.SMB, connection.sharedPath, connection.host, connection.port) smbClient = SmbFile(smbUrl, authContext) - return smbClient.exists() + Pair(smbClient.exists(),null) } catch (exp: Exception) { - Log.e("Exception", exp.toString()) + Pair(false,exp) } - return false } - override fun getFilesFromNetworkPath(): Array { - val files = smbClient.listFiles() - return files + override fun getFilesFromNetworkPath(): ApiResponse> { + return try { + val files = smbClient.listFiles() + ApiResponse(files,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) + } } override fun getMainSmbFile(): SmbFile = smbClient diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt index cbfb8e786..290189370 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt @@ -10,6 +10,7 @@ import okhttp3.OkHttpClient import org.fossify.filemanager.enums.Protocols import org.fossify.filemanager.interfaces.WebDavApi import org.fossify.filemanager.keyStores.CertificateStore +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream import java.security.cert.CertificateException @@ -23,39 +24,56 @@ class WebDavApiImpl: WebDavApi { connection: NetworkConnection, protocols: Protocols, context: Context - ): Boolean { - try { + ): Pair { + return try { sardine = if (protocols == Protocols.HTTP) { OkHttpSardine() } else { createHTTPSSardine(context,connection.host) } sardine.setCredentials(connection.username, connection.password) - return sardine.exists(connection.url) + Pair(sardine.exists(connection.url),null) } catch (exp: Exception) { Log.d("WebDav", exp.toString()) - return false + Pair(false,exp) } } - override suspend fun listAllFilesOnWebDav(url: String): List { - val resources = sardine.list(url) - return resources + override suspend fun listAllFilesOnWebDav(url: String): ApiResponse> { + return try { + val resources = sardine.list(url) + ApiResponse(resources,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) + } } - override fun getWebDavFileInputStream(url: String, start: Long, end: Long): InputStream { - val rangeHeader = "bytes=$start-$end" - val headers = mapOf("Range" to rangeHeader) - return sardine.get(url, headers) + override fun getWebDavFileInputStream(url: String, start: Long, end: Long): ApiResponse { + return try { + val rangeHeader = "bytes=$start-$end" + val headers = mapOf("Range" to rangeHeader) + val stream = sardine.get(url, headers) + ApiResponse(stream,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) + } } - override fun listWebDavFileDetail(url: String): DavResource? { - val resources = sardine.list(url) + override fun listWebDavFileDetail(url: String): ApiResponse { - if (resources.isNotEmpty()) { - return resources[0] + return try { + val resources = sardine.list(url) + var resource:DavResource? = null + if (resources.isNotEmpty()) { + resource = resources[0] + } + ApiResponse(resource,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) } - return null } private fun createHTTPSSardine(context: Context, host: String): Sardine { diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index d5a07c4ed..dcd2d6497 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -25,6 +25,7 @@ import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb import org.fossify.filemanager.interfaces.SFTPApi import org.fossify.filemanager.interfaces.SMBApi import org.fossify.filemanager.interfaces.WebDavApi +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.ConnectionResult import org.fossify.filemanager.models.NetworkConnection import java.io.InputStream @@ -46,11 +47,11 @@ class NetworkBrowserViewModel( val verifyFTP = MutableSharedFlow() - val sftpFiles = MutableStateFlow>(emptyList()) + val sftpFiles = MutableStateFlow>?>(null) - val webDavFiles = MutableStateFlow>(emptyList()) + val webDavFiles = MutableStateFlow< ApiResponse>?>(null) - val ftpFiles = MutableStateFlow>(emptyList()) + val ftpFiles = MutableStateFlow>?>(null) fun saveNetwork(networkConnection: NetworkConnection) { @@ -70,11 +71,11 @@ class NetworkBrowserViewModel( fun verifyNetwork(connection: NetworkConnection, saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { val value = smbApi.verifyConnection(connection) - verifyNetwork.emit(ConnectionResult(connection, value, saveInfo = saveInfo)) + verifyNetwork.emit(ConnectionResult(connection, value.first, saveInfo = saveInfo,value.second)) } } - fun getFilesFromNetworkPath(): Array { + fun getFilesFromNetworkPath(): ApiResponse> { return smbApi.getFilesFromNetworkPath() } @@ -85,7 +86,7 @@ class NetworkBrowserViewModel( fun connectAndAuthenticateWebDav(connection: NetworkConnection, protocol: Protocols, saveInfo: Boolean, context: Context) { viewModelScope.launch(Dispatchers.IO) { val result = webDavApi.connectAndVerifyWebDav(connection, protocol, context) - verifyWebDav.emit(ConnectionResult(connection, result, saveInfo)) + verifyWebDav.emit(ConnectionResult(connection, result.first, saveInfo,result.second)) } } @@ -106,7 +107,7 @@ class NetworkBrowserViewModel( fun connectSFTP(connection: NetworkConnection, saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { val res = sftpApi.connectToSftp(connection) - verifySFTP.emit(ConnectionResult(connection, res, saveInfo)) + verifySFTP.emit(ConnectionResult(connection, res.first, saveInfo,res.second)) } } @@ -135,7 +136,7 @@ class NetworkBrowserViewModel( fun connectFTP(connection: NetworkConnection, saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { val res = ftpApi.connectToFTP(connection) - verifyFTP.emit(ConnectionResult(connection, res, saveInfo)) + verifyFTP.emit(ConnectionResult(connection, res.first, saveInfo,res.second)) } } From b1ac464c40f3f6d56eeeaf9944e8cdcfd8bc3cb1 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Mon, 18 May 2026 00:01:27 +0500 Subject: [PATCH 22/37] Added annoymous login for ftp --- .../filemanager/activities/CloudActivity.kt | 5 +- .../filemanager/dialogs/ConnectionDialog.kt | 82 +++++++++++++------ .../filemanager/repository/FTPApiImpl.kt | 17 ++-- 3 files changed, 70 insertions(+), 34 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index a6aa01280..b35a68760 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -219,7 +219,8 @@ class CloudActivity : SimpleActivity() { host = host, port = port, connectionType = ConnectionTypes.FTP, - authentication = authentication + authentication = authentication, + displayName = displayName, ), true ) } @@ -443,7 +444,7 @@ class CloudActivity : SimpleActivity() { connectionType = connectionType, port = it.item.port, displayName = it.item.displayName, - url = viewModel.getFTP().printWorkingDirectory(), + url = "/", authentication = it.item.authentication ) ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 0bcdc02f8..1ae019af5 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -23,7 +23,7 @@ import java.io.File class ConnectionDialog( val activity: BaseSimpleActivity, - dispatch: (String, String, String, String, String, Uri?, String,String, Int, ConnectionTypes, Protocols?, Authentication) -> Unit + dispatch: (String, String, String, String, String, Uri?, String, String, Int, ConnectionTypes, Protocols?, Authentication) -> Unit ) { private var binding: DialogAddConnectionBinding val items = listOf(ConnectionTypes.DAVx5.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.FTP.type) @@ -95,29 +95,40 @@ class ConnectionDialog( binding.authDropDownMenu.setText(authentications[0].toString(), false) } - private fun registerAuthClickListener() { - binding.authDropDownMenu.setOnItemClickListener { parent, view, position, id -> - val selectedItem = parent.getItemAtPosition(position).toString() + private fun onAuthSelected(selectedItem: String){ + if (binding.dropdownMenu.value == ConnectionTypes.SMB.type) { if (Authentication.valueOf(selectedItem) == Authentication.Anonymous) { toggleCredentialsVisibility(View.GONE) toggleSFTPAuthVisibility(View.GONE) + } else { + toggleCredentialsVisibility(View.VISIBLE) + toggleSFTPAuthVisibility(View.GONE) } - else if(Authentication.valueOf(selectedItem) == Authentication.PrivateKey && binding.dropdownMenu.value == ConnectionTypes.SFTP.type){ + } else if (binding.dropdownMenu.value == ConnectionTypes.SFTP.type) { + if (Authentication.valueOf(selectedItem) == Authentication.PrivateKey) { binding.userTf.visibility = View.VISIBLE binding.passwordTf.visibility = View.GONE toggleSFTPAuthVisibility(View.VISIBLE) - } - else if(Authentication.valueOf(selectedItem) == Authentication.Password && binding.dropdownMenu.value == ConnectionTypes.SFTP.type){ - binding.userTf.visibility = View.VISIBLE - binding.passwordTf.visibility = View.VISIBLE + } else { + toggleCredentialsVisibility(View.VISIBLE) toggleSFTPAuthVisibility(View.GONE) } - else { + } else if (binding.dropdownMenu.value == ConnectionTypes.FTP.type) { + if (Authentication.valueOf(selectedItem) == Authentication.Password) { toggleCredentialsVisibility(View.VISIBLE) toggleSFTPAuthVisibility(View.GONE) + } else { + toggleCredentialsVisibility(View.GONE) + toggleSFTPAuthVisibility(View.GONE) } } } + private fun registerAuthClickListener() { + binding.authDropDownMenu.setOnItemClickListener { parent, view, position, id -> + val selectedItem = parent.getItemAtPosition(position).toString() + onAuthSelected(selectedItem) + } + } private fun toggleSFTPAuthVisibility(visibility: Int) { binding.privateKeyTf.visibility = visibility @@ -137,7 +148,10 @@ class ConnectionDialog( private fun dropDownItemSelected() { binding.dropdownMenu.setOnItemClickListener { parent, view, position, id -> val selectedItem = parent.getItemAtPosition(position).toString() - togglePortValue(ConnectionTypes.valueOf(selectedItem), if(binding.dropdownMenuProtocol.value != "") Protocols.valueOf(binding.dropdownMenuProtocol.value) else Protocols.HTTP) + togglePortValue( + ConnectionTypes.valueOf(selectedItem), + if (binding.dropdownMenuProtocol.value != "") Protocols.valueOf(binding.dropdownMenuProtocol.value) else Protocols.HTTP + ) if (selectedItem == ConnectionTypes.DAVx5.type) { binding.allFieldsExceptConnection.visibility = View.GONE promptUserToSelectStorage() @@ -147,27 +161,43 @@ class ConnectionDialog( binding.authDropDownLayout.visibility = View.GONE toggleSFTPAuthVisibility(View.GONE) toggleCredentialsVisibility(View.VISIBLE) - } - else if(selectedItem == ConnectionTypes.SMB.type){ + } else if (selectedItem == ConnectionTypes.SMB.type) { binding.dropdownProtocol.visibility = View.GONE binding.certRow.visibility = View.GONE binding.allFieldsExceptConnection.visibility = View.VISIBLE binding.authDropDownLayout.visibility = View.VISIBLE toggleSFTPAuthVisibility(View.GONE) binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, authentications)) - } - else if(selectedItem == ConnectionTypes.FTP.type || selectedItem == ConnectionTypes.SFTP.type){ + binding.authDropDownMenu.setText(authentications[0].toString(), false) + onAuthSelected(authentications[0].toString()) + } else if (selectedItem == ConnectionTypes.SFTP.type) { binding.allFieldsExceptConnection.visibility = View.VISIBLE binding.authDropDownLayout.visibility = View.VISIBLE binding.dropdownProtocol.visibility = View.GONE binding.certRow.visibility = View.GONE - if(Authentication.valueOf(binding.authDropDownMenu.value) == Authentication.Password && selectedItem == ConnectionTypes.SFTP.type){ + if (Authentication.valueOf(binding.authDropDownMenu.value) == Authentication.Password) { toggleSFTPAuthVisibility(View.GONE) - } - else{ + } else { toggleSFTPAuthVisibility(View.VISIBLE) } binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, sftpAuthentications)) + binding.authDropDownMenu.setText(sftpAuthentications[0].toString(), false) + onAuthSelected(sftpAuthentications[0].toString()) + + } else if (selectedItem == ConnectionTypes.FTP.type) { + binding.allFieldsExceptConnection.visibility = View.VISIBLE + binding.authDropDownLayout.visibility = View.VISIBLE + binding.dropdownProtocol.visibility = View.GONE + binding.certRow.visibility = View.GONE + toggleSFTPAuthVisibility(View.GONE) + if (Authentication.valueOf(binding.authDropDownMenu.value) == Authentication.Password) { + toggleCredentialsVisibility(View.VISIBLE) + } else { + toggleCredentialsVisibility(View.GONE) + } + binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, authentications)) + binding.authDropDownMenu.setText(authentications[0].toString(), false) + onAuthSelected(authentications[0].toString()) } } } @@ -185,19 +215,19 @@ class ConnectionDialog( } } - private fun togglePortValue(connectionTypes: ConnectionTypes,protocols: Protocols){ - when(connectionTypes){ + private fun togglePortValue(connectionTypes: ConnectionTypes, protocols: Protocols) { + when (connectionTypes) { ConnectionTypes.SMB -> binding.portEt.setText(DEFAULT_SMB_PORT.toString()) ConnectionTypes.FTP -> binding.portEt.setText(DEFAULT_FTP_PORT.toString()) ConnectionTypes.SFTP -> binding.portEt.setText(DEFAULT_SFTP_PORT.toString()) ConnectionTypes.WebDav -> { - if(protocols == Protocols.HTTP){ + if (protocols == Protocols.HTTP) { binding.portEt.setText(DEFAULT_WEBDAV_HTTP_PORT.toString()) - } - else{ + } else { binding.portEt.setText(DEFAULT_WEBDAV_HTTPS_PORT.toString()) } } + else -> Unit } } @@ -213,10 +243,10 @@ class ConnectionDialog( } private fun attachPrivateKeyBtnClickListener() { - binding.privateKeyTf.setEndIconOnClickListener { - (activity as CloudActivity).openFileLinkForPrivateKey{ + binding.privateKeyTf.setEndIconOnClickListener { + (activity as CloudActivity).openFileLinkForPrivateKey { privateKeyUri = it - privateKeyUri?.let {path-> + privateKeyUri?.let { path -> val inputStream = activity.contentResolver.openInputStream(path) val keyText = inputStream?.bufferedReader().use { it?.readText() } ?: "" binding.privateKeyEt.setText(keyText) diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt index 06fd49da0..12366e75e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt @@ -4,6 +4,7 @@ import org.apache.commons.net.ftp.FTP import org.apache.commons.net.ftp.FTPClient import org.apache.commons.net.ftp.FTPCmd import org.apache.commons.net.ftp.FTPFile +import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.interfaces.FTPApi import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection @@ -15,18 +16,22 @@ class FTPApiImpl: FTPApi { private lateinit var ftp: FTPClient private lateinit var ftpStream: FTPClient override suspend fun connectToFTP(connection: NetworkConnection): Pair { - return try { + return try { ftp = FTPClient() ftpStream = FTPClient() ftp.connect(connection.host, connection.port) ftpStream.connect(connection.host, connection.port) - val loginSuccess = ftp.login(connection.username, connection.password) - ftpStream.login(connection.username,connection.password) - - if (!loginSuccess) { - Pair(false, Exception("Login failed")) + val (username, password) = if (connection.authentication == Authentication.Anonymous) { + "anonymous" to "anonymous" + } else { + connection.username to connection.password } + val loginSuccess = ftp.login(username, password) + ftpStream.login(username, password) + + if (!loginSuccess) return Pair(false, Exception("Login failed")) + ftp.enterLocalPassiveMode() ftpStream.enterLocalPassiveMode() Pair(true, null) From 10701378ea7f21917672d399c1bbed25b1c746da Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sun, 24 May 2026 01:29:23 +0500 Subject: [PATCH 23/37] minor issues reealed to webdav breadcrumb fixed --- .../filemanager/activities/MainActivity.kt | 20 +++++++++++++++---- .../filemanager/fragments/ItemsFragment.kt | 17 ++++++++++++---- .../mapper/NetworkConnectionMapper.kt | 2 +- .../filemanager/repository/WebDavApiImpl.kt | 9 ++++++++- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt index 1bf71eb9b..dd2891b9f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt @@ -85,12 +85,9 @@ class MainActivity : SimpleActivity() { private const val BACK_PRESS_TIMEOUT = 5000 private const val PICKED_PATH = "picked_path" } - private val binding by viewBinding(ActivityMainBinding::inflate) - private var wasBackJustPressed = false private var mTabsToShow = ArrayList() - private var mStoredFontSize = 0 private var mStoredDateFormat = "" private var mStoredTimeFormat = "" @@ -195,7 +192,22 @@ class MainActivity : SimpleActivity() { } } else { currentFragment.getBreadcrumbs().removeBreadcrumb() - openPath(currentFragment.getBreadcrumbs().getLastItem().path) + var path = "" + val lastItem = currentFragment.getBreadcrumbs().getLastItem() + if (lastItem.connectionType == ConnectionTypes.WebDav){ + val fileItems = currentFragment.getBreadcrumbs().getAllItems() + fileItems.forEach { + path += "${it.path}/" + } + + } + else{ + path = lastItem.path + } + openPath( + path, + connectionType = lastItem.connectionType + ) return true } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index 538e6f6de..2215a7b02 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -5,6 +5,7 @@ import android.content.Context import android.net.Uri import android.os.Parcelable import android.util.AttributeSet +import android.util.Log import android.widget.Toast import androidx.activity.viewModels import androidx.documentfile.provider.DocumentFile @@ -58,6 +59,8 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF private var storedItems = ArrayList() private var itemsIgnoringSearch = ArrayList() private lateinit var binding: ItemsFragmentBinding + private var connectionType: ConnectionTypes = ConnectionTypes.Default + override fun onFinishInflate() { super.onFinishInflate() @@ -107,7 +110,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF progressBar.trackColor = properPrimaryColor.adjustAlpha(LOWER_ALPHA) if (currentPath != "") { - breadcrumbs.updateColor(textColor) + breadcrumbs.updateColor(textColor,connectionType) } itemsSwipeRefresh.isEnabled = lastSearchedText.isEmpty() && activity?.config?.enablePullToRefresh != false @@ -138,7 +141,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF if (realPath.isEmpty()) { realPath = "/" } - + this.connectionType = connectionType scrollStates[currentPath] = getScrollState()!! currentPath = realPath showHidden = context!!.config.shouldShowHidden() @@ -675,13 +678,19 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } override fun breadcrumbClicked(id: Int) { + val item = binding.breadcrumbs.getItem(id) if (id == 0) { + Log.d("BreadcrumbClicked", binding.breadcrumbs.getAllItems().toString()) StoragePickerDialog(activity as SimpleActivity, currentPath, context!!.config.enableRootAccess, true) { getRecyclerAdapter()?.finishActMode() - openPath(it) + if (item.connectionType != ConnectionTypes.Default){ + openPath(item.path, connectionType = item.connectionType) + } + else{ + openPath(item.path) + } } } else { - val item = binding.breadcrumbs.getItem(id) openPath(item.path, connectionType = item.connectionType) } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index 51340952b..ab153f37b 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -57,7 +57,7 @@ fun SmbFile.toFileItem(): FileDirItem { fun DavResource.toFileItem(): FileDirItem { return FileDirItem( - path = this.path, + path = this.href.toString(), name = this.name.trimEnd('/'), isDirectory = this.isDirectory, size = if (!this.isDirectory) (this.contentLength ?: 0L) else 0L, diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt index 290189370..5f6df3ede 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt @@ -2,6 +2,7 @@ package org.fossify.filemanager.repository import android.annotation.SuppressLint import android.content.Context +import android.net.Uri import android.util.Log import com.thegrizzlylabs.sardineandroid.DavResource import com.thegrizzlylabs.sardineandroid.Sardine @@ -17,6 +18,7 @@ import java.security.cert.CertificateException import java.security.cert.X509Certificate import javax.net.ssl.SSLContext import javax.net.ssl.X509TrustManager +import androidx.core.net.toUri class WebDavApiImpl: WebDavApi { lateinit var sardine: Sardine @@ -42,7 +44,12 @@ class WebDavApiImpl: WebDavApi { override suspend fun listAllFilesOnWebDav(url: String): ApiResponse> { return try { val resources = sardine.list(url) - ApiResponse(resources,null) + val b = Uri.decode(url.toUri().encodedPath?.trimEnd('/')) + val filteredItems = resources.filter { resource -> + val a = Uri.decode(resource.href.toString().toUri().encodedPath?.trimEnd('/')) + a != b + } + ApiResponse(filteredItems,null) } catch (exp: Exception){ ApiResponse(null,exp) From bc9bdf1de6a4226c133e9cd1e45daeadff814e6f Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Tue, 26 May 2026 00:20:17 +0500 Subject: [PATCH 24/37] back press and bread crumb click made stable for webdav --- .../filemanager/fileSystems/FileHelpers.kt | 8 ++++---- .../fossify/filemanager/fileSystems/HttpServer.kt | 7 +++++-- .../filemanager/fragments/ItemsFragment.kt | 15 ++++++++++++--- .../org/fossify/filemanager/helpers/Helpers.kt | 11 ++++++++--- .../filemanager/repository/WebDavApiImpl.kt | 4 ++-- 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt index ffe71366f..52268b930 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -14,7 +14,7 @@ import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.models.ListItem object FileHelpers { - val URL: String = "http://127.0.0.1:7871/" + val URL: String = "http://127.0.0.1:7871" fun launchSMB(item: ListItem, context: Context, smb: SmbFile) { try { CoroutineScope(Dispatchers.IO).launch { @@ -36,11 +36,11 @@ object FileHelpers { fun launchWebDav(connectionTypes: ConnectionTypes, item: ListItem, context: Context){ try { CoroutineScope(Dispatchers.IO).launch { - val port = Helpers.getPortForEachService(connectionTypes) - val uri = Helpers.createNanoHttpdUrl(connectionTypes, item.mPath, port = port).toUri() val i = Intent(Intent.ACTION_VIEW) - i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) + val extractedPath = Helpers.retrievePath(item.mPath) + val uri = "${URL}${extractedPath}" + i.setDataAndType(uri.toUri(), MimeTypes.getMimeTypes(item.mPath)) val packageManager: PackageManager = context.packageManager val resInfos = packageManager.queryIntentActivities(i, 0) diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index a5c35cb9c..89b35a4dc 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -35,7 +35,7 @@ class HttpServer( } private fun handleSmb(uri: String, rangeHeader: String?): Response { - val file = SmbFile("smb://$serverIp/$uri") + val file = SmbFile(uri) if (!file.exists()) return notFound() val fileLength = file.length() @@ -53,7 +53,10 @@ class HttpServer( } private fun handleWebDav(uri: String, rangeHeader: String?): Response { - val url = buildUrl(uri) + + val extractedPath = Helpers.retrievePath(uri) + val url = Helpers. createNanoHttpdUrl(connectionType, extractedPath, server = serverIp, port = machinePort, protocols = protocol) + val apiResponse = composition.webDavApiRepository.listWebDavFileDetail(url) return handleResponse(apiResponse) { val file = apiResponse.response ?: return@handleResponse notFound() diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index 2215a7b02..ae71aa090 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -680,7 +680,6 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF override fun breadcrumbClicked(id: Int) { val item = binding.breadcrumbs.getItem(id) if (id == 0) { - Log.d("BreadcrumbClicked", binding.breadcrumbs.getAllItems().toString()) StoragePickerDialog(activity as SimpleActivity, currentPath, context!!.config.enableRootAccess, true) { getRecyclerAdapter()?.finishActMode() if (item.connectionType != ConnectionTypes.Default){ @@ -691,12 +690,22 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } } else { - openPath(item.path, connectionType = item.connectionType) + var path = "" + if (item.connectionType == ConnectionTypes.WebDav){ + val items = binding.breadcrumbs.getItemsTillIndex(id) + items.forEach { item -> + path += "${item.path}/" + } + } + else + path = item.path + + openPath(path, connectionType = item.connectionType) } } override fun refreshFragment() { - openPath(currentPath) + openPath(currentPath,connectionType = connectionType) } override fun deleteFiles(files: ArrayList) { diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt index 87a1b5881..5cf6b0ce8 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -1,12 +1,13 @@ package org.fossify.filemanager.helpers +import android.net.Uri import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.enums.Protocols import java.util.Locale.getDefault object Helpers { val host: String = "127.0.0.1" - fun createNanoHttpdUrl(connectionTypes: ConnectionTypes, path: String = "", server: String = "", port: Int, protocols: Protocols = Protocols.HTTP): String{ + fun createNanoHttpdUrl(connectionTypes: ConnectionTypes, path: String? = "", server: String = "", port: Int, protocols: Protocols = Protocols.HTTP): String{ var protocol = Protocols.HTTP.toString().lowercase() if(connectionTypes.equals(ConnectionTypes.WebDav)){ protocol = protocols.name.lowercase() @@ -14,7 +15,7 @@ object Helpers { else if(connectionTypes.equals(ConnectionTypes.SMB)){ protocol = ConnectionTypes.SMB.toString().lowercase() } - val url = "${protocol}://${if (server.isEmpty()) host else server }:${port}/${path}" + val url = "${protocol}://${if (server.isEmpty()) host else server }:${port}${path}" return url } @@ -40,8 +41,12 @@ object Helpers { } return PORT_SMB } - fun createProtocolPath(protocol: Protocols?, server: String, port:Int, path:String): String{ return "${protocol?.name?.lowercase(getDefault())}://${server}:${port}/${path}" } + + fun retrievePath(url: String): String?{ + val uri = Uri.parse(url) + return uri.path + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt index 5f6df3ede..fca19f4ac 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt @@ -47,7 +47,7 @@ class WebDavApiImpl: WebDavApi { val b = Uri.decode(url.toUri().encodedPath?.trimEnd('/')) val filteredItems = resources.filter { resource -> val a = Uri.decode(resource.href.toString().toUri().encodedPath?.trimEnd('/')) - a != b + a != b } ApiResponse(filteredItems,null) } @@ -79,7 +79,7 @@ class WebDavApiImpl: WebDavApi { ApiResponse(resource,null) } catch (exp: Exception){ - ApiResponse(null,exp) + ApiResponse(null,null) } } From f71c9874ec9f37d291e79f88e557e2a0dc5238b4 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Wed, 27 May 2026 01:01:20 +0500 Subject: [PATCH 25/37] fixed issues for smb bread crumb --- .../fossify/filemanager/activities/CloudActivity.kt | 2 +- .../fossify/filemanager/fileSystems/FileHelpers.kt | 11 +++++++---- .../org/fossify/filemanager/fileSystems/HttpServer.kt | 3 ++- .../fossify/filemanager/fragments/ItemsFragment.kt | 4 ++-- .../kotlin/org/fossify/filemanager/helpers/Helpers.kt | 2 +- .../org/fossify/filemanager/interfaces/SMBApi.kt | 2 +- .../filemanager/mapper/NetworkConnectionMapper.kt | 7 +++++-- .../org/fossify/filemanager/repository/SMBApiImpl.kt | 10 +++++++--- .../filemanager/viewmodels/NetworkBrowserViewModel.kt | 4 ++-- 9 files changed, 28 insertions(+), 17 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index b35a68760..edc7bc78c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -375,7 +375,7 @@ class CloudActivity : SimpleActivity() { viewModel.verifyNetwork.collectLatest { if (it.success) { if (!it.saveInfo) { - val path = "${it.item.host.trimEnd('/')}/${it.item.sharedPath.trimStart('/')}" + val path = "smb://${it.item.host.trimEnd('/')}:${it.item.port}/${it.item.sharedPath.trimStart('/')}" startServer(it.item, connectionType = ConnectionTypes.SMB, machinePort = it.item.port) launchMainActivity(ConnectionTypes.SMB, path) } else { diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt index 52268b930..ff62bd865 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -11,17 +11,20 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.helpers.Helpers +import org.fossify.filemanager.helpers.PORT_SMB +import org.fossify.filemanager.helpers.PORT_WEBDAV import org.fossify.filemanager.models.ListItem object FileHelpers { - val URL: String = "http://127.0.0.1:7871" + val URL: String = "http://127.0.0.1" fun launchSMB(item: ListItem, context: Context, smb: SmbFile) { try { CoroutineScope(Dispatchers.IO).launch { - val uri = "${URL}${item.parent}${(item.path.toUri()).path}".toUri() + val extractedPath = Helpers.retrievePath(item.mPath) + val uri = "${URL}:${PORT_SMB}${extractedPath}" val i = Intent(Intent.ACTION_VIEW) - i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) + i.setDataAndType(uri.toUri(), MimeTypes.getMimeTypes(item.mPath)) val packageManager: PackageManager = context.packageManager val resInfos = packageManager.queryIntentActivities(i, 0) if (resInfos.size > 0) { @@ -39,7 +42,7 @@ object FileHelpers { val i = Intent(Intent.ACTION_VIEW) val extractedPath = Helpers.retrievePath(item.mPath) - val uri = "${URL}${extractedPath}" + val uri = "${URL}:${PORT_WEBDAV}/${extractedPath}" i.setDataAndType(uri.toUri(), MimeTypes.getMimeTypes(item.mPath)) val packageManager: PackageManager = context.packageManager diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 89b35a4dc..5c23463a2 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -35,7 +35,8 @@ class HttpServer( } private fun handleSmb(uri: String, rangeHeader: String?): Response { - val file = SmbFile(uri) + val url = Helpers. createNanoHttpdUrl(connectionType, uri, server = serverIp, port = machinePort, protocols = protocol) + val file = SmbFile(url) if (!file.exists()) return notFound() val fileLength = file.length() diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index ae71aa090..5ab9ac0b1 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -248,7 +248,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF if (activity?.isDestroyed == false && activity?.isFinishing == false) { val config = context!!.config if (connectionType.equals(ConnectionTypes.SMB)) { - val fileItems = viewModel.getFilesFromNetworkPath() + val fileItems = viewModel.getFilesFromNetworkPath(path) handleApiResponse(fileItems, path, connectionType, callback) } else if (connectionType.equals(ConnectionTypes.WebDav)) { CoroutineScope(Dispatchers.IO).launch { @@ -691,7 +691,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } else { var path = "" - if (item.connectionType == ConnectionTypes.WebDav){ + if (item.connectionType == ConnectionTypes.WebDav || item.connectionType == ConnectionTypes.SMB){ val items = binding.breadcrumbs.getItemsTillIndex(id) items.forEach { item -> path += "${item.path}/" diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt index 5cf6b0ce8..6388b3077 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -21,7 +21,7 @@ object Helpers { fun createUrl(connectionTypes: ConnectionTypes,path: String,server: String,port: Int = 0): String{ if(connectionTypes == ConnectionTypes.SMB){ - return "${connectionTypes.toString().lowercase()}://${server}:${port}/${path}" + return "${connectionTypes.toString().lowercase()}://${server}:${port}/${path}/" } return "" } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt index f219eb668..70d5d8f16 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt @@ -7,7 +7,7 @@ import org.fossify.filemanager.models.NetworkConnection interface SMBApi { suspend fun verifyConnection(connection: NetworkConnection): Pair - fun getFilesFromNetworkPath(): ApiResponse> + fun getFilesFromNetworkPath(path: String): ApiResponse> fun getMainSmbFile(): SmbFile } diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index ab153f37b..749117b41 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -44,9 +44,12 @@ fun NetworkConnection.toEntity(): NetworkConnectionEntity { } fun SmbFile.toFileItem(): FileDirItem { + + val normalizedPath = this.path.trimEnd('/') + val fileName = normalizedPath.substringAfterLast('/') return FileDirItem( - path = this.path, - name = this.name.trimEnd('/'), + path = this.canonicalPath, + name = fileName.ifEmpty { "/" }, isDirectory = this.isDirectory, size = if (!this.isDirectory) this.length() else 0L, modified = this.lastModified(), diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt index 2c3ad5baa..c1c1962e0 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt @@ -47,10 +47,14 @@ class SMBApiImpl : SMBApi { } } - override fun getFilesFromNetworkPath(): ApiResponse> { + override fun getFilesFromNetworkPath(path: String): ApiResponse> { return try { - val files = smbClient.listFiles() - ApiResponse(files,null) + if ("$path/" == smbClient.canonicalPath){ + val files = smbClient.listFiles() + return ApiResponse(files,null) + } + val subDir = SmbFile("$path/", smbClient.context) + ApiResponse(subDir.listFiles(),null) } catch (exp: Exception){ ApiResponse(null,exp) diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index dcd2d6497..6afe37bb1 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -75,8 +75,8 @@ class NetworkBrowserViewModel( } } - fun getFilesFromNetworkPath(): ApiResponse> { - return smbApi.getFilesFromNetworkPath() + fun getFilesFromNetworkPath(path: String): ApiResponse> { + return smbApi.getFilesFromNetworkPath(path) } fun getMainSmb(): SmbFile = smbApi.getMainSmbFile() From 013661de73608a320a3300acdb0b936eb641a1f1 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Wed, 27 May 2026 16:37:01 +0500 Subject: [PATCH 26/37] Added implementation for creating directories and files on connections --- .../filemanager/activities/MainActivity.kt | 2 +- .../filemanager/adapters/ItemsAdapter.kt | 14 ++- .../dialogs/CreateNewItemDialog.kt | 102 ++++++++++++++- .../filemanager/fragments/ItemsFragment.kt | 119 +++++++++--------- .../fossify/filemanager/interfaces/FTPApi.kt | 1 + .../fossify/filemanager/interfaces/SFTPApi.kt | 2 + .../fossify/filemanager/interfaces/SMBApi.kt | 4 +- .../filemanager/interfaces/WebDavApi.kt | 2 + .../mapper/NetworkConnectionMapper.kt | 37 ++++-- .../fossify/filemanager/models/ListItem.kt | 5 +- .../filemanager/repository/FTPApiImpl.kt | 12 ++ .../filemanager/repository/SFTPApiImpl.kt | 52 ++++---- .../filemanager/repository/SMBApiImpl.kt | 12 +- .../filemanager/repository/WebDavApiImpl.kt | 12 +- .../viewmodels/NetworkBrowserViewModel.kt | 37 ++++-- 15 files changed, 307 insertions(+), 106 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt index dd2891b9f..9e5aeafba 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt @@ -194,7 +194,7 @@ class MainActivity : SimpleActivity() { currentFragment.getBreadcrumbs().removeBreadcrumb() var path = "" val lastItem = currentFragment.getBreadcrumbs().getLastItem() - if (lastItem.connectionType == ConnectionTypes.WebDav){ + if (lastItem.connectionType == ConnectionTypes.WebDav || lastItem.connectionType == ConnectionTypes.SMB){ val fileItems = currentFragment.getBreadcrumbs().getAllItems() fileItems.forEach { path += "${it.path}/" diff --git a/app/src/main/kotlin/org/fossify/filemanager/adapters/ItemsAdapter.kt b/app/src/main/kotlin/org/fossify/filemanager/adapters/ItemsAdapter.kt index bea529466..a91897f94 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/adapters/ItemsAdapter.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/adapters/ItemsAdapter.kt @@ -9,6 +9,8 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.graphics.drawable.LayerDrawable import android.net.Uri +import android.provider.Settings.Global.getString +import android.util.Log import android.util.TypedValue import android.view.LayoutInflater import android.view.Menu @@ -42,6 +44,7 @@ import org.fossify.commons.dialogs.RadioGroupDialog import org.fossify.commons.dialogs.RenameDialog import org.fossify.commons.dialogs.RenameItemDialog import org.fossify.commons.dialogs.RenameItemsDialog +import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.applyColorFilter import org.fossify.commons.extensions.beGone import org.fossify.commons.extensions.beVisible @@ -1071,7 +1074,12 @@ class ItemsAdapter( if (listItem.isDirectory) { itemIcon?.setImageDrawable(folderDrawable) - itemDetails?.text = getChildrenCnt(listItem) + if(listItem.connectionType == ConnectionTypes.Default || listItem.connectionType == ConnectionTypes.SMB){ + itemDetails?.text = getChildrenCnt(listItem) + } + else{ + itemDetails?.text = noItemsText() + } itemDate?.beGone() } else { itemDetails?.text = listItem.size.formatSize() @@ -1106,6 +1114,10 @@ class ItemsAdapter( return activity.resources.getQuantityString(R.plurals.items, children, children) } + private fun noItemsText(): String { + return activity.resources.getString(R.string.unknown_items) + } + private fun getOTGPublicPath(itemToLoad: String): String { return "${baseConfig.OTGTreeUri}/document/${baseConfig.OTGPartition}%3A${ itemToLoad.substring(baseConfig.OTGPath.length).replace("/", "%2F") diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/CreateNewItemDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/CreateNewItemDialog.kt index e2aa6f525..67cb6081a 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/CreateNewItemDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/CreateNewItemDialog.kt @@ -2,16 +2,28 @@ package org.fossify.filemanager.dialogs import android.view.View import androidx.appcompat.app.AlertDialog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.* import org.fossify.commons.helpers.isRPlus import org.fossify.filemanager.R import org.fossify.filemanager.activities.SimpleActivity import org.fossify.filemanager.databinding.DialogCreateNewBinding import org.fossify.filemanager.helpers.RootHelpers +import org.fossify.filemanager.viewmodels.NetworkBrowserViewModel import java.io.File import java.io.IOException -class CreateNewItemDialog(val activity: SimpleActivity, val path: String, val callback: (success: Boolean) -> Unit) { +class CreateNewItemDialog( + val activity: SimpleActivity, + val path: String, + val connectionTypes: ConnectionTypes = ConnectionTypes.Default, + val viewModel: NetworkBrowserViewModel, + val callback: (success: Boolean) -> Unit +) { private val binding = DialogCreateNewBinding.inflate(activity.layoutInflater) init { @@ -32,6 +44,11 @@ class CreateNewItemDialog(val activity: SimpleActivity, val path: String, val ca return@OnClickListener } + if (connectionTypes != ConnectionTypes.Default) { + collectLatest(alertDialog) + createFileOrFolder(name, binding.dialogRadioGroup.checkedRadioButtonId == R.id.dialog_radio_directory) + return@OnClickListener + } if (binding.dialogRadioGroup.checkedRadioButtonId == R.id.dialog_radio_directory) { createDirectory(newPath, alertDialog) { callback(it) @@ -160,6 +177,89 @@ class CreateNewItemDialog(val activity: SimpleActivity, val path: String, val ca } } + + private fun createFileOrFolder(name: String, isFolder: Boolean) { + when (connectionTypes) { + ConnectionTypes.SMB -> { + viewModel.createFolderOrFileSMB(path, isFolder, name) + } + + ConnectionTypes.WebDav -> { + viewModel.createItem(path, isFolder, name) + } + + ConnectionTypes.SFTP -> { + viewModel.createItemSFTP(path, isFolder, name) + } + + ConnectionTypes.FTP -> { + viewModel.createItemFTP(path, isFolder, name) + } + + else -> Unit + } + } + + private fun collectLatest(alertDialog: AlertDialog) { + CoroutineScope(Dispatchers.IO).launch { + when (connectionTypes) { + ConnectionTypes.SMB -> { + viewModel.smbFolderOrFile.collectLatest { + if (it.response as Boolean) { + success(alertDialog) + } else { + it.exception?.message?.let { msg -> + activity.toast(msg) + success(alertDialog) + } + } + } + } + + ConnectionTypes.WebDav -> { + viewModel.webDavFolderOrFile.collectLatest { + if (it.response as Boolean) { + success(alertDialog) + } else { + it.exception?.message?.let { msg -> + activity.toast(msg) + success(alertDialog) + } + } + } + } + + ConnectionTypes.SFTP -> { + viewModel.sftpFolderOrFile.collectLatest { + if (it.response as Boolean) { + success(alertDialog) + } else { + it.exception?.message?.let { msg -> + activity.toast(msg) + success(alertDialog) + } + } + } + } + + ConnectionTypes.FTP -> { + viewModel.ftpFolderOrFile.collectLatest { + if (it.response as Boolean) { + success(alertDialog) + } else { + it.exception?.message?.let { msg -> + activity.toast(msg) + success(alertDialog) + } + } + } + } + + else -> Unit + } + } + } + private fun success(alertDialog: AlertDialog) { alertDialog.dismiss() callback(true) diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index 5ab9ac0b1..21fa0f9fb 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -5,9 +5,6 @@ import android.content.Context import android.net.Uri import android.os.Parcelable import android.util.AttributeSet -import android.util.Log -import android.widget.Toast -import androidx.activity.viewModels import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager @@ -17,6 +14,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import net.schmizz.sshj.sftp.RemoteResourceInfo import org.apache.commons.net.ftp.FTPFile import org.fossify.commons.activities.BaseSimpleActivity @@ -110,7 +108,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF progressBar.trackColor = properPrimaryColor.adjustAlpha(LOWER_ALPHA) if (currentPath != "") { - breadcrumbs.updateColor(textColor,connectionType) + breadcrumbs.updateColor(textColor, connectionType) } itemsSwipeRefresh.isEnabled = lastSearchedText.isEmpty() && activity?.config?.enablePullToRefresh != false @@ -163,7 +161,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF if (listItems.any { it.mIsDirectory } && listItems.any { !it.mIsDirectory }) { val firstFileIndex = listItems.indexOfFirst { !it.mIsDirectory } if (firstFileIndex != -1) { - val sectionTitle = ListItem("", "", false, 0, 0, 0, false, true) + val sectionTitle = ListItem("", "", false, 0, 0, 0, false, true, mConnectionType = connectionType) listItems.add(firstFileIndex, sectionTitle) } } @@ -213,11 +211,14 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF it?.let { item -> FileHelpers.launchFTP(connectionType, context = this@ItemsFragment.context, item = item) } + } else { + itemClicked(it as FileDirItem, connectionType) } } else if ((it as? ListItem)?.isSectionTitle == true) { openDirectory(it.mPath) searchClosed() - } else { + } + else { itemClicked(it as FileDirItem, connectionType) } }.apply { @@ -392,7 +393,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF private fun getListItemsFromFileDirItems(fileDirItems: ArrayList): ArrayList { val listItems = ArrayList() fileDirItems.forEach { - val listItem = ListItem(it.path, it.name, it.isDirectory, it.children, it.size, it.modified, false, false) + val listItem = ListItem(it.path, it.name, it.isDirectory, it.children, it.size, it.modified, false, false, mConnectionType = it.connectionType) if (wantedMimeTypes.any { mimeType -> isProperMimeType(mimeType, it.path, it.isDirectory) }) { listItems.add(listItem) } @@ -532,7 +533,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } private fun createNewItem() { - CreateNewItemDialog(activity as SimpleActivity, currentPath) { + CreateNewItemDialog(activity as SimpleActivity, currentPath, connectionType, viewModel) { if (it) { refreshFragment() } else { @@ -629,91 +630,89 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF if (connectionType == ConnectionTypes.SMB) { apiResponse?.response?.let { item -> val fileItems = item as Array - val items = fileItems.map { it -> it.toFileItem() } + val items = fileItems.map { it -> it.toFileItem(connectionType) } callback(path, getListItemsFromFileDirItems(ArrayList(items?.toList()))) } } else if (connectionType == ConnectionTypes.WebDav) { apiResponse?.response?.let { item -> val fileItems = item as List - val items = fileItems.map { it -> it.toFileItem() } + val items = fileItems.map { it -> it.toFileItem(connectionType) } callback(path, getListItemsFromFileDirItems(ArrayList(items?.toList()))) } } else if (connectionType == ConnectionTypes.SFTP) { apiResponse?.response?.let { item -> val fileItems = item as List - val items = fileItems?.map { it -> it.toFileItem(path) } + val items = fileItems?.map { it -> it.toFileItem(path, connectionType) } callback(path, getListItemsFromFileDirItems(ArrayList(items?.toList()))) } } else if (connectionType == ConnectionTypes.FTP) { apiResponse?.response?.let { item -> val fileItems = item as List - val items = fileItems?.map { it -> it.toFileItem(path) } + val items = fileItems?.map { it -> it.toFileItem(path, connectionType) } callback(path, getListItemsFromFileDirItems(ArrayList(items?.toList()))) } } } } - override fun columnCountChanged() { - (binding.itemsList.layoutManager as MyGridLayoutManager).spanCount = context!!.config.fileColumnCnt - (activity as? MainActivity)?.refreshMenuItems() - getRecyclerAdapter()?.apply { - notifyItemRangeChanged(0, listItems.size) - } + override fun columnCountChanged() { + (binding.itemsList.layoutManager as MyGridLayoutManager).spanCount = context!!.config.fileColumnCnt + (activity as? MainActivity)?.refreshMenuItems() + getRecyclerAdapter()?.apply { + notifyItemRangeChanged(0, listItems.size) } + } - fun showProgressBar() { - binding.progressBar.show() - } + fun showProgressBar() { + binding.progressBar.show() + } - private fun hideProgressBar() { - binding.progressBar.hide() - } + private fun hideProgressBar() { + binding.progressBar.hide() + } - fun getBreadcrumbs() = binding.breadcrumbs + fun getBreadcrumbs() = binding.breadcrumbs - override fun toggleFilenameVisibility() { - getRecyclerAdapter()?.updateDisplayFilenamesInGrid() - } + override fun toggleFilenameVisibility() { + getRecyclerAdapter()?.updateDisplayFilenamesInGrid() + } - override fun breadcrumbClicked(id: Int) { - val item = binding.breadcrumbs.getItem(id) - if (id == 0) { - StoragePickerDialog(activity as SimpleActivity, currentPath, context!!.config.enableRootAccess, true) { - getRecyclerAdapter()?.finishActMode() - if (item.connectionType != ConnectionTypes.Default){ - openPath(item.path, connectionType = item.connectionType) - } - else{ - openPath(item.path) - } + override fun breadcrumbClicked(id: Int) { + val item = binding.breadcrumbs.getItem(id) + if (id == 0) { + StoragePickerDialog(activity as SimpleActivity, currentPath, context!!.config.enableRootAccess, true) { + getRecyclerAdapter()?.finishActMode() + if (item.connectionType != ConnectionTypes.Default) { + openPath(item.path, connectionType = item.connectionType) + } else { + openPath(item.path) } - } else { - var path = "" - if (item.connectionType == ConnectionTypes.WebDav || item.connectionType == ConnectionTypes.SMB){ - val items = binding.breadcrumbs.getItemsTillIndex(id) - items.forEach { item -> - path += "${item.path}/" - } + } + } else { + var path = "" + if (item.connectionType == ConnectionTypes.WebDav || item.connectionType == ConnectionTypes.SMB) { + val items = binding.breadcrumbs.getItemsTillIndex(id) + items.forEach { item -> + path += "${item.path}/" } - else - path = item.path + } else + path = item.path - openPath(path, connectionType = item.connectionType) - } + openPath(path, connectionType = item.connectionType) } + } - override fun refreshFragment() { - openPath(currentPath,connectionType = connectionType) - } + override fun refreshFragment() { + openPath(currentPath, connectionType = connectionType) + } - override fun deleteFiles(files: ArrayList) { - val hasFolder = files.any { it.isDirectory } - handleFileDeleting(files, hasFolder) - } + override fun deleteFiles(files: ArrayList) { + val hasFolder = files.any { it.isDirectory } + handleFileDeleting(files, hasFolder) + } - override fun selectedPaths(paths: ArrayList) { - (activity as MainActivity).pickedPaths(paths) - } + override fun selectedPaths(paths: ArrayList) { + (activity as MainActivity).pickedPaths(paths) } +} diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt index c04a9aee1..298e145d1 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt @@ -15,5 +15,6 @@ interface FTPApi { fun getFTPFileInputStream(path: String,start: Long): ApiResponse + fun createItem(path: String, isFolder: Boolean, name: String): ApiResponse fun getFTPConn(): FTPClient } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt index 5713d78bd..9784c6f27 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt @@ -16,5 +16,7 @@ interface SFTPApi { fun getSFTPFileInputStream(url: String, startByte: Long): ApiResponse + fun createItem(path: String, isFolder: Boolean, name: String): ApiResponse + fun getSFTPConn(): SFTPClient } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt index 70d5d8f16..4c414309a 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt @@ -5,9 +5,11 @@ import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection interface SMBApi { - suspend fun verifyConnection(connection: NetworkConnection): Pair + suspend fun verifyConnection(connection: NetworkConnection): Pair fun getFilesFromNetworkPath(path: String): ApiResponse> + fun createFolderOrFile(path: String, isFolder: Boolean, name: String): ApiResponse + fun getMainSmbFile(): SmbFile } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt index 9811e2dce..b72b55890 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt @@ -14,5 +14,7 @@ interface WebDavApi { fun getWebDavFileInputStream(url: String, start: Long, end: Long): ApiResponse + fun createItem(path: String, isFolder: Boolean, name: String): ApiResponse + fun listWebDavFileDetail(url: String): ApiResponse } diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index 749117b41..19015e08e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -43,22 +43,36 @@ fun NetworkConnection.toEntity(): NetworkConnectionEntity { ) } -fun SmbFile.toFileItem(): FileDirItem { +fun SmbFile.toFileItem(connectionTypes: ConnectionTypes = ConnectionTypes.Default): FileDirItem { val normalizedPath = this.path.trimEnd('/') val fileName = normalizedPath.substringAfterLast('/') + + val isDir = this.isDirectory + + val childrenCount = if (isDir) { + try { + this.listFiles()?.size ?: 0 + } catch (e: Exception) { + 0 + } + } else { + 0 + } + return FileDirItem( path = this.canonicalPath, name = fileName.ifEmpty { "/" }, - isDirectory = this.isDirectory, + isDirectory = isDir, size = if (!this.isDirectory) this.length() else 0L, modified = this.lastModified(), - children = 0, - mediaStoreId = 0L + children = childrenCount, + mediaStoreId = 0L, + connectionType = connectionTypes ) } -fun DavResource.toFileItem(): FileDirItem { +fun DavResource.toFileItem(connectionTypes: ConnectionTypes = ConnectionTypes.Default): FileDirItem { return FileDirItem( path = this.href.toString(), name = this.name.trimEnd('/'), @@ -66,12 +80,13 @@ fun DavResource.toFileItem(): FileDirItem { size = if (!this.isDirectory) (this.contentLength ?: 0L) else 0L, modified = this.modified?.time ?: 0L, children = 0, - mediaStoreId = 0L + mediaStoreId = 0L, + connectionType = connectionTypes ) } -fun RemoteResourceInfo.toFileItem(parentPath: String): FileDirItem { +fun RemoteResourceInfo.toFileItem(parentPath: String,connectionType: ConnectionTypes = ConnectionTypes.Default): FileDirItem { val attrs = this.attributes val cleanParent = parentPath.trimEnd('/') @@ -82,11 +97,12 @@ fun RemoteResourceInfo.toFileItem(parentPath: String): FileDirItem { size = if (this.isRegularFile) attrs.size else 0L, modified = attrs.mtime * 1000L, children = 0, - mediaStoreId = 0L + mediaStoreId = 0L, + connectionType = connectionType ) } -fun FTPFile.toFileItem(parentPath: String): FileDirItem { +fun FTPFile.toFileItem(parentPath: String,connectionType: ConnectionTypes = ConnectionTypes.Default): FileDirItem { val cleanParent = parentPath.trimEnd('/') return FileDirItem( path = "$cleanParent/${this.name}", @@ -95,6 +111,7 @@ fun FTPFile.toFileItem(parentPath: String): FileDirItem { size = if (this.isFile) this.size else 0L, modified = this.timestamp?.timeInMillis ?: 0L, children = 0, - mediaStoreId = 0L + mediaStoreId = 0L, + connectionType = connectionType ) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/ListItem.kt b/app/src/main/kotlin/org/fossify/filemanager/models/ListItem.kt index 9b5573559..6b0a21672 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/models/ListItem.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/models/ListItem.kt @@ -1,9 +1,10 @@ package org.fossify.filemanager.models +import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.models.FileDirItem // isSectionTitle is used only at search results for showing the current folders path data class ListItem( val mPath: String, val mName: String = "", var mIsDirectory: Boolean = false, var mChildren: Int = 0, var mSize: Long = 0L, var mModified: Long = 0L, - var isSectionTitle: Boolean, val isGridTypeDivider: Boolean, var parent: String = "" -) : FileDirItem(mPath, mName, mIsDirectory, mChildren, mSize, mModified) + var isSectionTitle: Boolean, val isGridTypeDivider: Boolean, var parent: String = "", val mConnectionType: ConnectionTypes = ConnectionTypes.Default +) : FileDirItem(mPath, mName, mIsDirectory, mChildren, mSize, mModified, connectionType = mConnectionType) diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt index 12366e75e..13ba1c5cf 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt @@ -8,6 +8,7 @@ import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.interfaces.FTPApi import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection +import java.io.ByteArrayInputStream import java.io.File import java.io.InputStream @@ -83,6 +84,17 @@ class FTPApiImpl: FTPApi { } } + override fun createItem(path: String, isFolder: Boolean, name: String): ApiResponse { + return try { + val uri = "$path/$name" + if (isFolder) ftp.makeDirectory(uri) else ftp.storeFile(uri, ByteArrayInputStream(ByteArray(0))) + ApiResponse(true,null) + } + catch (exp: Exception){ + ApiResponse(false,exp) + } + } + override fun getFTPConn(): FTPClient = ftp } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt index 3059dd78b..7b0b54175 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt @@ -5,6 +5,7 @@ import net.schmizz.sshj.DefaultConfig import net.schmizz.sshj.SSHClient import net.schmizz.sshj.common.Factory import net.schmizz.sshj.sftp.FileAttributes +import net.schmizz.sshj.sftp.OpenMode import net.schmizz.sshj.sftp.RemoteResourceInfo import net.schmizz.sshj.sftp.SFTPClient import net.schmizz.sshj.transport.verification.PromiscuousVerifier @@ -19,7 +20,7 @@ import org.fossify.filemanager.models.NetworkConnection import java.io.IOException import java.io.InputStream -class SFTPApiImpl: SFTPApi { +class SFTPApiImpl : SFTPApi { private lateinit var ssh: SSHClient private lateinit var sftp: SFTPClient private val sftpLock = Any() @@ -29,29 +30,30 @@ class SFTPApiImpl: SFTPApi { if (!::ssh.isInitialized || !ssh.isConnected || !ssh.isAuthenticated) { ssh = SSHClient() ssh.addHostKeyVerifier(PromiscuousVerifier()) - ssh.connect(connection.host,connection.port) - if(connection.authentication == Authentication.PrivateKey){ - ssh.auth(connection.username, AuthPublickey(createKeyProvider(connection.privateKeyText,connection.privateKeyPass.takeIf { it.isNotBlank() }))) - } - else{ + ssh.connect(connection.host, connection.port) + if (connection.authentication == Authentication.PrivateKey) { + ssh.auth( + connection.username, + AuthPublickey(createKeyProvider(connection.privateKeyText, connection.privateKeyPass.takeIf { it.isNotBlank() })) + ) + } else { ssh.authPassword(connection.username, connection.password) } sftp = ssh.newSFTPClient() } - Pair(true,null) + Pair(true, null) } catch (e: Exception) { e.printStackTrace() - Pair(false,e) + Pair(false, e) } } override suspend fun listAllFilesSFTPRoot(path: String): ApiResponse> { return try { val files = sftp.ls(path) - ApiResponse(files,null) - } - catch (exp: Exception){ - ApiResponse(null,exp) + ApiResponse(files, null) + } catch (exp: Exception) { + ApiResponse(null, exp) } } @@ -60,26 +62,34 @@ class SFTPApiImpl: SFTPApi { return try { val myPath = path.replace("//", "/") val attributes = sftp.stat(myPath) - ApiResponse(attributes,null) - } - catch (exp: Exception){ - ApiResponse(null,exp) + ApiResponse(attributes, null) + } catch (exp: Exception) { + ApiResponse(null, exp) } } - override fun getSFTPFileInputStream(url: String, startByte: Long):ApiResponse { + override fun getSFTPFileInputStream(url: String, startByte: Long): ApiResponse { return try { val myPath = url.replace("//", "/") val remoteFile = sftp.open(myPath) val inputStream = remoteFile.RemoteFileInputStream(startByte) - ApiResponse(inputStream,null) - } - catch (exp: Exception){ - ApiResponse(null,exp) + ApiResponse(inputStream, null) + } catch (exp: Exception) { + ApiResponse(null, exp) } } + override fun createItem(path: String, isFolder: Boolean, name: String): ApiResponse { + return try { + val uri = "$path/$name" + if (isFolder) sftp.mkdir(uri) else sftp.open(uri, setOf(OpenMode.CREAT)) + ApiResponse(true, null) + } catch (exp: Exception) { + ApiResponse(false, exp) + } + } + override fun getSFTPConn() = sftp private val KEY_PROVIDER_FACTORIES = DefaultConfig().fileKeyProviderFactories diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt index c1c1962e0..17f257496 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt @@ -1,6 +1,5 @@ package org.fossify.filemanager.repository -import android.util.Log import jcifs.CIFSContext import jcifs.config.PropertyConfiguration import jcifs.context.BaseContext @@ -61,5 +60,16 @@ class SMBApiImpl : SMBApi { } } + override fun createFolderOrFile(path: String, isFolder: Boolean, name: String): ApiResponse { + return try { + val file = SmbFile("$path/$name", smbClient.context) + if (isFolder) file.mkdir() else file.createNewFile() + ApiResponse(true,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) + } + } + override fun getMainSmbFile(): SmbFile = smbClient } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt index fca19f4ac..e2f0a9262 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt @@ -36,7 +36,6 @@ class WebDavApiImpl: WebDavApi { sardine.setCredentials(connection.username, connection.password) Pair(sardine.exists(connection.url),null) } catch (exp: Exception) { - Log.d("WebDav", exp.toString()) Pair(false,exp) } } @@ -68,6 +67,17 @@ class WebDavApiImpl: WebDavApi { } } + override fun createItem(path: String, isFolder: Boolean, name: String): ApiResponse { + return try { + val uri = "$path/$name" + if (isFolder) sardine.createDirectory(uri) else sardine.put(uri, ByteArray(0)) + ApiResponse(true,null) + } + catch (exp: Exception){ + ApiResponse(false,exp) + } + } + override fun listWebDavFileDetail(url: String): ApiResponse { return try { diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 6afe37bb1..6e180af67 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -4,18 +4,12 @@ import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.thegrizzlylabs.sardineandroid.DavResource -import jcifs.CIFSContext -import jcifs.Configuration -import jcifs.config.PropertyConfiguration -import jcifs.context.BaseContext -import jcifs.smb.NtlmPasswordAuthenticator import jcifs.smb.SmbFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import net.schmizz.sshj.sftp.FileAttributes import net.schmizz.sshj.sftp.RemoteResourceInfo import net.schmizz.sshj.sftp.SFTPClient import org.apache.commons.net.ftp.FTPFile @@ -28,7 +22,6 @@ import org.fossify.filemanager.interfaces.WebDavApi import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.ConnectionResult import org.fossify.filemanager.models.NetworkConnection -import java.io.InputStream class NetworkBrowserViewModel( private val networkConnectionRepository: NetworkConnectionRepositoryDb, @@ -40,6 +33,7 @@ class NetworkBrowserViewModel( val savedNetworks = MutableStateFlow>(emptyList()) val verifyNetwork = MutableSharedFlow() + val smbFolderOrFile = MutableSharedFlow>() val verifyWebDav = MutableSharedFlow() @@ -48,10 +42,14 @@ class NetworkBrowserViewModel( val sftpFiles = MutableStateFlow>?>(null) + val sftpFolderOrFile = MutableSharedFlow>() val webDavFiles = MutableStateFlow< ApiResponse>?>(null) + val webDavFolderOrFile = MutableSharedFlow>() val ftpFiles = MutableStateFlow>?>(null) + val ftpFolderOrFile = MutableSharedFlow>() + fun saveNetwork(networkConnection: NetworkConnection) { @@ -79,6 +77,13 @@ class NetworkBrowserViewModel( return smbApi.getFilesFromNetworkPath(path) } + fun createFolderOrFileSMB(path: String, isFolder: Boolean, name: String) { + viewModelScope.launch(Dispatchers.IO) { + smbFolderOrFile.emit(smbApi.createFolderOrFile(path, isFolder,name)) + } + } + + fun getMainSmb(): SmbFile = smbApi.getMainSmbFile() fun getSFTPConn(): SFTPClient = sftpApi.getSFTPConn() @@ -96,6 +101,12 @@ class NetworkBrowserViewModel( } } + fun createItem(path: String, isFolder: Boolean, name: String) { + viewModelScope.launch(Dispatchers.IO) { + webDavFolderOrFile.emit(webDavApi.createItem(path, isFolder,name)) + } + } + // fun listWebDavFileStream(url: String, start: Long, end: Long): InputStream { // return webDavApi.getWebDavFileInputStream(url, start, end) // } @@ -118,6 +129,12 @@ class NetworkBrowserViewModel( } } + fun createItemSFTP(path: String, isFolder: Boolean, name: String) { + viewModelScope.launch(Dispatchers.IO) { + sftpFolderOrFile.emit(sftpApi.createItem(path, isFolder,name)) + } + } + // fun listAllFilesSFTPPath(path: String) { // viewModelScope.launch(Dispatchers.IO) { // val res = sftpApi.listAllFilesSFTPRoot(path) @@ -149,6 +166,12 @@ class NetworkBrowserViewModel( } } + fun createItemFTP(path: String, isFolder: Boolean, name: String) { + viewModelScope.launch(Dispatchers.IO) { + ftpFolderOrFile.emit(sftpApi.createItem(path, isFolder,name)) + } + } + fun getFTPFileDetail(path: String) = ftpApi.getFTPFileDetail(path) fun getFTPFileStream(path: String, start: Long) = ftpApi.getFTPFileInputStream(path, start) From 115f53313b2d644e8a66e8dfcb5e84e9ab07175e Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Thu, 28 May 2026 23:47:11 +0500 Subject: [PATCH 27/37] Added multiple file handling features --- .../activities/MimeTypesActivity.kt | 11 + .../filemanager/adapters/ItemsAdapter.kt | 40 ++- .../filemanager/extensions/Activity.kt | 9 + .../filemanager/fileSystems/FileHelpers.kt | 83 +++--- .../filemanager/fileSystems/MimeTypes.kt | 14 ++ .../filemanager/fragments/ItemsFragment.kt | 237 +++++++++++++++++- .../filemanager/fragments/RecentsFragment.kt | 12 + .../filemanager/fragments/StorageFragment.kt | 12 + .../fossify/filemanager/interfaces/FTPApi.kt | 5 +- .../interfaces/ItemOperationsListener.kt | 6 + .../fossify/filemanager/interfaces/SFTPApi.kt | 4 + .../fossify/filemanager/interfaces/SMBApi.kt | 6 + .../filemanager/interfaces/WebDavApi.kt | 3 + .../mapper/NetworkConnectionMapper.kt | 4 +- .../filemanager/repository/FTPApiImpl.kt | 70 ++++-- .../filemanager/repository/SFTPApiImpl.kt | 35 ++- .../filemanager/repository/SMBApiImpl.kt | 35 +++ .../filemanager/repository/WebDavApiImpl.kt | 36 +++ .../viewmodels/NetworkBrowserViewModel.kt | 59 +++++ 19 files changed, 617 insertions(+), 64 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/MimeTypesActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/MimeTypesActivity.kt index a8ff25e13..66c9026f6 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/MimeTypesActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/MimeTypesActivity.kt @@ -225,6 +225,17 @@ class MimeTypesActivity : SimpleActivity(), ItemOperationsListener { } override fun finishActMode() {} + override fun shareFile(paths: ArrayList) { + TODO("Not yet implemented") + } + + override fun openWith(path: String,mimType: String?) { + TODO("Not yet implemented") + } + + override fun setAs(path: String) { + TODO("Not yet implemented") + } private fun setupSearch(menu: Menu) { val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager diff --git a/app/src/main/kotlin/org/fossify/filemanager/adapters/ItemsAdapter.kt b/app/src/main/kotlin/org/fossify/filemanager/adapters/ItemsAdapter.kt index a91897f94..03b68cead 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/adapters/ItemsAdapter.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/adapters/ItemsAdapter.kt @@ -9,8 +9,6 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.graphics.drawable.LayerDrawable import android.net.Uri -import android.provider.Settings.Global.getString -import android.util.Log import android.util.TypedValue import android.view.LayoutInflater import android.view.Menu @@ -108,6 +106,7 @@ import org.fossify.filemanager.extensions.setLastModified import org.fossify.filemanager.extensions.sharePaths import org.fossify.filemanager.extensions.toggleItemVisibility import org.fossify.filemanager.extensions.tryOpenPathIntent +import org.fossify.filemanager.fileSystems.MimeTypes import org.fossify.filemanager.helpers.OPEN_AS_AUDIO import org.fossify.filemanager.helpers.OPEN_AS_IMAGE import org.fossify.filemanager.helpers.OPEN_AS_OTHER @@ -356,7 +355,13 @@ class ItemsAdapter( selectedItems.forEach { addFileUris(it.path, paths) } - activity.sharePaths(paths) + + if (selectedItems.first().connectionType != ConnectionTypes.Default){ + listener?.shareFile(paths) + } + else{ + activity.sharePaths(paths) + } } private fun toggleFileVisibility(hide: Boolean) { @@ -481,11 +486,23 @@ class ItemsAdapter( } private fun setAs() { - activity.setAs(getFirstSelectedItemPath()) + val item = getSelectedFileDirItems().first() + if (item.connectionType != ConnectionTypes.Default){ + listener?.setAs(item.path) + } + else { + activity.setAs(item.path) + } } private fun openWith() { - activity.tryOpenPathIntent(getFirstSelectedItemPath(), true) + val item = getSelectedFileDirItems().first() + if (item.connectionType != ConnectionTypes.Default){ + listener?.openWith(item.path) + } + else { + activity.tryOpenPathIntent(item.path, true) + } } private fun openAs() { @@ -498,8 +515,15 @@ class ItemsAdapter( RadioItem(OPEN_AS_OTHER, res.getString(R.string.other_file)) ) + val item = getSelectedFileDirItems().first() + RadioGroupDialog(activity, items) { - activity.tryOpenPathIntent(getFirstSelectedItemPath(), false, it as Int) + if (item.connectionType != ConnectionTypes.Default){ + listener?.openWith(item.path, MimeTypes.getMimeType(it as Int)) + } + else { + activity.tryOpenPathIntent(item.path, false,it as Int) + } } } @@ -910,7 +934,6 @@ class ItemsAdapter( } else { resources.getQuantityString(R.plurals.delete_items, itemsCnt, itemsCnt) } - val question = String.format(resources.getString(R.string.deletion_confirmation), items) ConfirmationDialog(activity, question) { deleteFiles() @@ -924,7 +947,8 @@ class ItemsAdapter( } val SAFPath = getFirstSelectedItemPath() - if (activity.isPathOnRoot(SAFPath) && !RootTools.isRootAvailable()) { + val firstSelectedItem = getSelectedFileDirItems().first() + if (activity.isPathOnRoot(SAFPath) && !RootTools.isRootAvailable() && firstSelectedItem.connectionType == ConnectionTypes.Default) { activity.toast(R.string.rooted_device_only) return } diff --git a/app/src/main/kotlin/org/fossify/filemanager/extensions/Activity.kt b/app/src/main/kotlin/org/fossify/filemanager/extensions/Activity.kt index 82603c18f..e81fc1337 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/extensions/Activity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/extensions/Activity.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Intent import androidx.core.content.FileProvider import org.fossify.commons.activities.BaseSimpleActivity +import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.getFilenameFromPath import org.fossify.commons.extensions.getMimeTypeFromUri import org.fossify.commons.extensions.getParentPath @@ -11,6 +12,8 @@ import org.fossify.commons.extensions.launchActivityIntent import org.fossify.commons.extensions.openPathIntent import org.fossify.commons.extensions.renameFile import org.fossify.commons.extensions.setAsIntent +import org.fossify.commons.extensions.setAsNetworkIntent +import org.fossify.commons.extensions.shareNetworkPathsIntent import org.fossify.commons.extensions.sharePathsIntent import org.fossify.filemanager.BuildConfig import org.fossify.filemanager.helpers.OPEN_AS_AUDIO @@ -24,6 +27,9 @@ fun Activity.sharePaths(paths: ArrayList) { sharePathsIntent(paths, BuildConfig.APPLICATION_ID) } +fun Activity.networkSharePaths(paths: ArrayList,file: File) { + shareNetworkPathsIntent(paths, BuildConfig.APPLICATION_ID,file) +} fun Activity.tryOpenPathIntent(path: String, forceChooser: Boolean, openAsType: Int = OPEN_AS_DEFAULT, finishActivity: Boolean = false) { if (!forceChooser && path.endsWith(".apk", true)) { val uri = FileProvider.getUriForFile( @@ -61,6 +67,9 @@ private fun getMimeType(type: Int) = when (type) { fun Activity.setAs(path: String) { setAsIntent(path, BuildConfig.APPLICATION_ID) } +fun Activity.setAsNetworkPath(path: String,file: File) { + setAsNetworkIntent(path, BuildConfig.APPLICATION_ID,file) +} fun BaseSimpleActivity.toggleItemVisibility(oldPath: String, hide: Boolean, callback: ((newPath: String) -> Unit)? = null) { val path = oldPath.getParentPath() diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt index ff62bd865..d112ef673 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -17,14 +17,21 @@ import org.fossify.filemanager.models.ListItem object FileHelpers { val URL: String = "http://127.0.0.1" - fun launchSMB(item: ListItem, context: Context, smb: SmbFile) { + fun launchSMB(mPath: String, context: Context, mimType: String? = null) { try { CoroutineScope(Dispatchers.IO).launch { - val extractedPath = Helpers.retrievePath(item.mPath) + val extractedPath = Helpers.retrievePath(mPath) val uri = "${URL}:${PORT_SMB}${extractedPath}" - val i = - Intent(Intent.ACTION_VIEW) - i.setDataAndType(uri.toUri(), MimeTypes.getMimeTypes(item.mPath)) + var i: Intent + if (mimType != null) { + i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri.toUri(), mimType) + } else { + i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri.toUri(), MimeTypes.getMimeTypes(mPath)) + } val packageManager: PackageManager = context.packageManager val resInfos = packageManager.queryIntentActivities(i, 0) if (resInfos.size > 0) { @@ -36,35 +43,49 @@ object FileHelpers { } } - fun launchWebDav(connectionTypes: ConnectionTypes, item: ListItem, context: Context){ + fun launchWebDav(mPath: String, context: Context, mimType: String? = null) { try { CoroutineScope(Dispatchers.IO).launch { - val i = - Intent(Intent.ACTION_VIEW) - val extractedPath = Helpers.retrievePath(item.mPath) + + val extractedPath = Helpers.retrievePath(mPath) val uri = "${URL}:${PORT_WEBDAV}/${extractedPath}" - i.setDataAndType(uri.toUri(), MimeTypes.getMimeTypes(item.mPath)) + var i: Intent + if (mimType != null) { + i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri.toUri(), mimType) + } else { + i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri.toUri(), MimeTypes.getMimeTypes(mPath)) + } val packageManager: PackageManager = context.packageManager val resInfos = packageManager.queryIntentActivities(i, 0) if (resInfos.size > 0) { context.startActivity(i) } } - } - catch (exp: Exception){ + } catch (exp: Exception) { Log.e("Activity Launch Failed", exp.toString()) } } - fun launchSFTP(connectionTypes: ConnectionTypes,item: ListItem,context: Context){ - try{ + fun launchSFTP(connectionTypes: ConnectionTypes, mPath: String, context: Context,mimType: String? = null) { + try { CoroutineScope(Dispatchers.IO).launch { val port = Helpers.getPortForEachService(connectionTypes) - val uri = Helpers.createNanoHttpdUrl(connectionTypes, item.mPath, port = port).toUri() - val i = - Intent(Intent.ACTION_VIEW) - i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) + val uri = Helpers.createNanoHttpdUrl(connectionTypes, mPath, port = port).toUri() + var i: Intent + if (mimType != null) { + i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri, mimType) + } else { + i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri, MimeTypes.getMimeTypes(mPath)) + } val packageManager: PackageManager = context.packageManager val resInfos = packageManager.queryIntentActivities(i, 0) @@ -72,19 +93,26 @@ object FileHelpers { context.startActivity(i) } } - } - catch (exp: Exception){ + } catch (exp: Exception) { Log.e("Activity Launch Failed", exp.toString()) } } - fun launchFTP(connectionTypes: ConnectionTypes,item: ListItem,context: Context){ - try{ + + fun launchFTP(connectionTypes: ConnectionTypes, mPath: String, context: Context,mimType: String? = null) { + try { CoroutineScope(Dispatchers.IO).launch { val port = Helpers.getPortForEachService(connectionTypes) - val uri = Helpers.createNanoHttpdUrl(connectionTypes, item.mPath, port = port).toUri() - val i = - Intent(Intent.ACTION_VIEW) - i.setDataAndType(uri, MimeTypes.getMimeTypes(item.mPath)) + val uri = Helpers.createNanoHttpdUrl(connectionTypes, mPath, port = port).toUri() + var i: Intent + if (mimType != null) { + i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri,mimType) + } else { + i = + Intent(Intent.ACTION_VIEW) + i.setDataAndType(uri, MimeTypes.getMimeTypes(mPath)) + } val packageManager: PackageManager = context.packageManager val resInfos = packageManager.queryIntentActivities(i, 0) @@ -92,8 +120,7 @@ object FileHelpers { context.startActivity(i) } } - } - catch (exp: Exception){ + } catch (exp: Exception) { Log.e("Activity Launch Failed", exp.toString()) } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/MimeTypes.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/MimeTypes.kt index 8dec85d78..7b92277f7 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/MimeTypes.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/MimeTypes.kt @@ -1,6 +1,11 @@ package org.fossify.filemanager.fileSystems import android.webkit.MimeTypeMap +import org.fossify.filemanager.helpers.OPEN_AS_AUDIO +import org.fossify.filemanager.helpers.OPEN_AS_DEFAULT +import org.fossify.filemanager.helpers.OPEN_AS_IMAGE +import org.fossify.filemanager.helpers.OPEN_AS_TEXT +import org.fossify.filemanager.helpers.OPEN_AS_VIDEO import java.util.Locale.getDefault object MimeTypes { @@ -14,4 +19,13 @@ object MimeTypes { extension = mime.getMimeTypeFromExtension(extension) return extension } + + public fun getMimeType(type: Int) = when (type) { + OPEN_AS_DEFAULT -> "" + OPEN_AS_TEXT -> "text/*" + OPEN_AS_IMAGE -> "image/*" + OPEN_AS_AUDIO -> "audio/*" + OPEN_AS_VIDEO -> "video/*" + else -> "*/*" + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index 21fa0f9fb..c88e4a5b3 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -14,7 +14,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import net.schmizz.sshj.sftp.RemoteResourceInfo import org.apache.commons.net.ftp.FTPFile import org.fossify.commons.activities.BaseSimpleActivity @@ -35,6 +34,8 @@ import org.fossify.filemanager.databinding.ItemsFragmentBinding import org.fossify.filemanager.dialogs.CreateNewItemDialog import org.fossify.filemanager.extensions.config import org.fossify.filemanager.extensions.isPathOnRoot +import org.fossify.filemanager.extensions.networkSharePaths +import org.fossify.filemanager.extensions.setAsNetworkPath import org.fossify.filemanager.fileSystems.FileHelpers import org.fossify.filemanager.helpers.MAX_COLUMN_COUNT import org.fossify.filemanager.helpers.RootHelpers @@ -130,6 +131,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF getRecyclerAdapter()?.finishActMode() } + fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, connectionType: ConnectionTypes = ConnectionTypes.Default) { if ((activity as? BaseSimpleActivity)?.isAskingPermissions == true) { return @@ -197,19 +199,19 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF if ((it as? ListItem)?.mIsDirectory == false) { if (connectionType == ConnectionTypes.SMB) { it?.let { item -> - FileHelpers.launchSMB(item, this@ItemsFragment.context, viewModel.getMainSmb()) + FileHelpers.launchSMB(item.mPath, this@ItemsFragment.context) } } else if (connectionType == ConnectionTypes.WebDav) { it?.let { item -> - FileHelpers.launchWebDav(connectionType, context = this@ItemsFragment.context, item = item) + FileHelpers.launchWebDav(item.mPath, context = this@ItemsFragment.context) } } else if (connectionType == ConnectionTypes.SFTP) { it?.let { item -> - FileHelpers.launchSFTP(connectionType, context = this@ItemsFragment.context, item = item) + FileHelpers.launchSFTP(connectionType,item.mPath, context = this@ItemsFragment.context, ) } } else if (connectionType == ConnectionTypes.FTP) { it?.let { item -> - FileHelpers.launchFTP(connectionType, context = this@ItemsFragment.context, item = item) + FileHelpers.launchFTP(connectionType,item.mPath, context = this@ItemsFragment.context) } } else { itemClicked(it as FileDirItem, connectionType) @@ -217,8 +219,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } else if ((it as? ListItem)?.isSectionTitle == true) { openDirectory(it.mPath) searchClosed() - } - else { + } else { itemClicked(it as FileDirItem, connectionType) } }.apply { @@ -708,11 +709,231 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } override fun deleteFiles(files: ArrayList) { + if (connectionType != ConnectionTypes.Default) { + collectLatest() + } val hasFolder = files.any { it.isDirectory } - handleFileDeleting(files, hasFolder) + deleteFileOrFolder(files, hasFolder) + } + + + private fun deleteFileOrFolder(files: ArrayList, hasFolder: Boolean) { + when (connectionType) { + ConnectionTypes.SMB -> { + files.forEach { + viewModel.deleteItemSMB(it.path) + } + } + + ConnectionTypes.WebDav -> { + files.forEach { + viewModel.deleteItemWebDav(it.path) + } + } + + ConnectionTypes.SFTP -> { + files.forEach { + viewModel.deleteItemSFTP(it.path, it.isDirectory) + } + } + + ConnectionTypes.FTP -> { + files.forEach { + viewModel.deleteItemFTP(it.path, it.isDirectory) + } + } + + ConnectionTypes.Default -> { + handleFileDeleting(files, hasFolder) + } + + else -> Unit + } + } + + private fun collectLatest() { + CoroutineScope(Dispatchers.IO).launch { + when (connectionType) { + ConnectionTypes.SMB -> { + viewModel.smbDelete.collectLatest { + it.exception?.message?.let { msg -> + activity?.toast(msg) + } + } + } + + ConnectionTypes.WebDav -> { + viewModel.webDavDelete.collectLatest { + it.exception?.message?.let { msg -> + activity?.toast(msg) + } + } + } + + ConnectionTypes.SFTP -> { + viewModel.sftpDelete.collectLatest { + it.exception?.message?.let { msg -> + activity?.toast(msg) + } + } + } + + ConnectionTypes.FTP -> { + viewModel.ftpDelete.collectLatest { + it.exception?.message?.let { msg -> + activity?.toast(msg) + } + } + } + + else -> Unit + } + } } override fun selectedPaths(paths: ArrayList) { (activity as MainActivity).pickedPaths(paths) } + + override fun shareFile(paths: ArrayList) { + collectFileShared(paths) + if (connectionType == ConnectionTypes.SMB) { + paths.forEach { + viewModel.writeSmbFileToCache(it, context) + } + } else if (connectionType == ConnectionTypes.WebDav) { + paths.forEach { + viewModel.writeWebDavFileToCache(it, context) + } + } else if (connectionType == ConnectionTypes.SFTP) { + paths.forEach { + viewModel.writeSftpFileToCache(it, context) + } + } else if (connectionType == ConnectionTypes.FTP) { + paths.forEach { + viewModel.writeFtpFileToCache(it, context) + } + } + } + + override fun openWith(path: String, mimType: String?) { + when(connectionType){ + ConnectionTypes.SMB -> { + FileHelpers.launchSMB(path, context,mimType) + } + ConnectionTypes.WebDav -> { + FileHelpers.launchWebDav( path, context,mimType) + } + ConnectionTypes.SFTP -> { + FileHelpers.launchSFTP(connectionType, path, context,mimType) + } + ConnectionTypes.FTP -> { + FileHelpers.launchFTP(connectionType, path, context,mimType) + } + else -> Unit + } + } + + + private fun collectFileShared(paths: ArrayList) { + CoroutineScope(Dispatchers.IO).launch { + when (connectionType) { + ConnectionTypes.SMB -> { + viewModel.smbFileShare.collectLatest { + handleFileSharedResponse(it, paths) + } + } + + ConnectionTypes.WebDav -> { + viewModel.webDavFileShare.collectLatest { + handleFileSharedResponse(it, paths) + } + } + + ConnectionTypes.SFTP -> { + viewModel.sftpFileShare.collectLatest { + handleFileSharedResponse(it, paths) + } + } + + ConnectionTypes.FTP -> { + viewModel.ftpFileShare.collectLatest { + handleFileSharedResponse(it, paths) + } + } + + else -> Unit + } + } + } + + private fun handleFileSharedResponse(response: ApiResponse, paths: ArrayList) { + if (response.exception != null) { + response.exception?.message?.let { msg -> + activity?.toast(msg) + } + } else { + activity?.networkSharePaths(paths, response.response as File) + } + } + + private fun handleFileWriteResponse(response: ApiResponse, paths: String) { + if (response.exception != null) { + response.exception?.message?.let { msg -> + activity?.toast(msg) + } + } else { + activity?.setAsNetworkPath(paths, response.response as File) + } + } + + + override fun setAs(path: String) { + collectFileCopied(path) + if (connectionType == ConnectionTypes.SMB) { + viewModel.writeSmbFileToCache(path, context) + + } else if (connectionType == ConnectionTypes.WebDav) { + viewModel.writeWebDavFileToCache(path, context) + + } else if (connectionType == ConnectionTypes.SFTP) { + viewModel.writeSftpFileToCache(path, context) + + } else if (connectionType == ConnectionTypes.FTP) { + viewModel.writeFtpFileToCache(path, context) + } + } + + private fun collectFileCopied(path: String) { + CoroutineScope(Dispatchers.IO).launch { + when (connectionType) { + ConnectionTypes.SMB -> { + viewModel.smbFileShare.collectLatest { + handleFileWriteResponse(it, path) + } + } + + ConnectionTypes.WebDav -> { + viewModel.webDavFileShare.collectLatest { + handleFileWriteResponse(it, path) + } + } + + ConnectionTypes.SFTP -> { + viewModel.sftpFileShare.collectLatest { + handleFileWriteResponse(it, path) + } + } + + ConnectionTypes.FTP -> { + viewModel.ftpFileShare.collectLatest { + handleFileWriteResponse(it, path) + } + } + + else -> Unit + } + } + } + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/RecentsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/RecentsFragment.kt index 910877a95..db53f25c5 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/RecentsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/RecentsFragment.kt @@ -261,4 +261,16 @@ class RecentsFragment(context: Context, attributeSet: AttributeSet) : MyViewPage override fun finishActMode() { getRecyclerAdapter()?.finishActMode() } + + override fun shareFile(paths: ArrayList) { + TODO("Not yet implemented") + } + + override fun openWith(path: String,mimType: String?) { + TODO("Not yet implemented") + } + + override fun setAs(path: String) { + TODO("Not yet implemented") + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/StorageFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/StorageFragment.kt index ecbbaccf8..3588007e3 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/StorageFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/StorageFragment.kt @@ -509,4 +509,16 @@ class StorageFragment(context: Context, attributeSet: AttributeSet) : MyViewPage override fun finishActMode() { getRecyclerAdapter()?.finishActMode() } + + override fun shareFile(paths: ArrayList) { + TODO("Not yet implemented") + } + + override fun openWith(path: String,mimType: String?) { + TODO("Not yet implemented") + } + + override fun setAs(path: String) { + TODO("Not yet implemented") + } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt index 298e145d1..196a05ce4 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/FTPApi.kt @@ -1,9 +1,11 @@ package org.fossify.filemanager.interfaces +import android.content.Context import org.apache.commons.net.ftp.FTPClient import org.apache.commons.net.ftp.FTPFile import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection +import java.io.File import java.io.InputStream interface FTPApi { @@ -14,7 +16,8 @@ interface FTPApi { fun getFTPFileDetail(path: String): ApiResponse fun getFTPFileInputStream(path: String,start: Long): ApiResponse - + fun deleteItem(path: String,isFolder: Boolean): ApiResponse fun createItem(path: String, isFolder: Boolean, name: String): ApiResponse + fun writeFileToCache(path: String,context: Context): ApiResponse fun getFTPConn(): FTPClient } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/ItemOperationsListener.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/ItemOperationsListener.kt index 2812527df..6fd3567d3 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/ItemOperationsListener.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/ItemOperationsListener.kt @@ -18,4 +18,10 @@ interface ItemOperationsListener { fun columnCountChanged() fun finishActMode() + + fun shareFile(paths: ArrayList) + + fun openWith(path: String,mimType:String? = null) + + fun setAs(path: String) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt index 9784c6f27..6cba92ef3 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SFTPApi.kt @@ -1,10 +1,12 @@ package org.fossify.filemanager.interfaces +import android.content.Context import net.schmizz.sshj.sftp.FileAttributes import net.schmizz.sshj.sftp.RemoteResourceInfo import net.schmizz.sshj.sftp.SFTPClient import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection +import java.io.File import java.io.InputStream interface SFTPApi { @@ -17,6 +19,8 @@ interface SFTPApi { fun getSFTPFileInputStream(url: String, startByte: Long): ApiResponse fun createItem(path: String, isFolder: Boolean, name: String): ApiResponse + fun deleteItem(path: String,isFolder: Boolean): ApiResponse + fun writeFileToCache(path: String,context: Context): ApiResponse fun getSFTPConn(): SFTPClient } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt index 4c414309a..04ada07f7 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt @@ -1,8 +1,10 @@ package org.fossify.filemanager.interfaces +import android.content.Context import jcifs.smb.SmbFile import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection +import java.io.File interface SMBApi { suspend fun verifyConnection(connection: NetworkConnection): Pair @@ -11,5 +13,9 @@ interface SMBApi { fun createFolderOrFile(path: String, isFolder: Boolean, name: String): ApiResponse + fun deleteItem(path: String): ApiResponse + + fun writeFileToCache(path: String,context: Context): ApiResponse + fun getMainSmbFile(): SmbFile } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt index b72b55890..696d9d92e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt @@ -5,6 +5,7 @@ import com.thegrizzlylabs.sardineandroid.DavResource import org.fossify.filemanager.enums.Protocols import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection +import java.io.File import java.io.InputStream interface WebDavApi { @@ -15,6 +16,8 @@ interface WebDavApi { fun getWebDavFileInputStream(url: String, start: Long, end: Long): ApiResponse fun createItem(path: String, isFolder: Boolean, name: String): ApiResponse + fun deleteItem(path: String): ApiResponse + fun writeFileToCache(url: String, context: Context): ApiResponse fun listWebDavFileDetail(url: String): ApiResponse } diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index 19015e08e..c566b4842 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -61,7 +61,7 @@ fun SmbFile.toFileItem(connectionTypes: ConnectionTypes = ConnectionTypes.Defaul } return FileDirItem( - path = this.canonicalPath, + path = this.canonicalPath.trimEnd('/'), name = fileName.ifEmpty { "/" }, isDirectory = isDir, size = if (!this.isDirectory) this.length() else 0L, @@ -74,7 +74,7 @@ fun SmbFile.toFileItem(connectionTypes: ConnectionTypes = ConnectionTypes.Defaul fun DavResource.toFileItem(connectionTypes: ConnectionTypes = ConnectionTypes.Default): FileDirItem { return FileDirItem( - path = this.href.toString(), + path = this.href.toString().trimEnd('/'), name = this.name.trimEnd('/'), isDirectory = this.isDirectory, size = if (!this.isDirectory) (this.contentLength ?: 0L) else 0L, diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt index 13ba1c5cf..a16d06d49 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/FTPApiImpl.kt @@ -1,5 +1,9 @@ package org.fossify.filemanager.repository +import android.content.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.apache.commons.net.ftp.FTP import org.apache.commons.net.ftp.FTPClient import org.apache.commons.net.ftp.FTPCmd @@ -12,10 +16,13 @@ import java.io.ByteArrayInputStream import java.io.File import java.io.InputStream -class FTPApiImpl: FTPApi { +class FTPApiImpl : FTPApi { private lateinit var currentStream: InputStream private lateinit var ftp: FTPClient private lateinit var ftpStream: FTPClient + + val scope = CoroutineScope(Dispatchers.IO) + override suspend fun connectToFTP(connection: NetworkConnection): Pair { return try { ftp = FTPClient() @@ -46,10 +53,9 @@ class FTPApiImpl: FTPApi { ftp.changeWorkingDirectory(path) val files: Array = ftp.listFiles() val theFiles = files.toList() - ApiResponse(theFiles,null) - } - catch (exp: Exception){ - ApiResponse(null,exp) + ApiResponse(theFiles, null) + } catch (exp: Exception) { + ApiResponse(null, exp) } } @@ -58,14 +64,13 @@ class FTPApiImpl: FTPApi { val myPath = path.replace("//", "/") if (ftp.hasFeature(FTPCmd.MLST)) { val file = ftp.mlistFile(myPath) - ApiResponse(file,null) + ApiResponse(file, null) } val mP = File(myPath) val files = ftp.listFiles(mP.parent).firstOrNull { it != null && it.name == mP.name } - ApiResponse(files,null) - } - catch (exp: Exception){ - ApiResponse(null,exp) + ApiResponse(files, null) + } catch (exp: Exception) { + ApiResponse(null, exp) } } @@ -77,10 +82,18 @@ class FTPApiImpl: FTPApi { ftpStream.setFileType(FTP.BINARY_FILE_TYPE) ftpStream.restartOffset = start currentStream = ftpStream.retrieveFileStream(path) - ApiResponse(currentStream,null) + ApiResponse(currentStream, null) + } catch (exp: Exception) { + ApiResponse(null, exp) } - catch (exp: Exception){ - ApiResponse(null,exp) + } + + override fun deleteItem(path: String, isFolder: Boolean): ApiResponse { + return try { + if (isFolder) ftp.removeDirectory(path) else ftp.deleteFile(path) + ApiResponse(true, null) + } catch (exp: Exception) { + ApiResponse(false, exp) } } @@ -88,10 +101,35 @@ class FTPApiImpl: FTPApi { return try { val uri = "$path/$name" if (isFolder) ftp.makeDirectory(uri) else ftp.storeFile(uri, ByteArrayInputStream(ByteArray(0))) - ApiResponse(true,null) + ApiResponse(true, null) + } catch (exp: Exception) { + ApiResponse(false, exp) } - catch (exp: Exception){ - ApiResponse(false,exp) + } + + override fun writeFileToCache(path: String, context: Context): ApiResponse { + return try { + + val fileName = path.substringAfterLast('/') + val localFile = File(context.cacheDir, fileName) + + scope.launch(Dispatchers.IO) { + try { + ftp.retrieveFileStream(path).use { input -> + localFile.outputStream().use { output -> + input.copyTo(output) + } + } + ftp.completePendingCommand() + } catch (e: Exception) { + e.printStackTrace() + } + } + + ApiResponse(localFile, null) + + } catch (exp: Exception) { + ApiResponse(null, exp) } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt index 7b0b54175..9f04d0b2c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/SFTPApiImpl.kt @@ -1,6 +1,10 @@ package org.fossify.filemanager.repository +import android.content.Context import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import net.schmizz.sshj.DefaultConfig import net.schmizz.sshj.SSHClient import net.schmizz.sshj.common.Factory @@ -17,13 +21,14 @@ import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.interfaces.SFTPApi import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection +import java.io.File import java.io.IOException import java.io.InputStream class SFTPApiImpl : SFTPApi { private lateinit var ssh: SSHClient private lateinit var sftp: SFTPClient - private val sftpLock = Any() + val scope = CoroutineScope(Dispatchers.IO) override suspend fun connectToSftp(connection: NetworkConnection): Pair { return try { @@ -90,6 +95,34 @@ class SFTPApiImpl : SFTPApi { } } + override fun deleteItem(path: String,isFolder: Boolean): ApiResponse { + return try { + if (isFolder) sftp.rmdir(path) else sftp.rm(path) + ApiResponse(true, null) + } catch (exp: Exception) { + ApiResponse(false, exp) + } + } + + override fun writeFileToCache(path: String,context: Context): ApiResponse{ + return try { + val fileName = path.substringAfterLast('/') + val localFile = File(context.cacheDir, fileName) + scope.launch(Dispatchers.IO) { + sftp.open(path).use { remoteFile -> + remoteFile.RemoteFileInputStream().use { input -> + localFile.outputStream().use { output -> + input.copyTo(output) + } + } + } + } + ApiResponse(localFile, null) + } catch (exp: Exception) { + ApiResponse(null, exp) + } + } + override fun getSFTPConn() = sftp private val KEY_PROVIDER_FACTORIES = DefaultConfig().fileKeyProviderFactories diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt index 17f257496..f7f09f491 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt @@ -1,20 +1,26 @@ package org.fossify.filemanager.repository +import android.content.Context import jcifs.CIFSContext import jcifs.config.PropertyConfiguration import jcifs.context.BaseContext import jcifs.smb.NtlmPasswordAuthenticator import jcifs.smb.SmbFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.interfaces.SMBApi import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection +import java.io.File import java.util.Properties class SMBApiImpl : SMBApi { lateinit var smbClient: SmbFile + val scope = CoroutineScope(Dispatchers.IO) private val defaultProperties: Properties = Properties().apply { @@ -71,5 +77,34 @@ class SMBApiImpl : SMBApi { } } + override fun deleteItem(path: String): ApiResponse { + return try { + val file = SmbFile(path, smbClient.context) + file.delete() + ApiResponse(true,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) + } + } + + override fun writeFileToCache(path: String,context: Context): ApiResponse { + return try { + val smbFile = SmbFile(path, smbClient.context) + val localFile = File(context.cacheDir, smbFile.name) + scope.launch { + smbFile.inputStream.use { input -> + localFile.outputStream().use { output -> + input.copyTo(output) + } + } + } + ApiResponse(localFile,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) + } + } + override fun getMainSmbFile(): SmbFile = smbClient } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt index e2f0a9262..cb79c5483 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt @@ -19,9 +19,14 @@ import java.security.cert.X509Certificate import javax.net.ssl.SSLContext import javax.net.ssl.X509TrustManager import androidx.core.net.toUri +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File class WebDavApiImpl: WebDavApi { lateinit var sardine: Sardine + val scope = CoroutineScope(Dispatchers.IO) override suspend fun connectAndVerifyWebDav( connection: NetworkConnection, protocols: Protocols, @@ -78,6 +83,37 @@ class WebDavApiImpl: WebDavApi { } } + override fun deleteItem(path: String): ApiResponse { + return try { + sardine.delete(path) + ApiResponse(true,null) + } + catch (exp: Exception){ + ApiResponse(false,exp) + } + } + + override fun writeFileToCache(url: String, context: Context): ApiResponse { + return try { + val localFile = File(context.cacheDir, Uri.parse(url).lastPathSegment) + scope.launch(Dispatchers.IO) { + try { + sardine.get(url).use { input -> + localFile.outputStream().use { output -> + input.copyTo(output) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + ApiResponse(localFile, null) + + } catch (exp: Exception) { + ApiResponse(null, exp) + } + } + override fun listWebDavFileDetail(url: String): ApiResponse { return try { diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 6e180af67..23f37da08 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -22,6 +22,7 @@ import org.fossify.filemanager.interfaces.WebDavApi import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.ConnectionResult import org.fossify.filemanager.models.NetworkConnection +import java.io.File class NetworkBrowserViewModel( private val networkConnectionRepository: NetworkConnectionRepositoryDb, @@ -34,6 +35,8 @@ class NetworkBrowserViewModel( val savedNetworks = MutableStateFlow>(emptyList()) val verifyNetwork = MutableSharedFlow() val smbFolderOrFile = MutableSharedFlow>() + val smbDelete = MutableSharedFlow>() + val smbFileShare = MutableSharedFlow>() val verifyWebDav = MutableSharedFlow() @@ -43,12 +46,18 @@ class NetworkBrowserViewModel( val sftpFiles = MutableStateFlow>?>(null) val sftpFolderOrFile = MutableSharedFlow>() + val sftpDelete = MutableSharedFlow>() + val sftpFileShare = MutableSharedFlow>() val webDavFiles = MutableStateFlow< ApiResponse>?>(null) val webDavFolderOrFile = MutableSharedFlow>() + val webDavDelete = MutableSharedFlow>() + val webDavFileShare = MutableSharedFlow>() val ftpFiles = MutableStateFlow>?>(null) val ftpFolderOrFile = MutableSharedFlow>() + val ftpDelete = MutableSharedFlow>() + val ftpFileShare = MutableSharedFlow>() @@ -83,6 +92,18 @@ class NetworkBrowserViewModel( } } + fun deleteItemSMB(path: String) { + viewModelScope.launch(Dispatchers.IO) { + smbDelete.emit(smbApi.deleteItem(path)) + } + } + + fun writeSmbFileToCache(path: String,context: Context) { + viewModelScope.launch(Dispatchers.IO) { + smbFileShare.emit(smbApi.writeFileToCache(path,context)) + } + } + fun getMainSmb(): SmbFile = smbApi.getMainSmbFile() @@ -107,6 +128,18 @@ class NetworkBrowserViewModel( } } + fun deleteItemWebDav(path: String) { + viewModelScope.launch(Dispatchers.IO) { + webDavDelete.emit(webDavApi.deleteItem(path)) + } + } + + fun writeWebDavFileToCache(url: String,context: Context) { + viewModelScope.launch(Dispatchers.IO) { + webDavFileShare.emit(webDavApi.writeFileToCache(url,context)) + } + } + // fun listWebDavFileStream(url: String, start: Long, end: Long): InputStream { // return webDavApi.getWebDavFileInputStream(url, start, end) // } @@ -135,6 +168,18 @@ class NetworkBrowserViewModel( } } + fun deleteItemSFTP(path: String,isFolder: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + sftpDelete.emit(sftpApi.deleteItem(path,isFolder)) + } + } + + fun writeSftpFileToCache(path: String,context: Context){ + viewModelScope.launch(Dispatchers.IO) { + sftpFileShare.emit(sftpApi.writeFileToCache(path,context)) + } + } + // fun listAllFilesSFTPPath(path: String) { // viewModelScope.launch(Dispatchers.IO) { // val res = sftpApi.listAllFilesSFTPRoot(path) @@ -172,6 +217,20 @@ class NetworkBrowserViewModel( } } + fun deleteItemFTP(path: String,isFolder: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + ftpDelete.emit(sftpApi.deleteItem(path,isFolder)) + } + } + + fun writeFtpFileToCache(path: String,context: Context){ + viewModelScope.launch(Dispatchers.IO) { + ftpFileShare.emit(sftpApi.writeFileToCache(path,context)) + } + } + + + fun getFTPFileDetail(path: String) = ftpApi.getFTPFileDetail(path) fun getFTPFileStream(path: String, start: Long) = ftpApi.getFTPFileInputStream(path, start) From be0e21836fd16d204f82d018a3c104cd030cdf40 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Wed, 3 Jun 2026 00:50:25 +0500 Subject: [PATCH 28/37] fixed bread crumbs for davx5 --- .../filemanager/activities/CloudActivity.kt | 8 +- .../filemanager/activities/MainActivity.kt | 19 +- .../filemanager/fragments/ItemsFragment.kt | 450 ++++++++++-------- .../fossify/filemanager/helpers/Constants.kt | 2 + 4 files changed, 267 insertions(+), 212 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index edc7bc78c..b9a9ce6af 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -32,6 +32,7 @@ import org.fossify.commons.extensions.toast import org.fossify.filemanager.dependencies.AppComposition import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.enums.Protocols +import org.fossify.filemanager.helpers.DAVX5_PATH_NAME import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.helpers.PORT_FTP import org.fossify.filemanager.interfaces.CertificateRepository @@ -247,7 +248,7 @@ class CloudActivity : SimpleActivity() { } ConnectionTypes.DAVx5 -> { - launchMainActivity(ConnectionTypes.DAVx5, item.sharedPath) + launchMainActivity(ConnectionTypes.DAVx5, item.sharedPath,item.displayName) } ConnectionTypes.WebDav -> { @@ -284,10 +285,13 @@ class CloudActivity : SimpleActivity() { } } - private fun launchMainActivity(connectionType: ConnectionTypes, path: String) { + private fun launchMainActivity(connectionType: ConnectionTypes, path: String, name: String = "") { val intent = Intent(this@CloudActivity, MainActivity::class.java).apply { putExtra(PATH, path) putExtra(CONNECTION_TYPE, connectionType) + if (name != ""){ + putExtra(DAVX5_PATH_NAME, name) + } } launcher.launch(intent) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt index 9e5aeafba..d53e95b08 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt @@ -71,6 +71,7 @@ import org.fossify.filemanager.fragments.MyViewPagerFragment import org.fossify.filemanager.fragments.RecentsFragment import org.fossify.filemanager.fragments.StorageFragment import org.fossify.filemanager.helpers.CONNECTION_TYPE +import org.fossify.filemanager.helpers.DAVX5_PATH_NAME import org.fossify.filemanager.helpers.MAX_COLUMN_COUNT import org.fossify.filemanager.helpers.NETWORK_PATH import org.fossify.filemanager.helpers.PATH @@ -131,6 +132,10 @@ class MainActivity : SimpleActivity() { return Triple(path ?: "",isNetworkPath,connectionType) } + private fun getDavX5PathName(): String{ + return intent.getStringExtra(DAVX5_PATH_NAME) ?: "" + } + override fun onResume() { super.onResume() if (mStoredShowTabs != config.showTabs) { @@ -340,17 +345,17 @@ class MainActivity : SimpleActivity() { } private fun initFileManager(refreshRecents: Boolean, path: String, isNetworkPath: Boolean, connectionType: ConnectionTypes) { - Log.d("File Path",path) + val pathName = getDavX5PathName() if (intent.action == Intent.ACTION_VIEW && intent.data != null) { val data = intent.data if (data?.scheme == "file") { - openPath(data.path!!, connectionType = connectionType) + openPath(data.path!!, connectionType = connectionType, pathName = pathName) } else { val path = getRealPathFromURI(data!!) if (path != null) { - openPath(path, connectionType = connectionType) + openPath(path, connectionType = connectionType, pathName = pathName) } else { - openPath(config.homeFolder, connectionType = connectionType) + openPath(config.homeFolder, connectionType = connectionType, pathName = pathName) } } @@ -360,7 +365,7 @@ class MainActivity : SimpleActivity() { binding.mainViewPager.currentItem = 0 } else { - openPath(if(path.isNotEmpty()) path else config.homeFolder,isNetworkPath = isNetworkPath, connectionType = connectionType) + openPath(if(path.isNotEmpty()) path else config.homeFolder,isNetworkPath = isNetworkPath, connectionType = connectionType, pathName = pathName) } if (refreshRecents) { @@ -486,7 +491,7 @@ class MainActivity : SimpleActivity() { } } - private fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false,connectionType: ConnectionTypes = ConnectionTypes.Default) { + private fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, pathName: String = "",connectionType: ConnectionTypes = ConnectionTypes.Default) { var newPath = path val file = File(path) if (config.OTGPath.isNotEmpty() && config.OTGPath == path.trimEnd('/')) { @@ -497,7 +502,7 @@ class MainActivity : SimpleActivity() { newPath = internalStoragePath } - getItemsFragment()?.openPath(newPath, forceRefresh, isNetworkPath,connectionType=connectionType) + getItemsFragment()?.openPath(newPath, forceRefresh, isNetworkPath,connectionType=connectionType, pathName = pathName) } private fun goHome() { diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index c88e4a5b3..5d8eec164 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -5,6 +5,7 @@ import android.content.Context import android.net.Uri import android.os.Parcelable import android.util.AttributeSet +import android.util.Log import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager @@ -132,7 +133,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } - fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, connectionType: ConnectionTypes = ConnectionTypes.Default) { + fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, pathName: String = "", connectionType: ConnectionTypes = ConnectionTypes.Default) { if ((activity as? BaseSimpleActivity)?.isAskingPermissions == true) { return } @@ -172,7 +173,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF itemsIgnoringSearch = listItems activity?.runOnUiThread { (activity as? MainActivity)?.refreshMenuItems() - addItems(listItems, forceRefresh, isNetworkPath, connectionType) + addItems(listItems, forceRefresh, isNetworkPath,pathName = pathName, connectionType) if (context != null && currentViewType != context!!.config.getFolderViewType(currentPath)) { setupLayoutManager(connectionType) } @@ -181,10 +182,12 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } - private fun addItems(items: ArrayList, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, connectionType: ConnectionTypes) { + private fun addItems(items: ArrayList, forceRefresh: Boolean = false, isNetworkPath: Boolean = false,pathName: String = "", connectionType: ConnectionTypes) { activity?.runOnUiThread { binding.itemsSwipeRefresh.isRefreshing = false - binding.breadcrumbs.setBreadcrumb(currentPath, connectionType) + if (connectionType != ConnectionTypes.DAVx5){ + binding.breadcrumbs.setBreadcrumb(currentPath, connectionType) + } if (!forceRefresh && items.hashCode() == storedItems.hashCode()) { return@runOnUiThread } @@ -193,9 +196,9 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF if (binding.itemsList.adapter == null) { binding.breadcrumbs.updateFontSize(context!!.getTextSize(), true, connectionType) } - + var lastClickedItem: ListItem? = null ItemsAdapter(activity as SimpleActivity, storedItems, this, binding.itemsList, isPickMultipleIntent, binding.itemsSwipeRefresh) { - + lastClickedItem = it as? ListItem if ((it as? ListItem)?.mIsDirectory == false) { if (connectionType == ConnectionTypes.SMB) { it?.let { item -> @@ -207,11 +210,11 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } else if (connectionType == ConnectionTypes.SFTP) { it?.let { item -> - FileHelpers.launchSFTP(connectionType,item.mPath, context = this@ItemsFragment.context, ) + FileHelpers.launchSFTP(connectionType, item.mPath, context = this@ItemsFragment.context) } } else if (connectionType == ConnectionTypes.FTP) { it?.let { item -> - FileHelpers.launchFTP(connectionType,item.mPath, context = this@ItemsFragment.context) + FileHelpers.launchFTP(connectionType, item.mPath, context = this@ItemsFragment.context) } } else { itemClicked(it as FileDirItem, connectionType) @@ -222,11 +225,19 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } else { itemClicked(it as FileDirItem, connectionType) } + if (connectionType == ConnectionTypes.DAVx5){ + if (lastClickedItem != null){ + binding.breadcrumbs.setBreadcrumbWithName(lastClickedItem!!.mPath,lastClickedItem?.mName,connectionType) + } + } }.apply { setupZoomListener(zoomListener) binding.itemsList.adapter = this } + if (lastClickedItem == null && connectionType == ConnectionTypes.DAVx5){ + binding.breadcrumbs.setBreadcrumbWithName(currentPath,pathName, connectionType) + } if (context.areSystemAnimationsEnabled) { binding.itemsList.scheduleLayoutAnimation() } @@ -274,7 +285,11 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF handleApiResponse(it, path, connectionType, callback) } } - } else if (context.isRestrictedSAFOnlyRoot(path)) { + } + else if (connectionType.equals(ConnectionTypes.DAVx5)){ + handleApiResponse(null, path, connectionType, callback) + } + else if (context.isRestrictedSAFOnlyRoot(path)) { activity?.runOnUiThread { hideProgressBar() } activity?.handleAndroidSAFDialog(path, openInSystemAppAllowed = true) { if (!it) { @@ -654,286 +669,315 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF callback(path, getListItemsFromFileDirItems(ArrayList(items?.toList()))) } } + else if (connectionType == ConnectionTypes.DAVx5) { + val items = ArrayList() + val uri = Uri.parse(path) + val docFile = DocumentFile.fromTreeUri(context, uri) + val files = docFile?.listFiles() + + files?.forEach { file -> + items.add( + ListItem( + mConnectionType = connectionType, + mPath = file.uri.toString(), + mName = file.name ?: "", + mIsDirectory = file.isDirectory, + mChildren = if (file.isDirectory) 1 else 0, + mSize = if (file.isFile) file.length() else 0L, + mModified = file.lastModified(), + isSectionTitle = false, + isGridTypeDivider = false, + parent = path + ) + ) + } + callback(path,items) + } } } - override fun columnCountChanged() { - (binding.itemsList.layoutManager as MyGridLayoutManager).spanCount = context!!.config.fileColumnCnt - (activity as? MainActivity)?.refreshMenuItems() - getRecyclerAdapter()?.apply { - notifyItemRangeChanged(0, listItems.size) + override fun columnCountChanged() { + (binding.itemsList.layoutManager as MyGridLayoutManager).spanCount = context!!.config.fileColumnCnt + (activity as? MainActivity)?.refreshMenuItems() + getRecyclerAdapter()?.apply { + notifyItemRangeChanged(0, listItems.size) + } } - } - fun showProgressBar() { - binding.progressBar.show() - } + fun showProgressBar() { + binding.progressBar.show() + } - private fun hideProgressBar() { - binding.progressBar.hide() - } + private fun hideProgressBar() { + binding.progressBar.hide() + } - fun getBreadcrumbs() = binding.breadcrumbs + fun getBreadcrumbs() = binding.breadcrumbs - override fun toggleFilenameVisibility() { - getRecyclerAdapter()?.updateDisplayFilenamesInGrid() - } + override fun toggleFilenameVisibility() { + getRecyclerAdapter()?.updateDisplayFilenamesInGrid() + } - override fun breadcrumbClicked(id: Int) { - val item = binding.breadcrumbs.getItem(id) - if (id == 0) { - StoragePickerDialog(activity as SimpleActivity, currentPath, context!!.config.enableRootAccess, true) { - getRecyclerAdapter()?.finishActMode() - if (item.connectionType != ConnectionTypes.Default) { - openPath(item.path, connectionType = item.connectionType) - } else { - openPath(item.path) + override fun breadcrumbClicked(id: Int) { + val item = binding.breadcrumbs.getItem(id) + if (id == 0) { + StoragePickerDialog(activity as SimpleActivity, currentPath, context!!.config.enableRootAccess, true) { + getRecyclerAdapter()?.finishActMode() + if (item.connectionType != ConnectionTypes.Default) { + openPath(item.path, connectionType = item.connectionType) + } else { + openPath(item.path) + } } - } - } else { - var path = "" - if (item.connectionType == ConnectionTypes.WebDav || item.connectionType == ConnectionTypes.SMB) { - val items = binding.breadcrumbs.getItemsTillIndex(id) - items.forEach { item -> - path += "${item.path}/" + } else { + var path = "" + if (item.connectionType == ConnectionTypes.WebDav || item.connectionType == ConnectionTypes.SMB) { + val items = binding.breadcrumbs.getItemsTillIndex(id) + items.forEach { item -> + path += "${item.path}/" + } } - } else - path = item.path + else + path = item.path - openPath(path, connectionType = item.connectionType) + openPath(path, connectionType = item.connectionType) + } } - } - override fun refreshFragment() { - openPath(currentPath, connectionType = connectionType) - } + override fun refreshFragment() { + openPath(currentPath, connectionType = connectionType) + } - override fun deleteFiles(files: ArrayList) { - if (connectionType != ConnectionTypes.Default) { - collectLatest() + override fun deleteFiles(files: ArrayList) { + if (connectionType != ConnectionTypes.Default) { + collectLatest() + } + val hasFolder = files.any { it.isDirectory } + deleteFileOrFolder(files, hasFolder) } - val hasFolder = files.any { it.isDirectory } - deleteFileOrFolder(files, hasFolder) - } - private fun deleteFileOrFolder(files: ArrayList, hasFolder: Boolean) { - when (connectionType) { - ConnectionTypes.SMB -> { - files.forEach { - viewModel.deleteItemSMB(it.path) + private fun deleteFileOrFolder(files: ArrayList, hasFolder: Boolean) { + when (connectionType) { + ConnectionTypes.SMB -> { + files.forEach { + viewModel.deleteItemSMB(it.path) + } } - } - ConnectionTypes.WebDav -> { - files.forEach { - viewModel.deleteItemWebDav(it.path) + ConnectionTypes.WebDav -> { + files.forEach { + viewModel.deleteItemWebDav(it.path) + } } - } - ConnectionTypes.SFTP -> { - files.forEach { - viewModel.deleteItemSFTP(it.path, it.isDirectory) + ConnectionTypes.SFTP -> { + files.forEach { + viewModel.deleteItemSFTP(it.path, it.isDirectory) + } + } + + ConnectionTypes.FTP -> { + files.forEach { + viewModel.deleteItemFTP(it.path, it.isDirectory) + } } - } - ConnectionTypes.FTP -> { - files.forEach { - viewModel.deleteItemFTP(it.path, it.isDirectory) + ConnectionTypes.Default -> { + handleFileDeleting(files, hasFolder) } - } - ConnectionTypes.Default -> { - handleFileDeleting(files, hasFolder) + else -> Unit } - - else -> Unit } - } - private fun collectLatest() { - CoroutineScope(Dispatchers.IO).launch { - when (connectionType) { - ConnectionTypes.SMB -> { - viewModel.smbDelete.collectLatest { - it.exception?.message?.let { msg -> - activity?.toast(msg) + private fun collectLatest() { + CoroutineScope(Dispatchers.IO).launch { + when (connectionType) { + ConnectionTypes.SMB -> { + viewModel.smbDelete.collectLatest { + it.exception?.message?.let { msg -> + activity?.toast(msg) + } } } - } - ConnectionTypes.WebDav -> { - viewModel.webDavDelete.collectLatest { - it.exception?.message?.let { msg -> - activity?.toast(msg) + ConnectionTypes.WebDav -> { + viewModel.webDavDelete.collectLatest { + it.exception?.message?.let { msg -> + activity?.toast(msg) + } } } - } - ConnectionTypes.SFTP -> { - viewModel.sftpDelete.collectLatest { - it.exception?.message?.let { msg -> - activity?.toast(msg) + ConnectionTypes.SFTP -> { + viewModel.sftpDelete.collectLatest { + it.exception?.message?.let { msg -> + activity?.toast(msg) + } } } - } - ConnectionTypes.FTP -> { - viewModel.ftpDelete.collectLatest { - it.exception?.message?.let { msg -> - activity?.toast(msg) + ConnectionTypes.FTP -> { + viewModel.ftpDelete.collectLatest { + it.exception?.message?.let { msg -> + activity?.toast(msg) + } } } - } - else -> Unit + else -> Unit + } } } - } - - override fun selectedPaths(paths: ArrayList) { - (activity as MainActivity).pickedPaths(paths) - } - override fun shareFile(paths: ArrayList) { - collectFileShared(paths) - if (connectionType == ConnectionTypes.SMB) { - paths.forEach { - viewModel.writeSmbFileToCache(it, context) - } - } else if (connectionType == ConnectionTypes.WebDav) { - paths.forEach { - viewModel.writeWebDavFileToCache(it, context) - } - } else if (connectionType == ConnectionTypes.SFTP) { - paths.forEach { - viewModel.writeSftpFileToCache(it, context) - } - } else if (connectionType == ConnectionTypes.FTP) { - paths.forEach { - viewModel.writeFtpFileToCache(it, context) - } + override fun selectedPaths(paths: ArrayList) { + (activity as MainActivity).pickedPaths(paths) } - } - override fun openWith(path: String, mimType: String?) { - when(connectionType){ - ConnectionTypes.SMB -> { - FileHelpers.launchSMB(path, context,mimType) - } - ConnectionTypes.WebDav -> { - FileHelpers.launchWebDav( path, context,mimType) - } - ConnectionTypes.SFTP -> { - FileHelpers.launchSFTP(connectionType, path, context,mimType) - } - ConnectionTypes.FTP -> { - FileHelpers.launchFTP(connectionType, path, context,mimType) + override fun shareFile(paths: ArrayList) { + collectFileShared(paths) + if (connectionType == ConnectionTypes.SMB) { + paths.forEach { + viewModel.writeSmbFileToCache(it, context) + } + } else if (connectionType == ConnectionTypes.WebDav) { + paths.forEach { + viewModel.writeWebDavFileToCache(it, context) + } + } else if (connectionType == ConnectionTypes.SFTP) { + paths.forEach { + viewModel.writeSftpFileToCache(it, context) + } + } else if (connectionType == ConnectionTypes.FTP) { + paths.forEach { + viewModel.writeFtpFileToCache(it, context) + } } - else -> Unit } - } - - private fun collectFileShared(paths: ArrayList) { - CoroutineScope(Dispatchers.IO).launch { + override fun openWith(path: String, mimType: String?) { when (connectionType) { ConnectionTypes.SMB -> { - viewModel.smbFileShare.collectLatest { - handleFileSharedResponse(it, paths) - } + FileHelpers.launchSMB(path, context, mimType) } ConnectionTypes.WebDav -> { - viewModel.webDavFileShare.collectLatest { - handleFileSharedResponse(it, paths) - } + FileHelpers.launchWebDav(path, context, mimType) } ConnectionTypes.SFTP -> { - viewModel.sftpFileShare.collectLatest { - handleFileSharedResponse(it, paths) - } + FileHelpers.launchSFTP(connectionType, path, context, mimType) } ConnectionTypes.FTP -> { - viewModel.ftpFileShare.collectLatest { - handleFileSharedResponse(it, paths) - } + FileHelpers.launchFTP(connectionType, path, context, mimType) } else -> Unit } } - } - private fun handleFileSharedResponse(response: ApiResponse, paths: ArrayList) { - if (response.exception != null) { - response.exception?.message?.let { msg -> - activity?.toast(msg) + + private fun collectFileShared(paths: ArrayList) { + CoroutineScope(Dispatchers.IO).launch { + when (connectionType) { + ConnectionTypes.SMB -> { + viewModel.smbFileShare.collectLatest { + handleFileSharedResponse(it, paths) + } + } + + ConnectionTypes.WebDav -> { + viewModel.webDavFileShare.collectLatest { + handleFileSharedResponse(it, paths) + } + } + + ConnectionTypes.SFTP -> { + viewModel.sftpFileShare.collectLatest { + handleFileSharedResponse(it, paths) + } + } + + ConnectionTypes.FTP -> { + viewModel.ftpFileShare.collectLatest { + handleFileSharedResponse(it, paths) + } + } + + else -> Unit + } } - } else { - activity?.networkSharePaths(paths, response.response as File) } - } - private fun handleFileWriteResponse(response: ApiResponse, paths: String) { - if (response.exception != null) { - response.exception?.message?.let { msg -> - activity?.toast(msg) + private fun handleFileSharedResponse(response: ApiResponse, paths: ArrayList) { + if (response.exception != null) { + response.exception?.message?.let { msg -> + activity?.toast(msg) + } + } else { + activity?.networkSharePaths(paths, response.response as File) + } + } + + private fun handleFileWriteResponse(response: ApiResponse, paths: String) { + if (response.exception != null) { + response.exception?.message?.let { msg -> + activity?.toast(msg) + } + } else { + activity?.setAsNetworkPath(paths, response.response as File) } - } else { - activity?.setAsNetworkPath(paths, response.response as File) } - } - override fun setAs(path: String) { - collectFileCopied(path) - if (connectionType == ConnectionTypes.SMB) { - viewModel.writeSmbFileToCache(path, context) + override fun setAs(path: String) { + collectFileCopied(path) + if (connectionType == ConnectionTypes.SMB) { + viewModel.writeSmbFileToCache(path, context) - } else if (connectionType == ConnectionTypes.WebDav) { - viewModel.writeWebDavFileToCache(path, context) + } else if (connectionType == ConnectionTypes.WebDav) { + viewModel.writeWebDavFileToCache(path, context) - } else if (connectionType == ConnectionTypes.SFTP) { - viewModel.writeSftpFileToCache(path, context) + } else if (connectionType == ConnectionTypes.SFTP) { + viewModel.writeSftpFileToCache(path, context) - } else if (connectionType == ConnectionTypes.FTP) { - viewModel.writeFtpFileToCache(path, context) + } else if (connectionType == ConnectionTypes.FTP) { + viewModel.writeFtpFileToCache(path, context) + } } - } - private fun collectFileCopied(path: String) { - CoroutineScope(Dispatchers.IO).launch { - when (connectionType) { - ConnectionTypes.SMB -> { - viewModel.smbFileShare.collectLatest { - handleFileWriteResponse(it, path) + private fun collectFileCopied(path: String) { + CoroutineScope(Dispatchers.IO).launch { + when (connectionType) { + ConnectionTypes.SMB -> { + viewModel.smbFileShare.collectLatest { + handleFileWriteResponse(it, path) + } } - } - ConnectionTypes.WebDav -> { - viewModel.webDavFileShare.collectLatest { - handleFileWriteResponse(it, path) + ConnectionTypes.WebDav -> { + viewModel.webDavFileShare.collectLatest { + handleFileWriteResponse(it, path) + } } - } - ConnectionTypes.SFTP -> { - viewModel.sftpFileShare.collectLatest { - handleFileWriteResponse(it, path) + ConnectionTypes.SFTP -> { + viewModel.sftpFileShare.collectLatest { + handleFileWriteResponse(it, path) + } } - } - ConnectionTypes.FTP -> { - viewModel.ftpFileShare.collectLatest { - handleFileWriteResponse(it, path) + ConnectionTypes.FTP -> { + viewModel.ftpFileShare.collectLatest { + handleFileWriteResponse(it, path) + } } - } - else -> Unit + else -> Unit + } } } - } -} + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt index e52c2df62..1bce0484e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt @@ -69,6 +69,8 @@ const val NETWORK_PATH = "network_path" const val CONNECTION_TYPE = "connection_type" +const val DAVX5_PATH_NAME = "davx5_path_name" + // what else should we count as an audio except "audio/*" mimetype val extraAudioMimeTypes = arrayListOf("application/ogg") val extraDocumentMimeTypes = arrayListOf( From db452f476e0f0a9464a7571b64a8ff5b88381703 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Fri, 5 Jun 2026 23:40:14 +0500 Subject: [PATCH 29/37] added delete and edit option for cloud connection --- .../filemanager/activities/CloudActivity.kt | 22 +++- .../adapters/ConnectionItemsAdapter.kt | 11 +- .../filemanager/dao/NetworkConnectionDao.kt | 5 +- .../filemanager/dialogs/ConnectionDialog.kt | 122 +++++++++++------- .../NetworkConnectionRepositoryDb.kt | 5 +- .../mapper/NetworkConnectionMapper.kt | 7 +- .../filemanager/models/NetworkConnection.kt | 1 + .../NetworkConnectionRepositoryDbImpl.kt | 8 +- .../viewmodels/NetworkBrowserViewModel.kt | 10 +- .../res/layout/item_network_connection.xml | 91 +++++++++---- 10 files changed, 187 insertions(+), 95 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index b9a9ce6af..abb4b5488 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -100,7 +100,7 @@ class CloudActivity : SimpleActivity() { val storage = DocumentFile.fromTreeUri(this, it) storage?.let { s -> if (s.name != null && it.path != null) { - viewModel.saveNetwork( + viewModel.insertUpdateConnection( NetworkConnection( displayName = s.name!!, sharedPath = s.uri.toString(), @@ -275,7 +275,7 @@ class CloudActivity : SimpleActivity() { } private fun updateAdapter(listItems: MutableList) { - ConnectionItemsAdapter(this, listItems, binding.connectionsList) { item -> + ConnectionItemsAdapter(this, listItems, binding.connectionsList,::deleteConnection,::updateConnection) { item -> lifecycleScope.launch { val itm = item as NetworkConnection handleConnection(itm, itm.connectionType) @@ -285,6 +285,16 @@ class CloudActivity : SimpleActivity() { } } + private fun deleteConnection(connection: NetworkConnection){ + viewModel.deleteConnection(connection) + } + + private fun updateConnection(connection: NetworkConnection){ + ConnectionDialog(this@CloudActivity,connection) { host, user, password, shared, displayName, certPath, privateKeyText, privateKeyPass, port, connection, protocol, auth -> + saveNetwork(host, user, password, shared, displayName, privateKeyText, privateKeyPass, certPath, port, connection, protocol, auth) + } + } + private fun launchMainActivity(connectionType: ConnectionTypes, path: String, name: String = "") { val intent = Intent(this@CloudActivity, MainActivity::class.java).apply { putExtra(PATH, path) @@ -353,7 +363,7 @@ class CloudActivity : SimpleActivity() { ) launchMainActivity(ConnectionTypes.WebDav, it.item.url) } else { - viewModel.saveNetwork( + viewModel.insertUpdateConnection( NetworkConnection( host = it.item.host, username = it.item.username, @@ -383,7 +393,7 @@ class CloudActivity : SimpleActivity() { startServer(it.item, connectionType = ConnectionTypes.SMB, machinePort = it.item.port) launchMainActivity(ConnectionTypes.SMB, path) } else { - viewModel.saveNetwork( + viewModel.insertUpdateConnection( NetworkConnection( host = it.item.host, username = it.item.username, @@ -410,7 +420,7 @@ class CloudActivity : SimpleActivity() { startServer(it.item, PORT_SFTP, connectionType = ConnectionTypes.SFTP, machinePort = it.item.port) launchMainActivity(ConnectionTypes.SFTP, it.item.url) } else { - viewModel.saveNetwork( + viewModel.insertUpdateConnection( NetworkConnection( host = it.item.host, username = it.item.username, @@ -440,7 +450,7 @@ class CloudActivity : SimpleActivity() { startServer(it.item, PORT_FTP, connectionType = ConnectionTypes.FTP, machinePort = it.item.port) launchMainActivity(ConnectionTypes.FTP, it.item.url) } else { - viewModel.saveNetwork( + viewModel.insertUpdateConnection( NetworkConnection( host = it.item.host, username = it.item.username, diff --git a/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt b/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt index 3905acd06..3ef844409 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt @@ -17,7 +17,14 @@ import org.fossify.filemanager.models.ListItem import org.fossify.filemanager.models.NetworkConnection import java.util.Locale -class ConnectionItemsAdapter(activity: SimpleActivity, var listItems: MutableList, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit) : +class ConnectionItemsAdapter( + activity: SimpleActivity, + var listItems: MutableList, + recyclerView: MyRecyclerView, + val deleteClick: (NetworkConnection) -> Unit, + val updateConnection: (NetworkConnection) -> Unit, + itemClick: (Any) -> Unit +) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) { override fun getActionMenuId(): Int { TODO("Not yet implemented") @@ -75,6 +82,8 @@ class ConnectionItemsAdapter(activity: SimpleActivity, var listItems: MutableLis tvType.text = listItem.connectionType.toString() tvDisplayName.text = listItem.displayName tvSharedPath.text = listItem.sharedPath + btnDelete.setOnClickListener { deleteClick(listItem) } + btnEdit.setOnClickListener { updateConnection(listItem) } } } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt b/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt index 525fc022c..f9fc4734c 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt @@ -14,5 +14,8 @@ interface NetworkConnectionDao { fun getAll(): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(connection: NetworkConnectionEntity): Long + suspend fun insertUpdateConnection(connection: NetworkConnectionEntity): Long + + @Delete + suspend fun delete(connection: NetworkConnectionEntity) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 1ae019af5..868696542 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -19,10 +19,12 @@ import org.fossify.filemanager.helpers.DEFAULT_SFTP_PORT import org.fossify.filemanager.helpers.DEFAULT_SMB_PORT import org.fossify.filemanager.helpers.DEFAULT_WEBDAV_HTTPS_PORT import org.fossify.filemanager.helpers.DEFAULT_WEBDAV_HTTP_PORT +import org.fossify.filemanager.models.NetworkConnection import java.io.File class ConnectionDialog( val activity: BaseSimpleActivity, + val connection: NetworkConnection? = null, dispatch: (String, String, String, String, String, Uri?, String, String, Int, ConnectionTypes, Protocols?, Authentication) -> Unit ) { private var binding: DialogAddConnectionBinding @@ -68,6 +70,7 @@ class ConnectionDialog( attachCertBtnClickListener() attachPrivateKeyBtnClickListener() dropDownMenuProtocolItemClickListener() + populateDialogValues() } @@ -148,57 +151,61 @@ class ConnectionDialog( private fun dropDownItemSelected() { binding.dropdownMenu.setOnItemClickListener { parent, view, position, id -> val selectedItem = parent.getItemAtPosition(position).toString() - togglePortValue( - ConnectionTypes.valueOf(selectedItem), - if (binding.dropdownMenuProtocol.value != "") Protocols.valueOf(binding.dropdownMenuProtocol.value) else Protocols.HTTP - ) - if (selectedItem == ConnectionTypes.DAVx5.type) { - binding.allFieldsExceptConnection.visibility = View.GONE - promptUserToSelectStorage() - } else if (selectedItem == ConnectionTypes.WebDav.type) { - binding.allFieldsExceptConnection.visibility = View.VISIBLE - binding.dropdownProtocol.visibility = View.VISIBLE - binding.authDropDownLayout.visibility = View.GONE - toggleSFTPAuthVisibility(View.GONE) - toggleCredentialsVisibility(View.VISIBLE) - } else if (selectedItem == ConnectionTypes.SMB.type) { - binding.dropdownProtocol.visibility = View.GONE - binding.certRow.visibility = View.GONE - binding.allFieldsExceptConnection.visibility = View.VISIBLE - binding.authDropDownLayout.visibility = View.VISIBLE - toggleSFTPAuthVisibility(View.GONE) - binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, authentications)) - binding.authDropDownMenu.setText(authentications[0].toString(), false) - onAuthSelected(authentications[0].toString()) - } else if (selectedItem == ConnectionTypes.SFTP.type) { - binding.allFieldsExceptConnection.visibility = View.VISIBLE - binding.authDropDownLayout.visibility = View.VISIBLE - binding.dropdownProtocol.visibility = View.GONE - binding.certRow.visibility = View.GONE - if (Authentication.valueOf(binding.authDropDownMenu.value) == Authentication.Password) { - toggleSFTPAuthVisibility(View.GONE) - } else { - toggleSFTPAuthVisibility(View.VISIBLE) - } - binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, sftpAuthentications)) - binding.authDropDownMenu.setText(sftpAuthentications[0].toString(), false) - onAuthSelected(sftpAuthentications[0].toString()) + handleConnectionTypeSelection(selectedItem) + } + } - } else if (selectedItem == ConnectionTypes.FTP.type) { - binding.allFieldsExceptConnection.visibility = View.VISIBLE - binding.authDropDownLayout.visibility = View.VISIBLE - binding.dropdownProtocol.visibility = View.GONE - binding.certRow.visibility = View.GONE + private fun handleConnectionTypeSelection(selectedItem: String) { + togglePortValue( + ConnectionTypes.valueOf(selectedItem), + if (binding.dropdownMenuProtocol.value != "") Protocols.valueOf(binding.dropdownMenuProtocol.value) else Protocols.HTTP + ) + if (selectedItem == ConnectionTypes.DAVx5.type) { + binding.allFieldsExceptConnection.visibility = View.GONE + promptUserToSelectStorage() + } else if (selectedItem == ConnectionTypes.WebDav.type) { + binding.allFieldsExceptConnection.visibility = View.VISIBLE + binding.dropdownProtocol.visibility = View.VISIBLE + binding.authDropDownLayout.visibility = View.GONE + toggleSFTPAuthVisibility(View.GONE) + toggleCredentialsVisibility(View.VISIBLE) + } else if (selectedItem == ConnectionTypes.SMB.type) { + binding.dropdownProtocol.visibility = View.GONE + binding.certRow.visibility = View.GONE + binding.allFieldsExceptConnection.visibility = View.VISIBLE + binding.authDropDownLayout.visibility = View.VISIBLE + toggleSFTPAuthVisibility(View.GONE) + binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, authentications)) + binding.authDropDownMenu.setText(authentications[0].toString(), false) + onAuthSelected(authentications[0].toString()) + } else if (selectedItem == ConnectionTypes.SFTP.type) { + binding.allFieldsExceptConnection.visibility = View.VISIBLE + binding.authDropDownLayout.visibility = View.VISIBLE + binding.dropdownProtocol.visibility = View.GONE + binding.certRow.visibility = View.GONE + if (Authentication.valueOf(binding.authDropDownMenu.value) == Authentication.Password) { toggleSFTPAuthVisibility(View.GONE) - if (Authentication.valueOf(binding.authDropDownMenu.value) == Authentication.Password) { - toggleCredentialsVisibility(View.VISIBLE) - } else { - toggleCredentialsVisibility(View.GONE) - } - binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, authentications)) - binding.authDropDownMenu.setText(authentications[0].toString(), false) - onAuthSelected(authentications[0].toString()) + } else { + toggleSFTPAuthVisibility(View.VISIBLE) } + binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, sftpAuthentications)) + binding.authDropDownMenu.setText(sftpAuthentications[0].toString(), false) + onAuthSelected(sftpAuthentications[0].toString()) + + } else if (selectedItem == ConnectionTypes.FTP.type) { + binding.allFieldsExceptConnection.visibility = View.VISIBLE + binding.authDropDownLayout.visibility = View.VISIBLE + binding.dropdownProtocol.visibility = View.GONE + binding.certRow.visibility = View.GONE + toggleSFTPAuthVisibility(View.GONE) + if (Authentication.valueOf(binding.authDropDownMenu.value) == Authentication.Password) { + toggleCredentialsVisibility(View.VISIBLE) + } else { + toggleCredentialsVisibility(View.GONE) + } + binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, authentications)) + binding.authDropDownMenu.setText(authentications[0].toString(), false) + onAuthSelected(authentications[0].toString()) } } @@ -256,4 +263,23 @@ class ConnectionDialog( } } + private fun populateDialogValues(){ + connection?.let { + binding.apply { + hostEt.setText(it.host) + userEt.setText(it.username) + passwordEt.setText(it.password) + sharedPathEt.setText(it.sharedPath) + displayEt.setText(it.displayName) + portEt.setText(it.port.toString()) + dropdownMenu.setText(it.connectionType.type, false) + privateKeyEt.setText(it.privateKeyText) + privateKeyPassEt.setText(it.privateKeyPass) + handleConnectionTypeSelection(it.connectionType.type) + authDropDownMenu.setText(it.authentication.toString(), false) + onAuthSelected(it.authentication.toString()) + } + } + } + } diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt index c2586b17c..100d85dac 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt @@ -1,11 +1,10 @@ package org.fossify.filemanager.interfaces import kotlinx.coroutines.flow.Flow -import org.fossify.filemanager.entity.NetworkConnectionEntity import org.fossify.filemanager.models.NetworkConnection interface NetworkConnectionRepositoryDb { - suspend fun saveConnection(connection: NetworkConnection): Long; + suspend fun insertUpdateConnection(connection: NetworkConnection): Long; suspend fun getAllSavedConnections(): Flow> - suspend fun deleteConnection() + suspend fun deleteConnection(connection: NetworkConnection) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index c566b4842..cd60b1f7d 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -23,7 +23,9 @@ fun NetworkConnectionEntity.toDomain(): NetworkConnection { url = url, authentication = Authentication.valueOf(authentication), privateKeyText = privateKey, - privateKeyPass = privateKeyPass + privateKeyPass = privateKeyPass, + id = id + ) } @@ -39,7 +41,8 @@ fun NetworkConnection.toEntity(): NetworkConnectionEntity { url = url, authentication = authentication.toString(), privateKey = privateKeyText, - privateKeyPass = privateKeyPass + privateKeyPass = privateKeyPass, + id = id ) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt index 2786fef6a..63bbc9fad 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt @@ -4,6 +4,7 @@ import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.enums.Authentication data class NetworkConnection( + val id: Long = 0, val host: String = "", val port: Int = 445, val username: String? = "", diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt index aca12464f..1925d7d59 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt @@ -9,15 +9,15 @@ import org.fossify.filemanager.mapper.toEntity import org.fossify.filemanager.models.NetworkConnection class NetworkConnectionRepositoryDbImpl(private val dao: NetworkConnectionDao): NetworkConnectionRepositoryDb { - override suspend fun saveConnection(connection: NetworkConnection): Long { - return dao.insert(connection.toEntity()) + override suspend fun insertUpdateConnection(connection: NetworkConnection): Long { + return dao.insertUpdateConnection(connection.toEntity()) } override suspend fun getAllSavedConnections(): Flow> { return dao.getAll().map { value -> value.map { entity -> entity.toDomain() } } } - override suspend fun deleteConnection() { - TODO("Not yet implemented") + override suspend fun deleteConnection(connection: NetworkConnection) { + dao.delete(connection.toEntity()) } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 23f37da08..114b76bbb 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -61,9 +61,9 @@ class NetworkBrowserViewModel( - fun saveNetwork(networkConnection: NetworkConnection) { + fun insertUpdateConnection(networkConnection: NetworkConnection) { viewModelScope.launch(Dispatchers.IO) { - networkConnectionRepository.saveConnection(networkConnection) + networkConnectionRepository.insertUpdateConnection(networkConnection) } } @@ -75,6 +75,12 @@ class NetworkBrowserViewModel( } } + fun deleteConnection(connection: NetworkConnection){ + viewModelScope.launch(Dispatchers.IO) { + networkConnectionRepository.deleteConnection(connection) + } + } + fun verifyNetwork(connection: NetworkConnection, saveInfo: Boolean) { viewModelScope.launch(Dispatchers.IO) { val value = smbApi.verifyConnection(connection) diff --git a/app/src/main/res/layout/item_network_connection.xml b/app/src/main/res/layout/item_network_connection.xml index 9da6fe89c..216ab30c4 100644 --- a/app/src/main/res/layout/item_network_connection.xml +++ b/app/src/main/res/layout/item_network_connection.xml @@ -8,39 +8,74 @@ app:cardElevation="6dp" app:cardBackgroundColor="?attr/colorSurface"> - + android:layout_height="wrap_content"> - + android:orientation="vertical" + android:padding="16dp" + android:paddingEnd="88dp"> - + - + + + + + + + + + + + + + - - From 2cdc6fc0ab3874d2a3761da8879f91a284e66f68 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sat, 6 Jun 2026 22:30:24 +0500 Subject: [PATCH 30/37] Addded validation for fields, code refactoring --- .../filemanager/activities/CloudActivity.kt | 148 ++++------------ .../adapters/ConnectionItemsAdapter.kt | 9 +- .../filemanager/dialogs/ConnectionDialog.kt | 159 +++++++++++++++--- .../entity/NetworkConnectionEntity.kt | 3 +- .../filemanager/fileSystems/HttpServer.kt | 4 +- .../filemanager/interfaces/WebDavApi.kt | 2 +- .../mapper/NetworkConnectionMapper.kt | 8 +- .../filemanager/models/NetworkConnection.kt | 26 +-- .../filemanager/repository/WebDavApiImpl.kt | 3 +- .../viewmodels/NetworkBrowserViewModel.kt | 4 +- .../main/res/layout/dialog_add_connection.xml | 8 + 11 files changed, 207 insertions(+), 167 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index abb4b5488..bda9f1e50 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -144,8 +144,8 @@ class CloudActivity : SimpleActivity() { } private fun showConnectionDialog() { - ConnectionDialog(this@CloudActivity) { host, user, password, shared, displayName, certPath, privateKeyText, privateKeyPass, port, connection, protocol, auth -> - saveNetwork(host, user, password, shared, displayName, privateKeyText, privateKeyPass, certPath, port, connection, protocol, auth) + ConnectionDialog(this@CloudActivity) { connection, certUri -> + saveNetwork(connection, certUri) } } @@ -157,72 +157,25 @@ class CloudActivity : SimpleActivity() { } private fun saveNetwork( - host: String, - user: String, - password: String, - shared: String, - displayName: String, - privateKeyText: String, - privateKeyPass: String, - certUri: Uri?, - port: Int, - connectionType: ConnectionTypes, - protocol: Protocols?, - authentication: Authentication + connection: NetworkConnection, + certUri: Uri? ) { - if (connectionType == ConnectionTypes.SMB) { + if (connection.connectionType == ConnectionTypes.SMB) { viewModel.verifyNetwork( - NetworkConnection( - host = host, - username = user, - password = password, - sharedPath = shared, - connectionType = connectionType, - displayName = displayName, - authentication = authentication, - port = port - ), true + connection, true ) - } else if (connectionType == ConnectionTypes.WebDav) { - if (protocol == Protocols.HTTPS) { - saveCertificate(certUri, host) + } else if (connection.connectionType == ConnectionTypes.WebDav) { + if (connection.protocols == Protocols.HTTPS) { + saveCertificate(certUri, connection.host) } - val url = Helpers.createProtocolPath(protocol, host, port, shared) - val network = NetworkConnection( - host = host, - username = user, - password = password, - connectionType = connectionType, - port = port, - displayName = displayName, - authentication = authentication, - url = url - ) - viewModel.connectAndAuthenticateWebDav(network, protocol!!, true, this@CloudActivity) - } else if (connectionType == ConnectionTypes.SFTP) { - val network = NetworkConnection( - host = host, - username = user, - password = password, - connectionType = connectionType, - port = port, - displayName = displayName, - authentication = authentication, - privateKeyText = privateKeyText, - privateKeyPass = privateKeyPass - ) - viewModel.connectSFTP(network, true) - } else if (connectionType == ConnectionTypes.FTP) { + val url = Helpers.createProtocolPath(connection.protocols, connection.host, connection.port, connection.sharedPath) + connection.url = url + viewModel.connectAndAuthenticateWebDav(connection, true, this@CloudActivity) + } else if (connection.connectionType == ConnectionTypes.SFTP) { + viewModel.connectSFTP(connection, true) + } else if (connection.connectionType == ConnectionTypes.FTP) { viewModel.connectFTP( - NetworkConnection( - username = user, - password = password, - host = host, - port = port, - connectionType = ConnectionTypes.FTP, - authentication = authentication, - displayName = displayName, - ), true + connection, true ) } } @@ -248,14 +201,13 @@ class CloudActivity : SimpleActivity() { } ConnectionTypes.DAVx5 -> { - launchMainActivity(ConnectionTypes.DAVx5, item.sharedPath,item.displayName) + launchMainActivity(ConnectionTypes.DAVx5, item.sharedPath, item.displayName) } ConnectionTypes.WebDav -> { val protocol = item.url.split(':')[0]; viewModel.connectAndAuthenticateWebDav( item, - Protocols.valueOf(protocol.uppercase(Locale.getDefault())), false, this@CloudActivity @@ -275,7 +227,7 @@ class CloudActivity : SimpleActivity() { } private fun updateAdapter(listItems: MutableList) { - ConnectionItemsAdapter(this, listItems, binding.connectionsList,::deleteConnection,::updateConnection) { item -> + ConnectionItemsAdapter(this, listItems, binding.connectionsList, ::deleteConnection, ::updateConnection) { item -> lifecycleScope.launch { val itm = item as NetworkConnection handleConnection(itm, itm.connectionType) @@ -285,13 +237,13 @@ class CloudActivity : SimpleActivity() { } } - private fun deleteConnection(connection: NetworkConnection){ + private fun deleteConnection(connection: NetworkConnection) { viewModel.deleteConnection(connection) } - private fun updateConnection(connection: NetworkConnection){ - ConnectionDialog(this@CloudActivity,connection) { host, user, password, shared, displayName, certPath, privateKeyText, privateKeyPass, port, connection, protocol, auth -> - saveNetwork(host, user, password, shared, displayName, privateKeyText, privateKeyPass, certPath, port, connection, protocol, auth) + private fun updateConnection(connection: NetworkConnection) { + ConnectionDialog(this@CloudActivity, connection) { con, uri -> + saveNetwork(con, uri) } } @@ -299,7 +251,7 @@ class CloudActivity : SimpleActivity() { val intent = Intent(this@CloudActivity, MainActivity::class.java).apply { putExtra(PATH, path) putExtra(CONNECTION_TYPE, connectionType) - if (name != ""){ + if (name != "") { putExtra(DAVX5_PATH_NAME, name) } } @@ -353,28 +305,18 @@ class CloudActivity : SimpleActivity() { viewModel.verifyWebDav.collectLatest { if (it.success) { if (!it.saveInfo) { - val protocol = it.item.url.split(':')[0]; startServer( it.item, PORT_WEBDAV, connectionType = ConnectionTypes.WebDav, machinePort = it.item.port, - Protocols.valueOf(protocol.uppercase(Locale.getDefault())) + it.item.protocols!! ) launchMainActivity(ConnectionTypes.WebDav, it.item.url) } else { + it.item.connectionType = connectionType viewModel.insertUpdateConnection( - NetworkConnection( - host = it.item.host, - username = it.item.username, - password = it.item.password, - sharedPath = it.item.sharedPath, - connectionType = connectionType, - displayName = it.item.displayName, - url = it.item.url, - port = it.item.port, - authentication = it.item.authentication - ) + it.item ) } } else { @@ -394,15 +336,7 @@ class CloudActivity : SimpleActivity() { launchMainActivity(ConnectionTypes.SMB, path) } else { viewModel.insertUpdateConnection( - NetworkConnection( - host = it.item.host, - username = it.item.username, - password = it.item.password, - connectionType = connectionType, - displayName = it.item.displayName, - sharedPath = it.item.sharedPath, - authentication = it.item.authentication - ) + it.item ) } } else { @@ -420,19 +354,9 @@ class CloudActivity : SimpleActivity() { startServer(it.item, PORT_SFTP, connectionType = ConnectionTypes.SFTP, machinePort = it.item.port) launchMainActivity(ConnectionTypes.SFTP, it.item.url) } else { + it.item.url = "/" viewModel.insertUpdateConnection( - NetworkConnection( - host = it.item.host, - username = it.item.username, - password = it.item.password, - connectionType = connectionType, - port = it.item.port, - displayName = it.item.displayName, - url = "/", - authentication = it.item.authentication, - privateKeyText = it.item.privateKeyText, - privateKeyPass = it.item.privateKeyPass - ) + it.item ) } } else { @@ -450,22 +374,14 @@ class CloudActivity : SimpleActivity() { startServer(it.item, PORT_FTP, connectionType = ConnectionTypes.FTP, machinePort = it.item.port) launchMainActivity(ConnectionTypes.FTP, it.item.url) } else { + it.item.url = "/" viewModel.insertUpdateConnection( - NetworkConnection( - host = it.item.host, - username = it.item.username, - password = it.item.password, - connectionType = connectionType, - port = it.item.port, - displayName = it.item.displayName, - url = "/", - authentication = it.item.authentication - ) + it.item + ) } } else { it.exception?.let { exception -> - toast(exception.message.toString()) } } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt b/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt index 3ef844409..db11ea1d2 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt @@ -9,7 +9,9 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions import org.fossify.commons.adapters.MyRecyclerViewAdapter +import org.fossify.commons.dialogs.ConfirmationDialog import org.fossify.commons.views.MyRecyclerView +import org.fossify.filemanager.R import org.fossify.filemanager.activities.SimpleActivity import org.fossify.filemanager.databinding.ItemDecompressionListFileDirBinding import org.fossify.filemanager.databinding.ItemNetworkConnectionBinding @@ -82,7 +84,12 @@ class ConnectionItemsAdapter( tvType.text = listItem.connectionType.toString() tvDisplayName.text = listItem.displayName tvSharedPath.text = listItem.sharedPath - btnDelete.setOnClickListener { deleteClick(listItem) } + btnDelete.setOnClickListener { + val question = String.format(resources.getString(R.string.deletion_confirmation_connection),listItem.displayName) + ConfirmationDialog(activity, question) { + deleteClick(listItem) + } + } btnEdit.setOnClickListener { updateConnection(listItem) } } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt index 868696542..b7c1025a9 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt @@ -1,12 +1,13 @@ package org.fossify.filemanager.dialogs import android.net.Uri -import android.util.Log import android.view.View import android.widget.ArrayAdapter +import androidx.core.widget.doAfterTextChanged import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.getAlertDialogBuilder +import org.fossify.commons.extensions.isVisible import org.fossify.commons.extensions.setupDialogStuff import org.fossify.commons.extensions.value import org.fossify.filemanager.R @@ -20,12 +21,11 @@ import org.fossify.filemanager.helpers.DEFAULT_SMB_PORT import org.fossify.filemanager.helpers.DEFAULT_WEBDAV_HTTPS_PORT import org.fossify.filemanager.helpers.DEFAULT_WEBDAV_HTTP_PORT import org.fossify.filemanager.models.NetworkConnection -import java.io.File class ConnectionDialog( val activity: BaseSimpleActivity, val connection: NetworkConnection? = null, - dispatch: (String, String, String, String, String, Uri?, String, String, Int, ConnectionTypes, Protocols?, Authentication) -> Unit + dispatch: (NetworkConnection, Uri?) -> Unit ) { private var binding: DialogAddConnectionBinding val items = listOf(ConnectionTypes.DAVx5.type, ConnectionTypes.SMB.type, ConnectionTypes.WebDav.type, ConnectionTypes.SFTP.type, ConnectionTypes.FTP.type) @@ -40,30 +40,22 @@ class ConnectionDialog( init { binding = DialogAddConnectionBinding.inflate(activity.layoutInflater) - activity.getAlertDialogBuilder() - .setPositiveButton(R.string.ok) { _, _ -> - dispatch( - binding.hostEt.value, - binding.userEt.value, - binding.passwordEt.value, - binding.sharedPathEt.value, - binding.displayEt.value, - certUri, - binding.privateKeyEt.value.trimIndent(), - binding.privateKeyPassEt.value, - binding.portEt.value.toIntOrNull() ?: 0, - ConnectionTypes.fromType(binding.dropdownMenu.value), - binding.dropdownMenuProtocol.text - ?.toString() - ?.takeIf { it.isNotBlank() } - ?.let { Protocols.valueOf(it) }, - Authentication.valueOf(binding.authDropDownMenu.text.toString()) - ) - } + val dialog = activity.getAlertDialogBuilder() + .setPositiveButton(R.string.ok, null) .setNegativeButton(R.string.cancel, null) .apply { - activity.setupDialogStuff(binding.root, this) + activity.setupDialogStuff(binding.root, this) { alertDialog -> + alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(View.OnClickListener { + val isValid = validateFields() + if (isValid) { + val connection = createConnection() + dispatch(connection, certUri) + alertDialog.dismiss() + } + }) + } } + .create() dropDownItemSelected() initializeDropDownList() registerAuthClickListener() @@ -71,6 +63,7 @@ class ConnectionDialog( attachPrivateKeyBtnClickListener() dropDownMenuProtocolItemClickListener() populateDialogValues() + textFieldsListener() } @@ -80,6 +73,33 @@ class ConnectionDialog( initializeProtocolDropDown() } + private fun createConnection():NetworkConnection { + val networkConnection = NetworkConnection( + host = binding.hostEt.value, + port = binding.portEt.value.toIntOrNull() ?: 445, + username = binding.userEt.value, + password = binding.passwordEt.value, + displayName = binding.displayEt.value, + connectionType = ConnectionTypes.fromType(binding.dropdownMenu.value), + sharedPath = binding.sharedPathEt.value, + url = "", + privateKeyText = binding.privateKeyEt.value.trimIndent(), + privateKeyPass = binding.privateKeyPassEt.value, + authentication = Authentication.valueOf( + binding.authDropDownMenu.text.toString() + ), + protocols = binding.dropdownMenuProtocol.text + ?.toString() + ?.takeIf { it.isNotBlank() } + ?.let { Protocols.valueOf(it) }, + ) + + connection?.let { + networkConnection.id = it.id + } + return networkConnection + } + private fun initializeConnectionsDropDown() { val adapter = ArrayAdapter(activity, android.R.layout.simple_list_item_1, items) binding.dropdownMenu.setAdapter(adapter) @@ -98,7 +118,7 @@ class ConnectionDialog( binding.authDropDownMenu.setText(authentications[0].toString(), false) } - private fun onAuthSelected(selectedItem: String){ + private fun onAuthSelected(selectedItem: String) { if (binding.dropdownMenu.value == ConnectionTypes.SMB.type) { if (Authentication.valueOf(selectedItem) == Authentication.Anonymous) { toggleCredentialsVisibility(View.GONE) @@ -126,6 +146,7 @@ class ConnectionDialog( } } } + private fun registerAuthClickListener() { binding.authDropDownMenu.setOnItemClickListener { parent, view, position, id -> val selectedItem = parent.getItemAtPosition(position).toString() @@ -138,6 +159,10 @@ class ConnectionDialog( binding.privateKeyPassTf.visibility = visibility } + private fun toggleSharedPathVisibility(visibility: Int) { + binding.sharedPathTf.visibility = visibility + } + private fun toggleCredentialsVisibility(visibility: Int) { binding.userTf.visibility = visibility binding.passwordTf.visibility = visibility @@ -169,6 +194,7 @@ class ConnectionDialog( binding.authDropDownLayout.visibility = View.GONE toggleSFTPAuthVisibility(View.GONE) toggleCredentialsVisibility(View.VISIBLE) + toggleSharedPathVisibility(View.VISIBLE) } else if (selectedItem == ConnectionTypes.SMB.type) { binding.dropdownProtocol.visibility = View.GONE binding.certRow.visibility = View.GONE @@ -177,6 +203,7 @@ class ConnectionDialog( toggleSFTPAuthVisibility(View.GONE) binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, authentications)) binding.authDropDownMenu.setText(authentications[0].toString(), false) + toggleSharedPathVisibility(View.VISIBLE) onAuthSelected(authentications[0].toString()) } else if (selectedItem == ConnectionTypes.SFTP.type) { binding.allFieldsExceptConnection.visibility = View.VISIBLE @@ -191,6 +218,7 @@ class ConnectionDialog( binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, sftpAuthentications)) binding.authDropDownMenu.setText(sftpAuthentications[0].toString(), false) onAuthSelected(sftpAuthentications[0].toString()) + toggleSharedPathVisibility(View.GONE) } else if (selectedItem == ConnectionTypes.FTP.type) { binding.allFieldsExceptConnection.visibility = View.VISIBLE @@ -206,6 +234,7 @@ class ConnectionDialog( binding.authDropDownMenu.setAdapter(ArrayAdapter(activity, android.R.layout.simple_list_item_1, authentications)) binding.authDropDownMenu.setText(authentications[0].toString(), false) onAuthSelected(authentications[0].toString()) + toggleSharedPathVisibility(View.GONE) } } @@ -263,7 +292,82 @@ class ConnectionDialog( } } - private fun populateDialogValues(){ + + private fun validateFields(): Boolean { + binding.apply { + if (hostTf.isVisible() && hostEt.value.isEmpty()) { + hostTf.error = activity.getString(R.string.host_name_error) + return false + } + if (userTf.isVisible() && userEt.value.isNullOrEmpty()) { + userTf.error = activity.getString(R.string.user_name_error) + return false + } + if (passwordTf.isVisible() && passwordEt.value.isNullOrEmpty()) { + passwordTf.error = activity.getString(R.string.password_error) + return false + } + if (sharedPathTf.isVisible() && sharedPathEt.value.isNullOrEmpty()) { + sharedPathTf.error = activity.getString(R.string.shared_path_error) + return false + } + if (displayTf.isVisible() && displayEt.value.isNullOrEmpty()) { + displayTf.error = activity.getString(R.string.display_name_error) + return false + } + if (portTf.isVisible() && portEt.value.isNullOrEmpty()) { + portTf.error = activity.getString(R.string.port_error) + return false + } + if (privateKeyTf.isVisible() && privateKeyEt.value.isNullOrEmpty()) { + privateKeyTf.error = activity.getString(R.string.private_key_error) + return false + } + if (privateKeyPassTf.isVisible() && privateKeyPassEt.value.isNullOrEmpty()) { + privateKeyPassTf.error = activity.getString(R.string.private_key_pass_error) + return false + } + } + return true + } + + private fun textFieldsListener() { + binding.apply { + hostEt.doAfterTextChanged { editable -> + hostTf.error = null + } + + userEt.doAfterTextChanged { editable -> + userTf.error = null + } + + passwordEt.doAfterTextChanged { editable -> + passwordTf.error = null + } + + sharedPathEt.doAfterTextChanged { editable -> + sharedPathTf.error = null + } + + displayEt.doAfterTextChanged { editable -> + displayTf.error = null + } + + portEt.doAfterTextChanged { editable -> + portTf.error = null + } + + privateKeyEt.doAfterTextChanged { editable -> + privateKeyTf.error = null + } + + privateKeyPassEt.doAfterTextChanged { editable -> + privateKeyPassTf.error = null + } + } + } + + private fun populateDialogValues() { connection?.let { binding.apply { hostEt.setText(it.host) @@ -271,13 +375,14 @@ class ConnectionDialog( passwordEt.setText(it.password) sharedPathEt.setText(it.sharedPath) displayEt.setText(it.displayName) - portEt.setText(it.port.toString()) dropdownMenu.setText(it.connectionType.type, false) privateKeyEt.setText(it.privateKeyText) privateKeyPassEt.setText(it.privateKeyPass) + dropdownMenuProtocol.setText(it.protocols.toString(), false) handleConnectionTypeSelection(it.connectionType.type) authDropDownMenu.setText(it.authentication.toString(), false) onAuthSelected(it.authentication.toString()) + portEt.setText(it.port.toString()) } } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt index d301037fe..d4903e554 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt @@ -17,5 +17,6 @@ data class NetworkConnectionEntity( val url: String, val authentication: String, val privateKey: String = "", - val privateKeyPass: String = "" + val privateKeyPass: String = "", + val protocols: String? = null ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 5c23463a2..62cfb8d92 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -55,8 +55,8 @@ class HttpServer( private fun handleWebDav(uri: String, rangeHeader: String?): Response { - val extractedPath = Helpers.retrievePath(uri) - val url = Helpers. createNanoHttpdUrl(connectionType, extractedPath, server = serverIp, port = machinePort, protocols = protocol) +// val extractedPath = Helpers.retrievePath(uri) + val url = Helpers. createNanoHttpdUrl(connectionType, uri, server = serverIp, port = machinePort, protocols = protocol) val apiResponse = composition.webDavApiRepository.listWebDavFileDetail(url) return handleResponse(apiResponse) { diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt index 696d9d92e..ea7482997 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/WebDavApi.kt @@ -9,7 +9,7 @@ import java.io.File import java.io.InputStream interface WebDavApi { - suspend fun connectAndVerifyWebDav(connection: NetworkConnection,protocols: Protocols, context: Context): Pair + suspend fun connectAndVerifyWebDav(connection: NetworkConnection, context: Context): Pair suspend fun listAllFilesOnWebDav(url: String): ApiResponse> diff --git a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt index cd60b1f7d..14818252e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/mapper/NetworkConnectionMapper.kt @@ -9,6 +9,7 @@ import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.models.FileDirItem import org.fossify.filemanager.entity.NetworkConnectionEntity import org.fossify.filemanager.enums.Authentication +import org.fossify.filemanager.enums.Protocols import org.fossify.filemanager.models.NetworkConnection fun NetworkConnectionEntity.toDomain(): NetworkConnection { @@ -24,8 +25,8 @@ fun NetworkConnectionEntity.toDomain(): NetworkConnection { authentication = Authentication.valueOf(authentication), privateKeyText = privateKey, privateKeyPass = privateKeyPass, - id = id - + id = id, + protocols = Protocols.valueOf(protocols ?: "") ) } @@ -42,7 +43,8 @@ fun NetworkConnection.toEntity(): NetworkConnectionEntity { authentication = authentication.toString(), privateKey = privateKeyText, privateKeyPass = privateKeyPass, - id = id + id = id, + protocols = protocols?.toString() ) } diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt index 63bbc9fad..e741183e2 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/models/NetworkConnection.kt @@ -2,20 +2,22 @@ package org.fossify.filemanager.models import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.enums.Authentication +import org.fossify.filemanager.enums.Protocols data class NetworkConnection( - val id: Long = 0, - val host: String = "", - val port: Int = 445, - val username: String? = "", - val password: String? = "", - val displayName: String = "", - val connectionType: ConnectionTypes, - val sharedPath: String = "", - val url: String = "", - val privateKeyText: String = "", - val privateKeyPass: String = "", - val authentication: Authentication = Authentication.Password + var id: Long = 0, + var host: String = "", + var port: Int = 445, + var username: String? = "", + var password: String? = "", + var displayName: String = "", + var connectionType: ConnectionTypes, + var sharedPath: String = "", + var url: String = "", + var privateKeyText: String = "", + var privateKeyPass: String = "", + var authentication: Authentication = Authentication.Password, + var protocols: Protocols? = Protocols.HTTP ) diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt index cb79c5483..822ecfb7d 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt @@ -29,11 +29,10 @@ class WebDavApiImpl: WebDavApi { val scope = CoroutineScope(Dispatchers.IO) override suspend fun connectAndVerifyWebDav( connection: NetworkConnection, - protocols: Protocols, context: Context ): Pair { return try { - sardine = if (protocols == Protocols.HTTP) { + sardine = if (connection.protocols == Protocols.HTTP) { OkHttpSardine() } else { createHTTPSSardine(context,connection.host) diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 114b76bbb..1b3f6503f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -115,9 +115,9 @@ class NetworkBrowserViewModel( fun getSFTPConn(): SFTPClient = sftpApi.getSFTPConn() - fun connectAndAuthenticateWebDav(connection: NetworkConnection, protocol: Protocols, saveInfo: Boolean, context: Context) { + fun connectAndAuthenticateWebDav(connection: NetworkConnection, saveInfo: Boolean, context: Context) { viewModelScope.launch(Dispatchers.IO) { - val result = webDavApi.connectAndVerifyWebDav(connection, protocol, context) + val result = webDavApi.connectAndVerifyWebDav(connection, context) verifyWebDav.emit(ConnectionResult(connection, result.first, saveInfo,result.second)) } } diff --git a/app/src/main/res/layout/dialog_add_connection.xml b/app/src/main/res/layout/dialog_add_connection.xml index a16628174..94befc5fd 100644 --- a/app/src/main/res/layout/dialog_add_connection.xml +++ b/app/src/main/res/layout/dialog_add_connection.xml @@ -72,6 +72,7 @@ android:id="@+id/host_tf" android:layout_width="match_parent" android:layout_height="wrap_content" + app:errorTextColor="@color/md_red" android:hint="Host"> Date: Sun, 7 Jun 2026 01:44:15 +0500 Subject: [PATCH 31/37] Added loadder and removed useless variables --- .../filemanager/activities/CloudActivity.kt | 19 +++++++++++++++-- .../filemanager/activities/MainActivity.kt | 21 +++++++++---------- .../filemanager/fragments/ItemsFragment.kt | 9 ++++---- app/src/main/res/layout/cloud_activity.xml | 14 +++++++------ 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index bda9f1e50..5f4b2e690 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -5,6 +5,7 @@ import android.net.Uri import android.os.Bundle import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.isVisible import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider @@ -12,6 +13,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.viewBinding import org.fossify.commons.helpers.NavigationIcon @@ -49,7 +51,7 @@ class CloudActivity : SimpleActivity() { private lateinit var certificateRepository: CertificateRepository private lateinit var composition: AppComposition private var https: HttpServer? = null - + private var isConnecting: Boolean = false private fun setupBouncyCastle() { val provider: Provider? = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) if (provider == null) { @@ -205,7 +207,6 @@ class CloudActivity : SimpleActivity() { } ConnectionTypes.WebDav -> { - val protocol = item.url.split(':')[0]; viewModel.connectAndAuthenticateWebDav( item, false, @@ -227,7 +228,11 @@ class CloudActivity : SimpleActivity() { } private fun updateAdapter(listItems: MutableList) { + if (isConnecting) return ConnectionItemsAdapter(this, listItems, binding.connectionsList, ::deleteConnection, ::updateConnection) { item -> + if (isConnecting) return@ConnectionItemsAdapter + isConnecting = true + binding.loader.isVisible = true lifecycleScope.launch { val itm = item as NetworkConnection handleConnection(itm, itm.connectionType) @@ -324,6 +329,7 @@ class CloudActivity : SimpleActivity() { toast(exception.message.toString()) } } + hideLoader() } } @@ -344,6 +350,7 @@ class CloudActivity : SimpleActivity() { toast(exception.message.toString()) } } + hideLoader() } } @@ -364,6 +371,7 @@ class CloudActivity : SimpleActivity() { toast(exception.message.toString()) } } + hideLoader() } } @@ -382,8 +390,10 @@ class CloudActivity : SimpleActivity() { } } else { it.exception?.let { exception -> + toast(exception.message.toString()) } } + hideLoader() } } @@ -392,6 +402,11 @@ class CloudActivity : SimpleActivity() { } } + private fun hideLoader() { + binding.loader.isVisible = false + isConnecting = false + } + private fun saveCertificate(uri: Uri?, name: String) { uri?.let { diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt index d53e95b08..97f8d70ec 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt @@ -118,18 +118,17 @@ class MainActivity : SimpleActivity() { config.temporarilyShowHidden = false initFragments() val data = getIntentDataIfAny() - tryInitFileManager(data.first,data.second, connectionType = data.third) + tryInitFileManager(data.first, connectionType = data.second) checkWhatsNewDialog() checkIfRootAvailable() checkInvalidFavorites() } } - private fun getIntentDataIfAny(): Triple { + private fun getIntentDataIfAny(): Pair { val path = intent.getStringExtra(PATH) - val isNetworkPath = intent.getBooleanExtra(NETWORK_PATH, false) val connectionType = intent.getSerializableExtra(CONNECTION_TYPE) as? ConnectionTypes ?: ConnectionTypes.Default - return Triple(path ?: "",isNetworkPath,connectionType) + return Pair(path ?: "",connectionType) } private fun getDavX5PathName(): String{ @@ -325,7 +324,7 @@ class MainActivity : SimpleActivity() { } } - private fun tryInitFileManager(path: String, isNetworkPath : Boolean = false, connectionType: ConnectionTypes = ConnectionTypes.Default) { + private fun tryInitFileManager(path: String, connectionType: ConnectionTypes = ConnectionTypes.Default) { val hadPermission = hasStoragePermission() handleStoragePermission { checkOTGPath() @@ -335,7 +334,7 @@ class MainActivity : SimpleActivity() { } binding.mainViewPager.onGlobalLayout { - initFileManager(!hadPermission,path,isNetworkPath,connectionType) + initFileManager(!hadPermission,path,connectionType) } } else { toast(R.string.no_storage_permissions) @@ -344,7 +343,7 @@ class MainActivity : SimpleActivity() { } } - private fun initFileManager(refreshRecents: Boolean, path: String, isNetworkPath: Boolean, connectionType: ConnectionTypes) { + private fun initFileManager(refreshRecents: Boolean, path: String, connectionType: ConnectionTypes) { val pathName = getDavX5PathName() if (intent.action == Intent.ACTION_VIEW && intent.data != null) { val data = intent.data @@ -365,7 +364,7 @@ class MainActivity : SimpleActivity() { binding.mainViewPager.currentItem = 0 } else { - openPath(if(path.isNotEmpty()) path else config.homeFolder,isNetworkPath = isNetworkPath, connectionType = connectionType, pathName = pathName) + openPath(if(path.isNotEmpty()) path else config.homeFolder, connectionType = connectionType, pathName = pathName) } if (refreshRecents) { @@ -491,18 +490,18 @@ class MainActivity : SimpleActivity() { } } - private fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, pathName: String = "",connectionType: ConnectionTypes = ConnectionTypes.Default) { + private fun openPath(path: String, forceRefresh: Boolean = false, pathName: String = "",connectionType: ConnectionTypes = ConnectionTypes.Default) { var newPath = path val file = File(path) if (config.OTGPath.isNotEmpty() && config.OTGPath == path.trimEnd('/')) { newPath = path - } else if (file.exists() && !file.isDirectory && !isNetworkPath) { + } else if (file.exists() && !file.isDirectory && connectionType == ConnectionTypes.Default) { newPath = file.parent } else if (!file.exists() && !isPathOnOTG(newPath) && connectionType.equals(ConnectionTypes.Default)) { newPath = internalStoragePath } - getItemsFragment()?.openPath(newPath, forceRefresh, isNetworkPath,connectionType=connectionType, pathName = pathName) + getItemsFragment()?.openPath(newPath, forceRefresh,connectionType=connectionType, pathName = pathName) } private fun goHome() { diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index 5d8eec164..da05d89b1 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -133,7 +133,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } - fun openPath(path: String, forceRefresh: Boolean = false, isNetworkPath: Boolean = false, pathName: String = "", connectionType: ConnectionTypes = ConnectionTypes.Default) { + fun openPath(path: String, forceRefresh: Boolean = false, pathName: String = "", connectionType: ConnectionTypes = ConnectionTypes.Default) { if ((activity as? BaseSimpleActivity)?.isAskingPermissions == true) { return } @@ -147,7 +147,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF currentPath = realPath showHidden = context!!.config.shouldShowHidden() showProgressBar() - getItems(currentPath, isNetworkPath, connectionType = connectionType) { originalPath, listItems -> + getItems(currentPath, connectionType = connectionType) { originalPath, listItems -> if (currentPath != originalPath) { return@getItems } @@ -173,7 +173,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF itemsIgnoringSearch = listItems activity?.runOnUiThread { (activity as? MainActivity)?.refreshMenuItems() - addItems(listItems, forceRefresh, isNetworkPath,pathName = pathName, connectionType) + addItems(listItems, forceRefresh,pathName = pathName, connectionType) if (context != null && currentViewType != context!!.config.getFolderViewType(currentPath)) { setupLayoutManager(connectionType) } @@ -182,7 +182,7 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } - private fun addItems(items: ArrayList, forceRefresh: Boolean = false, isNetworkPath: Boolean = false,pathName: String = "", connectionType: ConnectionTypes) { + private fun addItems(items: ArrayList, forceRefresh: Boolean = false,pathName: String = "", connectionType: ConnectionTypes) { activity?.runOnUiThread { binding.itemsSwipeRefresh.isRefreshing = false if (connectionType != ConnectionTypes.DAVx5){ @@ -253,7 +253,6 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF @SuppressLint("NewApi") private fun getItems( path: String, - isNetworkPath: Boolean = false, connectionType: ConnectionTypes, callback: (originalPath: String, items: ArrayList) -> Unit ) { diff --git a/app/src/main/res/layout/cloud_activity.xml b/app/src/main/res/layout/cloud_activity.xml index de5833795..40c583337 100644 --- a/app/src/main/res/layout/cloud_activity.xml +++ b/app/src/main/res/layout/cloud_activity.xml @@ -18,7 +18,6 @@ app:title="Cloud Connection" app:titleTextAppearance="@style/AppTheme.ActionBar.TitleTextStyle"/> - - - + + From ae65ceac8090dfc0f6fda97aa662b3c8ec91a44c Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sun, 7 Jun 2026 02:15:00 +0500 Subject: [PATCH 32/37] smb path not working fixed --- .../org/fossify/filemanager/fileSystems/HttpServer.kt | 3 ++- .../org/fossify/filemanager/interfaces/SMBApi.kt | 2 ++ .../org/fossify/filemanager/repository/SMBApiImpl.kt | 10 ++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 62cfb8d92..45d4c7da6 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -36,7 +36,8 @@ class HttpServer( private fun handleSmb(uri: String, rangeHeader: String?): Response { val url = Helpers. createNanoHttpdUrl(connectionType, uri, server = serverIp, port = machinePort, protocols = protocol) - val file = SmbFile(url) + val file = composition.smbApiRepository.getSmbFile(url).response + if (file == null) return notFound() if (!file.exists()) return notFound() val fileLength = file.length() diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt index 04ada07f7..a827f64b8 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/SMBApi.kt @@ -17,5 +17,7 @@ interface SMBApi { fun writeFileToCache(path: String,context: Context): ApiResponse + fun getSmbFile(path: String): ApiResponse + fun getMainSmbFile(): SmbFile } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt index f7f09f491..4e0ff660e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/SMBApiImpl.kt @@ -106,5 +106,15 @@ class SMBApiImpl : SMBApi { } } + override fun getSmbFile(path: String): ApiResponse { + return try { + val smbFile = SmbFile(path, smbClient.context) + ApiResponse(smbFile,null) + } + catch (exp: Exception){ + ApiResponse(null,exp) + } + } + override fun getMainSmbFile(): SmbFile = smbClient } From 8aa03489c4e90af23501b62527e4dcbf414e716e Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sat, 13 Jun 2026 13:26:56 +0500 Subject: [PATCH 33/37] some code refactoring of url and helper methods --- .../filemanager/activities/CloudActivity.kt | 105 ++++++++++++------ .../filemanager/dao/NetworkConnectionDao.kt | 8 +- .../entity/NetworkConnectionEntity.kt | 5 +- .../filemanager/fileSystems/FileHelpers.kt | 28 ++--- .../filemanager/fileSystems/HttpServer.kt | 8 +- .../filemanager/fragments/ItemsFragment.kt | 8 +- .../fossify/filemanager/helpers/Helpers.kt | 11 +- .../NetworkConnectionRepositoryDb.kt | 9 +- .../filemanager/models/ConnectionResult.kt | 2 +- .../NetworkConnectionRepositoryDbImpl.kt | 33 +++++- .../filemanager/repository/WebDavApiImpl.kt | 2 +- .../viewmodels/NetworkBrowserViewModel.kt | 35 ++++-- app/src/main/res/layout/cloud_activity.xml | 12 ++ 13 files changed, 176 insertions(+), 90 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 5f4b2e690..2fee8f835 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -3,6 +3,7 @@ package org.fossify.filemanager.activities import android.content.Intent import android.net.Uri import android.os.Bundle +import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isVisible @@ -13,7 +14,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex import org.fossify.commons.enums.ConnectionTypes import org.fossify.commons.extensions.viewBinding import org.fossify.commons.helpers.NavigationIcon @@ -32,14 +32,12 @@ import java.security.Security import org.bouncycastle.jce.provider.BouncyCastleProvider import org.fossify.commons.extensions.toast import org.fossify.filemanager.dependencies.AppComposition -import org.fossify.filemanager.enums.Authentication import org.fossify.filemanager.enums.Protocols import org.fossify.filemanager.helpers.DAVX5_PATH_NAME import org.fossify.filemanager.helpers.Helpers import org.fossify.filemanager.helpers.PORT_FTP import org.fossify.filemanager.interfaces.CertificateRepository import java.security.Provider -import java.util.Locale class CloudActivity : SimpleActivity() { @@ -102,7 +100,7 @@ class CloudActivity : SimpleActivity() { val storage = DocumentFile.fromTreeUri(this, it) storage?.let { s -> if (s.name != null && it.path != null) { - viewModel.insertUpdateConnection( + viewModel.updateConnection( NetworkConnection( displayName = s.name!!, sharedPath = s.uri.toString(), @@ -147,7 +145,7 @@ class CloudActivity : SimpleActivity() { private fun showConnectionDialog() { ConnectionDialog(this@CloudActivity) { connection, certUri -> - saveNetwork(connection, certUri) + saveNetwork(connection, certUri, isAddOperation = true) } } @@ -160,38 +158,61 @@ class CloudActivity : SimpleActivity() { private fun saveNetwork( connection: NetworkConnection, - certUri: Uri? + certUri: Uri?, + isAddOperation: Boolean = true ) { if (connection.connectionType == ConnectionTypes.SMB) { - viewModel.verifyNetwork( - connection, true - ) + viewModel.verifySMBNetwork(connection, true, isAddOperation) } else if (connection.connectionType == ConnectionTypes.WebDav) { if (connection.protocols == Protocols.HTTPS) { saveCertificate(certUri, connection.host) } val url = Helpers.createProtocolPath(connection.protocols, connection.host, connection.port, connection.sharedPath) connection.url = url - viewModel.connectAndAuthenticateWebDav(connection, true, this@CloudActivity) + viewModel.connectAndAuthenticateWebDav(connection, true, this@CloudActivity, isAddOperation) } else if (connection.connectionType == ConnectionTypes.SFTP) { - viewModel.connectSFTP(connection, true) + viewModel.connectSFTP(connection, true, isAddOperation) } else if (connection.connectionType == ConnectionTypes.FTP) { - viewModel.connectFTP( - connection, true - ) + viewModel.connectFTP(connection, true, isAddOperation) } } private fun getAllSavedNetworks() { viewModel.getAllSavedNetworks() - collectSavedNetworks() + collectDbCalls() } - private fun collectSavedNetworks() { + private fun collectDbCalls() { lifecycleScope.launch { - viewModel.savedNetworks.collectLatest { - if (it.isNotEmpty()) - updateAdapter(it.toMutableList()) + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.savedNetworks.collectLatest { + if (it.isNotEmpty()) { + binding.emptyView.visibility = View.GONE + updateAdapter(it.toMutableList()) + } + else { + binding.emptyView.visibility = View.VISIBLE + updateAdapter(mutableListOf()) + } + + } + } + launch { + viewModel.addConnection.collectLatest { + it.exception?.let { exp -> toast(exp.message.toString()) } + } + } + launch { + viewModel.deleteConnection.collectLatest { + it.exception?.let { exp -> toast(exp.message.toString()) } + } + } + launch { + viewModel.updateConnection.collectLatest { + it.exception?.let { exp -> toast(exp.message.toString()) } + } + } } } } @@ -199,7 +220,7 @@ class CloudActivity : SimpleActivity() { private fun handleConnection(item: NetworkConnection, connectionType: ConnectionTypes) { when (connectionType) { ConnectionTypes.SMB -> { - viewModel.verifyNetwork(item, false) + viewModel.verifySMBNetwork(item, false) } ConnectionTypes.DAVx5 -> { @@ -248,7 +269,7 @@ class CloudActivity : SimpleActivity() { private fun updateConnection(connection: NetworkConnection) { ConnectionDialog(this@CloudActivity, connection) { con, uri -> - saveNetwork(con, uri) + saveNetwork(con, uri, isAddOperation = false) } } @@ -320,9 +341,13 @@ class CloudActivity : SimpleActivity() { launchMainActivity(ConnectionTypes.WebDav, it.item.url) } else { it.item.connectionType = connectionType - viewModel.insertUpdateConnection( - it.item - ) + if (it.isAddCallOperation) { + viewModel.addConnection(it.item) + } else { + viewModel.updateConnection( + it.item + ) + } } } else { it.exception?.let { exception -> @@ -341,9 +366,14 @@ class CloudActivity : SimpleActivity() { startServer(it.item, connectionType = ConnectionTypes.SMB, machinePort = it.item.port) launchMainActivity(ConnectionTypes.SMB, path) } else { - viewModel.insertUpdateConnection( - it.item - ) + it.item.connectionType = connectionType + if (it.isAddCallOperation) { + viewModel.addConnection(it.item) + } else { + viewModel.updateConnection( + it.item + ) + } } } else { it.exception?.let { exception -> @@ -362,9 +392,13 @@ class CloudActivity : SimpleActivity() { launchMainActivity(ConnectionTypes.SFTP, it.item.url) } else { it.item.url = "/" - viewModel.insertUpdateConnection( - it.item - ) + if (it.isAddCallOperation) { + viewModel.addConnection(it.item) + } else { + viewModel.updateConnection( + it.item + ) + } } } else { it.exception?.let { exception -> @@ -383,10 +417,13 @@ class CloudActivity : SimpleActivity() { launchMainActivity(ConnectionTypes.FTP, it.item.url) } else { it.item.url = "/" - viewModel.insertUpdateConnection( - it.item - - ) + if (it.isAddCallOperation) { + viewModel.addConnection(it.item) + } else { + viewModel.updateConnection( + it.item + ) + } } } else { it.exception?.let { exception -> diff --git a/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt b/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt index f9fc4734c..4953c1596 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt @@ -5,6 +5,7 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Update import kotlinx.coroutines.flow.Flow import org.fossify.filemanager.entity.NetworkConnectionEntity @@ -13,9 +14,12 @@ interface NetworkConnectionDao { @Query("SELECT * FROM network_connections") fun getAll(): Flow> - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertUpdateConnection(connection: NetworkConnectionEntity): Long + @Update + suspend fun updateConnection(connection: NetworkConnectionEntity): Int @Delete suspend fun delete(connection: NetworkConnectionEntity) + + @Insert(onConflict = OnConflictStrategy.ABORT) + suspend fun addConnection(connection: NetworkConnectionEntity): Long } diff --git a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt index d4903e554..df0d4cc3a 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt @@ -1,10 +1,13 @@ package org.fossify.filemanager.entity import androidx.room.Entity +import androidx.room.Index import androidx.room.PrimaryKey import org.fossify.filemanager.enums.Authentication -@Entity(tableName = "network_connections") +@Entity(tableName = "network_connections", + indices = [Index(value = ["displayName"], unique = true)] +) data class NetworkConnectionEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, val host: String, diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt index d112ef673..641e6382f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt @@ -5,32 +5,27 @@ import android.content.Intent import android.content.pm.PackageManager import android.util.Log import androidx.core.net.toUri -import jcifs.smb.SmbFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.helpers.Helpers -import org.fossify.filemanager.helpers.PORT_SMB -import org.fossify.filemanager.helpers.PORT_WEBDAV -import org.fossify.filemanager.models.ListItem object FileHelpers { - val URL: String = "http://127.0.0.1" fun launchSMB(mPath: String, context: Context, mimType: String? = null) { try { CoroutineScope(Dispatchers.IO).launch { val extractedPath = Helpers.retrievePath(mPath) - val uri = "${URL}:${PORT_SMB}${extractedPath}" + val uri = Helpers.createNanoHttpdUrl(ConnectionTypes.SMB,extractedPath).toUri() var i: Intent if (mimType != null) { i = Intent(Intent.ACTION_VIEW) - i.setDataAndType(uri.toUri(), mimType) + i.setDataAndType(uri, mimType) } else { i = Intent(Intent.ACTION_VIEW) - i.setDataAndType(uri.toUri(), MimeTypes.getMimeTypes(mPath)) + i.setDataAndType(uri, MimeTypes.getMimeTypes(mPath)) } val packageManager: PackageManager = context.packageManager val resInfos = packageManager.queryIntentActivities(i, 0) @@ -48,17 +43,16 @@ object FileHelpers { CoroutineScope(Dispatchers.IO).launch { val extractedPath = Helpers.retrievePath(mPath) - val uri = "${URL}:${PORT_WEBDAV}/${extractedPath}" - + val uri = Helpers.createNanoHttpdUrl(ConnectionTypes.WebDav,extractedPath).toUri() var i: Intent if (mimType != null) { i = Intent(Intent.ACTION_VIEW) - i.setDataAndType(uri.toUri(), mimType) + i.setDataAndType(uri, mimType) } else { i = Intent(Intent.ACTION_VIEW) - i.setDataAndType(uri.toUri(), MimeTypes.getMimeTypes(mPath)) + i.setDataAndType(uri, MimeTypes.getMimeTypes(mPath)) } val packageManager: PackageManager = context.packageManager val resInfos = packageManager.queryIntentActivities(i, 0) @@ -71,11 +65,10 @@ object FileHelpers { } } - fun launchSFTP(connectionTypes: ConnectionTypes, mPath: String, context: Context,mimType: String? = null) { + fun launchSFTP(mPath: String, context: Context,mimType: String? = null) { try { CoroutineScope(Dispatchers.IO).launch { - val port = Helpers.getPortForEachService(connectionTypes) - val uri = Helpers.createNanoHttpdUrl(connectionTypes, mPath, port = port).toUri() + val uri = Helpers.createNanoHttpdUrl(ConnectionTypes.SMB).toUri() var i: Intent if (mimType != null) { i = @@ -98,11 +91,10 @@ object FileHelpers { } } - fun launchFTP(connectionTypes: ConnectionTypes, mPath: String, context: Context,mimType: String? = null) { + fun launchFTP(mPath: String, context: Context,mimType: String? = null) { try { CoroutineScope(Dispatchers.IO).launch { - val port = Helpers.getPortForEachService(connectionTypes) - val uri = Helpers.createNanoHttpdUrl(connectionTypes, mPath, port = port).toUri() + val uri = Helpers.createNanoHttpdUrl(ConnectionTypes.SMB).toUri() var i: Intent if (mimType != null) { i = diff --git a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt index 45d4c7da6..b9cc16fef 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt @@ -2,7 +2,6 @@ package org.fossify.filemanager.fileSystems import fi.iki.elonen.NanoHTTPD -import jcifs.smb.SmbFile import jcifs.smb.SmbFileInputStream import org.fossify.commons.enums.ConnectionTypes import org.fossify.filemanager.dependencies.AppComposition @@ -35,7 +34,7 @@ class HttpServer( } private fun handleSmb(uri: String, rangeHeader: String?): Response { - val url = Helpers. createNanoHttpdUrl(connectionType, uri, server = serverIp, port = machinePort, protocols = protocol) + val url = Helpers. createProtocolUrl(connectionType, uri, server = serverIp, port = machinePort, protocols = protocol) val file = composition.smbApiRepository.getSmbFile(url).response if (file == null) return notFound() if (!file.exists()) return notFound() @@ -56,8 +55,7 @@ class HttpServer( private fun handleWebDav(uri: String, rangeHeader: String?): Response { -// val extractedPath = Helpers.retrievePath(uri) - val url = Helpers. createNanoHttpdUrl(connectionType, uri, server = serverIp, port = machinePort, protocols = protocol) + val url = Helpers. createProtocolUrl(connectionType, uri, server = serverIp, port = machinePort, protocols = protocol) val apiResponse = composition.webDavApiRepository.listWebDavFileDetail(url) return handleResponse(apiResponse) { @@ -148,7 +146,7 @@ class HttpServer( } private fun buildUrl(path: String) = - Helpers.createNanoHttpdUrl(connectionType, server = serverIp, path = path, port = machinePort, protocols = protocol) + Helpers.createProtocolUrl(connectionType, server = serverIp, path = path, port = machinePort, protocols = protocol) private fun notFound() = diff --git a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt index da05d89b1..7531e7963 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt @@ -210,11 +210,11 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } } else if (connectionType == ConnectionTypes.SFTP) { it?.let { item -> - FileHelpers.launchSFTP(connectionType, item.mPath, context = this@ItemsFragment.context) + FileHelpers.launchSFTP(item.mPath, context = this@ItemsFragment.context) } } else if (connectionType == ConnectionTypes.FTP) { it?.let { item -> - FileHelpers.launchFTP(connectionType, item.mPath, context = this@ItemsFragment.context) + FileHelpers.launchFTP( item.mPath, context = this@ItemsFragment.context) } } else { itemClicked(it as FileDirItem, connectionType) @@ -866,11 +866,11 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF } ConnectionTypes.SFTP -> { - FileHelpers.launchSFTP(connectionType, path, context, mimType) + FileHelpers.launchSFTP( path, context, mimType) } ConnectionTypes.FTP -> { - FileHelpers.launchFTP(connectionType, path, context, mimType) + FileHelpers.launchFTP( path, context, mimType) } else -> Unit diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt index 6388b3077..c8968528f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Helpers.kt @@ -6,8 +6,8 @@ import org.fossify.filemanager.enums.Protocols import java.util.Locale.getDefault object Helpers { - val host: String = "127.0.0.1" - fun createNanoHttpdUrl(connectionTypes: ConnectionTypes, path: String? = "", server: String = "", port: Int, protocols: Protocols = Protocols.HTTP): String{ + val URL: String = "http://127.0.0.1" + fun createProtocolUrl(connectionTypes: ConnectionTypes, path: String? = "", server: String = "", port: Int, protocols: Protocols = Protocols.HTTP): String{ var protocol = Protocols.HTTP.toString().lowercase() if(connectionTypes.equals(ConnectionTypes.WebDav)){ protocol = protocols.name.lowercase() @@ -15,10 +15,15 @@ object Helpers { else if(connectionTypes.equals(ConnectionTypes.SMB)){ protocol = ConnectionTypes.SMB.toString().lowercase() } - val url = "${protocol}://${if (server.isEmpty()) host else server }:${port}${path}" + val url = "${protocol}://${if (server.isEmpty()) URL else server }:${port}${path}" return url } + fun createNanoHttpdUrl(connectionTypes: ConnectionTypes,path: String? = ""): String{ + val port = getPortForEachService(connectionTypes) + return "${URL}:${port}${path}" + } + fun createUrl(connectionTypes: ConnectionTypes,path: String,server: String,port: Int = 0): String{ if(connectionTypes == ConnectionTypes.SMB){ return "${connectionTypes.toString().lowercase()}://${server}:${port}/${path}/" diff --git a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt index 100d85dac..de2772a84 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/interfaces/NetworkConnectionRepositoryDb.kt @@ -1,10 +1,13 @@ package org.fossify.filemanager.interfaces import kotlinx.coroutines.flow.Flow +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection interface NetworkConnectionRepositoryDb { - suspend fun insertUpdateConnection(connection: NetworkConnection): Long; - suspend fun getAllSavedConnections(): Flow> - suspend fun deleteConnection(connection: NetworkConnection) + suspend fun updateConnection(connection: NetworkConnection): ApiResponse; + fun getAllSavedConnections(): Flow> + suspend fun deleteConnection(connection: NetworkConnection): ApiResponse + + suspend fun addConnection(connection: NetworkConnection) : ApiResponse } diff --git a/app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt b/app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt index ea94b2689..e486c8f1e 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/models/ConnectionResult.kt @@ -1,3 +1,3 @@ package org.fossify.filemanager.models -data class ConnectionResult(val item: NetworkConnection,val success: Boolean,val saveInfo: Boolean = true,val exception: Exception? = null) +data class ConnectionResult(val item: NetworkConnection, val success: Boolean, val saveInfo: Boolean = true, val isAddCallOperation: Boolean = false, val exception: Exception? = null) diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt index 1925d7d59..d88a75bb5 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/NetworkConnectionRepositoryDbImpl.kt @@ -6,18 +6,39 @@ import org.fossify.filemanager.dao.NetworkConnectionDao import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb import org.fossify.filemanager.mapper.toDomain import org.fossify.filemanager.mapper.toEntity +import org.fossify.filemanager.models.ApiResponse import org.fossify.filemanager.models.NetworkConnection -class NetworkConnectionRepositoryDbImpl(private val dao: NetworkConnectionDao): NetworkConnectionRepositoryDb { - override suspend fun insertUpdateConnection(connection: NetworkConnection): Long { - return dao.insertUpdateConnection(connection.toEntity()) +class NetworkConnectionRepositoryDbImpl(private val dao: NetworkConnectionDao) : NetworkConnectionRepositoryDb { + override suspend fun updateConnection(connection: NetworkConnection): ApiResponse { + return try { + dao.updateConnection(connection.toEntity()) + ApiResponse(true, null) + } catch (exp: Exception) { + ApiResponse(false, exp) + } } - override suspend fun getAllSavedConnections(): Flow> { + override fun getAllSavedConnections(): Flow> { return dao.getAll().map { value -> value.map { entity -> entity.toDomain() } } } - override suspend fun deleteConnection(connection: NetworkConnection) { - dao.delete(connection.toEntity()) + override suspend fun deleteConnection(connection: NetworkConnection): ApiResponse { + return try { + dao.delete(connection.toEntity()) + ApiResponse(true, null) + } catch (exp: Exception) { + ApiResponse(false, exp) + } + + } + + override suspend fun addConnection(connection: NetworkConnection): ApiResponse { + return try { + dao.addConnection(connection.toEntity()) + ApiResponse(true, null) + } catch (exp: Exception) { + ApiResponse(false, exp) + } } } diff --git a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt index 822ecfb7d..11033c53d 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/repository/WebDavApiImpl.kt @@ -37,7 +37,7 @@ class WebDavApiImpl: WebDavApi { } else { createHTTPSSardine(context,connection.host) } - sardine.setCredentials(connection.username, connection.password) + sardine.setCredentials(connection.username, connection.password, true) Pair(sardine.exists(connection.url),null) } catch (exp: Exception) { Pair(false,exp) diff --git a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt index 1b3f6503f..e7ca0f933 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/viewmodels/NetworkBrowserViewModel.kt @@ -13,7 +13,6 @@ import kotlinx.coroutines.launch import net.schmizz.sshj.sftp.RemoteResourceInfo import net.schmizz.sshj.sftp.SFTPClient import org.apache.commons.net.ftp.FTPFile -import org.fossify.filemanager.enums.Protocols import org.fossify.filemanager.interfaces.FTPApi import org.fossify.filemanager.interfaces.NetworkConnectionRepositoryDb import org.fossify.filemanager.interfaces.SFTPApi @@ -32,7 +31,13 @@ class NetworkBrowserViewModel( private val smbApi: SMBApi ) : ViewModel() { + + + val addConnection = MutableSharedFlow>() + val updateConnection = MutableSharedFlow>() + val deleteConnection = MutableSharedFlow>() val savedNetworks = MutableStateFlow>(emptyList()) + val verifyNetwork = MutableSharedFlow() val smbFolderOrFile = MutableSharedFlow>() val smbDelete = MutableSharedFlow>() @@ -61,9 +66,15 @@ class NetworkBrowserViewModel( - fun insertUpdateConnection(networkConnection: NetworkConnection) { + fun updateConnection(networkConnection: NetworkConnection) { + viewModelScope.launch(Dispatchers.IO) { + updateConnection.emit(networkConnectionRepository.updateConnection(networkConnection)) + } + } + + fun addConnection(networkConnection: NetworkConnection){ viewModelScope.launch(Dispatchers.IO) { - networkConnectionRepository.insertUpdateConnection(networkConnection) + addConnection.emit(networkConnectionRepository.addConnection(networkConnection)) } } @@ -77,14 +88,14 @@ class NetworkBrowserViewModel( fun deleteConnection(connection: NetworkConnection){ viewModelScope.launch(Dispatchers.IO) { - networkConnectionRepository.deleteConnection(connection) + deleteConnection.emit(networkConnectionRepository.deleteConnection(connection)) } } - fun verifyNetwork(connection: NetworkConnection, saveInfo: Boolean) { + fun verifySMBNetwork(connection: NetworkConnection, saveInfo: Boolean, isAddOperation: Boolean = true) { viewModelScope.launch(Dispatchers.IO) { val value = smbApi.verifyConnection(connection) - verifyNetwork.emit(ConnectionResult(connection, value.first, saveInfo = saveInfo,value.second)) + verifyNetwork.emit(ConnectionResult(connection, value.first, saveInfo = saveInfo,isAddOperation,value.second)) } } @@ -115,10 +126,10 @@ class NetworkBrowserViewModel( fun getSFTPConn(): SFTPClient = sftpApi.getSFTPConn() - fun connectAndAuthenticateWebDav(connection: NetworkConnection, saveInfo: Boolean, context: Context) { + fun connectAndAuthenticateWebDav(connection: NetworkConnection, saveInfo: Boolean, context: Context,isAddOperation: Boolean = true) { viewModelScope.launch(Dispatchers.IO) { val result = webDavApi.connectAndVerifyWebDav(connection, context) - verifyWebDav.emit(ConnectionResult(connection, result.first, saveInfo,result.second)) + verifyWebDav.emit(ConnectionResult(connection, result.first, saveInfo,isAddOperation,result.second)) } } @@ -154,10 +165,10 @@ class NetworkBrowserViewModel( // return webDavApi.listWebDavFileDetail(url) // } - fun connectSFTP(connection: NetworkConnection, saveInfo: Boolean) { + fun connectSFTP(connection: NetworkConnection, saveInfo: Boolean,isAddOperation: Boolean = true) { viewModelScope.launch(Dispatchers.IO) { val res = sftpApi.connectToSftp(connection) - verifySFTP.emit(ConnectionResult(connection, res.first, saveInfo,res.second)) + verifySFTP.emit(ConnectionResult(connection, res.first, saveInfo,isAddOperation,res.second)) } } @@ -201,10 +212,10 @@ class NetworkBrowserViewModel( // return sftpApi.getSFTPFileInputStream(url = path, startByte) // } - fun connectFTP(connection: NetworkConnection, saveInfo: Boolean) { + fun connectFTP(connection: NetworkConnection, saveInfo: Boolean,isAddOperation: Boolean = true) { viewModelScope.launch(Dispatchers.IO) { val res = ftpApi.connectToFTP(connection) - verifyFTP.emit(ConnectionResult(connection, res.first, saveInfo,res.second)) + verifyFTP.emit(ConnectionResult(connection, res.first, saveInfo,isAddOperation,res.second)) } } diff --git a/app/src/main/res/layout/cloud_activity.xml b/app/src/main/res/layout/cloud_activity.xml index 40c583337..729dbfa8e 100644 --- a/app/src/main/res/layout/cloud_activity.xml +++ b/app/src/main/res/layout/cloud_activity.xml @@ -44,6 +44,16 @@ android:scrollbars="none" app:layoutManager="org.fossify.commons.views.MyLinearLayoutManager" /> + + + + + From f010a81560e7fdfb650d1776f63cb27496f6a22f Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sat, 13 Jun 2026 14:11:35 +0500 Subject: [PATCH 34/37] updated strings in strings.xml --- .../filemanager/activities/CloudActivity.kt | 12 ---------- .../activities/ReadTextActivity.kt | 6 ++--- .../dependencies/AppComposition.kt | 3 ++- .../dialogs/ChangeViewTypeDialog.kt | 2 +- .../fossify/filemanager/helpers/Constants.kt | 4 ++-- app/src/main/res/layout/cloud_activity.xml | 7 +++--- .../main/res/layout/dialog_add_connection.xml | 22 +++++++++---------- .../res/layout/item_network_connection.xml | 8 +++---- app/src/main/res/values/strings.xml | 22 +++++++++++++++++++ 9 files changed, 48 insertions(+), 38 deletions(-) diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt index 2fee8f835..9928ebc0b 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt @@ -50,17 +50,6 @@ class CloudActivity : SimpleActivity() { private lateinit var composition: AppComposition private var https: HttpServer? = null private var isConnecting: Boolean = false - private fun setupBouncyCastle() { - val provider: Provider? = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) - if (provider == null) { - return - } - if (provider.javaClass.equals(BouncyCastleProvider::class.java)) { - return - } - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) - Security.insertProviderAt(BouncyCastleProvider(), 1) - } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -72,7 +61,6 @@ class CloudActivity : SimpleActivity() { registerAddConnectionListener() initializeCompositionAndViewModel() startConnectionsCollection() - setupBouncyCastle() getAllSavedNetworks() } diff --git a/app/src/main/kotlin/org/fossify/filemanager/activities/ReadTextActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/ReadTextActivity.kt index be3a3f6d5..9696e0650 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/activities/ReadTextActivity.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/activities/ReadTextActivity.kt @@ -158,7 +158,7 @@ class ReadTextActivity : SimpleActivity() { private fun openSearch() { isSearchActive = true -// binding.searchWrapper.beVisible() + binding.searchWrapper.beVisible() showKeyboard(searchQueryET) binding.readTextView.requestFocus() @@ -354,7 +354,7 @@ class ReadTextActivity : SimpleActivity() { false }) -// binding.searchWrapper.setBackgroundColor(getProperPrimaryColor()) + binding.searchWrapper.setBackgroundColor(getProperPrimaryColor()) val contrastColor = getProperPrimaryColor().getContrastColor() arrayListOf(searchPrevBtn, searchNextBtn, searchClearBtn).forEach { it.applyColorFilter(contrastColor) @@ -402,7 +402,7 @@ class ReadTextActivity : SimpleActivity() { private fun closeSearch() { searchQueryET.text?.clear() isSearchActive = false -// binding.searchWrapper.beGone() + binding.searchWrapper.beGone() hideKeyboard() } diff --git a/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt b/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt index 0f3a0135c..b2400563d 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.room.Room import org.fossify.filemanager.database.Database import org.fossify.filemanager.factory.NetworkBrowserViewModelFactory +import org.fossify.filemanager.helpers.DB_NAME import org.fossify.filemanager.repository.CertificateRepositoryImpl import org.fossify.filemanager.repository.FTPApiImpl import org.fossify.filemanager.repository.NetworkConnectionRepositoryDbImpl @@ -14,7 +15,7 @@ import org.fossify.filemanager.repository.WebDavApiImpl class AppComposition (private val context: Context) { private fun createDataBase(context: Context): Database { - return Room.databaseBuilder(context.applicationContext, Database::class.java,"app-db").build() + return Room.databaseBuilder(context.applicationContext, Database::class.java,DB_NAME).build() } private val database by lazy { diff --git a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ChangeViewTypeDialog.kt b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ChangeViewTypeDialog.kt index 063e85582..0bac2f17f 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/dialogs/ChangeViewTypeDialog.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ChangeViewTypeDialog.kt @@ -25,7 +25,7 @@ class ChangeViewTypeDialog(val activity: BaseSimpleActivity, val path: String = changeViewTypeDialogRadio.check(viewToCheck) if (!showFolderCheck) { -// useForThisFolderDivider.beGone() + useForThisFolderDivider.beGone() changeViewTypeDialogUseForThisFolder.beGone() } diff --git a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt index 1bce0484e..fde6c2c63 100644 --- a/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/filemanager/helpers/Constants.kt @@ -17,8 +17,6 @@ const val PORT_SFTP = 7860 const val PORT_FTP = 7850 -const val PORT_WEBDAV_MOUNT = 7840 - const val DEFAULT_SMB_PORT = 445 const val DEFAULT_FTP_PORT = 21 @@ -26,6 +24,8 @@ const val DEFAULT_SFTP_PORT = 22 const val DEFAULT_WEBDAV_HTTP_PORT = 80 const val DEFAULT_WEBDAV_HTTPS_PORT = 443 +const val DB_NAME = "app-db" + // shared preferences const val SHOW_HIDDEN = "show_hidden" diff --git a/app/src/main/res/layout/cloud_activity.xml b/app/src/main/res/layout/cloud_activity.xml index 729dbfa8e..e4b86ec6d 100644 --- a/app/src/main/res/layout/cloud_activity.xml +++ b/app/src/main/res/layout/cloud_activity.xml @@ -15,14 +15,14 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/color_primary" - app:title="Cloud Connection" + app:title="@string/cloud_connection" app:titleTextAppearance="@style/AppTheme.ActionBar.TitleTextStyle"/> @@ -49,11 +49,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" - android:text="No connections found" + android:text="@string/no_connection" android:textAppearance="?android:textAppearanceMedium" android:visibility="gone" /> - + android:hint="@string/select_option"> + android:hint="@string/username"> + android:hint="@string/password"> + android:hint="@string/private_key"> + android:hint="@string/password_private_key"> + android:hint="@string/display_name"> + android:hint="@string/shared_path"> @@ -230,7 +230,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="?attr/selectableItemBackgroundBorderless" - android:contentDescription="Attach certificate" + android:contentDescription="@string/attach_certificate" android:padding="@dimen/medium_margin" android:src="@drawable/baseline_attach_file_24" /> diff --git a/app/src/main/res/layout/item_network_connection.xml b/app/src/main/res/layout/item_network_connection.xml index 216ab30c4..0621fd844 100644 --- a/app/src/main/res/layout/item_network_connection.xml +++ b/app/src/main/res/layout/item_network_connection.xml @@ -23,7 +23,7 @@ android:id="@+id/tvDisplayName" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="Display Name" + android:text="@string/display_name" android:textAppearance="?attr/textAppearanceHeadline6"/> + android:text="@string/host_address"/> @@ -45,7 +45,7 @@ android:id="@+id/tvSharedPath" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="Shared Path: /Shared" + android:text="@string/shared_path" android:layout_marginTop="2dp"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7df039b60..4e7a01984 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,4 +61,26 @@ Haven't found some strings? There's more at https://github.com/FossifyOrg/Commons/tree/master/commons/src/main/res --> + + + Cloud Connection + Add Connection + No connections found + + Select Option + Select Protocol + Authentication + Username + Password + Private Key + Password Private Key + Display Name + Shared path + No certificate attached + Attach certificate + + Host: 192.168.1.1 + SMB + + From 7ee7e5dd14ce8f58e10203a12c797a3013b46686 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sat, 13 Jun 2026 21:34:04 +0500 Subject: [PATCH 35/37] Refcatored the build.gradle and also made other changes --- app/build.gradle.kts | 34 ++-- .../res/drawable/baseline_attach_file_24.xml | 5 + .../main/res/drawable/baseline_edit_24.xml | 5 + .../baseline_insert_drive_file_24.xml | 5 + app/src/main/res/values/strings.xml | 16 ++ build.gradle.kts | 3 +- gradle/libs.versions.toml | 149 +++--------------- settings.gradle.kts | 5 - 8 files changed, 70 insertions(+), 152 deletions(-) create mode 100644 app/src/main/res/drawable/baseline_attach_file_24.xml create mode 100644 app/src/main/res/drawable/baseline_edit_24.xml create mode 100644 app/src/main/res/drawable/baseline_insert_drive_file_24.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 06c2e3d65..aa79b6afe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,7 +7,7 @@ plugins { alias(libs.plugins.android) alias(libs.plugins.kotlinAndroid) alias(libs.plugins.detekt) - id("com.google.devtools.ksp") + alias(libs.plugins.ksp) } val keystorePropertiesFile: File = rootProject.file("keystore.properties") @@ -139,7 +139,6 @@ detekt { } dependencies { - api(project(":commons")) implementation(libs.androidx.documentfile) implementation(libs.androidx.swiperefreshlayout) implementation(libs.roottools) @@ -147,24 +146,29 @@ dependencies { implementation(libs.gestureviews) implementation(libs.autofittextview) implementation(libs.zip4j) - implementation(libs.jcifs.ng){ + implementation(libs.fossify.commons) + implementation(libs.androidx.documentfile) + implementation(libs.androidx.swiperefreshlayout) + implementation(libs.roottools) + implementation(libs.rootshell) + implementation(libs.gestureviews) + implementation(libs.autofittextview) + implementation(libs.zip4j) + implementation(libs.jcifs.ng) { exclude(group = "org.bouncycastle", module = "bcprov-jdk18on") exclude(group = "org.bouncycastle", module = "bcpkix-jdk18on") } detektPlugins(libs.compose.detekt) - - val roomVersion = "2.7.0-alpha11" - implementation("androidx.room:room-runtime:$roomVersion") - ksp("androidx.room:room-compiler:$roomVersion") - implementation (libs.androidx.room.ktx) - implementation("org.nanohttpd:nanohttpd:2.3.1") - implementation("com.github.thegrizzlylabs:sardine-android:0.9") - implementation("com.hierynomus:sshj:0.40.0") { + implementation(libs.androidx.room.runtime) + ksp(libs.androidx.room.compiler) + implementation(libs.androidx.room.ktx) + implementation(libs.nanohttpd) + implementation(libs.sardine.android) + implementation(libs.sshj) { exclude(group = "org.bouncycastle", module = "bcprov-jdk18on") exclude(group = "org.bouncycastle", module = "bcpkix-jdk18on") } - val bouncy_castle_version = "1.81" - implementation("org.bouncycastle:bcprov-jdk15to18:$bouncy_castle_version") - implementation("org.bouncycastle:bcpkix-jdk15to18:${bouncy_castle_version}") - implementation("commons-net:commons-net:3.10.0") + implementation(libs.commons.net) + implementation(libs.bouncycastle.provider) + implementation(libs.bouncycastle.pkix) } diff --git a/app/src/main/res/drawable/baseline_attach_file_24.xml b/app/src/main/res/drawable/baseline_attach_file_24.xml new file mode 100644 index 000000000..7b56c632d --- /dev/null +++ b/app/src/main/res/drawable/baseline_attach_file_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_edit_24.xml b/app/src/main/res/drawable/baseline_edit_24.xml new file mode 100644 index 000000000..3c53db7ec --- /dev/null +++ b/app/src/main/res/drawable/baseline_edit_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_insert_drive_file_24.xml b/app/src/main/res/drawable/baseline_insert_drive_file_24.xml new file mode 100644 index 000000000..9cc3bdb23 --- /dev/null +++ b/app/src/main/res/drawable/baseline_insert_drive_file_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4e7a01984..904053a3f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,5 +82,21 @@ Host: 192.168.1.1 SMB + + 445 + 21 + 22 + 80 + 443 + + Host name cannot be empty + User name cannot be empty + Password cannot be empty + Port cannot be empty + Display name cannot be empty + Shared path cannot be empty + Private key cannot be empty + Private key password cannot be empty + diff --git a/build.gradle.kts b/build.gradle.kts index 91f6f0867..1acf96732 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,5 @@ plugins { alias(libs.plugins.android).apply(false) alias(libs.plugins.kotlinAndroid).apply(false) alias(libs.plugins.detekt).apply(false) - id("com.google.devtools.ksp") version "2.2.0-2.0.2" - + alias(libs.plugins.ksp) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e1d90e41c..7d0765de1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,165 +1,54 @@ [versions] -# jetbrains & Kotlin kotlin = "2.2.21" -kotlin-immutable-collections = "0.4.0" -kotlinxSerializationJson = "1.10.0" -ksp = "2.2.0-2.0.2" - -# Detekt detekt = "1.23.8" detektCompose = "0.4.28" - -# AndroidX -androidx-appcompat = "1.7.1" -androidx-biometricKtx = "1.4.0-alpha02" -androidx-constraintlayout = "2.2.1" -androidx-coreKtx = "1.18.0" -androidx-customView = "1.2.0" -androidx-customViewPooling = "1.1.0" -androidx-documentfile = "1.1.0" -androidx-exifinterface = "1.4.2" -androidx-lifecycle = "2.10.0" androidx-swiperefreshlayout = "1.2.0" - -# Compose -compose = "1.7.6" -composeActivity = "1.13.0" -composeMaterial3 = "1.4.0" - -# Material -material = "1.13.0" - -# Databases & Networking -room = "2.8.4" -jcifs = "2.1.10" -gson = "2.13.2" - -# Glide -glide = "5.0.5" -glideCompose = "4.14.0" - -# Helpers & Fossify +androidx-documentfile = "1.1.0" commons = "5.12.0" autofittextview = "0.2.1" gestureviews = "2.8.3" rootshell = "bc7e5d398e" roottools = "965c154e20" zip4j = "2.11.5" -patternLockView = "a90b0d4bf0" -reprint = "2cb206415d" -recyclerviewFastscroller = "5a95285b1f" -rtlViewpager = "2.0.2" -ezVcard = "0.12.2" -jodaTime = "2.14.1" - -# Gradle & Build gradlePlugins-agp = "8.11.1" app-build-compileSDKVersion = "36" app-build-targetSDK = "36" app-build-minimumSDK = "26" app-build-javaVersion = "VERSION_17" app-build-kotlinJVMTarget = "17" +ksp = "2.2.0-2.0.2" +room = "2.8.4" +jcifs = "2.1.10" +gson = "2.13.2" +nanohttpd = "2.3.1" +sardine-android = "0.9" +sshj = "0.40.0" +bouncycastle = "1.81" +commons-net = "3.10.0" [libraries] -# AndroidX Core -androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } -androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "androidx-biometricKtx" } -androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } -androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-coreKtx" } -androidx-customView = { module = "androidx.customview:customview", version.ref = "androidx-customView" } -androidx-customViewPooling = { module = "androidx.customview:customview-poolingcontainer", version.ref = "androidx-customViewPooling" } -androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "androidx-documentfile" } -androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "androidx-exifinterface" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" } - -# AndroidX Lifecycle -androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } -androidx-lifecycle-viewModel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } -androidx-lifecycle-viewModel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } -androidx-lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } -androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" } - -# Room +androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "androidx-documentfile" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } - -# Compose -compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } -compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial3" } -compose-material2 = { module = "androidx.compose.material:material", version.ref = "compose" } -compose-material-icons = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } -compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose" } -compose-activity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" } -compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } -compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } -compose-uiTooling-debug = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } -compose-uiTooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } -compose-view-binding = { module = "androidx.compose.ui:ui-viewbinding", version.ref = "compose" } compose-detekt = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } - -# Glide -glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } -glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glideCompose" } -glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } - -# Material -material = { module = "com.google.android.material:material", version.ref = "material" } - -# Fossify & Others fossify-commons = { module = "org.fossify:commons", version.ref = "commons" } +jcifs-ng = { module = "eu.agno3.jcifs:jcifs-ng", version.ref = "jcifs" } autofittextview = { module = "me.grantland:autofittextview", version.ref = "autofittextview" } gestureviews = { module = "com.alexvasilkov:gesture-views", version.ref = "gestureviews" } rootshell = { module = "com.github.naveensingh:RootShell", version.ref = "rootshell" } roottools = { module = "com.github.naveensingh:RootTools", version.ref = "roottools" } zip4j = { module = "net.lingala.zip4j:zip4j", version.ref = "zip4j" } -jcifs-ng = { module = "eu.agno3.jcifs:jcifs-ng", version.ref = "jcifs" } -patternLockView = { module = "com.github.aritraroy:patternLockView", version.ref = "patternLockView" } -reprint = { module = "com.github.tibbi:reprint", version.ref = "reprint" } -recyclerView-fastScroller = { module = "com.github.tibbi:RecyclerView-FastScroller", version.ref = "recyclerviewFastscroller" } -rtl-viewpager = { module = "com.github.naveensingh:rtl-viewpager", version.ref = "rtlViewpager" } -ez-vcard = { module = "com.googlecode.ez-vcard:ez-vcard", version.ref = "ezVcard" } -gson = { module = "com.google.code.gson:gson", version.ref = "gson" } -joda-time = { module = "joda-time:joda-time", version.ref = "jodaTime" } -# Kotlin -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } -kotlin-immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlin-immutable-collections" } - -[bundles] -compose = [ - "compose-activity", - "compose-animation", - "compose-foundation", - "compose-material-icons", - "compose-material3", - "compose-runtime", - "compose-ui", - "compose-uiTooling-preview", -] -compose-preview = [ - "androidx-customView", - "androidx-customViewPooling", - "compose-uiTooling-debug", -] -room = [ - "androidx-room-ktx", - "androidx-room-runtime", -] -lifecycle = [ - "androidx-lifecycle-compose", - "androidx-lifecycle-runtime", - "androidx-lifecycle-viewModel", - "androidx-lifecycle-viewModel-compose", -] +nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" } +sardine-android = { module = "com.github.thegrizzlylabs:sardine-android", version.ref = "sardine-android" } +sshj = { module = "com.hierynomus:sshj", version.ref = "sshj" } +bouncycastle-provider = { module = "org.bouncycastle:bcprov-jdk15to18", version.ref = "bouncycastle" } +bouncycastle-pkix = { module = "org.bouncycastle:bcpkix-jdk15to18", version.ref = "bouncycastle" } +commons-net = { module = "commons-net:commons-net", version.ref = "commons-net" } [plugins] android = { id = "com.android.application", version.ref = "gradlePlugins-agp" } -library = { id = "com.android.library", version.ref = "gradlePlugins-agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } -compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } - - +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 4320caaa4..45478cbec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,11 +11,6 @@ dependencyResolutionManagement { google() mavenCentral() maven { setUrl("https://jitpack.io") } - mavenLocal() } } include(":app") -include(":commons") - -// Point specifically to the 'commons' subfolder -project(":commons").projectDir = file("../Fossify/Common/commons") From 478cdea813a0737195809117d16795feec0e72e0 Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sun, 14 Jun 2026 11:30:33 +0500 Subject: [PATCH 36/37] removed gradle home --- gradle.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index bd81ed48d..ebf6c7249 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,4 +23,3 @@ org.gradle.jvmargs=-Xmx4g VERSION_NAME=1.6.1 VERSION_CODE=13 APP_ID=org.fossify.filemanager -org.gradle.java.home=C:\\Users\\ba269\\.jdks\\ms-17.0.17 From fefc1e44f9af17b5715adb894271f64d9b3ffdbf Mon Sep 17 00:00:00 2001 From: "BILAL\\ba269" Date: Sun, 14 Jun 2026 11:35:15 +0500 Subject: [PATCH 37/37] set dissallow kotlin set to false to pass cci/ccd pipeline --- gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle.properties b/gradle.properties index ebf6c7249..f42dee71c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,7 @@ android.enableJetifier=true android.nonTransitiveRClass=false android.useAndroidX=true org.gradle.jvmargs=-Xmx4g +android.disallowKotlinSourceSets=false # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit