Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

package eu.opencloud.android.presentation.documentsprovider

import android.content.Context
import android.content.res.AssetFileDescriptor
import android.database.Cursor
import android.database.MatrixCursor
Expand All @@ -35,6 +36,7 @@ import android.os.CancellationSignal
import android.os.Handler
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import org.json.JSONObject
import android.provider.DocumentsProvider
import eu.opencloud.android.MainApp
import eu.opencloud.android.R
Expand All @@ -61,6 +63,7 @@ import eu.opencloud.android.presentation.documentsprovider.cursors.FileCursor
import eu.opencloud.android.presentation.documentsprovider.cursors.RootCursor
import eu.opencloud.android.presentation.documentsprovider.cursors.SpaceCursor
import eu.opencloud.android.presentation.settings.security.SettingsSecurityFragment.Companion.PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER
import eu.opencloud.android.presentation.settings.advanced.SettingsAdvancedFragment.Companion.PREFERENCE_PRETEND_LOCAL_STORAGE
import eu.opencloud.android.usecases.synchronization.SynchronizeFileUseCase
import eu.opencloud.android.usecases.transfers.downloads.DownloadFileUseCase
import eu.opencloud.android.usecases.synchronization.SynchronizeFolderUseCase
Expand Down Expand Up @@ -94,6 +97,9 @@ class DocumentsStorageProvider : DocumentsProvider() {
private var spacesSyncRequired = true

private lateinit var fileToUpload: OCFile
private val pendingUploadsPrefs by lazy {
context!!.getSharedPreferences("saf_pending_uploads", Context.MODE_PRIVATE)
}

// Cache to avoid redundant PROPFINDs when apps (e.g. Google Photos) call
// openDocument many times for the same file. Two layers:
Expand All @@ -106,6 +112,59 @@ class DocumentsStorageProvider : DocumentsProvider() {
private var propfindCacheFileId: Long? = null
private var propfindCacheTimestamp: Long = 0

private fun savePendingDocument(pendingId: String, ocFile: OCFile) {
val json = JSONObject().apply {
put("remotePath", ocFile.remotePath)
put("mimeType", ocFile.mimeType)
put("owner", ocFile.owner)
if (ocFile.parentId != null) put("parentId", ocFile.parentId)
if (ocFile.spaceId != null) put("spaceId", ocFile.spaceId)
put("storagePath", ocFile.storagePath)
}
pendingUploadsPrefs.edit().putString(pendingId, json.toString()).apply()
}

private fun getPendingDocument(pendingId: String): OCFile? {
val jsonString = pendingUploadsPrefs.getString(pendingId, null) ?: return null
val json = JSONObject(jsonString)

if (json.has("dbId")) {
val dbId = json.getLong("dbId")
return try {
getFileByIdOrException(dbId.toInt())
} catch (_: Exception) {
null
}
}

val remotePath = json.getString("remotePath")
val owner = json.getString("owner")
val spaceId = if (json.has("spaceId")) json.getString("spaceId") else null

return try {
val fileFromDb = getFileByPathOrException(remotePath, owner, spaceId)
json.put("dbId", fileFromDb.id)
pendingUploadsPrefs.edit().putString(pendingId, json.toString()).apply()
fileFromDb
} catch (_: Exception) {
OCFile(
remotePath = remotePath,
mimeType = json.getString("mimeType"),
parentId = if (json.has("parentId")) json.getLong("parentId") else null,
owner = owner,
spaceId = spaceId,
modificationTimestamp = 0,
length = 0
).apply {
storagePath = json.getString("storagePath")
val localFile = File(storagePath ?: "")
if (localFile.exists()) {
length = localFile.length()
}
}
}
}

override fun openDocument(
documentId: String,
mode: String,
Expand All @@ -115,7 +174,7 @@ class DocumentsStorageProvider : DocumentsProvider() {

// If documentId == NONEXISTENT_DOCUMENT_ID only Upload is needed because file does not exist in our database yet.
var ocFile: OCFile
val uploadOnly: Boolean = documentId == NONEXISTENT_DOCUMENT_ID || documentId == "null"
val uploadOnly: Boolean = documentId == NONEXISTENT_DOCUMENT_ID || documentId == "null" || documentId.startsWith("pending_")

var accessMode: Int = ParcelFileDescriptor.parseMode(mode)
val isWrite: Boolean = mode.contains("w")
Expand Down Expand Up @@ -197,11 +256,11 @@ class DocumentsStorageProvider : DocumentsProvider() {
}
}
} else {
ocFile = fileToUpload
ocFile = getPendingDocument(documentId) ?: fileToUpload
accessMode = accessMode or ParcelFileDescriptor.MODE_CREATE
}

val fileToOpen = File(ocFile.storagePath)
val fileToOpen = File(ocFile.storagePath ?: "")

return if (!isWrite) {
ParcelFileDescriptor.open(fileToOpen, accessMode)
Expand Down Expand Up @@ -318,7 +377,13 @@ class DocumentsStorageProvider : DocumentsProvider() {
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
Timber.d("Query Document: $documentId")
if (documentId == NONEXISTENT_DOCUMENT_ID) return FileCursor(projection).apply {
addFile(fileToUpload)
if (this@DocumentsStorageProvider::fileToUpload.isInitialized) addFile(fileToUpload, documentId)
}

if (documentId.startsWith("pending_")) {
return FileCursor(projection).apply {
getPendingDocument(documentId)?.let { addFile(it, documentId) }
}
}

val fileId = try {
Expand Down Expand Up @@ -350,6 +415,10 @@ class DocumentsStorageProvider : DocumentsProvider() {
// If access from document provider is not allowed, return empty cursor
val preferences: SharedPreferencesProvider by inject()
val lockAccessFromDocumentProvider = preferences.getBoolean(PREFERENCE_LOCK_ACCESS_FROM_DOCUMENT_PROVIDER, false)

// Get if user selected to pretend local storage
val pretendLocal = preferences.getBoolean(PREFERENCE_PRETEND_LOCAL_STORAGE, false)

return if (lockAccessFromDocumentProvider && accounts.isNotEmpty()) {
result.apply { addProtectedRoot(contextApp) }
} else {
Expand All @@ -362,7 +431,7 @@ class DocumentsStorageProvider : DocumentsProvider() {
)
val spacesFeatureAllowedForAccount = AccountUtils.isSpacesFeatureAllowedForAccount(contextApp, account, capabilities)

result.addRoot(account, contextApp, spacesFeatureAllowedForAccount)
result.addRoot(account, contextApp, spacesFeatureAllowedForAccount, pretendLocal)
}
result
}
Expand All @@ -376,7 +445,7 @@ class DocumentsStorageProvider : DocumentsProvider() {
// To do: Show thumbnail for spaces
val file = getFileByIdOrException(documentId.toInt())

val realFile = File(file.storagePath)
val realFile = File(file.storagePath ?: "")

return AssetFileDescriptor(
ParcelFileDescriptor.open(realFile, ParcelFileDescriptor.MODE_READ_ONLY), 0, AssetFileDescriptor.UNKNOWN_LENGTH
Expand Down Expand Up @@ -497,6 +566,43 @@ class DocumentsStorageProvider : DocumentsProvider() {
}
}

override fun isChildDocument(parentDocumentId: String, documentId: String): Boolean {
Timber.d("isChildDocument($parentDocumentId, $documentId)")

// If they are the same, Android specs usually consider it a child/match
if (parentDocumentId == documentId) return true

return try {
// Parse the child file. If it's a new un-uploaded file, pull from memory. Otherwise, query DB.
val childFile = if (documentId == NONEXISTENT_DOCUMENT_ID && this::fileToUpload.isInitialized) {
fileToUpload
} else if (documentId.startsWith("pending_")) {
getPendingDocument(documentId) ?: return false
} else {
getFileByIdOrException(documentId.toInt())
}

val parentIdInt = parentDocumentId.toIntOrNull()

if (parentIdInt != null) {
// The parent is a standard folder
val parentFile = getFileByIdOrException(parentIdInt)

// Check if the child belongs to the same account and its path sits inside the parent's path and space
childFile.owner == parentFile.owner &&
childFile.spaceId == parentFile.spaceId &&
childFile.remotePath.startsWith(parentFile.remotePath)
} else {
// The parentDocumentId is a string, meaning it's the account root (e.g., "user@server.com")
// Just verify the child file belongs to this account
childFile.owner == parentDocumentId
}
} catch (e: Exception) {
Timber.e(e, "Error evaluating isChildDocument for parent: $parentDocumentId, child: $documentId")
false
}
}

private fun checkUseCaseResult(result: UseCaseResult<Any>, folderToNotify: String) {
if (!result.isSuccess) {
Timber.e(result.getThrowableOrNull()!!)
Expand Down Expand Up @@ -530,22 +636,26 @@ class DocumentsStorageProvider : DocumentsProvider() {
mimeType: String,
displayName: String,
): String {
// We just need to return a Document ID, so we'll return an empty one. File does not exist in our db yet.
// File will be created at [openDocument] method.
val pendingId = "pending_${UUID.randomUUID()}"
val tempDir = File(FileStorageUtils.getTemporalPath(parentDocument.owner, parentDocument.spaceId))
val newFile = File(tempDir, displayName)
val newFile = File(File(tempDir, pendingId), displayName)
newFile.parentFile?.mkdirs()
fileToUpload = OCFile(

val ocFile = OCFile(
remotePath = parentDocument.remotePath + displayName,
mimeType = mimeType,
parentId = parentDocument.id,
owner = parentDocument.owner,
spaceId = parentDocument.spaceId
spaceId = parentDocument.spaceId,
modificationTimestamp = 0,
length = 0
).apply {
storagePath = newFile.path
}

return NONEXISTENT_DOCUMENT_ID
savePendingDocument(pendingId, ocFile)

return pendingId
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ class FileCursor(projection: Array<String>?) : MatrixCursor(projection ?: DEFAUL
cursorExtras = Bundle().apply { putBoolean(DocumentsContract.EXTRA_LOADING, hasMoreToSync) }
}

fun addFile(file: OCFile) {
fun addFile(
file: OCFile,
documentId: String = requireNotNull(file.id).toString(),
) {
val iconRes = MimetypeIconUtil.getFileTypeIconId(file.mimeType, file.fileName)
val mimeType = if (file.isFolder) Document.MIME_TYPE_DIR else file.mimeType
val imagePath = if (file.isImage && file.isAvailableLocally) file.storagePath else null
Expand All @@ -57,7 +60,7 @@ class FileCursor(projection: Array<String>?) : MatrixCursor(projection ?: DEFAUL
}

newRow()
.add(Document.COLUMN_DOCUMENT_ID, file.id.toString())
.add(Document.COLUMN_DOCUMENT_ID, documentId)
.add(Document.COLUMN_DISPLAY_NAME, file.fileName)
.add(Document.COLUMN_LAST_MODIFIED, file.modificationTimestamp)
.add(Document.COLUMN_SIZE, file.length)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,23 @@ import eu.opencloud.android.datamodel.FileDataStorageManager

class RootCursor(projection: Array<String>?) : MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION) {

fun addRoot(account: Account, context: Context, spacesAllowed: Boolean) {
fun addRoot(account: Account, context: Context, spacesAllowed: Boolean, pretendLocal: Boolean) {
val manager = FileDataStorageManager(account)
val mainDirId = if (spacesAllowed) {
// To display the list of spaces for an account, we need to do this trick.
// If the document id is not a number, we will know that it is the time to display the list of spaces for the account
account.name
} else {
// Root directory of the personal space or "Files" (old server)
manager.getRootPersonalFolder()?.id
}

val flags = Root.FLAG_SUPPORTS_SEARCH or Root.FLAG_SUPPORTS_CREATE
// Add FLAG_SUPPORTS_IS_CHILD to enable Folder selection
var flags = Root.FLAG_SUPPORTS_SEARCH or Root.FLAG_SUPPORTS_CREATE or Root.FLAG_SUPPORTS_IS_CHILD

// Add FLAG_LOCAL_ONLY if the user enabled it
if (pretendLocal) {
flags = flags or Root.FLAG_LOCAL_ONLY
}

newRow()
.add(Root.COLUMN_ROOT_ID, account.name)
Expand Down Expand Up @@ -72,8 +77,7 @@ class RootCursor(projection: Array<String>?) : MatrixCursor(projection ?: DEFAUL
Root.COLUMN_TITLE,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES,
Root.COLUMN_SUMMARY,
Root.COLUMN_FLAGS
Root.COLUMN_SUMMARY
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ package eu.opencloud.android.presentation.settings.advanced

import android.os.Bundle
import android.view.View
import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import eu.opencloud.android.R
import eu.opencloud.android.presentation.documentsprovider.DocumentsProviderUtils.notifyDocumentsProviderRoots
import org.koin.androidx.viewmodel.ext.android.viewModel

class SettingsAdvancedFragment : PreferenceFragmentCompat() {
Expand All @@ -37,11 +39,13 @@ class SettingsAdvancedFragment : PreferenceFragmentCompat() {

private var prefShowHiddenFiles: SwitchPreferenceCompat? = null
private var prefRemoveLocalFiles: ListPreference? = null
private var prefPretendLocal: CheckBoxPreference? = null

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_advanced, rootKey)

prefShowHiddenFiles = findPreference(PREF_SHOW_HIDDEN_FILES)
prefPretendLocal = findPreference(PREFERENCE_PRETEND_LOCAL_STORAGE)
prefRemoveLocalFiles = findPreference<ListPreference>(PREFERENCE_REMOVE_LOCAL_FILES)?.apply {
entries = listOf(
getString(R.string.prefs_delete_local_files_entries_never),
Expand Down Expand Up @@ -80,9 +84,15 @@ class SettingsAdvancedFragment : PreferenceFragmentCompat() {
advancedViewModel.scheduleDeleteLocalFiles(newValue)
true
}

prefPretendLocal?.setOnPreferenceChangeListener { _: Preference?, _: Any ->
notifyDocumentsProviderRoots(requireContext())
true
}
}

companion object {
const val PREF_SHOW_HIDDEN_FILES = "show_hidden_files"
const val PREFERENCE_PRETEND_LOCAL_STORAGE = "pretend_local_storage"
}
}
2 changes: 2 additions & 0 deletions opencloudApp/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
<string name="prefs_lock_application_entries_30minutes">Nach 30 Minuten</string>
<string name="prefs_lock_access_from_document_provider">Zugriff vom Dokumentanbieter sperren</string>
<string name="prefs_lock_access_from_document_provider_summary">Sperren Sie den Zugriff anderer Apps auf die Dateien der Konten innerhalb der App über den nativen Android-Dateimanager.</string>
<string name="prefs_pretend_local_title">Als lokaler Speicher ausgeben</string>
<string name="prefs_pretend_local_summary">Ermöglicht Drittanbieter-Apps, OpenCloud zu sehen, wenn diese strikt nach lokalen Dateien suchen. Dies umgeht standardmäßige Android-Einschränkungen, kann jedoch in einigen Apps zu Rucklern oder Aufhängern der Benutzeroberfläche führen, wenn Ihre Netzwerkverbindung langsam ist.</string>
<string name="prefs_touches_with_other_visible_windows">Berührungen mit anderen sichtbaren Fenstern</string>
<string name="prefs_touches_with_other_visible_windows_summary">Ermöglicht Berührungen, wenn die Ansicht durch ein anderes sichtbares Fenster verdeckt ist. Aktivieren Sie diese Option, um Apps zur Lichtfilterung zu nutzen.</string>
<string name="confirmation_touches_with_other_windows_title">Sind Sie sicher, dass Sie diese Funktion aktivieren möchten\?</string>
Expand Down
2 changes: 2 additions & 0 deletions opencloudApp/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
<string name="prefs_lock_application_entries_30minutes">Después de 30 minutos</string>
<string name="prefs_lock_access_from_document_provider">Bloquear acceso desde administrador de archivos</string>
<string name="prefs_lock_access_from_document_provider_summary">Bloquear el acceso de otras aplicaciones a los archivos de las cuentas a través del administrador de archivos de Android.</string>
<string name="prefs_pretend_local_title">Simular almacenamiento local</string>
<string name="prefs_pretend_local_summary">Permite que las aplicaciones de terceros vean OpenCloud cuando soliciten estrictamente archivos locales. Esto elude las limitaciones estándar de Android, pero puede causar bloqueos en la interfaz de algunas aplicaciones si la conexión de red es lenta.</string>
<string name="prefs_touches_with_other_visible_windows">Bloquear pulsaciones con aplicaciones superpuestas</string>
<string name="prefs_touches_with_other_visible_windows_summary">Permite interactuar con la aplicación aunque haya otras ventanas visibles por encima. Activa para usar aplicaciones de atenuación de pantalla.</string>
<string name="confirmation_touches_with_other_windows_title">¿Seguro que quieres activar esta función\?</string>
Expand Down
2 changes: 2 additions & 0 deletions opencloudApp/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
<string name="prefs_lock_application_entries_30minutes">Après 30 minutes</string>
<string name="prefs_lock_access_from_document_provider">Verrouiller l’accès au gestionnaire de fichier</string>
<string name="prefs_lock_access_from_document_provider_summary">Verrouiller l\'accès des autres applications aux fichiers d\'utilisateurs de l\'application via le navigateur de fichiers natif d\'Android.</string>
<string name="prefs_pretend_local_title">Simuler un stockage local</string>
<string name="prefs_pretend_local_summary">Permet aux applications tierces de voir OpenCloud lorsqu\'elles demandent strictement des fichiers locaux. Cela contourne les limitations standard d\'Android, mais peut provoquer des blocages de l\'interface dans certaines applications si votre réseau est lent.</string>
<string name="prefs_touches_with_other_visible_windows">Interactions avec d\'autres fenêtres visibles</string>
<string name="prefs_touches_with_other_visible_windows_summary">Autoriser les interactions quand une autre fenêtre visible se superpose à l\'affichage. Activer pour utiliser le fonctionnement des applications de filtre lumineux.</string>
<string name="confirmation_touches_with_other_windows_title">Êtes-vous sûr de vouloir activer cette fonctionnalité \?</string>
Expand Down
Loading