From 020edd6500194d564194acdb57f58ad6b6d951e9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 29 Oct 2026 18:18:58 +0300 Subject: [PATCH 1/8] Rename .java to .kt Signed-off-by: alperozturk96 --- ...{DocumentsStorageProvider.java => DocumentsStorageProvider.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/providers/{DocumentsStorageProvider.java => DocumentsStorageProvider.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java b/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java rename to app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt From ed2bfb4599a0a9766249af19080f70104b229b4e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 29 Oct 2026 18:18:59 +0300 Subject: [PATCH 2/8] refactor(document-storage-provider): kotlin Signed-off-by: alperozturk96 --- .../providers/DocumentsProviderUtils.kt | 2 +- .../providers/DocumentsStorageProviderIT.kt | 2 +- .../providers/DocumentsStorageProvider.kt | 1210 +++-------------- .../providers/documentstorage/AccessMode.kt | 25 + .../providers/documentstorage/Document.kt | 65 + .../documentstorage/DocumentCreator.kt | 170 +++ .../documentstorage/DocumentEditor.kt | 118 ++ .../documentstorage/DocumentLister.kt | 115 ++ .../documentstorage/DocumentNameValidator.kt | 24 + .../documentstorage/DocumentNotifier.kt | 28 + .../documentstorage/DocumentOpener.kt | 257 ++++ .../documentstorage/DocumentResolver.kt | 61 + .../DocumentStorageFailures.kt | 24 + .../documentstorage/FolderRefresher.kt | 59 + .../documentstorage/OpenedDocumentFile.kt | 38 + .../providers/cursors/FileCursor.java | 15 +- .../providers/cursors/RootCursor.java | 6 +- 17 files changed, 1197 insertions(+), 1022 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/AccessMode.kt create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/Document.kt create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentCreator.kt create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentEditor.kt create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentLister.kt create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNameValidator.kt create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNotifier.kt create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentOpener.kt create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentResolver.kt create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentStorageFailures.kt create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/FolderRefresher.kt create mode 100644 app/src/main/java/com/owncloud/android/providers/documentstorage/OpenedDocumentFile.kt diff --git a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsProviderUtils.kt b/app/src/androidTest/java/com/owncloud/android/providers/DocumentsProviderUtils.kt index cd5f69559114..5f01fa10d808 100644 --- a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsProviderUtils.kt +++ b/app/src/androidTest/java/com/owncloud/android/providers/DocumentsProviderUtils.kt @@ -25,7 +25,7 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation -import com.owncloud.android.providers.DocumentsStorageProvider.DOCUMENTID_SEPARATOR +import com.owncloud.android.providers.DocumentsStorageProvider.Companion.DOCUMENTID_SEPARATOR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.suspendCancellableCoroutine diff --git a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt b/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt index 53e10d7052a9..9105c1d59b73 100644 --- a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt @@ -29,7 +29,7 @@ import com.owncloud.android.providers.DocumentsProviderUtils.assertRegularFolder import com.owncloud.android.providers.DocumentsProviderUtils.findFileBlocking import com.owncloud.android.providers.DocumentsProviderUtils.getOCFile import com.owncloud.android.providers.DocumentsProviderUtils.listFilesBlocking -import com.owncloud.android.providers.DocumentsStorageProvider.DOCUMENTID_SEPARATOR +import com.owncloud.android.providers.DocumentsStorageProvider.Companion.DOCUMENTID_SEPARATOR import kotlinx.coroutines.runBlocking import org.apache.commons.httpclient.HttpStatus import org.apache.commons.httpclient.methods.ByteArrayRequestEntity diff --git a/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt b/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt index 4444483c277c..f22d32217a87 100644 --- a/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt +++ b/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt @@ -1,1098 +1,290 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2020-2022 Tobias Kaminsky - * SPDX-FileCopyrightText: 2019-2021 Chris Narkiewicz - * SPDX-FileCopyrightText: 2016 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers; - -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.annotation.SuppressLint; -import android.content.ContentResolver; -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.database.Cursor; -import android.graphics.Point; -import android.net.Uri; -import android.os.Binder; -import android.os.Bundle; -import android.os.CancellationSignal; -import android.os.Handler; -import android.os.Looper; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.provider.DocumentsContract; -import android.provider.DocumentsProvider; - -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.jobs.upload.FileUploadHelper; -import com.nextcloud.client.jobs.upload.FileUploadWorker; -import com.nextcloud.client.preferences.AppPreferencesImpl; -import com.nextcloud.client.utils.HashUtil; -import com.nextcloud.utils.AppConstants; -import com.nextcloud.utils.extensions.ContextExtensionsKt; -import com.nextcloud.utils.extensions.FileExtensionsKt; -import com.nextcloud.utils.fileNameValidator.FileNameValidator; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.ThumbnailsCacheManager; -import com.owncloud.android.files.services.NameCollisionPolicy; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation; -import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation; -import com.owncloud.android.lib.resources.status.OCCapability; -import com.owncloud.android.operations.CopyFileOperation; -import com.owncloud.android.operations.CreateFolderOperation; -import com.owncloud.android.operations.DownloadFileOperation; -import com.owncloud.android.operations.MoveFileOperation; -import com.owncloud.android.operations.RefreshFolderOperation; -import com.owncloud.android.operations.RemoveFileOperation; -import com.owncloud.android.operations.RenameFileOperation; -import com.owncloud.android.ui.activity.SettingsActivity; -import com.owncloud.android.ui.helpers.FileOperationsHelper; -import com.owncloud.android.utils.FileStorageUtils; -import com.owncloud.android.utils.FileUtil; -import com.owncloud.android.utils.MimeTypeUtil; -import com.owncloud.android.utils.theme.CapabilityUtils; - -import org.nextcloud.providers.cursors.FileCursor; -import org.nextcloud.providers.cursors.RootCursor; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.FutureTask; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import dagger.android.AndroidInjection; - -import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; -import static android.os.ParcelFileDescriptor.MODE_TRUNCATE; -import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY; -import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR; -import static com.owncloud.android.datamodel.OCFile.ROOT_PATH; - -public class DocumentsStorageProvider extends DocumentsProvider { - private static final String TAG = DocumentsStorageProvider.class.getSimpleName(); - - private static final long CACHE_EXPIRATION = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES); - - @Inject UserAccountManager accountManager; - - private volatile boolean isFolderPathValid = true; - - @VisibleForTesting - static final String DOCUMENTID_SEPARATOR = "/"; - private static final int DOCUMENTID_PARTS = 2; - private volatile Map rootIdToStorageManager = new HashMap<>(); - - private final Executor executor = Executors.newCachedThreadPool(); - - @Override - public boolean onCreate() { - AndroidInjection.inject(this); - initStoragePathIfMissing(); - - initiateStorageMap(); - - return true; - } - - private void initStoragePathIfMissing() { +package com.owncloud.android.providers + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.AssetFileDescriptor +import android.database.Cursor +import android.graphics.Point +import android.os.Binder +import android.os.CancellationSignal +import android.os.ParcelFileDescriptor +import android.os.Process +import android.provider.DocumentsContract +import android.provider.DocumentsProvider +import androidx.annotation.VisibleForTesting +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.preferences.AppPreferencesImpl +import com.nextcloud.client.utils.HashUtil +import com.nextcloud.utils.extensions.getSmallThumbnailKey +import com.nextcloud.utils.extensions.showToast +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.RemoveFileOperation +import com.owncloud.android.providers.documentstorage.Document +import com.owncloud.android.providers.documentstorage.DocumentCreator +import com.owncloud.android.providers.documentstorage.DocumentEditor +import com.owncloud.android.providers.documentstorage.DocumentLister +import com.owncloud.android.providers.documentstorage.DocumentNameValidator +import com.owncloud.android.providers.documentstorage.DocumentNotifier +import com.owncloud.android.providers.documentstorage.DocumentOpener +import com.owncloud.android.providers.documentstorage.DocumentResolver +import com.owncloud.android.providers.documentstorage.FolderRefresher +import com.owncloud.android.ui.activity.SettingsActivity +import dagger.android.AndroidInjection +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import org.nextcloud.providers.cursors.FileCursor +import org.nextcloud.providers.cursors.RootCursor +import java.io.FileNotFoundException +import javax.inject.Inject + +class DocumentsStorageProvider : DocumentsProvider() { + + @Inject + lateinit var accountManager: UserAccountManager + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val resolver = DocumentResolver() + private val notifier by lazy { DocumentNotifier(nonNullContext) } + private val folderRefresher by lazy { FolderRefresher(nonNullContext) } + private val nameValidator by lazy { DocumentNameValidator(nonNullContext, accountManager) } + private val lister by lazy { DocumentLister(nonNullContext, scope, notifier, folderRefresher) } + private val opener by lazy { DocumentOpener(nonNullContext, scope) } + private val creator by lazy { + DocumentCreator(nonNullContext, accountManager, resolver, nameValidator, folderRefresher, notifier) + } + private val editor by lazy { DocumentEditor(nonNullContext, resolver, notifier, folderRefresher, nameValidator) } + + private val nonNullContext: Context + get() = checkNotNull(context) + + private val isAppLocked: Boolean + get() = AppPreferencesImpl.fromContext(MainApp.getAppContext()).lockPreference in + listOf(SettingsActivity.LOCK_PASSCODE, SettingsActivity.LOCK_DEVICE_CREDENTIALS) + + override fun onCreate(): Boolean { + AndroidInjection.inject(this) + initStoragePathIfMissing() + refreshStorageManagers() + return true + } + + private fun initStoragePathIfMissing() { if (MainApp.getStoragePath() != null) { - return; + return } - Context context = getNonNullContext(); - String defaultStoragePath = context.getFilesDir().getAbsolutePath(); - MainApp.setStoragePath(AppPreferencesImpl.fromContext(context).getStoragePath(defaultStoragePath)); + val defaultStoragePath = nonNullContext.filesDir.absolutePath + MainApp.setStoragePath(AppPreferencesImpl.fromContext(nonNullContext).getStoragePath(defaultStoragePath)) } - public static void notifyRootsChanged(Context context) { - String authority = context.getString(R.string.document_provider_authority); - Uri rootsUri = DocumentsContract.buildRootsUri(authority); - context.getContentResolver().notifyChange(rootsUri, null); + private fun refreshStorageManagers() { + resolver.refresh(accountManager.allUsers, nonNullContext.contentResolver) } - @Override - public Cursor queryRoots(String[] projection) { - final long token = Binder.clearCallingIdentity(); - try { - initiateStorageMap(); + override fun queryRoots(projection: Array?): Cursor = withClearedCallingIdentity { + refreshStorageManagers() - if (isAppLocked()) { - return new FileCursor(); - } - - final RootCursor result = new RootCursor(projection); - for (FileDataStorageManager manager : rootIdToStorageManager.values()) { - result.addRoot(new Document(manager, ROOT_PATH), getContext()); + if (isAppLocked) { + FileCursor(emptyArray()) + } else { + RootCursor(projection).apply { + resolver.storageManagers.forEach { manager -> + addRoot(Document.fromPath(manager, OCFile.ROOT_PATH), nonNullContext) + } } - return result; - } finally { - Binder.restoreCallingIdentity(token); } } - private boolean isAppLocked() { - String lockPreference = AppPreferencesImpl.fromContext(MainApp.getAppContext()).getLockPreference(); - return SettingsActivity.LOCK_PASSCODE.equals(lockPreference) || - SettingsActivity.LOCK_DEVICE_CREDENTIALS.equals(lockPreference); - } + override fun queryDocument(documentId: String, projection: Array?): Cursor { + Log_OC.d(TAG, "queryDocument(), id=$documentId") - @Override - public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { - Log_OC.d(TAG, "queryDocument(), id=" + documentId); - - final long token = Binder.clearCallingIdentity(); - try { - Document document = toDocument(documentId); - document.requireFile(); - - final FileCursor result = new FileCursor(projection); - result.addFile(document); - return result; - } finally { - Binder.restoreCallingIdentity(token); + return withClearedCallingIdentity { + val document = resolver.toDocument(documentId) + document.requireFile() + FileCursor(projection).apply { addFile(document) } } } @SuppressLint("LongLogTag") - @Override - public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) - throws FileNotFoundException { - Log_OC.d(TAG, "queryChildDocuments(), id=" + parentDocumentId); + override fun queryChildDocuments(parentDocumentId: String, projection: Array?, sortOrder: String?): Cursor { + Log_OC.d(TAG, "queryChildDocuments(), id=$parentDocumentId") - final long token = Binder.clearCallingIdentity(); - try { - Context context = getNonNullContext(); - Document parentFolder = toDocument(parentDocumentId); - OCFile parentFile = parentFolder.requireFile(); - final FileCursor resultCursor = new FileCursor(projection); - - if (!canListChildren(context, parentFolder, parentFile)) { - return resultCursor; - } - - addReadableChildren(resultCursor, parentFolder.getStorageManager(), parentFile); - boolean isLoading = startReloadIfExpired(parentFolder, parentFile, resultCursor); - - final Bundle extra = new Bundle(); - extra.putBoolean(DocumentsContract.EXTRA_LOADING, isLoading); - resultCursor.setExtras(extra); - resultCursor.setNotificationUri(context.getContentResolver(), toNotifyUri(parentFolder)); - return resultCursor; - } finally { - Binder.restoreCallingIdentity(token); + return withClearedCallingIdentity { + lister.listChildren(resolver.toDocument(parentDocumentId), projection) } } - private boolean canListChildren(Context context, Document folder, OCFile folderFile) { - if (!folderFile.canRead()) { - showToast(R.string.document_storage_provider_cannot_read); - return false; - } + override fun querySearchDocuments(rootId: String?, query: String, projection: Array?): Cursor { + Log_OC.d(TAG, "querySearchDocuments(), rootId=$rootId") - if (folderFile.isEncrypted() && !FileOperationsHelper.isEndToEndEncryptionSetup(context, folder.getUser())) { - showToast(R.string.e2e_not_yet_setup); - return false; - } - - return true; - } - - private void addReadableChildren(FileCursor cursor, FileDataStorageManager storageManager, OCFile folderFile) { - for (OCFile file : storageManager.getFolderContent(folderFile, false)) { - if (!file.canRead()) { - Log_OC.w(TAG, "Skipping file, doesn't have read permission. RemotePath: " + file.getRemotePath()); - continue; - } - - cursor.addFile(new Document(storageManager, file)); - } - } - - private boolean startReloadIfExpired(Document folder, OCFile folderFile, FileCursor cursor) { - if (folderFile.getLastSyncDateForData() + CACHE_EXPIRATION >= System.currentTimeMillis()) { - return false; - } - - final FutureTask task = new FutureTask<>(() -> reloadFolder(folder), null) { - @Override - protected void done() { - if (!isCancelled()) { - notifyChanged(folder); - } - } - }; - executor.execute(task); - cursor.setLoadingTask(task); - return true; - } - - private void reloadFolder(Document folder) { - Log_OC.d(TAG, "reloadFolder(), id=" + folder.getDocumentId()); - try { - new RefreshFolderOperation(folder.requireFile(), - System.currentTimeMillis(), - false, - true, - true, - folder.getStorageManager(), - folder.getUser(), - MainApp.getAppContext()) - .execute(folder.requireClient()); - } catch (FileNotFoundException e) { - Log_OC.e(TAG, "reloadFolder() failed, id=" + folder.getDocumentId(), e); + return withClearedCallingIdentity { + lister.search(resolver.storageManagerFor(rootId), query, projection) } } @SuppressLint("LongLogTag") - @Override - public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal cancellationSignal) - throws FileNotFoundException { - final int callerUid = Binder.getCallingUid(); - Log_OC.d(TAG, "openDocument(), id=" + documentId + ", mode=" + mode + ", callerUid=" + callerUid); - - if (!isFolderPathValid) { - Log_OC.d(TAG, "Folder path is not valid, operation is cancelled"); - return null; - } - - final long token = Binder.clearCallingIdentity(); - try { - Document document = toDocument(documentId); - Context context = getNonNullContext(); - - OCFile ocFile = document.requireFile(); - if (ocFile.isFolder()) { - Log_OC.w(TAG, "openDocument called on a folder, which is not openable: " + documentId); - return null; - } - - AccessMode accessMode = AccessMode.parse(mode); - prepareLocalFile(context, document, ocFile, accessMode); - OpenedDocumentFile openedFile = - createOpenedDocumentFile(context, ocFile, accessMode, callerUid == Process.SYSTEM_UID); - - if (accessMode.isReadOnly()) { - return openForRead(openedFile); - } - return openForWrite(context, document, ocFile, openedFile, accessMode); - } catch (FileNotFoundException | RuntimeException e) { - Log_OC.e(TAG, "openDocument() failed, id=" + documentId + ", mode=" + mode, e); - throw e; - } finally { - Binder.restoreCallingIdentity(token); - } - } - - private void prepareLocalFile(Context context, Document document, OCFile ocFile, AccessMode accessMode) - throws FileNotFoundException { - if (accessMode.isTruncate() && !ocFile.existsOnDevice()) { - assignDefaultStoragePath(document.getUser(), ocFile); - } - - boolean needsDownload = !accessMode.isTruncate() && - (!ocFile.existsOnDevice() || (!accessMode.isWriteOnly() && hasServerChange(context, document, ocFile))); - Log_OC.d(TAG, "openDocument(), remotePath=" + ocFile.getRemotePath() + ", storagePath=" + - ocFile.getStoragePath() + ", existsOnDevice=" + ocFile.existsOnDevice() + ", truncate=" + - accessMode.isTruncate() + ", needsDownload=" + needsDownload); - - if (!needsDownload) { - return; - } - - if (ocFile.getLocalModificationTimestamp() > ocFile.getLastSyncDateForData()) { - Log_OC.w(TAG, "Conflict found!"); - return; - } - - downloadOffBinderThread(context, document, ocFile); - } - - private void assignDefaultStoragePath(User user, OCFile ocFile) throws FileNotFoundException { - File localFile = new File(FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), ocFile)); - File parent = localFile.getParentFile(); - if (parent != null && !parent.exists() && !parent.mkdirs()) { - throw new FileNotFoundException("Local folder could not be created: " + parent.getAbsolutePath()); - } - ocFile.setStoragePath(localFile.getAbsolutePath()); - } - - private boolean hasServerChange(Context context, Document document, OCFile ocFile) throws FileNotFoundException { - RemoteOperationResult result = new CheckEtagRemoteOperation(ocFile.getRemotePath(), ocFile.getEtag()) - .execute(document.getUser(), context); - return switch (result.getCode()) { - case ETAG_CHANGED -> result.getData() != null; - case ETAG_UNCHANGED -> false; - default -> { - Log_OC.e(TAG, result.toString()); - throw new FileNotFoundException("Error synchronizing file: " + ocFile.getFileName()); - } - }; - } - - private void downloadOffBinderThread(Context context, Document document, OCFile ocFile) - throws FileNotFoundException { - FutureTask download = new FutureTask<>(() -> downloadFile(context, document, ocFile)); - executor.execute(download); - try { - if (!download.get()) { - throw new FileNotFoundException("Error downloading file: " + ocFile.getFileName()); - } - } catch (InterruptedException | ExecutionException e) { - throw getFileNotFoundExceptionWithCause("Error downloading file: " + ocFile.getFileName(), e); - } - } - - private boolean downloadFile(Context context, Document document, OCFile ocFile) throws FileNotFoundException { - var downloadFileOperation = new DownloadFileOperation(document.getUser(), ocFile, context); - final var result = downloadFileOperation.execute(document.requireClient()); - if (result.isSuccess()) { - saveDownloadedFile(document.getStorageManager(), downloadFileOperation, ocFile); - return true; - } - - if (ocFile.isDown()) { - new Handler(Looper.getMainLooper()).post(() -> showToast(R.string.file_not_synced)); - return true; - } - - Log_OC.e(TAG, result.toString()); - return false; - } - - private void saveDownloadedFile(FileDataStorageManager storageManager, DownloadFileOperation dfo, OCFile file) { - long syncDate = System.currentTimeMillis(); - file.setLastSyncDateForProperties(syncDate); - file.setLastSyncDateForData(syncDate); - file.setUpdateThumbnailNeeded(true); - file.setModificationTimestamp(dfo.getModificationTimestamp()); - file.setModificationTimestampAtLastSyncForData(dfo.getModificationTimestamp()); - file.setEtag(dfo.getEtag()); - file.setMimeType(dfo.getMimeType()); - String savePath = dfo.getSavePath(); - file.setStoragePath(savePath); - file.setFileLength(new File(savePath).length()); - file.setRemoteId(dfo.getFile().getRemoteId()); - storageManager.saveFile(file); - if (MimeTypeUtil.isMedia(dfo.getMimeType())) { - FileDataStorageManager.triggerMediaScan(file.getStoragePath(), file); - } - storageManager.saveConflict(file, null); - } + override fun openDocument( + documentId: String, + mode: String?, + cancellationSignal: CancellationSignal? + ): ParcelFileDescriptor? { + val callerUid = Binder.getCallingUid() + Log_OC.d(TAG, "openDocument(), id=$documentId, mode=$mode, callerUid=$callerUid") - private OpenedDocumentFile createOpenedDocumentFile(Context context, - OCFile ocFile, - AccessMode accessMode, - boolean isSystemServerCaller) throws FileNotFoundException { - String storagePath = ocFile.getStoragePath(); - if (storagePath == null || storagePath.isEmpty()) { - throw new FileNotFoundException("No local file for " + ocFile.getRemotePath()); + if (!editor.isFolderPathValid) { + Log_OC.d(TAG, "Folder path is not valid, operation is cancelled") + return null } - File localFile = new File(storagePath); - if (!isSystemServerCaller) { - return new OpenedDocumentFile(localFile, localFile); - } - - return new OpenedDocumentFile(localFile, - createSystemServerAccessibleCopy(context, localFile, accessMode.isTruncate())); - } - - private File createSystemServerAccessibleCopy(Context context, File file, boolean truncate) - throws FileNotFoundException { - try { - File copy = File.createTempFile("document", null, context.getCacheDir()); - if (!truncate && file.exists()) { - Files.copy(file.toPath(), copy.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - return copy; - } catch (IOException e) { - throw getFileNotFoundExceptionWithCause("Failed to create copy of " + file.getAbsolutePath(), e); - } - } - - private ParcelFileDescriptor openForRead(OpenedDocumentFile openedFile) throws FileNotFoundException { - ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(openedFile.descriptorFile(), MODE_READ_ONLY); - openedFile.deleteCopy(); - return descriptor; - } - - private ParcelFileDescriptor openForWrite(Context context, - Document document, - OCFile ocFile, - OpenedDocumentFile openedFile, - AccessMode accessMode) throws FileNotFoundException { - Handler handler = new Handler(context.getMainLooper()); - try { - return ParcelFileDescriptor.open(openedFile.descriptorFile(), accessMode.bits(), handler, error -> { - if (error != null) { - Log_OC.e(TAG, "File was closed with an error: " + ocFile.getFileName(), error); - openedFile.deleteCopy(); - return; - } - - executor.execute(() -> uploadWrittenDocument(document, ocFile, openedFile)); - }); - } catch (IOException e) { - throw getFileNotFoundExceptionWithCause("Failed to open document for writing " + - openedFile.descriptorFile().getAbsolutePath(), e); - } - } - - private void uploadWrittenDocument(Document document, OCFile ocFile, OpenedDocumentFile openedFile) { - File file = openedFile.localFile(); - try { - openedFile.moveCopyToLocalFile(); - } catch (IOException e) { - Log_OC.e(TAG, "Failed to move written copy to " + file.getAbsolutePath(), e); - openedFile.deleteCopy(); - return; - } - - Log_OC.d(TAG, "openDocument(), closed " + ocFile.getRemotePath() + ", length=" + file.length() + - ", starting upload"); - ocFile.setFileLength(file.length()); - ocFile.setModificationTimestamp(System.currentTimeMillis()); - document.getStorageManager().saveFile(ocFile); - - FileUploadHelper.Companion.instance().uploadUpdatedFile( - document.getUser(), - new OCFile[]{ ocFile }, - FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, - NameCollisionPolicy.OVERWRITE); - } - - @Override - public AssetFileDescriptor openDocumentThumbnail(String documentId, - Point sizeHint, - CancellationSignal signal) - throws FileNotFoundException { - Log_OC.d(TAG, "openDocumentThumbnail(), id=" + documentId); - - final long token = Binder.clearCallingIdentity(); - try { - Document document = toDocument(documentId); - OCFile file = document.requireFile(); - - if (!ThumbnailsCacheManager.containsBitmap(FileExtensionsKt.getSmallThumbnailKey(file))) { - ThumbnailsCacheManager.generateThumbnailFromOCFile(file, document.getUser(), getContext()); - } - - return new AssetFileDescriptor(DiskLruImageCacheFileProvider.getParcelFileDescriptorForOCFile(file), - 0, - file.getFileLength()); - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override - public String renameDocument(String documentId, String displayName) throws FileNotFoundException { - Log_OC.d(TAG, "renameDocument(), id=" + documentId); - - final long token = Binder.clearCallingIdentity(); - try { - String errorMessage = checkFileName(displayName); - if (errorMessage != null) { - showToast(errorMessage); - return null; - } - - Document document = toDocument(documentId); - OCFile file = document.requireFile(); - if (!file.canRename()) { - showToast(R.string.document_storage_provider_cannot_rename); - return null; - } - - final var result = new RenameFileOperation(file.getRemotePath(), displayName, document.getStorageManager()) - .execute(document.requireClient()); - - if (!result.isSuccess()) { - Log_OC.e(TAG, result.toString()); - throw new FileNotFoundException("Failed to rename document with documentId " + documentId + ": " + - result.getException()); - } - - document.getParent().ifPresent(this::notifyChanged); - return null; - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override - public String copyDocument(String sourceDocumentId, String targetParentDocumentId) throws FileNotFoundException { - Log_OC.d(TAG, "copyDocument(), id=" + sourceDocumentId); - - final long token = Binder.clearCallingIdentity(); - try { - Document targetFolder = toDocument(targetParentDocumentId); - OCFile targetFile = targetFolder.requireFile(); - if (!validateTargetFolder(targetFile)) { - return null; - } - - Document document = toDocument(sourceDocumentId); - OCFile sourceFile = document.requireFile(); - FileDataStorageManager storageManager = document.getStorageManager(); - - final var result = new CopyFileOperation(sourceFile.getRemotePath(), - targetFile.getRemotePath(), - storageManager) - .execute(document.requireClient()); - - if (!result.isSuccess()) { - Log_OC.e(TAG, result.toString()); - throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId - + " to " + targetParentDocumentId); - } - - final var updateParent = refreshFolder(targetFolder, - storageManager, - document.getUser(), - targetFolder.requireClient()); - - if (!updateParent.isSuccess()) { - Log_OC.e(TAG, updateParent.toString()); - throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId - + " to " + targetParentDocumentId); + return withClearedCallingIdentity { + try { + opener.open(resolver.toDocument(documentId), mode, callerUid == Process.SYSTEM_UID) + } catch (e: Exception) { + Log_OC.e(TAG, "openDocument() failed, id=$documentId, mode=$mode", e) + throw e } - - String newPath = targetFile.getRemotePath() + sourceFile.getFileName() + - (sourceFile.isFolder() ? PATH_SEPARATOR : ""); - String newDocumentId = getDocumentIdForPath(storageManager, newPath); - - notifyChanged(targetFolder); - return newDocumentId; - } finally { - Binder.restoreCallingIdentity(token); } } - @Override - public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) - throws FileNotFoundException { - Log_OC.d(TAG, "moveDocument(), id=" + sourceDocumentId); - - final long token = Binder.clearCallingIdentity(); - try { - Document targetFolder = toDocument(targetParentDocumentId); - OCFile targetFile = targetFolder.requireFile(); - if (!validateTargetFolder(targetFile)) { - return null; - } + override fun openDocumentThumbnail( + documentId: String, + sizeHint: Point?, + signal: CancellationSignal? + ): AssetFileDescriptor { + Log_OC.d(TAG, "openDocumentThumbnail(), id=$documentId") - Document document = toDocument(sourceDocumentId); - OCFile sourceFile = document.requireFile(); - if (!sourceFile.canMove()) { - showToast(R.string.document_storage_provider_cannot_move); - return null; - } + return withClearedCallingIdentity { + val document = resolver.toDocument(documentId) + val file = document.requireFile() - final var result = new MoveFileOperation(sourceFile.getRemotePath(), - targetFile.getRemotePath(), - document.getStorageManager()) - .execute(document.requireClient()); - - if (!result.isSuccess()) { - Log_OC.e(TAG, result.toString()); - throw new FileNotFoundException("Failed to move document with documentId " + sourceDocumentId - + " to " + targetParentDocumentId); + if (!ThumbnailsCacheManager.containsBitmap(file.getSmallThumbnailKey())) { + ThumbnailsCacheManager.generateThumbnailFromOCFile(file, document.user, context) } - notifyChanged(toDocument(sourceParentDocumentId)); - notifyChanged(targetFolder); - return sourceDocumentId; - } finally { - Binder.restoreCallingIdentity(token); + val descriptor = DiskLruImageCacheFileProvider.getParcelFileDescriptorForOCFile(file) + AssetFileDescriptor(descriptor, 0, file.fileLength) } } - private boolean validateTargetFolder(OCFile targetFolder) { - isFolderPathValid = checkFolderPath(targetFolder.getFileName()); - if (!isFolderPathValid) { - showToast(R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters); - } - return isFolderPathValid; - } - - @Override - public Cursor querySearchDocuments(String rootId, String query, String[] projection) { - Log_OC.d(TAG, "querySearchDocuments(), rootId=" + rootId); - - final long token = Binder.clearCallingIdentity(); - try { - FileCursor result = new FileCursor(projection); - - FileDataStorageManager storageManager = rootIdToStorageManager.get(rootId); - if (storageManager == null) { - return result; - } - - OCFile root = storageManager.getFileByPath(ROOT_PATH); - if (root == null) { - return result; - } + override fun renameDocument(documentId: String, displayName: String): String? { + Log_OC.d(TAG, "renameDocument(), id=$documentId") - addMatchingFiles(result, storageManager, root, query); - return result; - } finally { - Binder.restoreCallingIdentity(token); - } + withClearedCallingIdentity { editor.rename(documentId, displayName) } + return null } - private void addMatchingFiles(FileCursor cursor, FileDataStorageManager storageManager, OCFile folder, - String query) { - for (OCFile file : storageManager.getFolderContent(folder, false)) { - if (file.isFolder()) { - addMatchingFiles(cursor, storageManager, file, query); - } else if (file.getFileName().contains(query)) { - cursor.addFile(new Document(storageManager, file)); - } - } - } - - private OCCapability getCapabilities() { - return CapabilityUtils.getCapability(accountManager.getUser(), getNonNullContext()); - } - - private boolean checkFolderPath(String filename) { - return FileNameValidator.INSTANCE.checkFolderPath(filename, getCapabilities(), getNonNullContext()); - } + override fun copyDocument(sourceDocumentId: String, targetParentDocumentId: String): String? { + Log_OC.d(TAG, "copyDocument(), id=$sourceDocumentId") - private String checkFileName(String filename) { - return FileNameValidator.INSTANCE.checkFileName(filename, getCapabilities(), getNonNullContext()); + return withClearedCallingIdentity { editor.copy(sourceDocumentId, targetParentDocumentId) } } - @Override - public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException { - Log_OC.d(TAG, "createDocument(), id=" + documentId + ", name=" + displayName + ", mimeType=" + mimeType); + override fun moveDocument( + sourceDocumentId: String, + sourceParentDocumentId: String, + targetParentDocumentId: String + ): String? { + Log_OC.d(TAG, "moveDocument(), id=$sourceDocumentId") - final long token = Binder.clearCallingIdentity(); - try { - String errorMessage = checkFileName(displayName); - if (errorMessage != null) { - showToast(errorMessage); - return null; - } - - Document folderDocument = toDocument(documentId); - if (!folderDocument.requireFile().canCreateFileAndFolder()) { - showToast(R.string.document_storage_provider_cannot_create_file_and_folder); - return null; - } - - return DocumentsContract.Document.MIME_TYPE_DIR.equalsIgnoreCase(mimeType) - ? createFolder(folderDocument, displayName) - : createFile(folderDocument, displayName, mimeType); - } finally { - Binder.restoreCallingIdentity(token); + return withClearedCallingIdentity { + editor.move(sourceDocumentId, sourceParentDocumentId, targetParentDocumentId) } } - private String createFolder(Document targetFolder, String displayName) throws FileNotFoundException { - OCFile targetFile = targetFolder.requireFile(); - if (!targetFile.canCreateFileAndFolder()) { - showToast(R.string.document_storage_provider_cannot_create_folder_inside_folder); - return null; - } - - String newDirPath = targetFile.getRemotePath() + displayName + PATH_SEPARATOR; - FileDataStorageManager storageManager = targetFolder.getStorageManager(); - OwnCloudClient client = targetFolder.requireClient(); - - final var result = new CreateFolderOperation(newDirPath, - accountManager.getUser(), - getNonNullContext(), - storageManager) - .execute(client); + override fun createDocument(documentId: String, mimeType: String?, displayName: String): String? { + Log_OC.d(TAG, "createDocument(), id=$documentId, name=$displayName, mimeType=$mimeType") - if (!result.isSuccess()) { - Log_OC.e(TAG, result.toString()); - throw new FileNotFoundException("Failed to create document with name " + - displayName + " and documentId " + targetFolder.getDocumentId()); - } - - final var updateParent = refreshFolder(targetFolder, storageManager, targetFolder.getUser(), client); - if (!updateParent.isSuccess()) { - Log_OC.e(TAG, updateParent.toString()); - throw new FileNotFoundException("Failed to create document with documentId " - + targetFolder.getDocumentId()); - } - - String newFolderId = getDocumentIdForPath(storageManager, newDirPath); - notifyChanged(targetFolder); - return newFolderId; - } - - private String createFile(Document targetFolder, String displayName, String mimeType) throws FileNotFoundException { - OCFile targetFile = targetFolder.requireFile(); - if (!targetFile.canCreateFileAndFolder()) { - showToast(R.string.document_storage_provider_cannot_create_file_inside_folder); - return null; - } - - User user = targetFolder.getUser(); - File emptyFile = createEmptyTempFile(user, displayName); - String newFilePath = targetFile.getRemotePath() + displayName; - - try { - OwnCloudClient client = targetFolder.requireClient(); - uploadEmptyFile(client, emptyFile, newFilePath, mimeType); - - final var updateParent = refreshFolder(targetFolder, targetFolder.getStorageManager(), user, client); - if (!updateParent.isSuccess()) { - Log_OC.e(TAG, updateParent.toString()); - notifyRemoteOperationError(updateParent); - throw new FileNotFoundException("Failed to create document with documentId " - + targetFolder.getDocumentId()); - } - - String newFileId = getDocumentIdForPath(targetFolder.getStorageManager(), newFilePath); - notifyChanged(targetFolder); - return newFileId; - } finally { - if (emptyFile.exists() && !emptyFile.delete()) { - Log_OC.w(TAG, "Temp file could not be deleted: " + emptyFile.getAbsolutePath()); - } - } - } - - private File createEmptyTempFile(User user, String displayName) throws FileNotFoundException { - File tempDir = new File(FileStorageUtils.getTemporalPath(user.getAccountName())); - if (!tempDir.exists() && !tempDir.mkdirs()) { - throw new FileNotFoundException("Temp folder could not be created: " + tempDir.getAbsolutePath()); - } - - File emptyFile = new File(tempDir, displayName); - if (emptyFile.exists() && !emptyFile.delete()) { - throw new FileNotFoundException("Previous file could not be deleted"); - } - - try { - if (!emptyFile.createNewFile()) { - throw new FileNotFoundException("File could not be created"); - } - } catch (IOException e) { - throw getFileNotFoundExceptionWithCause("File could not be created", e); - } - - return emptyFile; - } - - private void uploadEmptyFile(OwnCloudClient client, File emptyFile, String newFilePath, String mimeType) - throws FileNotFoundException { - final long nowSeconds = System.currentTimeMillis() / AppConstants.MILLIS_PER_SECOND; - final var result = new UploadFileRemoteOperation(emptyFile.getAbsolutePath(), - newFilePath, - mimeType, - "", - nowSeconds, - FileUtil.getCreationTimestamp(emptyFile), - false) - .execute(client); - - if (!result.isSuccess()) { - Log_OC.e(TAG, result.toString()); - notifyRemoteOperationError(result); - throw new FileNotFoundException("Failed to upload document with path " + newFilePath); - } + return withClearedCallingIdentity { creator.create(documentId, mimeType, displayName) } } - private RemoteOperationResult refreshFolder(Document folder, - FileDataStorageManager storageManager, - User user, - OwnCloudClient client) throws FileNotFoundException { - return new RefreshFolderOperation(folder.requireFile(), - System.currentTimeMillis(), - false, - false, - true, - storageManager, - user, - getNonNullContext()) - .execute(client); + override fun removeDocument(documentId: String, parentDocumentId: String?) { + deleteDocument(documentId) } - private void notifyRemoteOperationError(RemoteOperationResult result) { - if (result.getCode() == RemoteOperationResult.ResultCode.MAINTENANCE_MODE) { - showToast(R.string.maintenance_mode); - } - } - - @Override - public void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException { - deleteDocument(documentId); - } - - @Override - public void deleteDocument(String documentId) throws FileNotFoundException { - Log_OC.d(TAG, "deleteDocument(), id=" + documentId); - - Context context = getNonNullContext(); + override fun deleteDocument(documentId: String) { + Log_OC.d(TAG, "deleteDocument(), id=$documentId") - Document document = toDocument(documentId); - OCFile file = document.requireFile(); + val document = resolver.toDocument(documentId) + val file = document.requireFile() if (!file.canDeleteOrLeaveShare()) { - showToast(R.string.document_storage_provider_cannot_delete); - return; + nonNullContext.showToast(R.string.document_storage_provider_cannot_delete) + return } - Optional parentFolder = document.getParent(); + val parentFolder = document.parent + recursiveRevokePermission(document) - recursiveRevokePermission(document); + withClearedCallingIdentity { + val result = RemoveFileOperation(file, false, document.user, true, nonNullContext, document.storageManager) + .execute(document.requireClient(nonNullContext)) - final long token = Binder.clearCallingIdentity(); - try { - final var result = new RemoveFileOperation(file, - false, - document.getUser(), - true, - context, - document.getStorageManager()) - .execute(document.requireClient()); - - if (!result.isSuccess()) { - throw new FileNotFoundException("Failed to delete document with documentId " + documentId); + if (!result.isSuccess) { + throw FileNotFoundException("Failed to delete document with documentId $documentId") } - parentFolder.ifPresent(this::notifyChanged); - } finally { - Binder.restoreCallingIdentity(token); + parentFolder?.let(notifier::notifyChanged) } } - private void recursiveRevokePermission(Document document) { - FileDataStorageManager storageManager = document.getStorageManager(); - OCFile file = document.getFile(); - if (file != null && file.isFolder()) { - for (OCFile child : storageManager.getFolderContent(file, false)) { - recursiveRevokePermission(new Document(storageManager, child)); + private fun recursiveRevokePermission(document: Document) { + val file = document.file + if (file?.isFolder == true) { + val storageManager = document.storageManager + storageManager.getFolderContent(file, false).forEach { child -> + recursiveRevokePermission(Document(storageManager, child)) } } - revokeDocumentPermission(document.getDocumentId()); + revokeDocumentPermission(document.documentId) } - @Override - public boolean isChildDocument(String parentDocumentId, String documentId) { - Log_OC.d(TAG, "isChildDocument(), parent=" + parentDocumentId + ", id=" + documentId); + override fun isChildDocument(parentDocumentId: String, documentId: String): Boolean { + Log_OC.d(TAG, "isChildDocument(), parent=$parentDocumentId, id=$documentId") - final long token = Binder.clearCallingIdentity(); - try { - Document parentDocument = toDocument(parentDocumentId); - OCFile parentFile = parentDocument.getFile(); - if (parentFile == null) { - throw new FileNotFoundException("No parent file with ID " + parentDocumentId); - } - - Document currentDocument = toDocument(documentId); - OCFile childFile = currentDocument.getFile(); - if (childFile == null) { - throw new FileNotFoundException("No child file with ID " + documentId); + return withClearedCallingIdentity { + try { + resolver.isChildDocument(parentDocumentId, documentId) + } catch (e: FileNotFoundException) { + Log_OC.e(TAG, "failed to check for child document", e) + false } - - return parentDocument.getUser().nameEquals(currentDocument.getUser()) && - childFile.getDecryptedRemotePath().startsWith(parentFile.getDecryptedRemotePath()); - } catch (FileNotFoundException e) { - Log_OC.e(TAG, "failed to check for child document", e); - return false; - } finally { - Binder.restoreCallingIdentity(token); - } - } - - private static FileNotFoundException getFileNotFoundExceptionWithCause(String msg, Exception cause) { - FileNotFoundException e = new FileNotFoundException(msg); - e.initCause(cause); - return e; - } - - @VisibleForTesting - public static String rootIdForUser(User user) { - return HashUtil.md5Hash(user.getAccountName()); - } - - private void initiateStorageMap() { - ContentResolver contentResolver = getNonNullContext().getContentResolver(); - rootIdToStorageManager = accountManager.getAllUsers().stream() - .collect(Collectors.toMap(DocumentsStorageProvider::rootIdForUser, - user -> new FileDataStorageManager(user, contentResolver), - (first, second) -> second)); - } - - private String getDocumentIdForPath(FileDataStorageManager storageManager, String remotePath) - throws FileNotFoundException { - OCFile file = storageManager.getFileByPath(remotePath); - if (file == null) { - throw new FileNotFoundException("No file in database for path " + remotePath); } - return new Document(storageManager, file).getDocumentId(); } - private void notifyChanged(Document document) { - getNonNullContext().getContentResolver().notifyChange(toNotifyUri(document), null, false); - } - - private Uri toNotifyUri(Document document) { - return DocumentsContract.buildDocumentUri( - getNonNullContext().getString(R.string.document_provider_authority), - document.getDocumentId()); - } - - private Document toDocument(String documentId) throws FileNotFoundException { - String[] separated = documentId.split(DOCUMENTID_SEPARATOR, DOCUMENTID_PARTS); - if (separated.length != DOCUMENTID_PARTS) { - throw new FileNotFoundException("Invalid documentID " + documentId + "!"); - } - - FileDataStorageManager storageManager = rootIdToStorageManager.get(separated[0]); - if (storageManager == null) { - throw new FileNotFoundException("No storage manager associated for " + documentId + "!"); - } - + private inline fun withClearedCallingIdentity(block: () -> T): T { + val token = Binder.clearCallingIdentity() try { - return new Document(storageManager, Long.parseLong(separated[1])); - } catch (NumberFormatException e) { - throw getFileNotFoundExceptionWithCause("Invalid documentID " + documentId + "!", e); - } - } - - @NonNull - private Context getNonNullContext() { - Context context = getContext(); - if (context == null) { - throw new IllegalStateException(); - } - return context; - } - - private void showToast(int messageId) { - ContextExtensionsKt.showToast(getNonNullContext(), messageId); - } - - private void showToast(String message) { - ContextExtensionsKt.showToast(getNonNullContext(), message); - } - - private record AccessMode(int bits) { - static AccessMode parse(String mode) { - return new AccessMode(ParcelFileDescriptor.parseMode(mode)); - } - - boolean isReadOnly() { - return bits == MODE_READ_ONLY; - } - - boolean isWriteOnly() { - return (bits & MODE_WRITE_ONLY) != 0; - } - - boolean isTruncate() { - return (bits & MODE_TRUNCATE) != 0; - } - } - - private record OpenedDocumentFile(File localFile, File descriptorFile) { - boolean isCopy() { - return !descriptorFile.equals(localFile); - } - - void moveCopyToLocalFile() throws IOException { - if (isCopy()) { - Files.move(descriptorFile.toPath(), localFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - } - - void deleteCopy() { - if (isCopy() && !descriptorFile.delete()) { - Log_OC.w(TAG, "Copy could not be deleted: " + descriptorFile.getAbsolutePath()); - } + return block() + } finally { + Binder.restoreCallingIdentity(token) } } - public class Document { - private final FileDataStorageManager storageManager; - private final long fileId; - - Document(FileDataStorageManager storageManager, long fileId) { - this.storageManager = storageManager; - this.fileId = fileId; - } - - Document(FileDataStorageManager storageManager, OCFile file) { - this(storageManager, file.getFileId()); - } - - Document(FileDataStorageManager storageManager, String filePath) { - this(storageManager, storageManager.getFileByPath(filePath).getFileId()); - } - - public String getDocumentId() { - return rootIdForUser(getUser()) + DOCUMENTID_SEPARATOR + fileId; - } - - FileDataStorageManager getStorageManager() { - return storageManager; - } - - public User getUser() { - return storageManager.getUser(); - } + companion object { + private val TAG = DocumentsStorageProvider::class.java.simpleName - public OCFile getFile() { - return storageManager.getFileById(fileId); - } + @VisibleForTesting + const val DOCUMENTID_SEPARATOR = "/" - OCFile requireFile() throws FileNotFoundException { - OCFile file = getFile(); - if (file == null) { - throw new FileNotFoundException("No file in database for documentId " + getDocumentId()); - } - return file; + @JvmStatic + fun notifyRootsChanged(context: Context) { + val authority = context.getString(R.string.document_provider_authority) + val rootsUri = DocumentsContract.buildRootsUri(authority) + context.contentResolver.notifyChange(rootsUri, null) } - OwnCloudClient requireClient() throws FileNotFoundException { - try { - return OwnCloudClientManagerFactory.getDefaultSingleton() - .getClientFor(getUser().toOwnCloudAccount(), getContext()); - } catch (OperationCanceledException | IOException | AuthenticatorException e) { - Log_OC.e(TAG, "Failed to set client", e); - throw getFileNotFoundExceptionWithCause("Failed to get client for " + getDocumentId(), e); - } - } - - Optional getParent() { - OCFile file = getFile(); - if (file == null || file.getParentId() <= 0) { - return Optional.empty(); - } - return Optional.of(new Document(storageManager, file.getParentId())); - } + @VisibleForTesting + fun rootIdForUser(user: User): String = HashUtil.md5Hash(user.accountName) } } diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/AccessMode.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/AccessMode.kt new file mode 100644 index 000000000000..1af2fdb9d55a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/AccessMode.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import android.os.ParcelFileDescriptor + +internal data class AccessMode(val bits: Int) { + + val isReadOnly: Boolean + get() = bits == ParcelFileDescriptor.MODE_READ_ONLY + + val isWriteOnly: Boolean + get() = (bits and ParcelFileDescriptor.MODE_WRITE_ONLY) != 0 + + val isTruncate: Boolean + get() = (bits and ParcelFileDescriptor.MODE_TRUNCATE) != 0 + + companion object { + fun parse(mode: String?): AccessMode = AccessMode(ParcelFileDescriptor.parseMode(mode)) + } +} diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/Document.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/Document.kt new file mode 100644 index 000000000000..dc69efc2f47f --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/Document.kt @@ -0,0 +1,65 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import android.accounts.AuthenticatorException +import android.accounts.OperationCanceledException +import android.content.Context +import com.nextcloud.client.account.User +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.providers.DocumentsStorageProvider +import java.io.FileNotFoundException +import java.io.IOException + +class Document(val storageManager: FileDataStorageManager, private val fileId: Long) { + + constructor(storageManager: FileDataStorageManager, file: OCFile) : this(storageManager, file.fileId) + + val documentId: String + get() = DocumentsStorageProvider.rootIdForUser(user) + DocumentsStorageProvider.DOCUMENTID_SEPARATOR + fileId + + val user: User + get() = storageManager.user + + val file: OCFile? + get() = storageManager.getFileById(fileId) + + val parent: Document? + get() = file?.parentId?.takeIf { it > 0 }?.let { Document(storageManager, it) } + + @Throws(FileNotFoundException::class) + fun requireFile(): OCFile = file ?: throw FileNotFoundException("No file in database for documentId $documentId") + + @Throws(FileNotFoundException::class) + fun requireClient(context: Context): OwnCloudClient = try { + OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.toOwnCloudAccount(), context) + } catch (e: Exception) { + when (e) { + is OperationCanceledException, is IOException, is AuthenticatorException -> { + Log_OC.e(TAG, "Failed to set client", e) + throw fileNotFoundException("Failed to get client for $documentId", e) + } + + else -> throw e + } + } + + companion object { + private val TAG = Document::class.java.simpleName + + @Throws(FileNotFoundException::class) + fun fromPath(storageManager: FileDataStorageManager, remotePath: String): Document { + val file = storageManager.getFileByEncryptedRemotePath(remotePath) + ?: throw FileNotFoundException("No file in database for path $remotePath") + return Document(storageManager, file) + } + } +} diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentCreator.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentCreator.kt new file mode 100644 index 000000000000..3ab504438717 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentCreator.kt @@ -0,0 +1,170 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import android.content.Context +import android.provider.DocumentsContract +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.utils.AppConstants +import com.nextcloud.utils.extensions.showToast +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation +import com.owncloud.android.operations.CreateFolderOperation +import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.FileUtil +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException + +internal class DocumentCreator( + private val context: Context, + private val accountManager: UserAccountManager, + private val resolver: DocumentResolver, + private val nameValidator: DocumentNameValidator, + private val folderRefresher: FolderRefresher, + private val notifier: DocumentNotifier +) { + + @Throws(FileNotFoundException::class) + fun create(documentId: String, mimeType: String?, displayName: String): String? { + val errorMessage = nameValidator.fileNameError(displayName) + if (errorMessage != null) { + context.showToast(errorMessage) + return null + } + + val folderDocument = resolver.toDocument(documentId) + if (!folderDocument.requireFile().canCreateFileAndFolder()) { + context.showToast(R.string.document_storage_provider_cannot_create_file_and_folder) + return null + } + + return if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType, ignoreCase = true)) { + createFolder(folderDocument, displayName) + } else { + createFile(folderDocument, displayName, mimeType) + } + } + + @Throws(FileNotFoundException::class) + private fun createFolder(targetFolder: Document, displayName: String): String? { + val targetFile = targetFolder.requireFile() + if (!targetFile.canCreateFileAndFolder()) { + context.showToast(R.string.document_storage_provider_cannot_create_folder_inside_folder) + return null + } + + val newDirPath = targetFile.remotePath + displayName + OCFile.PATH_SEPARATOR + val storageManager = targetFolder.storageManager + val client = targetFolder.requireClient(context) + + CreateFolderOperation(newDirPath, accountManager.user, context, storageManager) + .execute(client) + .requireSuccess(TAG) { + "Failed to create document with name $displayName and documentId ${targetFolder.documentId}" + } + + folderRefresher.refresh(targetFolder, storageManager, targetFolder.user, client) + .requireSuccess(TAG) { "Failed to create document with documentId ${targetFolder.documentId}" } + + val newFolderId = Document.fromPath(storageManager, newDirPath).documentId + notifier.notifyChanged(targetFolder) + return newFolderId + } + + @Throws(FileNotFoundException::class) + private fun createFile(targetFolder: Document, displayName: String, mimeType: String?): String? { + val targetFile = targetFolder.requireFile() + if (!targetFile.canCreateFileAndFolder()) { + context.showToast(R.string.document_storage_provider_cannot_create_file_inside_folder) + return null + } + + val user = targetFolder.user + val emptyFile = createEmptyTempFile(user, displayName) + val newFilePath = targetFile.remotePath + displayName + + try { + val client = targetFolder.requireClient(context) + uploadEmptyFile(client, emptyFile, newFilePath, mimeType) + + val updateParent = folderRefresher.refresh(targetFolder, targetFolder.storageManager, user, client) + showToastIfMaintenanceMode(updateParent) + updateParent.requireSuccess(TAG) { "Failed to create document with documentId ${targetFolder.documentId}" } + + val newFileId = Document.fromPath(targetFolder.storageManager, newFilePath).documentId + notifier.notifyChanged(targetFolder) + return newFileId + } finally { + if (emptyFile.exists() && !emptyFile.delete()) { + Log_OC.w(TAG, "Temp file could not be deleted: ${emptyFile.absolutePath}") + } + } + } + + @Throws(FileNotFoundException::class) + private fun createEmptyTempFile(user: User, displayName: String): File { + val tempDir = File(FileStorageUtils.getTemporalPath(user.accountName)) + if (!tempDir.exists() && !tempDir.mkdirs()) { + throw FileNotFoundException("Temp folder could not be created: ${tempDir.absolutePath}") + } + + val emptyFile = File(tempDir, displayName) + if (emptyFile.exists() && !emptyFile.delete()) { + throw FileNotFoundException("Previous file could not be deleted") + } + + createNewEmptyFile(emptyFile) + return emptyFile + } + + @Throws(FileNotFoundException::class) + private fun createNewEmptyFile(file: File) { + val isCreated = try { + file.createNewFile() + } catch (e: IOException) { + throw fileNotFoundException(FILE_CREATION_ERROR, e) + } + + if (!isCreated) { + throw FileNotFoundException(FILE_CREATION_ERROR) + } + } + + @Throws(FileNotFoundException::class) + private fun uploadEmptyFile(client: OwnCloudClient, emptyFile: File, newFilePath: String, mimeType: String?) { + val nowSeconds = System.currentTimeMillis() / AppConstants.MILLIS_PER_SECOND + val result = UploadFileRemoteOperation( + emptyFile.absolutePath, + newFilePath, + mimeType, + "", + nowSeconds, + FileUtil.getCreationTimestamp(emptyFile), + false + ).execute(client) + + showToastIfMaintenanceMode(result) + result.requireSuccess(TAG) { "Failed to upload document with path $newFilePath" } + } + + private fun showToastIfMaintenanceMode(result: RemoteOperationResult<*>) { + if (result.code == RemoteOperationResult.ResultCode.MAINTENANCE_MODE) { + context.showToast(R.string.maintenance_mode) + } + } + + companion object { + private val TAG = DocumentCreator::class.java.simpleName + private const val FILE_CREATION_ERROR = "File could not be created" + } +} diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentEditor.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentEditor.kt new file mode 100644 index 000000000000..ec3e554a1c9e --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentEditor.kt @@ -0,0 +1,118 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import android.content.Context +import com.nextcloud.utils.extensions.showToast +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.operations.CopyFileOperation +import com.owncloud.android.operations.MoveFileOperation +import com.owncloud.android.operations.RenameFileOperation +import java.io.FileNotFoundException + +internal class DocumentEditor( + private val context: Context, + private val resolver: DocumentResolver, + private val notifier: DocumentNotifier, + private val folderRefresher: FolderRefresher, + private val nameValidator: DocumentNameValidator +) { + + @Volatile + var isFolderPathValid = true + private set + + @Throws(FileNotFoundException::class) + fun rename(documentId: String, displayName: String) { + val errorMessage = nameValidator.fileNameError(displayName) + if (errorMessage != null) { + context.showToast(errorMessage) + return + } + + val document = resolver.toDocument(documentId) + val file = document.requireFile() + if (!file.canRename()) { + context.showToast(R.string.document_storage_provider_cannot_rename) + return + } + + val result = RenameFileOperation(file.remotePath, displayName, document.storageManager) + .execute(document.requireClient(context)) + result.requireSuccess(TAG) { "Failed to rename document with documentId $documentId: ${result.exception}" } + + document.parent?.let(notifier::notifyChanged) + } + + @Throws(FileNotFoundException::class) + fun copy(sourceDocumentId: String, targetParentDocumentId: String): String? { + val targetFolder = resolver.toDocument(targetParentDocumentId) + val targetFile = targetFolder.requireFile() + if (!isValidTargetFolder(targetFile)) { + return null + } + + val document = resolver.toDocument(sourceDocumentId) + val sourceFile = document.requireFile() + val storageManager = document.storageManager + val failureMessage = "Failed to copy document with documentId $sourceDocumentId to $targetParentDocumentId" + + CopyFileOperation(sourceFile.remotePath, targetFile.remotePath, storageManager) + .execute(document.requireClient(context)) + .requireSuccess(TAG) { failureMessage } + + folderRefresher + .refresh(targetFolder, storageManager, document.user, targetFolder.requireClient(context)) + .requireSuccess(TAG) { failureMessage } + + val folderSuffix = if (sourceFile.isFolder) OCFile.PATH_SEPARATOR else "" + val newPath = targetFile.remotePath + sourceFile.fileName + folderSuffix + val newDocumentId = Document.fromPath(storageManager, newPath).documentId + + notifier.notifyChanged(targetFolder) + return newDocumentId + } + + @Throws(FileNotFoundException::class) + fun move(sourceDocumentId: String, sourceParentDocumentId: String, targetParentDocumentId: String): String? { + val targetFolder = resolver.toDocument(targetParentDocumentId) + val targetFile = targetFolder.requireFile() + if (!isValidTargetFolder(targetFile)) { + return null + } + + val document = resolver.toDocument(sourceDocumentId) + val sourceFile = document.requireFile() + if (!sourceFile.canMove()) { + context.showToast(R.string.document_storage_provider_cannot_move) + return null + } + + MoveFileOperation(sourceFile.remotePath, targetFile.remotePath, document.storageManager) + .execute(document.requireClient(context)) + .requireSuccess(TAG) { + "Failed to move document with documentId $sourceDocumentId to $targetParentDocumentId" + } + + notifier.notifyChanged(resolver.toDocument(sourceParentDocumentId)) + notifier.notifyChanged(targetFolder) + return sourceDocumentId + } + + private fun isValidTargetFolder(targetFolder: OCFile): Boolean { + isFolderPathValid = nameValidator.isValidFolderPath(targetFolder.fileName) + if (!isFolderPathValid) { + context.showToast(R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters) + } + return isFolderPathValid + } + + companion object { + private val TAG = DocumentEditor::class.java.simpleName + } +} diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentLister.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentLister.kt new file mode 100644 index 000000000000..b640a2ba6b75 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentLister.kt @@ -0,0 +1,115 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import android.content.Context +import android.provider.DocumentsContract +import androidx.core.os.bundleOf +import com.nextcloud.utils.extensions.showToast +import com.owncloud.android.R +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.helpers.FileOperationsHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.nextcloud.providers.cursors.FileCursor +import java.io.FileNotFoundException +import java.util.concurrent.TimeUnit + +internal class DocumentLister( + private val context: Context, + private val scope: CoroutineScope, + private val notifier: DocumentNotifier, + private val folderRefresher: FolderRefresher +) { + + @Throws(FileNotFoundException::class) + fun listChildren(parentFolder: Document, projection: Array?): FileCursor { + val parentFile = parentFolder.requireFile() + val cursor = FileCursor(projection) + + if (!canListChildren(parentFolder, parentFile)) { + return cursor + } + + addReadableChildren(cursor, parentFolder.storageManager, parentFile) + val isLoading = startReloadIfExpired(parentFolder, parentFile, cursor) + + return cursor.apply { + extras = bundleOf(DocumentsContract.EXTRA_LOADING to isLoading) + setNotificationUri(context.contentResolver, notifier.notifyUri(parentFolder)) + } + } + + fun search(storageManager: FileDataStorageManager?, query: String, projection: Array?): FileCursor { + val cursor = FileCursor(projection) + val root = storageManager?.getFileByEncryptedRemotePath(OCFile.ROOT_PATH) ?: return cursor + addMatchingFiles(cursor, storageManager, root, query) + return cursor + } + + private fun canListChildren(folder: Document, folderFile: OCFile): Boolean { + if (!folderFile.canRead()) { + context.showToast(R.string.document_storage_provider_cannot_read) + return false + } + + if (folderFile.isEncrypted && !FileOperationsHelper.isEndToEndEncryptionSetup(context, folder.user)) { + context.showToast(R.string.e2e_not_yet_setup) + return false + } + + return true + } + + private fun addReadableChildren(cursor: FileCursor, storageManager: FileDataStorageManager, folderFile: OCFile) { + storageManager.getFolderContent(folderFile, false).forEach { file -> + if (!file.canRead()) { + Log_OC.w(TAG, "Skipping file, doesn't have read permission. RemotePath: ${file.remotePath}") + return@forEach + } + + cursor.addFile(Document(storageManager, file)) + } + } + + private fun startReloadIfExpired(folder: Document, folderFile: OCFile, cursor: FileCursor): Boolean { + if (folderFile.lastSyncDateForData + CACHE_EXPIRATION >= System.currentTimeMillis()) { + return false + } + + val reloadJob = scope.launch { + runCatching { folderRefresher.reload(folder) } + if (isActive) { + notifier.notifyChanged(folder) + } + } + cursor.setLoadingTask(reloadJob) + return true + } + + private fun addMatchingFiles( + cursor: FileCursor, + storageManager: FileDataStorageManager, + folder: OCFile, + query: String + ) { + storageManager.getFolderContent(folder, false).forEach { file -> + when { + file.isFolder -> addMatchingFiles(cursor, storageManager, file, query) + file.fileName.contains(query) -> cursor.addFile(Document(storageManager, file)) + } + } + } + + companion object { + private val TAG = DocumentLister::class.java.simpleName + private val CACHE_EXPIRATION = TimeUnit.MINUTES.toMillis(1) + } +} diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNameValidator.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNameValidator.kt new file mode 100644 index 000000000000..ba02ecef0b1a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNameValidator.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import android.content.Context +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.utils.fileNameValidator.FileNameValidator +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.utils.theme.CapabilityUtils + +internal class DocumentNameValidator(private val context: Context, private val accountManager: UserAccountManager) { + + private val capabilities: OCCapability + get() = CapabilityUtils.getCapability(accountManager.user, context) + + fun fileNameError(filename: String): String? = FileNameValidator.checkFileName(filename, capabilities, context) + + fun isValidFolderPath(folderPath: String): Boolean = + FileNameValidator.checkFolderPath(folderPath, capabilities, context) +} diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNotifier.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNotifier.kt new file mode 100644 index 000000000000..f05be38b4385 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNotifier.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import android.content.Context +import android.net.Uri +import android.provider.DocumentsContract +import com.owncloud.android.R + +internal class DocumentNotifier(private val context: Context) { + + fun notifyChanged(document: Document) { + context.contentResolver.notifyChange(notifyUri(document), null, NO_NOTIFY_FLAGS) + } + + fun notifyUri(document: Document): Uri = DocumentsContract.buildDocumentUri( + context.getString(R.string.document_provider_authority), + document.documentId + ) + + companion object { + private const val NO_NOTIFY_FLAGS = 0 + } +} diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentOpener.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentOpener.kt new file mode 100644 index 000000000000..527203637c41 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentOpener.kt @@ -0,0 +1,257 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import android.content.Context +import android.os.Handler +import android.os.ParcelFileDescriptor +import com.nextcloud.client.account.User +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.utils.extensions.showToast +import com.owncloud.android.R +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation +import com.owncloud.android.operations.DownloadFileOperation +import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.MimeTypeUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +internal class DocumentOpener(private val context: Context, private val scope: CoroutineScope) { + + @Throws(FileNotFoundException::class) + fun open(document: Document, mode: String?, isSystemServerCaller: Boolean): ParcelFileDescriptor? { + val ocFile = document.requireFile() + if (ocFile.isFolder) { + Log_OC.w(TAG, "openDocument called on a folder, which is not openable: ${document.documentId}") + return null + } + + val accessMode = AccessMode.parse(mode) + prepareLocalFile(document, ocFile, accessMode) + val openedFile = createOpenedDocumentFile(ocFile, accessMode, isSystemServerCaller) + + if (accessMode.isReadOnly) { + return openForRead(openedFile) + } + + return openForWrite(document, ocFile, openedFile, accessMode) + } + + @Throws(FileNotFoundException::class) + private fun prepareLocalFile(document: Document, ocFile: OCFile, accessMode: AccessMode) { + if (accessMode.isTruncate && !ocFile.existsOnDevice()) { + assignDefaultStoragePath(document.user, ocFile) + } + + val needsDownload = !accessMode.isTruncate && + (!ocFile.existsOnDevice() || (!accessMode.isWriteOnly && hasServerChange(document, ocFile))) + Log_OC.d( + TAG, + "openDocument(), remotePath=${ocFile.remotePath}, storagePath=${ocFile.storagePath}, " + + "existsOnDevice=${ocFile.existsOnDevice()}, truncate=${accessMode.isTruncate}, " + + "needsDownload=$needsDownload" + ) + + if (!needsDownload) { + return + } + + if (ocFile.localModificationTimestamp > ocFile.lastSyncDateForData) { + Log_OC.w(TAG, "Conflict found!") + return + } + + downloadOffBinderThread(document, ocFile) + } + + @Throws(FileNotFoundException::class) + private fun assignDefaultStoragePath(user: User, ocFile: OCFile) { + val localFile = File(FileStorageUtils.getDefaultSavePathFor(user.accountName, ocFile)) + val parent = localFile.parentFile + if (parent != null && !parent.exists() && !parent.mkdirs()) { + throw FileNotFoundException("Local folder could not be created: ${parent.absolutePath}") + } + ocFile.storagePath = localFile.absolutePath + } + + @Throws(FileNotFoundException::class) + private fun hasServerChange(document: Document, ocFile: OCFile): Boolean { + val result = CheckEtagRemoteOperation(ocFile.remotePath, ocFile.etag).execute(document.user, context) + return when (result.code) { + ResultCode.ETAG_CHANGED -> result.data != null + + ResultCode.ETAG_UNCHANGED -> false + + else -> { + Log_OC.e(TAG, result.toString()) + throw FileNotFoundException("Error synchronizing file: ${ocFile.fileName}") + } + } + } + + @Throws(FileNotFoundException::class) + private fun downloadOffBinderThread(document: Document, ocFile: OCFile) { + val errorMessage = "Error downloading file: ${ocFile.fileName}" + val isDownloaded = try { + runBlocking(Dispatchers.IO) { downloadFile(document, ocFile) } + } catch (e: Exception) { + throw fileNotFoundException(errorMessage, e) + } + + if (!isDownloaded) { + throw FileNotFoundException(errorMessage) + } + } + + @Throws(FileNotFoundException::class) + private fun downloadFile(document: Document, ocFile: OCFile): Boolean { + val operation = DownloadFileOperation(document.user, ocFile, context) + val result = operation.execute(document.requireClient(context)) + if (result.isSuccess) { + saveDownloadedFile(document.storageManager, operation, ocFile) + return true + } + + if (ocFile.isDown) { + context.showToast(R.string.file_not_synced) + return true + } + + Log_OC.e(TAG, result.toString()) + return false + } + + private fun saveDownloadedFile( + storageManager: FileDataStorageManager, + operation: DownloadFileOperation, + file: OCFile + ) { + val syncDate = System.currentTimeMillis() + val savePath = operation.savePath + file.apply { + lastSyncDateForProperties = syncDate + lastSyncDateForData = syncDate + isUpdateThumbnailNeeded = true + modificationTimestamp = operation.getModificationTimestamp() + modificationTimestampAtLastSyncForData = operation.getModificationTimestamp() + etag = operation.etag + mimeType = operation.mimeType + storagePath = savePath + fileLength = File(savePath).length() + remoteId = operation.file.remoteId + } + storageManager.saveFile(file) + if (MimeTypeUtil.isMedia(operation.mimeType)) { + FileDataStorageManager.triggerMediaScan(file.storagePath, file) + } + storageManager.saveConflict(file, null) + } + + @Throws(FileNotFoundException::class) + private fun createOpenedDocumentFile( + ocFile: OCFile, + accessMode: AccessMode, + isSystemServerCaller: Boolean + ): OpenedDocumentFile { + val storagePath = ocFile.storagePath + if (storagePath.isNullOrEmpty()) { + throw FileNotFoundException("No local file for ${ocFile.remotePath}") + } + + val localFile = File(storagePath) + val descriptorFile = if (isSystemServerCaller) { + createSystemServerAccessibleCopy(localFile, accessMode.isTruncate) + } else { + localFile + } + return OpenedDocumentFile(localFile, descriptorFile) + } + + @Throws(FileNotFoundException::class) + private fun createSystemServerAccessibleCopy(file: File, truncate: Boolean): File = try { + File.createTempFile(COPY_PREFIX, null, context.cacheDir).also { copy -> + if (!truncate && file.exists()) { + Files.copy(file.toPath(), copy.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + } + } catch (e: IOException) { + throw fileNotFoundException("Failed to create copy of ${file.absolutePath}", e) + } + + @Throws(FileNotFoundException::class) + private fun openForRead(openedFile: OpenedDocumentFile): ParcelFileDescriptor = + ParcelFileDescriptor.open(openedFile.descriptorFile, ParcelFileDescriptor.MODE_READ_ONLY) + .also { openedFile.deleteCopy() } + + @Throws(FileNotFoundException::class) + private fun openForWrite( + document: Document, + ocFile: OCFile, + openedFile: OpenedDocumentFile, + accessMode: AccessMode + ): ParcelFileDescriptor { + val onClose = ParcelFileDescriptor.OnCloseListener { error -> + if (error != null) { + Log_OC.e(TAG, "File was closed with an error: ${ocFile.fileName}", error) + openedFile.deleteCopy() + return@OnCloseListener + } + + scope.launch { uploadWrittenDocument(document, ocFile, openedFile) } + } + + return try { + ParcelFileDescriptor.open(openedFile.descriptorFile, accessMode.bits, Handler(context.mainLooper), onClose) + } catch (e: IOException) { + throw fileNotFoundException( + "Failed to open document for writing ${openedFile.descriptorFile.absolutePath}", + e + ) + } + } + + private fun uploadWrittenDocument(document: Document, ocFile: OCFile, openedFile: OpenedDocumentFile) { + val file = openedFile.localFile + try { + openedFile.moveCopyToLocalFile() + } catch (e: IOException) { + Log_OC.e(TAG, "Failed to move written copy to ${file.absolutePath}", e) + openedFile.deleteCopy() + return + } + + Log_OC.d(TAG, "openDocument(), closed ${ocFile.remotePath}, length=${file.length()}, starting upload") + ocFile.fileLength = file.length() + ocFile.modificationTimestamp = System.currentTimeMillis() + document.storageManager.saveFile(ocFile) + + FileUploadHelper.instance().uploadUpdatedFile( + document.user, + arrayOf(ocFile), + FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, + NameCollisionPolicy.OVERWRITE + ) + } + + companion object { + private val TAG = DocumentOpener::class.java.simpleName + private const val COPY_PREFIX = "document" + } +} diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentResolver.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentResolver.kt new file mode 100644 index 000000000000..9bd43ebbc0b7 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentResolver.kt @@ -0,0 +1,61 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import android.content.ContentResolver +import com.nextcloud.client.account.User +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.providers.DocumentsStorageProvider +import java.io.FileNotFoundException + +internal class DocumentResolver { + + @Volatile + private var rootIdToStorageManager: Map = emptyMap() + + val storageManagers: Collection + get() = rootIdToStorageManager.values + + fun refresh(users: List, contentResolver: ContentResolver) { + rootIdToStorageManager = users.associateByTo(HashMap(), DocumentsStorageProvider::rootIdForUser) { + FileDataStorageManager(it, contentResolver) + } + } + + fun storageManagerFor(rootId: String?): FileDataStorageManager? = rootIdToStorageManager[rootId] + + @Throws(FileNotFoundException::class) + fun toDocument(documentId: String): Document { + val (rootId, fileId) = splitDocumentId(documentId) + val storageManager = rootIdToStorageManager[rootId] + ?: throw FileNotFoundException("No storage manager associated for $documentId!") + val parsedFileId = fileId.toLongOrNull() ?: throw FileNotFoundException("Invalid documentID $documentId!") + return Document(storageManager, parsedFileId) + } + + @Throws(FileNotFoundException::class) + fun isChildDocument(parentDocumentId: String, documentId: String): Boolean { + val parentDocument = toDocument(parentDocumentId) + val parentFile = parentDocument.file ?: throw FileNotFoundException("No parent file with ID $parentDocumentId") + + val currentDocument = toDocument(documentId) + val childFile = currentDocument.file ?: throw FileNotFoundException("No child file with ID $documentId") + + return parentDocument.user.nameEquals(currentDocument.user) && + childFile.decryptedRemotePath.startsWith(parentFile.decryptedRemotePath) + } + + @Throws(FileNotFoundException::class) + private fun splitDocumentId(documentId: String): List = + documentId.split(DocumentsStorageProvider.DOCUMENTID_SEPARATOR, limit = DOCUMENTID_PARTS) + .takeIf { it.size == DOCUMENTID_PARTS } + ?: throw FileNotFoundException("Invalid documentID $documentId!") + + companion object { + private const val DOCUMENTID_PARTS = 2 + } +} diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentStorageFailures.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentStorageFailures.kt new file mode 100644 index 000000000000..fe9536e2b028 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentStorageFailures.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import java.io.FileNotFoundException + +internal fun fileNotFoundException(message: String, cause: Throwable): FileNotFoundException = + FileNotFoundException(message).apply { initCause(cause) } + +@Throws(FileNotFoundException::class) +internal inline fun RemoteOperationResult<*>.requireSuccess(tag: String, failureMessage: () -> String) { + if (isSuccess) { + return + } + + Log_OC.e(tag, toString()) + throw FileNotFoundException(failureMessage()) +} diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/FolderRefresher.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/FolderRefresher.kt new file mode 100644 index 000000000000..a14b36ece17a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/FolderRefresher.kt @@ -0,0 +1,59 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import android.content.Context +import com.nextcloud.client.account.User +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.RefreshFolderOperation +import java.io.FileNotFoundException + +internal class FolderRefresher(private val context: Context) { + + @Throws(FileNotFoundException::class) + fun refresh( + folder: Document, + storageManager: FileDataStorageManager, + user: User, + client: OwnCloudClient + ): RemoteOperationResult<*> = RefreshFolderOperation( + folder.requireFile(), + System.currentTimeMillis(), + false, + false, + true, + storageManager, + user, + context + ).execute(client) + + fun reload(folder: Document) { + Log_OC.d(TAG, "reloadFolder(), id=${folder.documentId}") + try { + RefreshFolderOperation( + folder.requireFile(), + System.currentTimeMillis(), + false, + true, + true, + folder.storageManager, + folder.user, + MainApp.getAppContext() + ).execute(folder.requireClient(context)) + } catch (e: FileNotFoundException) { + Log_OC.e(TAG, "reloadFolder() failed, id=${folder.documentId}", e) + } + } + + companion object { + private val TAG = FolderRefresher::class.java.simpleName + } +} diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/OpenedDocumentFile.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/OpenedDocumentFile.kt new file mode 100644 index 000000000000..1d608d47a9c0 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/OpenedDocumentFile.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.providers.documentstorage + +import com.owncloud.android.lib.common.utils.Log_OC +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +internal data class OpenedDocumentFile(val localFile: File, val descriptorFile: File) { + + private val isCopy: Boolean + get() = descriptorFile != localFile + + @Throws(IOException::class) + fun moveCopyToLocalFile() { + if (!isCopy) { + return + } + + Files.move(descriptorFile.toPath(), localFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } + + fun deleteCopy() { + if (isCopy && !descriptorFile.delete()) { + Log_OC.w(TAG, "Copy could not be deleted: ${descriptorFile.absolutePath}") + } + } + + companion object { + private val TAG = OpenedDocumentFile::class.java.simpleName + } +} diff --git a/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.java b/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.java index 203843381a6d..00941f95b3ab 100644 --- a/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.java +++ b/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.java @@ -14,10 +14,9 @@ import android.provider.DocumentsContract.Document; import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.providers.DocumentsStorageProvider; import com.owncloud.android.utils.MimeTypeUtil; -import java.util.concurrent.Future; +import kotlinx.coroutines.Job; public class FileCursor extends MatrixCursor { @@ -28,13 +27,13 @@ public class FileCursor extends MatrixCursor { }; private Bundle extra; - private Future loadingTask; + private Job loadingTask; - public FileCursor(String... projection) { + public FileCursor(String[] projection) { super(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); } - public void setLoadingTask(Future task) { + public void setLoadingTask(Job task) { this.loadingTask = task; } @@ -51,12 +50,12 @@ public Bundle getExtras() { @Override public void close() { super.close(); - if (loadingTask != null && !loadingTask.isDone()) { - loadingTask.cancel(false); + if (loadingTask != null && !loadingTask.isCompleted()) { + loadingTask.cancel(null); } } - public void addFile(DocumentsStorageProvider.Document document) { + public void addFile(com.owncloud.android.providers.documentstorage.Document document) { if (document == null) { return; } diff --git a/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.java b/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.java index 44648f5b3b17..f8fede69b131 100644 --- a/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.java +++ b/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.java @@ -14,7 +14,7 @@ import com.nextcloud.client.account.User; import com.owncloud.android.R; -import com.owncloud.android.providers.DocumentsStorageProvider; +import com.owncloud.android.providers.documentstorage.Document; public class RootCursor extends MatrixCursor { @@ -29,11 +29,11 @@ public class RootCursor extends MatrixCursor { Root.COLUMN_FLAGS }; - public RootCursor(String... projection) { + public RootCursor(String[] projection) { super(projection != null ? projection : DEFAULT_ROOT_PROJECTION); } - public void addRoot(DocumentsStorageProvider.Document document, Context context) { + public void addRoot(Document document, Context context) { User user = document.getUser(); int rootFlags = Root.FLAG_SUPPORTS_CREATE From 528bd1f1913e69af506c7d5b85170391c88e70df Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 29 Oct 2026 18:25:46 +0300 Subject: [PATCH 3/8] restructure package Signed-off-by: alperozturk96 --- .../DocumentsProviderUtils.kt | 4 ++-- .../DocumentsStorageProviderIT.kt | 22 +++++++++--------- app/src/main/AndroidManifest.xml | 2 +- .../nextcloud/client/di/ComponentsModule.kt | 2 +- .../client/jobs/AccountRemovalWork.kt | 2 +- .../java/com/owncloud/android/MainApp.java | 3 ++- .../authentication/AuthenticatorActivity.kt | 2 +- .../DocumentsStorageProvider.kt | 23 ++++++++++--------- .../documentstorage/{ => model}/Document.kt | 5 ++-- .../{ => model}/DocumentResolver.kt | 4 ++-- .../{ => modify}/DocumentCreator.kt | 10 ++++++-- .../{ => modify}/DocumentEditor.kt | 7 +++++- .../{ => modify}/DocumentNameValidator.kt | 2 +- .../documentstorage/{ => open}/AccessMode.kt | 2 +- .../{ => open}/DocumentOpener.kt | 4 +++- .../{ => open}/OpenedDocumentFile.kt | 2 +- .../{ => query}/DocumentLister.kt | 5 +++- .../{ => sync}/DocumentNotifier.kt | 3 ++- .../{ => sync}/FolderRefresher.kt | 3 ++- .../{ => util}/DocumentStorageFailures.kt | 2 +- .../android/ui/activity/SettingsActivity.java | 2 +- .../providers/cursors/FileCursor.java | 2 +- .../providers/cursors/RootCursor.java | 2 +- 23 files changed, 68 insertions(+), 47 deletions(-) rename app/src/androidTest/java/com/owncloud/android/providers/{ => documentstorage}/DocumentsProviderUtils.kt (98%) rename app/src/androidTest/java/com/owncloud/android/providers/{ => documentstorage}/DocumentsStorageProviderIT.kt (91%) rename app/src/main/java/com/owncloud/android/providers/{ => documentstorage}/DocumentsStorageProvider.kt (92%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => model}/Document.kt (91%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => model}/DocumentResolver.kt (94%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => modify}/DocumentCreator.kt (92%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => modify}/DocumentEditor.kt (91%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => modify}/DocumentNameValidator.kt (93%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => open}/AccessMode.kt (91%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => open}/DocumentOpener.kt (97%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => open}/OpenedDocumentFile.kt (94%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => query}/DocumentLister.kt (93%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => sync}/DocumentNotifier.kt (85%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => sync}/FolderRefresher.kt (93%) rename app/src/main/java/com/owncloud/android/providers/documentstorage/{ => util}/DocumentStorageFailures.kt (92%) diff --git a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsProviderUtils.kt b/app/src/androidTest/java/com/owncloud/android/providers/documentstorage/DocumentsProviderUtils.kt similarity index 98% rename from app/src/androidTest/java/com/owncloud/android/providers/DocumentsProviderUtils.kt rename to app/src/androidTest/java/com/owncloud/android/providers/documentstorage/DocumentsProviderUtils.kt index 5f01fa10d808..07c337223c89 100644 --- a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsProviderUtils.kt +++ b/app/src/androidTest/java/com/owncloud/android/providers/documentstorage/DocumentsProviderUtils.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2020 Torsten Grote * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.providers +package com.owncloud.android.providers.documentstorage import android.content.Context import android.database.ContentObserver @@ -25,7 +25,7 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation -import com.owncloud.android.providers.DocumentsStorageProvider.Companion.DOCUMENTID_SEPARATOR +import com.owncloud.android.providers.documentstorage.DocumentsStorageProvider.Companion.DOCUMENTID_SEPARATOR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.suspendCancellableCoroutine diff --git a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt b/app/src/androidTest/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProviderIT.kt similarity index 91% rename from app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt rename to app/src/androidTest/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProviderIT.kt index 9105c1d59b73..dd0567a94477 100644 --- a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProviderIT.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2020 Torsten Grote * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package com.owncloud.android.providers +package com.owncloud.android.providers.documentstorage import android.provider.DocumentsContract import androidx.documentfile.provider.DocumentFile @@ -20,16 +20,16 @@ import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus import com.owncloud.android.db.OCUpload import com.owncloud.android.db.UploadResult import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.providers.DocumentsProviderUtils.assertExistsOnServer -import com.owncloud.android.providers.DocumentsProviderUtils.assertListFilesEquals -import com.owncloud.android.providers.DocumentsProviderUtils.assertReadEquals -import com.owncloud.android.providers.DocumentsProviderUtils.assertRecentlyModified -import com.owncloud.android.providers.DocumentsProviderUtils.assertRegularFile -import com.owncloud.android.providers.DocumentsProviderUtils.assertRegularFolder -import com.owncloud.android.providers.DocumentsProviderUtils.findFileBlocking -import com.owncloud.android.providers.DocumentsProviderUtils.getOCFile -import com.owncloud.android.providers.DocumentsProviderUtils.listFilesBlocking -import com.owncloud.android.providers.DocumentsStorageProvider.Companion.DOCUMENTID_SEPARATOR +import com.owncloud.android.providers.documentstorage.DocumentsProviderUtils.assertExistsOnServer +import com.owncloud.android.providers.documentstorage.DocumentsProviderUtils.assertListFilesEquals +import com.owncloud.android.providers.documentstorage.DocumentsProviderUtils.assertReadEquals +import com.owncloud.android.providers.documentstorage.DocumentsProviderUtils.assertRecentlyModified +import com.owncloud.android.providers.documentstorage.DocumentsProviderUtils.assertRegularFile +import com.owncloud.android.providers.documentstorage.DocumentsProviderUtils.assertRegularFolder +import com.owncloud.android.providers.documentstorage.DocumentsProviderUtils.findFileBlocking +import com.owncloud.android.providers.documentstorage.DocumentsProviderUtils.getOCFile +import com.owncloud.android.providers.documentstorage.DocumentsProviderUtils.listFilesBlocking +import com.owncloud.android.providers.documentstorage.DocumentsStorageProvider.Companion.DOCUMENTID_SEPARATOR import kotlinx.coroutines.runBlocking import org.apache.commons.httpclient.HttpStatus import org.apache.commons.httpclient.methods.ByteArrayRequestEntity diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f20c2687a55f..1f1c5a60abbe 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -481,7 +481,7 @@ android:exported="false" android:label="@string/share_search" /> * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers - +package com.owncloud.android.providers.documentstorage + +import com.owncloud.android.providers.DiskLruImageCacheFileProvider +import com.owncloud.android.providers.documentstorage.model.Document +import com.owncloud.android.providers.documentstorage.model.DocumentResolver +import com.owncloud.android.providers.documentstorage.modify.DocumentCreator +import com.owncloud.android.providers.documentstorage.modify.DocumentEditor +import com.owncloud.android.providers.documentstorage.modify.DocumentNameValidator +import com.owncloud.android.providers.documentstorage.open.DocumentOpener +import com.owncloud.android.providers.documentstorage.query.DocumentLister +import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier +import com.owncloud.android.providers.documentstorage.sync.FolderRefresher import android.annotation.SuppressLint import android.content.Context import android.content.res.AssetFileDescriptor @@ -30,15 +40,6 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.RemoveFileOperation -import com.owncloud.android.providers.documentstorage.Document -import com.owncloud.android.providers.documentstorage.DocumentCreator -import com.owncloud.android.providers.documentstorage.DocumentEditor -import com.owncloud.android.providers.documentstorage.DocumentLister -import com.owncloud.android.providers.documentstorage.DocumentNameValidator -import com.owncloud.android.providers.documentstorage.DocumentNotifier -import com.owncloud.android.providers.documentstorage.DocumentOpener -import com.owncloud.android.providers.documentstorage.DocumentResolver -import com.owncloud.android.providers.documentstorage.FolderRefresher import com.owncloud.android.ui.activity.SettingsActivity import dagger.android.AndroidInjection import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/Document.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/model/Document.kt similarity index 91% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/Document.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/model/Document.kt index dc69efc2f47f..43b96fae1b15 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/Document.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/model/Document.kt @@ -4,8 +4,10 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage +package com.owncloud.android.providers.documentstorage.model +import com.owncloud.android.providers.documentstorage.DocumentsStorageProvider +import com.owncloud.android.providers.documentstorage.util.fileNotFoundException import android.accounts.AuthenticatorException import android.accounts.OperationCanceledException import android.content.Context @@ -15,7 +17,6 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.providers.DocumentsStorageProvider import java.io.FileNotFoundException import java.io.IOException diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentResolver.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/model/DocumentResolver.kt similarity index 94% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentResolver.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/model/DocumentResolver.kt index 9bd43ebbc0b7..2ebf2b24dc58 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentResolver.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/model/DocumentResolver.kt @@ -4,12 +4,12 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage +package com.owncloud.android.providers.documentstorage.model +import com.owncloud.android.providers.documentstorage.DocumentsStorageProvider import android.content.ContentResolver import com.nextcloud.client.account.User import com.owncloud.android.datamodel.FileDataStorageManager -import com.owncloud.android.providers.DocumentsStorageProvider import java.io.FileNotFoundException internal class DocumentResolver { diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentCreator.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentCreator.kt similarity index 92% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentCreator.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentCreator.kt index 3ab504438717..ee9922151e85 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentCreator.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentCreator.kt @@ -4,8 +4,14 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage - +package com.owncloud.android.providers.documentstorage.modify + +import com.owncloud.android.providers.documentstorage.model.Document +import com.owncloud.android.providers.documentstorage.model.DocumentResolver +import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier +import com.owncloud.android.providers.documentstorage.sync.FolderRefresher +import com.owncloud.android.providers.documentstorage.util.fileNotFoundException +import com.owncloud.android.providers.documentstorage.util.requireSuccess import android.content.Context import android.provider.DocumentsContract import com.nextcloud.client.account.User diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentEditor.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentEditor.kt similarity index 91% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentEditor.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentEditor.kt index ec3e554a1c9e..9dcd35fd72e5 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentEditor.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentEditor.kt @@ -4,8 +4,13 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage +package com.owncloud.android.providers.documentstorage.modify +import com.owncloud.android.providers.documentstorage.model.Document +import com.owncloud.android.providers.documentstorage.model.DocumentResolver +import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier +import com.owncloud.android.providers.documentstorage.sync.FolderRefresher +import com.owncloud.android.providers.documentstorage.util.requireSuccess import android.content.Context import com.nextcloud.utils.extensions.showToast import com.owncloud.android.R diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNameValidator.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentNameValidator.kt similarity index 93% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNameValidator.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentNameValidator.kt index ba02ecef0b1a..6df49a2b8b87 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNameValidator.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentNameValidator.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage +package com.owncloud.android.providers.documentstorage.modify import android.content.Context import com.nextcloud.client.account.UserAccountManager diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/AccessMode.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/open/AccessMode.kt similarity index 91% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/AccessMode.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/open/AccessMode.kt index 1af2fdb9d55a..b0fd2368cfbd 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/AccessMode.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/open/AccessMode.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage +package com.owncloud.android.providers.documentstorage.open import android.os.ParcelFileDescriptor diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentOpener.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/open/DocumentOpener.kt similarity index 97% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentOpener.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/open/DocumentOpener.kt index 527203637c41..ef3d0214adce 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentOpener.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/open/DocumentOpener.kt @@ -4,8 +4,10 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage +package com.owncloud.android.providers.documentstorage.open +import com.owncloud.android.providers.documentstorage.model.Document +import com.owncloud.android.providers.documentstorage.util.fileNotFoundException import android.content.Context import android.os.Handler import android.os.ParcelFileDescriptor diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/OpenedDocumentFile.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/open/OpenedDocumentFile.kt similarity index 94% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/OpenedDocumentFile.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/open/OpenedDocumentFile.kt index 1d608d47a9c0..a666b6e33631 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/OpenedDocumentFile.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/open/OpenedDocumentFile.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage +package com.owncloud.android.providers.documentstorage.open import com.owncloud.android.lib.common.utils.Log_OC import java.io.File diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentLister.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/query/DocumentLister.kt similarity index 93% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentLister.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/query/DocumentLister.kt index b640a2ba6b75..33a1aa5a3613 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentLister.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/query/DocumentLister.kt @@ -4,8 +4,11 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage +package com.owncloud.android.providers.documentstorage.query +import com.owncloud.android.providers.documentstorage.model.Document +import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier +import com.owncloud.android.providers.documentstorage.sync.FolderRefresher import android.content.Context import android.provider.DocumentsContract import androidx.core.os.bundleOf diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNotifier.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/DocumentNotifier.kt similarity index 85% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNotifier.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/sync/DocumentNotifier.kt index f05be38b4385..615269736564 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentNotifier.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/DocumentNotifier.kt @@ -4,8 +4,9 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage +package com.owncloud.android.providers.documentstorage.sync +import com.owncloud.android.providers.documentstorage.model.Document import android.content.Context import android.net.Uri import android.provider.DocumentsContract diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/FolderRefresher.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/FolderRefresher.kt similarity index 93% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/FolderRefresher.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/sync/FolderRefresher.kt index a14b36ece17a..ee9887abf333 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/FolderRefresher.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/FolderRefresher.kt @@ -4,8 +4,9 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage +package com.owncloud.android.providers.documentstorage.sync +import com.owncloud.android.providers.documentstorage.model.Document import android.content.Context import com.nextcloud.client.account.User import com.owncloud.android.MainApp diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentStorageFailures.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/util/DocumentStorageFailures.kt similarity index 92% rename from app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentStorageFailures.kt rename to app/src/main/java/com/owncloud/android/providers/documentstorage/util/DocumentStorageFailures.kt index fe9536e2b028..e9f10592add7 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentStorageFailures.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/util/DocumentStorageFailures.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.providers.documentstorage +package com.owncloud.android.providers.documentstorage.util import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java index 0faac11a5ee6..6580a0119b24 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java @@ -70,7 +70,7 @@ import com.owncloud.android.operations.e2e.E2ECertificateRenewalService; import com.owncloud.android.operations.e2e.E2ECertificateValidity; import com.owncloud.android.operations.e2e.E2EDeletionService; -import com.owncloud.android.providers.DocumentsStorageProvider; +import com.owncloud.android.providers.documentstorage.DocumentsStorageProvider; import com.owncloud.android.ui.ThemeableSwitchPreference; import com.owncloud.android.ui.asynctasks.LoadingVersionNumberTask; import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment; diff --git a/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.java b/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.java index 00941f95b3ab..13d2331d42b3 100644 --- a/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.java +++ b/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.java @@ -55,7 +55,7 @@ public void close() { } } - public void addFile(com.owncloud.android.providers.documentstorage.Document document) { + public void addFile(com.owncloud.android.providers.documentstorage.model.Document document) { if (document == null) { return; } diff --git a/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.java b/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.java index f8fede69b131..163d1b0bcfb7 100644 --- a/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.java +++ b/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.java @@ -14,7 +14,7 @@ import com.nextcloud.client.account.User; import com.owncloud.android.R; -import com.owncloud.android.providers.documentstorage.Document; +import com.owncloud.android.providers.documentstorage.model.Document; public class RootCursor extends MatrixCursor { From ecefd79e9130a1782b7295be6d22a7cae5fe6704 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 29 Oct 2026 18:52:55 +0300 Subject: [PATCH 4/8] optimize imports Signed-off-by: alperozturk96 --- .../DocumentsStorageProvider.kt | 23 ++++++++----------- .../documentstorage/model/Document.kt | 4 ++-- .../documentstorage/model/DocumentResolver.kt | 2 +- .../documentstorage/modify/DocumentCreator.kt | 12 +++++----- .../documentstorage/modify/DocumentEditor.kt | 10 ++++---- .../documentstorage/open/DocumentOpener.kt | 4 ++-- .../documentstorage/query/DocumentLister.kt | 6 ++--- .../documentstorage/sync/DocumentNotifier.kt | 2 +- .../documentstorage/sync/FolderRefresher.kt | 2 +- 9 files changed, 31 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt index a6bb2eff6c5a..dae8109bfdc8 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt @@ -6,16 +6,6 @@ */ package com.owncloud.android.providers.documentstorage -import com.owncloud.android.providers.DiskLruImageCacheFileProvider -import com.owncloud.android.providers.documentstorage.model.Document -import com.owncloud.android.providers.documentstorage.model.DocumentResolver -import com.owncloud.android.providers.documentstorage.modify.DocumentCreator -import com.owncloud.android.providers.documentstorage.modify.DocumentEditor -import com.owncloud.android.providers.documentstorage.modify.DocumentNameValidator -import com.owncloud.android.providers.documentstorage.open.DocumentOpener -import com.owncloud.android.providers.documentstorage.query.DocumentLister -import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier -import com.owncloud.android.providers.documentstorage.sync.FolderRefresher import android.annotation.SuppressLint import android.content.Context import android.content.res.AssetFileDescriptor @@ -27,7 +17,6 @@ import android.os.ParcelFileDescriptor import android.os.Process import android.provider.DocumentsContract import android.provider.DocumentsProvider -import androidx.annotation.VisibleForTesting import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.preferences.AppPreferencesImpl @@ -40,6 +29,16 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.RemoveFileOperation +import com.owncloud.android.providers.DiskLruImageCacheFileProvider +import com.owncloud.android.providers.documentstorage.model.Document +import com.owncloud.android.providers.documentstorage.model.DocumentResolver +import com.owncloud.android.providers.documentstorage.modify.DocumentCreator +import com.owncloud.android.providers.documentstorage.modify.DocumentEditor +import com.owncloud.android.providers.documentstorage.modify.DocumentNameValidator +import com.owncloud.android.providers.documentstorage.open.DocumentOpener +import com.owncloud.android.providers.documentstorage.query.DocumentLister +import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier +import com.owncloud.android.providers.documentstorage.sync.FolderRefresher import com.owncloud.android.ui.activity.SettingsActivity import dagger.android.AndroidInjection import kotlinx.coroutines.CoroutineScope @@ -275,7 +274,6 @@ class DocumentsStorageProvider : DocumentsProvider() { companion object { private val TAG = DocumentsStorageProvider::class.java.simpleName - @VisibleForTesting const val DOCUMENTID_SEPARATOR = "/" @JvmStatic @@ -285,7 +283,6 @@ class DocumentsStorageProvider : DocumentsProvider() { context.contentResolver.notifyChange(rootsUri, null) } - @VisibleForTesting fun rootIdForUser(user: User): String = HashUtil.md5Hash(user.accountName) } } diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/model/Document.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/model/Document.kt index 43b96fae1b15..078ddd0db3b5 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/model/Document.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/model/Document.kt @@ -6,8 +6,6 @@ */ package com.owncloud.android.providers.documentstorage.model -import com.owncloud.android.providers.documentstorage.DocumentsStorageProvider -import com.owncloud.android.providers.documentstorage.util.fileNotFoundException import android.accounts.AuthenticatorException import android.accounts.OperationCanceledException import android.content.Context @@ -17,6 +15,8 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.providers.documentstorage.DocumentsStorageProvider +import com.owncloud.android.providers.documentstorage.util.fileNotFoundException import java.io.FileNotFoundException import java.io.IOException diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/model/DocumentResolver.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/model/DocumentResolver.kt index 2ebf2b24dc58..11d0d77eb1d4 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/model/DocumentResolver.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/model/DocumentResolver.kt @@ -6,10 +6,10 @@ */ package com.owncloud.android.providers.documentstorage.model -import com.owncloud.android.providers.documentstorage.DocumentsStorageProvider import android.content.ContentResolver import com.nextcloud.client.account.User import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.providers.documentstorage.DocumentsStorageProvider import java.io.FileNotFoundException internal class DocumentResolver { diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentCreator.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentCreator.kt index ee9922151e85..45adb77abd2f 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentCreator.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentCreator.kt @@ -6,12 +6,6 @@ */ package com.owncloud.android.providers.documentstorage.modify -import com.owncloud.android.providers.documentstorage.model.Document -import com.owncloud.android.providers.documentstorage.model.DocumentResolver -import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier -import com.owncloud.android.providers.documentstorage.sync.FolderRefresher -import com.owncloud.android.providers.documentstorage.util.fileNotFoundException -import com.owncloud.android.providers.documentstorage.util.requireSuccess import android.content.Context import android.provider.DocumentsContract import com.nextcloud.client.account.User @@ -25,6 +19,12 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation import com.owncloud.android.operations.CreateFolderOperation +import com.owncloud.android.providers.documentstorage.model.Document +import com.owncloud.android.providers.documentstorage.model.DocumentResolver +import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier +import com.owncloud.android.providers.documentstorage.sync.FolderRefresher +import com.owncloud.android.providers.documentstorage.util.fileNotFoundException +import com.owncloud.android.providers.documentstorage.util.requireSuccess import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.FileUtil import java.io.File diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentEditor.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentEditor.kt index 9dcd35fd72e5..b38149b228b1 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentEditor.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/modify/DocumentEditor.kt @@ -6,11 +6,6 @@ */ package com.owncloud.android.providers.documentstorage.modify -import com.owncloud.android.providers.documentstorage.model.Document -import com.owncloud.android.providers.documentstorage.model.DocumentResolver -import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier -import com.owncloud.android.providers.documentstorage.sync.FolderRefresher -import com.owncloud.android.providers.documentstorage.util.requireSuccess import android.content.Context import com.nextcloud.utils.extensions.showToast import com.owncloud.android.R @@ -18,6 +13,11 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.operations.CopyFileOperation import com.owncloud.android.operations.MoveFileOperation import com.owncloud.android.operations.RenameFileOperation +import com.owncloud.android.providers.documentstorage.model.Document +import com.owncloud.android.providers.documentstorage.model.DocumentResolver +import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier +import com.owncloud.android.providers.documentstorage.sync.FolderRefresher +import com.owncloud.android.providers.documentstorage.util.requireSuccess import java.io.FileNotFoundException internal class DocumentEditor( diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/open/DocumentOpener.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/open/DocumentOpener.kt index ef3d0214adce..07e5a4007528 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/open/DocumentOpener.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/open/DocumentOpener.kt @@ -6,8 +6,6 @@ */ package com.owncloud.android.providers.documentstorage.open -import com.owncloud.android.providers.documentstorage.model.Document -import com.owncloud.android.providers.documentstorage.util.fileNotFoundException import android.content.Context import android.os.Handler import android.os.ParcelFileDescriptor @@ -23,6 +21,8 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCo import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation import com.owncloud.android.operations.DownloadFileOperation +import com.owncloud.android.providers.documentstorage.model.Document +import com.owncloud.android.providers.documentstorage.util.fileNotFoundException import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/query/DocumentLister.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/query/DocumentLister.kt index 33a1aa5a3613..004d212a48b9 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/query/DocumentLister.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/query/DocumentLister.kt @@ -6,9 +6,6 @@ */ package com.owncloud.android.providers.documentstorage.query -import com.owncloud.android.providers.documentstorage.model.Document -import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier -import com.owncloud.android.providers.documentstorage.sync.FolderRefresher import android.content.Context import android.provider.DocumentsContract import androidx.core.os.bundleOf @@ -17,6 +14,9 @@ import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.providers.documentstorage.model.Document +import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier +import com.owncloud.android.providers.documentstorage.sync.FolderRefresher import com.owncloud.android.ui.helpers.FileOperationsHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.isActive diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/DocumentNotifier.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/DocumentNotifier.kt index 615269736564..820ab2829964 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/DocumentNotifier.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/DocumentNotifier.kt @@ -6,11 +6,11 @@ */ package com.owncloud.android.providers.documentstorage.sync -import com.owncloud.android.providers.documentstorage.model.Document import android.content.Context import android.net.Uri import android.provider.DocumentsContract import com.owncloud.android.R +import com.owncloud.android.providers.documentstorage.model.Document internal class DocumentNotifier(private val context: Context) { diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/FolderRefresher.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/FolderRefresher.kt index ee9887abf333..c71813ff7888 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/FolderRefresher.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/FolderRefresher.kt @@ -6,7 +6,6 @@ */ package com.owncloud.android.providers.documentstorage.sync -import com.owncloud.android.providers.documentstorage.model.Document import android.content.Context import com.nextcloud.client.account.User import com.owncloud.android.MainApp @@ -15,6 +14,7 @@ import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.RefreshFolderOperation +import com.owncloud.android.providers.documentstorage.model.Document import java.io.FileNotFoundException internal class FolderRefresher(private val context: Context) { From 60be5044ea6aecfb25b2458814208da95c613c76 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 29 Oct 2026 19:01:38 +0300 Subject: [PATCH 5/8] use injections Signed-off-by: alperozturk96 --- .../java/com/nextcloud/client/di/AppModule.kt | 4 ++ .../DocumentsStorageProvider.kt | 48 +++++++++++-------- .../documentstorage/open/DocumentOpener.kt | 9 +++- .../documentstorage/sync/FolderRefresher.kt | 3 +- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/di/AppModule.kt b/app/src/main/java/com/nextcloud/client/di/AppModule.kt index 3436eda0496f..7dad415d7bc1 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/AppModule.kt @@ -28,6 +28,7 @@ import com.nextcloud.client.core.ThreadPoolAsyncRunner import com.nextcloud.client.database.dao.ArbitraryDataDao import com.nextcloud.client.device.DeviceInfo import com.nextcloud.client.jobs.operation.FileOperationHelper +import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.logger.FileLogHandler import com.nextcloud.client.logger.Logger import com.nextcloud.client.logger.LoggerImpl @@ -119,6 +120,9 @@ internal class AppModule { @Provides fun clientRepository(repository: RemoteClientRepository): ClientRepository = repository + @Provides + fun fileUploadHelper(): FileUploadHelper = FileUploadHelper.instance() + @Provides fun uploadsStorageManager(currentAccountProvider: CurrentAccountProvider, context: Context): UploadsStorageManager = UploadsStorageManager(currentAccountProvider, context.contentResolver) diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt index dae8109bfdc8..f051150401d4 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt @@ -19,7 +19,8 @@ import android.provider.DocumentsContract import android.provider.DocumentsProvider import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager -import com.nextcloud.client.preferences.AppPreferencesImpl +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.client.utils.HashUtil import com.nextcloud.utils.extensions.getSmallThumbnailKey import com.nextcloud.utils.extensions.showToast @@ -40,6 +41,7 @@ import com.owncloud.android.providers.documentstorage.query.DocumentLister import com.owncloud.android.providers.documentstorage.sync.DocumentNotifier import com.owncloud.android.providers.documentstorage.sync.FolderRefresher import com.owncloud.android.ui.activity.SettingsActivity +import dagger.Lazy import dagger.android.AndroidInjection import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -54,23 +56,29 @@ class DocumentsStorageProvider : DocumentsProvider() { @Inject lateinit var accountManager: UserAccountManager + @Inject + lateinit var preferences: AppPreferences + + @Inject + lateinit var appContext: Context + + @Inject + lateinit var fileUploadHelper: Lazy + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val resolver = DocumentResolver() - private val notifier by lazy { DocumentNotifier(nonNullContext) } - private val folderRefresher by lazy { FolderRefresher(nonNullContext) } - private val nameValidator by lazy { DocumentNameValidator(nonNullContext, accountManager) } - private val lister by lazy { DocumentLister(nonNullContext, scope, notifier, folderRefresher) } - private val opener by lazy { DocumentOpener(nonNullContext, scope) } + private val notifier by lazy { DocumentNotifier(appContext) } + private val folderRefresher by lazy { FolderRefresher(appContext) } + private val nameValidator by lazy { DocumentNameValidator(appContext, accountManager) } + private val lister by lazy { DocumentLister(appContext, scope, notifier, folderRefresher) } + private val opener by lazy { DocumentOpener(appContext, scope, fileUploadHelper) } private val creator by lazy { - DocumentCreator(nonNullContext, accountManager, resolver, nameValidator, folderRefresher, notifier) + DocumentCreator(appContext, accountManager, resolver, nameValidator, folderRefresher, notifier) } - private val editor by lazy { DocumentEditor(nonNullContext, resolver, notifier, folderRefresher, nameValidator) } - - private val nonNullContext: Context - get() = checkNotNull(context) + private val editor by lazy { DocumentEditor(appContext, resolver, notifier, folderRefresher, nameValidator) } private val isAppLocked: Boolean - get() = AppPreferencesImpl.fromContext(MainApp.getAppContext()).lockPreference in + get() = preferences.lockPreference in listOf(SettingsActivity.LOCK_PASSCODE, SettingsActivity.LOCK_DEVICE_CREDENTIALS) override fun onCreate(): Boolean { @@ -85,12 +93,12 @@ class DocumentsStorageProvider : DocumentsProvider() { return } - val defaultStoragePath = nonNullContext.filesDir.absolutePath - MainApp.setStoragePath(AppPreferencesImpl.fromContext(nonNullContext).getStoragePath(defaultStoragePath)) + val defaultStoragePath = appContext.filesDir.absolutePath + MainApp.setStoragePath(preferences.getStoragePath(defaultStoragePath)) } private fun refreshStorageManagers() { - resolver.refresh(accountManager.allUsers, nonNullContext.contentResolver) + resolver.refresh(accountManager.allUsers, appContext.contentResolver) } override fun queryRoots(projection: Array?): Cursor = withClearedCallingIdentity { @@ -101,7 +109,7 @@ class DocumentsStorageProvider : DocumentsProvider() { } else { RootCursor(projection).apply { resolver.storageManagers.forEach { manager -> - addRoot(Document.fromPath(manager, OCFile.ROOT_PATH), nonNullContext) + addRoot(Document.fromPath(manager, OCFile.ROOT_PATH), appContext) } } } @@ -170,7 +178,7 @@ class DocumentsStorageProvider : DocumentsProvider() { val file = document.requireFile() if (!ThumbnailsCacheManager.containsBitmap(file.getSmallThumbnailKey())) { - ThumbnailsCacheManager.generateThumbnailFromOCFile(file, document.user, context) + ThumbnailsCacheManager.generateThumbnailFromOCFile(file, document.user, appContext) } val descriptor = DiskLruImageCacheFileProvider.getParcelFileDescriptorForOCFile(file) @@ -219,7 +227,7 @@ class DocumentsStorageProvider : DocumentsProvider() { val document = resolver.toDocument(documentId) val file = document.requireFile() if (!file.canDeleteOrLeaveShare()) { - nonNullContext.showToast(R.string.document_storage_provider_cannot_delete) + appContext.showToast(R.string.document_storage_provider_cannot_delete) return } @@ -227,8 +235,8 @@ class DocumentsStorageProvider : DocumentsProvider() { recursiveRevokePermission(document) withClearedCallingIdentity { - val result = RemoveFileOperation(file, false, document.user, true, nonNullContext, document.storageManager) - .execute(document.requireClient(nonNullContext)) + val result = RemoveFileOperation(file, false, document.user, true, appContext, document.storageManager) + .execute(document.requireClient(appContext)) if (!result.isSuccess) { throw FileNotFoundException("Failed to delete document with documentId $documentId") diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/open/DocumentOpener.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/open/DocumentOpener.kt index 07e5a4007528..c453468a61f2 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/open/DocumentOpener.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/open/DocumentOpener.kt @@ -25,6 +25,7 @@ import com.owncloud.android.providers.documentstorage.model.Document import com.owncloud.android.providers.documentstorage.util.fileNotFoundException import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.MimeTypeUtil +import dagger.Lazy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -35,7 +36,11 @@ import java.io.IOException import java.nio.file.Files import java.nio.file.StandardCopyOption -internal class DocumentOpener(private val context: Context, private val scope: CoroutineScope) { +internal class DocumentOpener( + private val context: Context, + private val scope: CoroutineScope, + private val fileUploadHelper: Lazy +) { @Throws(FileNotFoundException::class) fun open(document: Document, mode: String?, isSystemServerCaller: Boolean): ParcelFileDescriptor? { @@ -244,7 +249,7 @@ internal class DocumentOpener(private val context: Context, private val scope: C ocFile.modificationTimestamp = System.currentTimeMillis() document.storageManager.saveFile(ocFile) - FileUploadHelper.instance().uploadUpdatedFile( + fileUploadHelper.get().uploadUpdatedFile( document.user, arrayOf(ocFile), FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/FolderRefresher.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/FolderRefresher.kt index c71813ff7888..261fb812df07 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/FolderRefresher.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/FolderRefresher.kt @@ -8,7 +8,6 @@ package com.owncloud.android.providers.documentstorage.sync import android.content.Context import com.nextcloud.client.account.User -import com.owncloud.android.MainApp import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult @@ -47,7 +46,7 @@ internal class FolderRefresher(private val context: Context) { true, folder.storageManager, folder.user, - MainApp.getAppContext() + context ).execute(folder.requireClient(context)) } catch (e: FileNotFoundException) { Log_OC.e(TAG, "reloadFolder() failed, id=${folder.documentId}", e) From 722823c9c419d3dfcb051d9fd0ad404d726d3722 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 30 Oct 2026 06:04:26 +0300 Subject: [PATCH 6/8] Rename .java to .kt Signed-off-by: alperozturk96 --- .../providers/cursors/{FileCursor.java => FileCursor.kt} | 0 .../providers/cursors/{RootCursor.java => RootCursor.kt} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/org/nextcloud/providers/cursors/{FileCursor.java => FileCursor.kt} (100%) rename app/src/main/java/org/nextcloud/providers/cursors/{RootCursor.java => RootCursor.kt} (100%) diff --git a/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.java b/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.kt similarity index 100% rename from app/src/main/java/org/nextcloud/providers/cursors/FileCursor.java rename to app/src/main/java/org/nextcloud/providers/cursors/FileCursor.kt diff --git a/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.java b/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.kt similarity index 100% rename from app/src/main/java/org/nextcloud/providers/cursors/RootCursor.java rename to app/src/main/java/org/nextcloud/providers/cursors/RootCursor.kt From 93d1eeb8ce8143c9a5f406750e56e970fccc4168 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 30 Oct 2026 06:04:27 +0300 Subject: [PATCH 7/8] fix spotbugs and convert cursor implm to kotlin Signed-off-by: alperozturk96 --- .../DocumentsStorageProvider.kt | 4 +- .../documentstorage/query/DocumentLister.kt | 6 +- .../nextcloud/providers/cursors/FileCursor.kt | 130 ++++++++---------- .../nextcloud/providers/cursors/RootCursor.kt | 86 ++++++------ 4 files changed, 113 insertions(+), 113 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt index f051150401d4..5000c009cbf8 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/DocumentsStorageProvider.kt @@ -120,8 +120,8 @@ class DocumentsStorageProvider : DocumentsProvider() { return withClearedCallingIdentity { val document = resolver.toDocument(documentId) - document.requireFile() - FileCursor(projection).apply { addFile(document) } + val file = document.requireFile() + FileCursor(projection).apply { addFile(document, file) } } } diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/query/DocumentLister.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/query/DocumentLister.kt index 004d212a48b9..a7f053192481 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/query/DocumentLister.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/query/DocumentLister.kt @@ -78,7 +78,7 @@ internal class DocumentLister( return@forEach } - cursor.addFile(Document(storageManager, file)) + cursor.addFile(Document(storageManager, file), file) } } @@ -93,7 +93,7 @@ internal class DocumentLister( notifier.notifyChanged(folder) } } - cursor.setLoadingTask(reloadJob) + cursor.loadingTask = reloadJob return true } @@ -106,7 +106,7 @@ internal class DocumentLister( storageManager.getFolderContent(folder, false).forEach { file -> when { file.isFolder -> addMatchingFiles(cursor, storageManager, file, query) - file.fileName.contains(query) -> cursor.addFile(Document(storageManager, file)) + file.fileName.contains(query) -> cursor.addFile(Document(storageManager, file), file) } } } diff --git a/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.kt b/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.kt index 13d2331d42b3..2cab535b440e 100644 --- a/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.kt +++ b/app/src/main/java/org/nextcloud/providers/cursors/FileCursor.kt @@ -1,86 +1,78 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Jens Mueller * SPDX-FileCopyrightText: 2016 Iskra Delta * SPDX-FileCopyrightText: 2015 Bartosz Przybylski * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ -package org.nextcloud.providers.cursors; - -import android.database.MatrixCursor; -import android.os.Bundle; -import android.provider.DocumentsContract.Document; - -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.utils.MimeTypeUtil; - -import kotlinx.coroutines.Job; - -public class FileCursor extends MatrixCursor { - - static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{ - Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, - Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE, - Document.COLUMN_FLAGS, Document.COLUMN_LAST_MODIFIED - }; - - private Bundle extra; - private Job loadingTask; - - public FileCursor(String[] projection) { - super(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); - } - - public void setLoadingTask(Job task) { - this.loadingTask = task; - } - - @Override - public void setExtras(Bundle extras) { - this.extra = extras; +package org.nextcloud.providers.cursors + +import android.database.MatrixCursor +import android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME +import android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID +import android.provider.DocumentsContract.Document.COLUMN_FLAGS +import android.provider.DocumentsContract.Document.COLUMN_ICON +import android.provider.DocumentsContract.Document.COLUMN_LAST_MODIFIED +import android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE +import android.provider.DocumentsContract.Document.COLUMN_SIZE +import android.provider.DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE +import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_COPY +import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_DELETE +import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_MOVE +import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_REMOVE +import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_RENAME +import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL +import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_WRITE +import android.provider.DocumentsContract.Document.MIME_TYPE_DIR +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.providers.documentstorage.model.Document +import com.owncloud.android.utils.MimeTypeUtil +import kotlinx.coroutines.Job + +class FileCursor(projection: Array?) : MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION) { + + var loadingTask: Job? = null + + override fun close() { + super.close() + loadingTask?.cancel() } - @Override - public Bundle getExtras() { - return extra; + fun addFile(document: Document, file: OCFile) { + newRow() + .add(COLUMN_DOCUMENT_ID, document.documentId) + .add(COLUMN_DISPLAY_NAME, file.fileName) + .add(COLUMN_LAST_MODIFIED, file.modificationTimestamp) + .add(COLUMN_SIZE, file.fileLength) + .add(COLUMN_FLAGS, documentFlags(file)) + .add(COLUMN_ICON, MimeTypeUtil.getFileTypeIconId(file.mimeType, file.fileName)) + .add(COLUMN_MIME_TYPE, if (file.isFolder) MIME_TYPE_DIR else file.mimeType) } - @Override - public void close() { - super.close(); - if (loadingTask != null && !loadingTask.isCompleted()) { - loadingTask.cancel(null); - } + private fun documentFlags(file: OCFile): Int { + val thumbnailFlag = if (MimeTypeUtil.isImage(file)) FLAG_SUPPORTS_THUMBNAIL else 0 + val createFlag = if (file.isFolder) FLAG_DIR_SUPPORTS_CREATE else 0 + return BASE_DOCUMENT_FLAGS or thumbnailFlag or createFlag } - public void addFile(com.owncloud.android.providers.documentstorage.model.Document document) { - if (document == null) { - return; - } - - OCFile file = document.getFile(); - - final int iconRes = MimeTypeUtil.getFileTypeIconId(file.getMimeType(), file.getFileName()); - final String mimeType = file.isFolder() ? Document.MIME_TYPE_DIR : file.getMimeType(); - int flags = Document.FLAG_SUPPORTS_DELETE | - Document.FLAG_SUPPORTS_WRITE | - (MimeTypeUtil.isImage(file) ? Document.FLAG_SUPPORTS_THUMBNAIL : 0) | - Document.FLAG_SUPPORTS_COPY | Document.FLAG_SUPPORTS_MOVE | Document.FLAG_SUPPORTS_REMOVE; - - if (file.isFolder()) { - flags = flags | Document.FLAG_DIR_SUPPORTS_CREATE; - } - - flags = Document.FLAG_SUPPORTS_RENAME | flags; - - newRow().add(Document.COLUMN_DOCUMENT_ID, document.getDocumentId()) - .add(Document.COLUMN_DISPLAY_NAME, file.getFileName()) - .add(Document.COLUMN_LAST_MODIFIED, file.getModificationTimestamp()) - .add(Document.COLUMN_SIZE, file.getFileLength()) - .add(Document.COLUMN_FLAGS, flags) - .add(Document.COLUMN_ICON, iconRes) - .add(Document.COLUMN_MIME_TYPE, mimeType); + companion object { + private const val BASE_DOCUMENT_FLAGS = FLAG_SUPPORTS_RENAME or + FLAG_SUPPORTS_DELETE or + FLAG_SUPPORTS_WRITE or + FLAG_SUPPORTS_COPY or + FLAG_SUPPORTS_MOVE or + FLAG_SUPPORTS_REMOVE + + private val DEFAULT_DOCUMENT_PROJECTION = arrayOf( + COLUMN_DOCUMENT_ID, + COLUMN_DISPLAY_NAME, + COLUMN_MIME_TYPE, + COLUMN_SIZE, + COLUMN_FLAGS, + COLUMN_LAST_MODIFIED + ) } } diff --git a/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.kt b/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.kt index 163d1b0bcfb7..d36ff90bc0f5 100644 --- a/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.kt +++ b/app/src/main/java/org/nextcloud/providers/cursors/RootCursor.kt @@ -1,51 +1,59 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2021 Chris Narkiewicz * SPDX-FileCopyrightText: 2019 Jens Mueller * SPDX-FileCopyrightText: 2016 Bartosz Przybylski * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ -package org.nextcloud.providers.cursors; - -import android.content.Context; -import android.database.MatrixCursor; -import android.provider.DocumentsContract.Root; - -import com.nextcloud.client.account.User; -import com.owncloud.android.R; -import com.owncloud.android.providers.documentstorage.model.Document; - -public class RootCursor extends MatrixCursor { - - private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { - Root.COLUMN_ROOT_ID, - Root.COLUMN_FLAGS, - Root.COLUMN_ICON, - Root.COLUMN_TITLE, - Root.COLUMN_DOCUMENT_ID, - Root.COLUMN_AVAILABLE_BYTES, - Root.COLUMN_SUMMARY, - Root.COLUMN_FLAGS - }; - - public RootCursor(String[] projection) { - super(projection != null ? projection : DEFAULT_ROOT_PROJECTION); +package org.nextcloud.providers.cursors + +import android.content.Context +import android.database.MatrixCursor +import android.provider.DocumentsContract.Root.COLUMN_AVAILABLE_BYTES +import android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID +import android.provider.DocumentsContract.Root.COLUMN_FLAGS +import android.provider.DocumentsContract.Root.COLUMN_ICON +import android.provider.DocumentsContract.Root.COLUMN_ROOT_ID +import android.provider.DocumentsContract.Root.COLUMN_SUMMARY +import android.provider.DocumentsContract.Root.COLUMN_TITLE +import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_CREATE +import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD +import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_RECENTS +import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_SEARCH +import com.owncloud.android.R +import com.owncloud.android.providers.documentstorage.model.Document + +class RootCursor(projection: Array?) : MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION) { + + fun addRoot(document: Document, context: Context) { + val accountName = document.user.accountName + + newRow() + .add(COLUMN_ROOT_ID, accountName) + .add(COLUMN_DOCUMENT_ID, document.documentId) + .add(COLUMN_SUMMARY, accountName) + .add(COLUMN_TITLE, context.getString(R.string.app_name)) + .add(COLUMN_ICON, R.mipmap.ic_launcher) + .add(COLUMN_FLAGS, ROOT_FLAGS) } - public void addRoot(Document document, Context context) { - User user = document.getUser(); - - int rootFlags = Root.FLAG_SUPPORTS_CREATE - | Root.FLAG_SUPPORTS_RECENTS - | Root.FLAG_SUPPORTS_SEARCH - | Root.FLAG_SUPPORTS_IS_CHILD; - - newRow().add(Root.COLUMN_ROOT_ID, user.getAccountName()) - .add(Root.COLUMN_DOCUMENT_ID, document.getDocumentId()) - .add(Root.COLUMN_SUMMARY, user.getAccountName()) - .add(Root.COLUMN_TITLE, context.getString(R.string.app_name)) - .add(Root.COLUMN_ICON, R.mipmap.ic_launcher) - .add(Root.COLUMN_FLAGS, rootFlags); + companion object { + private const val ROOT_FLAGS = FLAG_SUPPORTS_CREATE or + FLAG_SUPPORTS_RECENTS or + FLAG_SUPPORTS_SEARCH or + FLAG_SUPPORTS_IS_CHILD + + private val DEFAULT_ROOT_PROJECTION = arrayOf( + COLUMN_ROOT_ID, + COLUMN_FLAGS, + COLUMN_ICON, + COLUMN_TITLE, + COLUMN_DOCUMENT_ID, + COLUMN_AVAILABLE_BYTES, + COLUMN_SUMMARY, + COLUMN_FLAGS + ) } } From 84122f24e4fd21ad9dc632ac8058a6c6c42080c7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 30 Oct 2026 13:29:44 +0300 Subject: [PATCH 8/8] wip Signed-off-by: alperozturk96 --- .../providers/documentstorage/sync/DocumentNotifier.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/DocumentNotifier.kt b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/DocumentNotifier.kt index 820ab2829964..b73fc9d8d166 100644 --- a/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/DocumentNotifier.kt +++ b/app/src/main/java/com/owncloud/android/providers/documentstorage/sync/DocumentNotifier.kt @@ -15,15 +15,11 @@ import com.owncloud.android.providers.documentstorage.model.Document internal class DocumentNotifier(private val context: Context) { fun notifyChanged(document: Document) { - context.contentResolver.notifyChange(notifyUri(document), null, NO_NOTIFY_FLAGS) + context.contentResolver.notifyChange(notifyUri(document), null, 0) } fun notifyUri(document: Document): Uri = DocumentsContract.buildDocumentUri( context.getString(R.string.document_provider_authority), document.documentId ) - - companion object { - private const val NO_NOTIFY_FLAGS = 0 - } }