diff --git a/CHANGELOG.md b/CHANGELOG.md index 286c16cf100..b3a3709df71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ ownCloud admins and users. * Enhancement - New field "last usage" in database: [#4173](https://github.com/owncloud/android/issues/4173) * Enhancement - Use invoke operator to execute usecases: [#4179](https://github.com/owncloud/android/pull/4179) * Enhancement - Deep link open app correctly: [#4181](https://github.com/owncloud/android/issues/4181) +* Enhancement - Select user and navigate to file when opening via deep link: [#4194](https://github.com/owncloud/android/issues/4194) ## Details @@ -161,6 +162,14 @@ ownCloud admins and users. https://github.com/owncloud/android/issues/4181 https://github.com/owncloud/android/pull/4191 +* Enhancement - Select user and navigate to file when opening via deep link: [#4194](https://github.com/owncloud/android/issues/4194) + + Select the correct user owner of the deep link file, managing possible errors + and navigating to the correct file. + + https://github.com/owncloud/android/issues/4194 + https://github.com/owncloud/android/pull/4212 + # Changelog for ownCloud Android Client [4.1.1] (2023-10-18) The following sections list the changes in ownCloud Android Client 4.1.1 relevant to diff --git a/changelog/unreleased/4212 b/changelog/unreleased/4212 new file mode 100644 index 00000000000..fa5ffd474ce --- /dev/null +++ b/changelog/unreleased/4212 @@ -0,0 +1,6 @@ +Enhancement: Select user and navigate to file when opening via deep link + +Select the correct user owner of the deep link file, managing possible errors and navigating to the correct file. + +https://github.com/owncloud/android/issues/4194 +https://github.com/owncloud/android/pull/4212 diff --git a/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt b/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt index b8066225ba1..1fd24713d6d 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/extensions/ActivityExt.kt @@ -37,6 +37,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.snackbar.Snackbar import com.owncloud.android.R import com.owncloud.android.data.providers.implementation.OCSharedPreferencesProvider @@ -61,6 +64,9 @@ import com.owncloud.android.ui.dialog.ShareLinkToDialog import com.owncloud.android.utils.CONFIGURATION_DEVICE_PROTECTION import com.owncloud.android.utils.MimetypeIconUtil import com.owncloud.android.utils.UriUtilsKt.getExposedFileUriForOCFile +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.koin.android.ext.android.inject import timber.log.Timber import java.io.File @@ -436,3 +442,15 @@ fun Activity.openOCFile(ocFile: OCFile) { showErrorInSnackbar(genericErrorMessageId = R.string.file_list_no_app_for_file_type, anfe) } } + +fun FragmentActivity.collectLatestLifecycleFlow( + flow: Flow, + lifecycleState: Lifecycle.State = Lifecycle.State.STARTED, + collect: suspend (T) -> Unit +) { + lifecycleScope.launch { + repeatOnLifecycle(lifecycleState) { + flow.collectLatest(collect) + } + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt index 69a73d93b5f..a935c83d0e2 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/MainFileListFragment.kt @@ -60,7 +60,6 @@ import com.owncloud.android.R import com.owncloud.android.databinding.MainFileListFragmentBinding import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.domain.appregistry.model.AppRegistryMimeType -import com.owncloud.android.domain.exceptions.DeepLinkException import com.owncloud.android.domain.exceptions.InstanceNotConfiguredException import com.owncloud.android.domain.exceptions.TooEarlyException import com.owncloud.android.domain.files.model.FileListOption @@ -621,33 +620,11 @@ class MainFileListFragment : Fragment(), } } - observeDeepLink() - /* TransfersViewModel observables */ observeTransfers() } - private fun observeDeepLink() { - collectLatestLifecycleFlow(fileOperationsViewModel.deepLinkFlow) { - val uiResult = it?.peekContent() - if (uiResult is UIResult.Error) { - showMessageInSnackbar( - getString( - if (uiResult.error is DeepLinkException) { - R.string.invalid_deep_link_format - } else { - R.string.default_error_msg - } - ) - ) - } else if (uiResult is UIResult.Success) { - //TODO "Remove this message when end with managing the deep links" - showMessageInSnackbar("Deep link managed correctly") - } - } - } - private fun observeTransfers() { val maxUploadsToRefresh = resources.getInteger(R.integer.max_uploads_to_refresh) collectLatestLifecycleFlow(transfersViewModel.transfersWithSpaceStateFlow) { transfers -> diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt index 5cda58aa704..3fd471a7f9f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/operations/FileOperationsViewModel.kt @@ -136,6 +136,15 @@ class FileOperationsViewModel( } } + fun handleDeepLink(uri: Uri, accountName: String) { + runUseCaseWithResult( + coroutineDispatcher = coroutinesDispatcherProvider.io, + showLoading = true, + flow = _deepLinkFlow, + useCase = manageDeepLinkUseCase, + useCaseParams = ManageDeepLinkUseCase.Params(URI(uri.toString()), accountName), + ) + } private fun createFolderOperation(fileOperation: FileOperation.CreateFolder) { runOperation( @@ -309,13 +318,4 @@ class FileOperationsViewModel( } } } - - fun handleDeepLink(uri: Uri) { - runUseCaseWithResult( - coroutineDispatcher = coroutinesDispatcherProvider.io, - flow = _deepLinkFlow, - useCase = manageDeepLinkUseCase, - useCaseParams = ManageDeepLinkUseCase.Params(URI(uri.toString())), - ) - } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 10365cf30d7..990276da48c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -56,6 +56,8 @@ import com.owncloud.android.data.providers.SharedPreferencesProvider import com.owncloud.android.databinding.ActivityMainBinding import com.owncloud.android.domain.camerauploads.model.UploadBehavior import com.owncloud.android.domain.capabilities.model.OCCapability +import com.owncloud.android.domain.exceptions.DeepLinkException +import com.owncloud.android.domain.exceptions.FileNotFoundException import com.owncloud.android.domain.exceptions.SSLRecoverablePeerUnverifiedException import com.owncloud.android.domain.exceptions.UnauthorizedException import com.owncloud.android.domain.files.model.FileListOption @@ -64,6 +66,7 @@ import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PARENT_ID import com.owncloud.android.domain.spaces.model.OCSpace import com.owncloud.android.domain.utils.Event import com.owncloud.android.extensions.checkPasscodeEnforced +import com.owncloud.android.extensions.collectLatestLifecycleFlow import com.owncloud.android.extensions.isDownloadPending import com.owncloud.android.extensions.manageOptionLockSelected import com.owncloud.android.extensions.observeWorkerTillItFinishes @@ -79,6 +82,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode import com.owncloud.android.lib.resources.status.OwnCloudVersion import com.owncloud.android.operations.SyncProfileOperation +import com.owncloud.android.presentation.authentication.AccountUtils.getCurrentOwnCloudAccount import com.owncloud.android.presentation.capabilities.CapabilityViewModel import com.owncloud.android.presentation.common.UIResult import com.owncloud.android.presentation.conflicts.ConflictsResolveActivity @@ -1664,6 +1668,8 @@ class FileDisplayActivity : FileActivity(), } private fun startListeningToOperations() { + onDeepLinkManaged() + fileOperationsViewModel.copyFileLiveData.observe(this, Event.EventObserver { onCopyFileOperationFinish(it) }) @@ -1768,8 +1774,71 @@ class FileDisplayActivity : FileActivity(), private fun handleDeepLink() { intent.data?.let { uri -> - fileOperationsViewModel.handleDeepLink(uri) - intent.data = null + fileOperationsViewModel.handleDeepLink(uri, getCurrentOwnCloudAccount(baseContext).name) + } + } + + private fun onDeepLinkManaged() { + collectLatestLifecycleFlow(fileOperationsViewModel.deepLinkFlow) { + it?.getContentIfNotHandled()?.let { uiResult -> + when (uiResult) { + is UIResult.Loading -> { + showLoadingDialog(R.string.deep_link_loading) + } + is UIResult.Success -> { + intent?.data = null + dismissLoadingDialog() + uiResult.data?.let { it1 -> manageItem(it1) } + } + is UIResult.Error -> { + dismissLoadingDialog() + if (uiResult.error is FileNotFoundException) { + showMessageInSnackbar(message = getString(R.string.deep_link_user_no_access)) + changeUser() + } else { + showMessageInSnackbar(message = getString( + if (uiResult.error is DeepLinkException) { + R.string.invalid_deep_link_format + } else { + R.string.default_error_msg + } + )) + } + } + } + } + } + } + + private fun changeUser() { + val currentUser = getCurrentOwnCloudAccount(this) + val usersChecked = intent?.getStringArrayListExtra(KEY_DEEP_LINK_ACCOUNTS_CHECKED) ?: arrayListOf() + usersChecked.add(currentUser.name) + com.owncloud.android.presentation.authentication.AccountUtils.getAccounts(this).forEach { + if (!usersChecked.contains(it.name)) { + MainApp.initDependencyInjection() + val i = Intent( + this, + FileDisplayActivity::class.java + ) + i.data = intent?.data + i.putExtra(EXTRA_ACCOUNT, it) + i.putExtra(KEY_DEEP_LINK_ACCOUNTS_CHECKED, usersChecked) + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(i) + } + } + } + + private fun manageItem(file: OCFile) { + setFile(file) + account = com.owncloud.android.presentation.authentication.AccountUtils.getOwnCloudAccountByName(this, file.owner) + + if (file.isFolder) { + refreshListOfFilesFragment() + } else { + initFragmentsWithFile() + onFileClicked(file) } } @@ -1783,6 +1852,7 @@ class FileDisplayActivity : FileActivity(), private const val KEY_WAITING_TO_SEND = "WAITING_TO_SEND" private const val KEY_UPLOAD_HELPER = "FILE_UPLOAD_HELPER" private const val KEY_FILE_LIST_OPTION = "FILE_LIST_OPTION" + const val KEY_DEEP_LINK_ACCOUNTS_CHECKED = "DEEP_LINK_ACCOUNTS_CHECKED" private const val CUSTOM_DIALOG_TAG = "CUSTOM_DIALOG" diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index f33ee307a10..2ee91e1f799 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -753,6 +753,8 @@ Invalid link format + The user doesn\'t have access to file + Opening file from link Refresh Synchronization already in progress diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/webdav/properties/OCFileId.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/webdav/properties/OCFileId.kt new file mode 100644 index 00000000000..15f6efe4e6a --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/webdav/properties/OCFileId.kt @@ -0,0 +1,47 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2023 ownCloud GmbH. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.owncloud.android.lib.common.http.methods.webdav.properties + +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.PropertyFactory +import at.bitfire.dav4jvm.XmlUtils +import org.xmlpull.v1.XmlPullParser + +data class OCFileId(val fileId: String) : Property { + class Factory : PropertyFactory { + override fun getName() = NAME + + override fun create(parser: XmlPullParser): OCFileId? { + XmlUtils.readText(parser)?.let { + return OCFileId(it) + } + return null + } + } + + companion object { + @JvmField + val NAME = Property.Name(XmlUtils.NS_OWNCLOUD, "fileid") + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/webdav/properties/OCMetaPathForUser.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/webdav/properties/OCMetaPathForUser.kt new file mode 100644 index 00000000000..e77d0b5fb0e --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/webdav/properties/OCMetaPathForUser.kt @@ -0,0 +1,47 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2023 ownCloud GmbH. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.owncloud.android.lib.common.http.methods.webdav.properties + +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.PropertyFactory +import at.bitfire.dav4jvm.XmlUtils +import org.xmlpull.v1.XmlPullParser + +data class OCMetaPathForUser(val path: String) : Property { + class Factory : PropertyFactory { + override fun getName() = NAME + + override fun create(parser: XmlPullParser): OCMetaPathForUser? { + XmlUtils.readText(parser)?.let { + return OCMetaPathForUser(it) + } + return null + } + } + + companion object { + @JvmField + val NAME = Property.Name(XmlUtils.NS_OWNCLOUD, "meta-path-for-user") + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/webdav/properties/OCSpaceId.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/webdav/properties/OCSpaceId.kt new file mode 100644 index 00000000000..499faf594c9 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/methods/webdav/properties/OCSpaceId.kt @@ -0,0 +1,47 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2023 ownCloud GmbH. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.owncloud.android.lib.common.http.methods.webdav.properties + +import at.bitfire.dav4jvm.Property +import at.bitfire.dav4jvm.PropertyFactory +import at.bitfire.dav4jvm.XmlUtils +import org.xmlpull.v1.XmlPullParser + +data class OCSpaceId(val spaceId: String) : Property { + class Factory : PropertyFactory { + override fun getName() = NAME + + override fun create(parser: XmlPullParser): OCSpaceId? { + XmlUtils.readText(parser)?.let { + return OCSpaceId(it) + } + return null + } + } + + companion object { + @JvmField + val NAME = Property.Name(XmlUtils.NS_OWNCLOUD, "spaceid") + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/GetRemoteMetaFileOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/GetRemoteMetaFileOperation.kt new file mode 100644 index 00000000000..e6290930b19 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/GetRemoteMetaFileOperation.kt @@ -0,0 +1,96 @@ +/* ownCloud Android Library is available under MIT license +* Copyright (C) 2023 ownCloud GmbH. +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +package com.owncloud.android.lib.resources.files + +import at.bitfire.dav4jvm.PropertyRegistry +import at.bitfire.dav4jvm.property.OCId +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.http.HttpConstants +import com.owncloud.android.lib.common.http.methods.webdav.DavConstants +import com.owncloud.android.lib.common.http.methods.webdav.PropfindMethod +import com.owncloud.android.lib.common.http.methods.webdav.properties.OCFileId +import com.owncloud.android.lib.common.http.methods.webdav.properties.OCMetaPathForUser +import com.owncloud.android.lib.common.http.methods.webdav.properties.OCSpaceId +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import timber.log.Timber +import java.net.URL +import java.util.concurrent.TimeUnit + +class GetRemoteMetaFileOperation(val fileId: String) : RemoteOperation() { + + override fun run(client: OwnCloudClient): RemoteOperationResult { + PropertyRegistry.register(OCMetaPathForUser.Factory()) + PropertyRegistry.register(OCFileId.Factory()) + PropertyRegistry.register(OCSpaceId.Factory()) + + val stringUrl = "${client.baseUri}$META_PATH$fileId" + return try { + + val propFindMethod = + PropfindMethod(URL(stringUrl), DavConstants.DEPTH_0, + arrayOf(OCMetaPathForUser.NAME, OCId.NAME, OCFileId.NAME, OCSpaceId.NAME) + ).apply { + setReadTimeout(TIMEOUT, TimeUnit.SECONDS) + setConnectionTimeout(TIMEOUT, TimeUnit.SECONDS) + } + + val status = client.executeHttpMethod(propFindMethod) + if (isSuccess(status)) RemoteOperationResult(RemoteOperationResult.ResultCode.OK).apply { + val remoteMetaFile = RemoteMetaFile() + propFindMethod.root?.properties?.let { properties -> + properties.forEach { property -> + when (property) { + is OCMetaPathForUser -> { + remoteMetaFile.metaPathForUser = property.path + } + is OCId -> { + remoteMetaFile.id = property.id + } + is OCFileId -> { + remoteMetaFile.fileId = property.fileId + } + is OCSpaceId -> { + remoteMetaFile.spaceId = property.spaceId + } + } + } + } + data = remoteMetaFile + } + else RemoteOperationResult(propFindMethod) + } catch (e: Exception) { + Timber.e(e, "Exception while getting remote meta file") + RemoteOperationResult(e) + } + } + + private fun isSuccess(status: Int) = status == HttpConstants.HTTP_OK || status == HttpConstants.HTTP_MULTI_STATUS + + companion object { + private const val META_PATH = "/remote.php/dav/meta/" + private const val TIMEOUT = 10_000L + } + +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/RemoteMetaFile.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/RemoteMetaFile.kt new file mode 100644 index 00000000000..438d9bfbe32 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/RemoteMetaFile.kt @@ -0,0 +1,31 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2023 ownCloud GmbH. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.owncloud.android.lib.resources.files + +data class RemoteMetaFile( + var metaPathForUser: String? = "", + var id: String? = null, + var fileId: String? = null, + var spaceId: String? = null, +) diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/services/FileService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/services/FileService.kt index 43f5c57c653..be6d9dc2d86 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/services/FileService.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/services/FileService.kt @@ -19,13 +19,14 @@ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - * */ + package com.owncloud.android.lib.resources.files.services import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.resources.Service import com.owncloud.android.lib.resources.files.RemoteFile +import com.owncloud.android.lib.resources.files.RemoteMetaFile interface FileService : Service { fun checkPathExistence( @@ -83,4 +84,9 @@ interface FileService : Service { isFolder: Boolean, spaceWebDavUrl: String? = null, ): RemoteOperationResult + + fun getMetaFileInfo( + fileId: String, + ): RemoteOperationResult + } diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/services/implementation/OCFileService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/services/implementation/OCFileService.kt index 4d1108584c9..ade436a3719 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/services/implementation/OCFileService.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/files/services/implementation/OCFileService.kt @@ -29,10 +29,12 @@ import com.owncloud.android.lib.resources.files.CheckPathExistenceRemoteOperatio import com.owncloud.android.lib.resources.files.CopyRemoteFileOperation import com.owncloud.android.lib.resources.files.CreateRemoteFolderOperation import com.owncloud.android.lib.resources.files.DownloadRemoteFileOperation +import com.owncloud.android.lib.resources.files.GetRemoteMetaFileOperation import com.owncloud.android.lib.resources.files.MoveRemoteFileOperation import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation import com.owncloud.android.lib.resources.files.RemoteFile +import com.owncloud.android.lib.resources.files.RemoteMetaFile import com.owncloud.android.lib.resources.files.RemoveRemoteFileOperation import com.owncloud.android.lib.resources.files.RenameRemoteFileOperation import com.owncloud.android.lib.resources.files.services.FileService @@ -140,4 +142,9 @@ class OCFileService(override val client: OwnCloudClient) : FileService { isFolder = isFolder, spaceWebDavUrl = spaceWebDavUrl, ).execute(client) + + override fun getMetaFileInfo( + fileId: String, + ): RemoteOperationResult = + GetRemoteMetaFileOperation(fileId).execute(client) } diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacesOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacesOperation.kt index 35a6727bbf6..4fdba404699 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacesOperation.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/spaces/GetRemoteSpacesOperation.kt @@ -51,7 +51,7 @@ class GetRemoteSpacesOperation : RemoteOperation>() { onResultUnsuccessful(getMethod, response, status) } } catch (e: Exception) { - Timber.e(e, "Exception while getting remote shares") + Timber.e(e, "Exception while getting remote spaces") RemoteOperationResult(e) } } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt index 22cc669aa17..d568f9cea08 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt @@ -22,6 +22,7 @@ package com.owncloud.android.data.files.datasources import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.domain.files.model.OCMetaFile interface RemoteFileDataSource { fun checkPathExistence( @@ -89,4 +90,10 @@ interface RemoteFileDataSource { accountName: String, spaceWebDavUrl: String? = null, ) + + fun getMetaFile( + fileId: String, + accountName: String, + ): OCMetaFile + } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt index f820031b640..71c5d2bb671 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt @@ -27,7 +27,9 @@ import com.owncloud.android.data.ClientManager import com.owncloud.android.data.executeRemoteOperation import com.owncloud.android.data.files.datasources.RemoteFileDataSource import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.domain.files.model.OCMetaFile import com.owncloud.android.lib.resources.files.RemoteFile +import com.owncloud.android.lib.resources.files.RemoteMetaFile class OCRemoteFileDataSource( private val clientManager: ClientManager, @@ -203,6 +205,13 @@ class OCRemoteFileDataSource( ) } + override fun getMetaFile( + fileId: String, + accountName: String, + ): OCMetaFile = executeRemoteOperation { + clientManager.getFileService(accountName).getMetaFileInfo(fileId) + }.toModel() + companion object { @VisibleForTesting fun RemoteFile.toModel(): OCFile = @@ -224,5 +233,14 @@ class OCRemoteFileDataSource( sharedWithSharee = sharedWithSharee, sharedByLink = sharedByLink, ) + + @VisibleForTesting + fun RemoteMetaFile.toModel(): OCMetaFile = + OCMetaFile( + path = metaPathForUser, + id = id, + fileId = fileId, + spaceId = spaceId, + ) } } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt index 8b4097ac598..acc4cc177d4 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt @@ -25,8 +25,8 @@ package com.owncloud.android.data.files.repository import com.owncloud.android.data.files.datasources.LocalFileDataSource import com.owncloud.android.data.files.datasources.RemoteFileDataSource -import com.owncloud.android.data.spaces.datasources.LocalSpacesDataSource import com.owncloud.android.data.providers.LocalStorageProvider +import com.owncloud.android.data.spaces.datasources.LocalSpacesDataSource import com.owncloud.android.domain.availableoffline.model.AvailableOfflineStatus import com.owncloud.android.domain.availableoffline.model.AvailableOfflineStatus.AVAILABLE_OFFLINE_PARENT import com.owncloud.android.domain.availableoffline.model.AvailableOfflineStatus.NOT_AVAILABLE_OFFLINE @@ -37,6 +37,7 @@ import com.owncloud.android.domain.files.FileRepository import com.owncloud.android.domain.files.model.FileListOption import com.owncloud.android.domain.files.model.MIME_DIR import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.domain.files.model.OCFile.Companion.PATH_SEPARATOR import com.owncloud.android.domain.files.model.OCFile.Companion.ROOT_PATH import com.owncloud.android.domain.files.model.OCFileWithSyncInfo import kotlinx.coroutines.flow.Flow @@ -164,6 +165,33 @@ class OCFileRepository( override fun getFileByRemotePath(remotePath: String, owner: String, spaceId: String?): OCFile? = localFileDataSource.getFileByRemotePath(remotePath, owner, spaceId) + override fun getFileFromRemoteId(fileId: String, accountName: String): OCFile? { + val metaFile = remoteFileDataSource.getMetaFile(fileId, accountName) + val remotePath = metaFile.path!! + + val splitPath = remotePath.split(PATH_SEPARATOR) + var containerFolder = listOf() + for (i in 0..splitPath.size - 2) { + var path = splitPath[0] + for (j in 1..i) { + path += "$PATH_SEPARATOR${splitPath[j]}" + } + containerFolder = refreshFolder(path, accountName, metaFile.spaceId) + } + refreshFolder(remotePath, accountName, metaFile.spaceId) + return if (remotePath == ROOT_PATH) { + getFileByRemotePath(remotePath, accountName, metaFile.spaceId) + } else { + containerFolder.find { file -> + if (file.isFolder) { + file.remotePath.dropLast(1) + } else { + file.remotePath + } == remotePath + } + } + } + override fun getPersonalRootFolderForAccount(owner: String): OCFile { val personalSpace = localSpacesDataSource.getPersonalSpaceForAccount(owner) if (personalSpace == null) { diff --git a/owncloudData/src/test/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSourceTest.kt b/owncloudData/src/test/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSourceTest.kt index ab560b7a34e..4de17c8f651 100644 --- a/owncloudData/src/test/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSourceTest.kt +++ b/owncloudData/src/test/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSourceTest.kt @@ -3,6 +3,7 @@ * * @author Abel García de Prada * @author Aitor Ballesteros Pavón + * @author Juan Carlos Garrote Gascón * * Copyright (C) 2023 ownCloud GmbH. * @@ -30,6 +31,7 @@ import com.owncloud.android.testutil.OC_ACCOUNT_NAME import com.owncloud.android.testutil.OC_FILE import com.owncloud.android.testutil.OC_FOLDER import com.owncloud.android.testutil.REMOTE_FILE +import com.owncloud.android.testutil.REMOTE_META_FILE import com.owncloud.android.utils.createRemoteOperationResultMock import io.mockk.every import io.mockk.mockk @@ -377,4 +379,27 @@ class OCRemoteFileDataSourceTest { ocFileService.renameFile(oldName, oldRemotePath, newName, false, null) } } + + @Test + fun `getMetaFile returns a OCMetaFile`() { + val remoteResult = createRemoteOperationResultMock(data = REMOTE_META_FILE, isSuccess = true) + + every { + ocFileService.getMetaFileInfo( + fileId = OC_FILE.remoteId!!, + ) + } returns remoteResult + + val result = ocRemoteFileDataSource.getMetaFile( + OC_FILE.remoteId!!, + OC_ACCOUNT_NAME, + ) + + assertEquals(REMOTE_META_FILE.toModel(), result) + + verify(exactly = 1) { + clientManager.getFileService(OC_ACCOUNT_NAME) + ocFileService.getMetaFileInfo(OC_FILE.remoteId!!) + } + } } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt index d765588cfc3..380dddbc230 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt @@ -39,6 +39,7 @@ interface FileRepository { fun getFileByIdAsFlow(fileId: Long): Flow fun getFileWithSyncInfoByIdAsFlow(fileId: Long): Flow fun getFileByRemotePath(remotePath: String, owner: String, spaceId: String? = null): OCFile? + fun getFileFromRemoteId(fileId: String, accountName: String): OCFile? fun getPersonalRootFolderForAccount(owner: String): OCFile fun getSharesRootFolderForAccount(owner: String): OCFile? fun getSearchFolderContent(fileListOption: FileListOption, folderId: Long, search: String): List @@ -67,4 +68,5 @@ interface FileRepository { fun updateFileWithNewAvailableOfflineStatus(ocFile: OCFile, newAvailableOfflineStatus: AvailableOfflineStatus) fun updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory: String, newDirectory: String) fun updateFileWithLastUsage(fileId: Long, lastUsage: Long?) + } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/model/OCMetaFile.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/model/OCMetaFile.kt new file mode 100644 index 00000000000..629746b2d8a --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/model/OCMetaFile.kt @@ -0,0 +1,28 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.files.model + +data class OCMetaFile( + val path: String?, + val id: String? = null, + val fileId: String? = null, + val spaceId: String? = null, +) diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/ManageDeepLinkUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/ManageDeepLinkUseCase.kt index cc711413781..3d03cc2c1d3 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/ManageDeepLinkUseCase.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/ManageDeepLinkUseCase.kt @@ -22,11 +22,15 @@ package com.owncloud.android.domain.files.usecases import com.owncloud.android.domain.BaseUseCaseWithResult import com.owncloud.android.domain.exceptions.DeepLinkException +import com.owncloud.android.domain.files.FileRepository import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.model.OCFile.Companion.PATH_SEPARATOR import java.net.URI -class ManageDeepLinkUseCase : BaseUseCaseWithResult() { +class ManageDeepLinkUseCase( + private val fileRepository: FileRepository, +) : + BaseUseCaseWithResult() { override fun run(params: Params): OCFile? { val path = params.uri.fragment ?: params.uri.path @@ -34,10 +38,11 @@ class ManageDeepLinkUseCase : BaseUseCaseWithResult