diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 57154336e..0a1149e2d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -6,6 +6,7 @@ import java.io.FileInputStream
plugins {
alias(libs.plugins.android)
alias(libs.plugins.detekt)
+ alias(libs.plugins.ksp)
}
val keystorePropertiesFile: File = rootProject.file("keystore.properties")
@@ -28,7 +29,11 @@ base {
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()
@@ -137,6 +142,13 @@ detekt {
}
dependencies {
+ 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.fossify.commons)
implementation(libs.androidx.documentfile)
implementation(libs.androidx.swiperefreshlayout)
@@ -145,5 +157,21 @@ dependencies {
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)
+ 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")
+ }
+ implementation(libs.commons.net)
+ implementation(libs.bouncycastle.provider)
+ implementation(libs.bouncycastle.pkix)
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2684edd7f..16015c281 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,9 @@
+
+
+
-
+ android:theme="@style/AppTheme"
+ android:usesCleartextTraffic="true">
+
+
diff --git a/app/src/main/kotlin/org/fossify/filemanager/App.kt b/app/src/main/kotlin/org/fossify/filemanager/App.kt
index 499db7a17..48941474b 100644
--- a/app/src/main/kotlin/org/fossify/filemanager/App.kt
+++ b/app/src/main/kotlin/org/fossify/filemanager/App.kt
@@ -2,10 +2,13 @@ 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() {
+ val appComposition: AppComposition by lazy {
+ AppComposition(this)
+ }
override val isAppLockFeatureAvailable = true
-
override fun onCreate() {
super.onCreate()
Reprint.initialize(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
new file mode 100644
index 000000000..9928ebc0b
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/activities/CloudActivity.kt
@@ -0,0 +1,442 @@
+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
+import androidx.documentfile.provider.DocumentFile
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+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.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
+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.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
+
+
+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
+ private var https: HttpServer? = null
+ private var isConnecting: Boolean = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(binding.root)
+ binding.apply {
+ setupMaterialScrollListener(binding.connectionsList, binding.cloudAppbar)
+ }
+ setupToolBar()
+ registerAddConnectionListener()
+ initializeCompositionAndViewModel()
+ startConnectionsCollection()
+ getAllSavedNetworks()
+ }
+
+ private val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ stopServer()
+ }
+
+ private fun initializeCompositionAndViewModel() {
+ composition = (application as App).appComposition
+ certificateRepository = composition.certificateRepository
+ val factory = composition.provideNetworkBrowserViewModelFactory()
+ viewModel = ViewModelProvider(this, factory)
+ .get(NetworkBrowserViewModel::class.java)
+ }
+
+ 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.updateConnection(
+ NetworkConnection(
+ displayName = s.name!!,
+ sharedPath = s.uri.toString(),
+ connectionType = ConnectionTypes.DAVx5
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private val pickCert = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
+ uri?.let {
+ onCertPicked?.invoke(it)
+ }
+ }
+
+ private val pickPrivateKey = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
+ uri?.let {
+ onPrivateKeyPicked?.invoke(it)
+ }
+ }
+
+ fun promptUserToSelectStorage() {
+ openDocumentTreeLauncher.launch(null)
+ }
+
+
+ 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) { connection, certUri ->
+ saveNetwork(connection, certUri, isAddOperation = true)
+ }
+ }
+
+
+ private fun registerAddConnectionListener() {
+ binding.addButton.setOnClickListener {
+ showConnectionDialog()
+ }
+ }
+
+ private fun saveNetwork(
+ connection: NetworkConnection,
+ certUri: Uri?,
+ isAddOperation: Boolean = true
+ ) {
+ if (connection.connectionType == ConnectionTypes.SMB) {
+ 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, isAddOperation)
+ } else if (connection.connectionType == ConnectionTypes.SFTP) {
+ viewModel.connectSFTP(connection, true, isAddOperation)
+ } else if (connection.connectionType == ConnectionTypes.FTP) {
+ viewModel.connectFTP(connection, true, isAddOperation)
+ }
+ }
+
+ private fun getAllSavedNetworks() {
+ viewModel.getAllSavedNetworks()
+ collectDbCalls()
+ }
+
+ private fun collectDbCalls() {
+ lifecycleScope.launch {
+ 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()) }
+ }
+ }
+ }
+ }
+ }
+
+ private fun handleConnection(item: NetworkConnection, connectionType: ConnectionTypes) {
+ when (connectionType) {
+ ConnectionTypes.SMB -> {
+ viewModel.verifySMBNetwork(item, false)
+ }
+
+ ConnectionTypes.DAVx5 -> {
+ launchMainActivity(ConnectionTypes.DAVx5, item.sharedPath, item.displayName)
+ }
+
+ ConnectionTypes.WebDav -> {
+ viewModel.connectAndAuthenticateWebDav(
+ item,
+ false,
+ this@CloudActivity
+
+ )
+ }
+
+ ConnectionTypes.SFTP -> {
+ viewModel.connectSFTP(item, false)
+ }
+
+ ConnectionTypes.FTP -> {
+ viewModel.connectFTP(item, false)
+ }
+
+ else -> Unit
+ }
+ }
+
+ 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)
+ }
+ }.apply {
+ binding.connectionsList.adapter = this
+ }
+ }
+
+ private fun deleteConnection(connection: NetworkConnection) {
+ viewModel.deleteConnection(connection)
+ }
+
+ private fun updateConnection(connection: NetworkConnection) {
+ ConnectionDialog(this@CloudActivity, connection) { con, uri ->
+ saveNetwork(con, uri, isAddOperation = false)
+ }
+ }
+
+ 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)
+ }
+
+ private fun startServer(
+ connection: NetworkConnection,
+ port: Int = 7871,
+ connectionType: ConnectionTypes,
+ machinePort: Int,
+ protocol: Protocols = Protocols.HTTP
+ ) {
+ https = HttpServer(port, connection.host, connectionType, composition, machinePort, protocol) {
+ it?.message?.let { exp ->
+ toast(exp, Toast.LENGTH_LONG)
+ }
+ }
+ https?.start()
+ }
+
+ 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) {
+ startServer(
+ it.item,
+ PORT_WEBDAV,
+ connectionType = ConnectionTypes.WebDav,
+ machinePort = it.item.port,
+ it.item.protocols!!
+ )
+ launchMainActivity(ConnectionTypes.WebDav, it.item.url)
+ } else {
+ it.item.connectionType = connectionType
+ if (it.isAddCallOperation) {
+ viewModel.addConnection(it.item)
+ } else {
+ viewModel.updateConnection(
+ it.item
+ )
+ }
+ }
+ } else {
+ it.exception?.let { exception ->
+ toast(exception.message.toString())
+ }
+ }
+ hideLoader()
+ }
+ }
+
+ ConnectionTypes.SMB -> {
+ viewModel.verifyNetwork.collectLatest {
+ if (it.success) {
+ if (!it.saveInfo) {
+ 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 {
+ it.item.connectionType = connectionType
+ if (it.isAddCallOperation) {
+ viewModel.addConnection(it.item)
+ } else {
+ viewModel.updateConnection(
+ it.item
+ )
+ }
+ }
+ } else {
+ it.exception?.let { exception ->
+ toast(exception.message.toString())
+ }
+ }
+ hideLoader()
+ }
+ }
+
+ 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 {
+ it.item.url = "/"
+ if (it.isAddCallOperation) {
+ viewModel.addConnection(it.item)
+ } else {
+ viewModel.updateConnection(
+ it.item
+ )
+ }
+ }
+ } else {
+ it.exception?.let { exception ->
+ toast(exception.message.toString())
+ }
+ }
+ hideLoader()
+ }
+ }
+
+ 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 {
+ it.item.url = "/"
+ if (it.isAddCallOperation) {
+ viewModel.addConnection(it.item)
+ } else {
+ viewModel.updateConnection(
+ it.item
+ )
+ }
+ }
+ } else {
+ it.exception?.let { exception ->
+ toast(exception.message.toString())
+ }
+ }
+ hideLoader()
+ }
+ }
+
+ ConnectionTypes.DAVx5 -> Unit
+ else -> Unit
+ }
+ }
+
+ private fun hideLoader() {
+ binding.loader.isVisible = false
+ isConnecting = false
+ }
+
+
+ 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/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt
index 4a6e4be77..97f8d70ec 100644
--- a/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt
+++ b/app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt
@@ -7,12 +7,14 @@ 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
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
@@ -21,7 +23,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
@@ -69,24 +70,25 @@ 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.DAVX5_PATH_NAME
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"
}
-
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 = ""
@@ -115,13 +117,24 @@ class MainActivity : SimpleActivity() {
if (savedInstanceState == null) {
config.temporarilyShowHidden = false
initFragments()
- tryInitFileManager()
+ val data = getIntentDataIfAny()
+ tryInitFileManager(data.first, connectionType = data.second)
checkWhatsNewDialog()
checkIfRootAvailable()
checkInvalidFavorites()
}
}
+ private fun getIntentDataIfAny(): Pair {
+ val path = intent.getStringExtra(PATH)
+ val connectionType = intent.getSerializableExtra(CONNECTION_TYPE) as? ConnectionTypes ?: ConnectionTypes.Default
+ return Pair(path ?: "",connectionType)
+ }
+
+ private fun getDavX5PathName(): String{
+ return intent.getStringExtra(DAVX5_PATH_NAME) ?: ""
+ }
+
override fun onResume() {
super.onResume()
if (mStoredShowTabs != config.showTabs) {
@@ -183,7 +196,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 || lastItem.connectionType == ConnectionTypes.SMB){
+ val fileItems = currentFragment.getBreadcrumbs().getAllItems()
+ fileItems.forEach {
+ path += "${it.path}/"
+ }
+
+ }
+ else{
+ path = lastItem.path
+ }
+ openPath(
+ path,
+ connectionType = lastItem.connectionType
+ )
return true
}
}
@@ -253,6 +281,7 @@ class MainActivity : SimpleActivity() {
R.id.more_apps_from_us -> launchMoreAppsFromUsIntent()
R.id.settings -> launchSettings()
R.id.about -> launchAbout()
+ R.id.cloud -> launchCloudActivity()
else -> return@setOnMenuItemClickListener false
}
return@setOnMenuItemClickListener true
@@ -260,6 +289,10 @@ 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 ?: "")
@@ -291,7 +324,7 @@ class MainActivity : SimpleActivity() {
}
}
- private fun tryInitFileManager() {
+ private fun tryInitFileManager(path: String, connectionType: ConnectionTypes = ConnectionTypes.Default) {
val hadPermission = hasStoragePermission()
handleStoragePermission {
checkOTGPath()
@@ -301,7 +334,7 @@ class MainActivity : SimpleActivity() {
}
binding.mainViewPager.onGlobalLayout {
- initFileManager(!hadPermission)
+ initFileManager(!hadPermission,path,connectionType)
}
} else {
toast(R.string.no_storage_permissions)
@@ -310,17 +343,18 @@ class MainActivity : SimpleActivity() {
}
}
- private fun initFileManager(refreshRecents: Boolean) {
+ 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
if (data?.scheme == "file") {
- openPath(data.path!!)
+ openPath(data.path!!, connectionType = connectionType, pathName = pathName)
} else {
val path = getRealPathFromURI(data!!)
if (path != null) {
- openPath(path)
+ openPath(path, connectionType = connectionType, pathName = pathName)
} else {
- openPath(config.homeFolder)
+ openPath(config.homeFolder, connectionType = connectionType, pathName = pathName)
}
}
@@ -330,7 +364,7 @@ class MainActivity : SimpleActivity() {
binding.mainViewPager.currentItem = 0
} else {
- openPath(config.homeFolder)
+ openPath(if(path.isNotEmpty()) path else config.homeFolder, connectionType = connectionType, pathName = pathName)
}
if (refreshRecents) {
@@ -367,8 +401,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) {
@@ -456,18 +490,18 @@ class MainActivity : SimpleActivity() {
}
}
- private fun openPath(path: String, forceRefresh: Boolean = false) {
+ 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) {
+ } else if (file.exists() && !file.isDirectory && connectionType == ConnectionTypes.Default) {
newPath = file.parent
- } else if (!file.exists() && !isPathOnOTG(newPath)) {
+ } else if (!file.exists() && !isPathOnOTG(newPath) && connectionType.equals(ConnectionTypes.Default)) {
newPath = internalStoragePath
}
- getItemsFragment()?.openPath(newPath, forceRefresh)
+ getItemsFragment()?.openPath(newPath, forceRefresh,connectionType=connectionType, pathName = pathName)
}
private fun goHome() {
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/ConnectionItemsAdapter.kt b/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt
new file mode 100644
index 000000000..db11ea1d2
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/adapters/ConnectionItemsAdapter.kt
@@ -0,0 +1,96 @@
+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.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
+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,
+ val deleteClick: (NetworkConnection) -> Unit,
+ val updateConnection: (NetworkConnection) -> Unit,
+ 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.toString()
+ tvDisplayName.text = listItem.displayName
+ tvSharedPath.text = listItem.sharedPath
+ 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/adapters/ItemsAdapter.kt b/app/src/main/kotlin/org/fossify/filemanager/adapters/ItemsAdapter.kt
index 90b471f4b..9faa58628 100644
--- a/app/src/main/kotlin/org/fossify/filemanager/adapters/ItemsAdapter.kt
+++ b/app/src/main/kotlin/org/fossify/filemanager/adapters/ItemsAdapter.kt
@@ -42,6 +42,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
@@ -105,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
@@ -353,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) {
@@ -478,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() {
@@ -495,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)
+ }
}
}
@@ -915,7 +942,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()
@@ -929,7 +955,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
}
@@ -1079,7 +1106,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()
@@ -1114,6 +1146,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/dao/NetworkConnectionDao.kt b/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt
new file mode 100644
index 000000000..4953c1596
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/dao/NetworkConnectionDao.kt
@@ -0,0 +1,25 @@
+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 androidx.room.Update
+import kotlinx.coroutines.flow.Flow
+import org.fossify.filemanager.entity.NetworkConnectionEntity
+
+@Dao
+interface NetworkConnectionDao {
+ @Query("SELECT * FROM network_connections")
+ fun getAll(): Flow>
+
+ @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/database/Database.kt b/app/src/main/kotlin/org/fossify/filemanager/database/Database.kt
new file mode 100644
index 000000000..f9126a848
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/database/Database.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 Database : 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..b2400563d
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/dependencies/AppComposition.kt
@@ -0,0 +1,54 @@
+package org.fossify.filemanager.dependencies
+
+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
+import org.fossify.filemanager.repository.SFTPApiImpl
+import org.fossify.filemanager.repository.SMBApiImpl
+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,DB_NAME).build()
+ }
+
+ private val database by lazy {
+ createDataBase(context)
+ }
+
+ val networkDbRepository by lazy {
+ NetworkConnectionRepositoryDbImpl(database.networkConnectionDao())
+ }
+
+
+ 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,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
new file mode 100644
index 000000000..b7c1025a9
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/dialogs/ConnectionDialog.kt
@@ -0,0 +1,390 @@
+package org.fossify.filemanager.dialogs
+
+import android.net.Uri
+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
+import org.fossify.filemanager.activities.CloudActivity
+import org.fossify.filemanager.databinding.DialogAddConnectionBinding
+import org.fossify.filemanager.enums.Authentication
+import org.fossify.filemanager.enums.Protocols
+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.models.NetworkConnection
+
+class ConnectionDialog(
+ val activity: BaseSimpleActivity,
+ val connection: NetworkConnection? = null,
+ 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)
+ 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)
+ val dialog = activity.getAlertDialogBuilder()
+ .setPositiveButton(R.string.ok, null)
+ .setNegativeButton(R.string.cancel, null)
+ .apply {
+ 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()
+ attachCertBtnClickListener()
+ attachPrivateKeyBtnClickListener()
+ dropDownMenuProtocolItemClickListener()
+ populateDialogValues()
+ textFieldsListener()
+ }
+
+
+ private fun initializeDropDownList() {
+ initializeConnectionsDropDown()
+ initializeAuthDropdown()
+ 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)
+ 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 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 (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 {
+ toggleCredentialsVisibility(View.VISIBLE)
+ toggleSFTPAuthVisibility(View.GONE)
+ }
+ } 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
+ 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
+ }
+
+ private fun promptUserToSelectStorage() {
+ (activity as CloudActivity).promptUserToSelectStorage()
+ }
+
+
+ private fun dropDownItemSelected() {
+ binding.dropdownMenu.setOnItemClickListener { parent, view, position, id ->
+ val selectedItem = parent.getItemAtPosition(position).toString()
+ handleConnectionTypeSelection(selectedItem)
+ }
+ }
+
+ 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)
+ toggleSharedPathVisibility(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)
+ toggleSharedPathVisibility(View.VISIBLE)
+ 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())
+ toggleSharedPathVisibility(View.GONE)
+
+ } 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())
+ toggleSharedPathVisibility(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())
+ }
+ }
+
+ else -> Unit
+ }
+ }
+
+ private fun attachCertBtnClickListener() {
+ binding.certAttachBtn.setOnClickListener {
+ (activity as CloudActivity).openFileLinkForCert {
+ certUri = it
+ binding.certStatusTv.text = it.path
+ }
+
+ }
+ }
+
+ 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)
+ }
+
+ }
+ }
+ }
+
+
+ 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)
+ userEt.setText(it.username)
+ passwordEt.setText(it.password)
+ sharedPathEt.setText(it.sharedPath)
+ displayEt.setText(it.displayName)
+ 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/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/entity/NetworkConnectionEntity.kt b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt
new file mode 100644
index 000000000..df0d4cc3a
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/entity/NetworkConnectionEntity.kt
@@ -0,0 +1,25 @@
+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",
+ indices = [Index(value = ["displayName"], unique = true)]
+)
+data class NetworkConnectionEntity(
+ @PrimaryKey(autoGenerate = true) val id: Long = 0,
+ 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 authentication: String,
+ val privateKey: String = "",
+ val privateKeyPass: String = "",
+ val protocols: String? = null
+)
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..9d1c2609d
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/enums/Authentication.kt
@@ -0,0 +1,8 @@
+package org.fossify.filemanager.enums
+
+enum class Authentication {
+ Password,
+ Anonymous,
+ PrivateKey
+}
+
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..403296895
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/enums/Protocols.kt
@@ -0,0 +1,6 @@
+package org.fossify.filemanager.enums
+
+enum class Protocols {
+ HTTP,
+ HTTPS
+}
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/factory/NetworkBrowserViewModelFactory.kt b/app/src/main/kotlin/org/fossify/filemanager/factory/NetworkBrowserViewModelFactory.kt
new file mode 100644
index 000000000..3969dfdd5
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/factory/NetworkBrowserViewModelFactory.kt
@@ -0,0 +1,16 @@
+package org.fossify.filemanager.factory
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+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 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,webDavApi,ftpApi,sftpApi,smbApi) 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..641e6382f
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/FileHelpers.kt
@@ -0,0 +1,119 @@
+package org.fossify.filemanager.fileSystems
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.util.Log
+import androidx.core.net.toUri
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.fossify.commons.enums.ConnectionTypes
+import org.fossify.filemanager.helpers.Helpers
+
+object FileHelpers {
+ fun launchSMB(mPath: String, context: Context, mimType: String? = null) {
+ try {
+ CoroutineScope(Dispatchers.IO).launch {
+ val extractedPath = Helpers.retrievePath(mPath)
+ val uri = Helpers.createNanoHttpdUrl(ConnectionTypes.SMB,extractedPath).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)
+ if (resInfos.size > 0) {
+ context.startActivity(i)
+ }
+ }
+ } catch (exp: Exception) {
+ Log.e("Activity Launch Failed", exp.toString())
+ }
+ }
+
+ fun launchWebDav(mPath: String, context: Context, mimType: String? = null) {
+ try {
+ CoroutineScope(Dispatchers.IO).launch {
+
+ val extractedPath = Helpers.retrievePath(mPath)
+ val uri = Helpers.createNanoHttpdUrl(ConnectionTypes.WebDav,extractedPath).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)
+ if (resInfos.size > 0) {
+ context.startActivity(i)
+ }
+ }
+ } catch (exp: Exception) {
+ Log.e("Activity Launch Failed", exp.toString())
+ }
+ }
+
+ fun launchSFTP(mPath: String, context: Context,mimType: String? = null) {
+ try {
+ CoroutineScope(Dispatchers.IO).launch {
+ val uri = Helpers.createNanoHttpdUrl(ConnectionTypes.SMB).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)
+ if (resInfos.size > 0) {
+ context.startActivity(i)
+ }
+ }
+ } catch (exp: Exception) {
+ Log.e("Activity Launch Failed", exp.toString())
+ }
+ }
+
+ fun launchFTP(mPath: String, context: Context,mimType: String? = null) {
+ try {
+ CoroutineScope(Dispatchers.IO).launch {
+ val uri = Helpers.createNanoHttpdUrl(ConnectionTypes.SMB).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)
+ 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..b9cc16fef
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/HttpServer.kt
@@ -0,0 +1,195 @@
+package org.fossify.filemanager.fileSystems
+
+import fi.iki.elonen.NanoHTTPD
+
+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.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 connectionType: ConnectionTypes,
+ private val composition: AppComposition,
+ private val machinePort: Int,
+ 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"]
+
+ return when (connectionType) {
+ ConnectionTypes.SMB -> handleSmb(uri, rangeHeader)
+ ConnectionTypes.WebDav -> handleWebDav(uri, rangeHeader)
+ ConnectionTypes.SFTP -> handleSftp(uri, rangeHeader)
+ else -> handleFtp(uri, rangeHeader)
+ }
+ }
+
+ private fun handleSmb(uri: String, rangeHeader: String?): Response {
+ 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()
+
+ 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))
+ }
+ catch (exp: Exception){
+ dispatchException.invoke(exp)
+ notFound()
+ }
+ }
+
+ private fun handleWebDav(uri: String, rangeHeader: String?): Response {
+
+ val url = Helpers. createProtocolUrl(connectionType, uri, server = serverIp, port = machinePort, protocols = protocol)
+
+ 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 handleSftp(uri: String, rangeHeader: String?): Response {
+ val apiResponse = composition.sftpApiRepository.listSFTPFileDetails(uri)
+
+ return handleResponse(apiResponse) {
+ val file = apiResponse.response ?: return@handleResponse notFound()
+ val (start, end) = parseRange(rangeHeader, file.size)
+ ?: return@handleResponse rangeNotSatisfiable()
+
+ 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))
+ }
+ }
+
+ private fun handleFtp(uri: String, rangeHeader: String?): Response {
+ val apiResponse = composition.ftpApiRepository.getFTPFileDetail(uri)
+
+ return handleResponse(apiResponse) {
+ val file = apiResponse.response ?: return@handleResponse notFound()
+
+ val (start, end) = parseRange(rangeHeader, file.size)
+ ?: return@handleResponse rangeNotSatisfiable()
+
+ 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 parseRange(header: String?, fileLength: Long): Pair? {
+ var start = 0L
+ var end = fileLength - 1
+
+ if (header != null && header.startsWith("bytes=")) {
+ val parts = header.removePrefix("bytes=").split("-")
+ try {
+ if (parts[0].isNotEmpty()) start = parts[0].toLong()
+ if (parts.size > 1 && parts[1].isNotEmpty()) end = parts[1].toLong()
+ } catch (_: NumberFormatException) {
+ }
+ }
+
+ return if (start >= fileLength) null else start to end
+ }
+
+ 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")
+ addHeader("Connection", "keep-alive")
+ }
+ }
+
+ private fun buildUrl(path: String) =
+ Helpers.createProtocolUrl(connectionType, server = serverIp, path = path, port = machinePort, protocols = protocol)
+
+
+ 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 handleResponse(
+ apiResponse: ApiResponse,
+ callBack: () -> Response
+ ): Response {
+ return if (apiResponse.exception != null) {
+ dispatchException.invoke(apiResponse.exception)
+ serverError(apiResponse.exception.message)
+ } else {
+ callBack.invoke()
+ }
+ }
+
+ 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/fileSystems/MimeTypes.kt b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/MimeTypes.kt
new file mode 100644
index 000000000..7b92277f7
--- /dev/null
+++ b/app/src/main/kotlin/org/fossify/filemanager/fileSystems/MimeTypes.kt
@@ -0,0 +1,31 @@
+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 {
+ 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
+ }
+
+ 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 d7589648c..7531e7963 100644
--- a/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt
+++ b/app/src/main/kotlin/org/fossify/filemanager/fragments/ItemsFragment.kt
@@ -2,17 +2,31 @@ 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 android.util.Log
+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
import org.fossify.commons.extensions.*
import org.fossify.commons.helpers.*
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 +35,22 @@ 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
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
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()
@@ -38,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()
@@ -59,6 +82,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)
}
}
@@ -77,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
@@ -99,7 +132,8 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF
getRecyclerAdapter()?.finishActMode()
}
- fun openPath(path: String, forceRefresh: Boolean = false) {
+
+ fun openPath(path: String, forceRefresh: Boolean = false, pathName: String = "", connectionType: ConnectionTypes = ConnectionTypes.Default) {
if ((activity as? BaseSimpleActivity)?.isAskingPermissions == true) {
return
}
@@ -108,24 +142,29 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF
if (realPath.isEmpty()) {
realPath = "/"
}
-
+ this.connectionType = connectionType
scrollStates[currentPath] = getScrollState()!!
currentPath = realPath
showHidden = context!!.config.shouldShowHidden()
showProgressBar()
- getItems(currentPath) { originalPath, listItems ->
+ getItems(currentPath, connectionType = connectionType) { originalPath, listItems ->
if (currentPath != originalPath) {
return@getItems
}
FileDirItem.sorting = context!!.config.getFolderSorting(currentPath)
+ if (connectionType != ConnectionTypes.Default) {
+ listItems.forEach {
+ it.parent = originalPath.substringAfter('/')
+ }
+ }
listItems.sort()
if (context!!.config.getFolderViewType(currentPath) == VIEW_TYPE_GRID && listItems.none { it.isSectionTitle }) {
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)
}
}
@@ -134,40 +173,71 @@ class ItemsFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerF
itemsIgnoringSearch = listItems
activity?.runOnUiThread {
(activity as? MainActivity)?.refreshMenuItems()
- addItems(listItems, forceRefresh)
+ addItems(listItems, forceRefresh,pathName = pathName, connectionType)
if (context != null && currentViewType != context!!.config.getFolderViewType(currentPath)) {
- setupLayoutManager()
+ setupLayoutManager(connectionType)
}
hideProgressBar()
}
}
}
- private fun addItems(items: ArrayList, forceRefresh: Boolean = false) {
+ private fun addItems(items: ArrayList, forceRefresh: Boolean = false,pathName: String = "", connectionType: ConnectionTypes) {
activity?.runOnUiThread {
binding.itemsSwipeRefresh.isRefreshing = false
- binding.breadcrumbs.setBreadcrumb(currentPath)
+ if (connectionType != ConnectionTypes.DAVx5){
+ 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)
}
-
+ var lastClickedItem: ListItem? = null
ItemsAdapter(activity as SimpleActivity, storedItems, this, binding.itemsList, isPickMultipleIntent, binding.itemsSwipeRefresh) {
- if ((it as? ListItem)?.isSectionTitle == true) {
+ lastClickedItem = it as? ListItem
+ if ((it as? ListItem)?.mIsDirectory == false) {
+ if (connectionType == ConnectionTypes.SMB) {
+ it?.let { item ->
+ FileHelpers.launchSMB(item.mPath, this@ItemsFragment.context)
+ }
+ } else if (connectionType == ConnectionTypes.WebDav) {
+ it?.let { item ->
+ FileHelpers.launchWebDav(item.mPath, context = this@ItemsFragment.context)
+ }
+ } else if (connectionType == ConnectionTypes.SFTP) {
+ it?.let { item ->
+ FileHelpers.launchSFTP(item.mPath, context = this@ItemsFragment.context)
+ }
+ } else if (connectionType == ConnectionTypes.FTP) {
+ it?.let { item ->
+ FileHelpers.launchFTP( item.mPath, context = this@ItemsFragment.context)
+ }
+ } else {
+ itemClicked(it as FileDirItem, connectionType)
+ }
+ } else if ((it as? ListItem)?.isSectionTitle == true) {
openDirectory(it.mPath)
searchClosed()
} else {
- itemClicked(it as FileDirItem)
+ 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()
}
@@ -181,11 +251,44 @@ 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,
+ connectionType: ConnectionTypes,
+ callback: (originalPath: String, items: ArrayList) -> Unit
+ ) {
ensureBackgroundThread {
if (activity?.isDestroyed == false && activity?.isFinishing == false) {
val config = context!!.config
- if (context.isRestrictedSAFOnlyRoot(path)) {
+ if (connectionType.equals(ConnectionTypes.SMB)) {
+ val fileItems = viewModel.getFilesFromNetworkPath(path)
+ handleApiResponse(fileItems, path, connectionType, callback)
+ } else if (connectionType.equals(ConnectionTypes.WebDav)) {
+ CoroutineScope(Dispatchers.IO).launch {
+ viewModel.listWebDavFiles(path)
+ viewModel.webDavFiles.collectLatest {
+ handleApiResponse(it, path, connectionType, callback)
+ }
+ }
+
+ } else if (connectionType.equals(ConnectionTypes.SFTP)) {
+ CoroutineScope(Dispatchers.IO).launch {
+ viewModel.listAllFilesSFTPRoot(path)
+ viewModel.sftpFiles.collectLatest {
+ handleApiResponse(it, path, connectionType, callback)
+ }
+ }
+ } else if (connectionType.equals(ConnectionTypes.FTP)) {
+ CoroutineScope(Dispatchers.IO).launch {
+ viewModel.listAllFTPFiles(path)
+ viewModel.ftpFiles.collectLatest {
+ handleApiResponse(it, path, connectionType, callback)
+ }
+ }
+ }
+ else if (connectionType.equals(ConnectionTypes.DAVx5)){
+ handleApiResponse