diff --git a/app/src/main/java/com/infomaniak/drive/MatomoDrive.kt b/app/src/main/java/com/infomaniak/drive/MatomoDrive.kt index 8a19003bce..b2dd05e258 100644 --- a/app/src/main/java/com/infomaniak/drive/MatomoDrive.kt +++ b/app/src/main/java/com/infomaniak/drive/MatomoDrive.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2022-2024 Infomaniak Network SA + * Copyright (C) 2022-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -121,7 +121,6 @@ object MatomoDrive : Matomo { OpenBookmark("openBookmark"), OpenCreationWebview("openCreationWebview"), OpenFromUserMenuCard("openFromUserMenuCard"), - OpenInBrowser("openInBrowser"), OpenLoginWebview("openLoginWebview"), OpenWith("openWith"), Pause("pause"), @@ -166,6 +165,7 @@ object MatomoDrive : Matomo { TryAddingFileWithDriveFull("tryAddingFileWithDriveFull"), Update("update"), UploadFile("uploadFile"), + ValidatePassword("validatePassword"), ViewGrid("viewGrid"), ViewList("viewList"), } diff --git a/app/src/main/java/com/infomaniak/drive/data/api/ApiRoutes.kt b/app/src/main/java/com/infomaniak/drive/data/api/ApiRoutes.kt index c4457037e6..ce282097cf 100644 --- a/app/src/main/java/com/infomaniak/drive/data/api/ApiRoutes.kt +++ b/app/src/main/java/com/infomaniak/drive/data/api/ApiRoutes.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2022-2025 Infomaniak Network SA + * Copyright (C) 2022-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -114,7 +114,7 @@ object ApiRoutes { //region File url fun getDownloadFileUrl(file: File): String = with(file) { val downloadUrl = if (isPublicShared()) { - downloadPublicShareFile(driveId, publicShareUuid, id) + downloadPublicShareFile(driveId, publicShareUuid, id, publicShareAuthToken) } else { downloadFile(file) } @@ -124,7 +124,7 @@ object ApiRoutes { fun getThumbnailUrl(file: File) = with(file) { when { - isPublicShared() -> getPublicShareFileThumbnail(driveId, publicShareUuid, id) + isPublicShared() -> getPublicShareFileThumbnail(driveId, publicShareUuid, id, publicShareAuthToken) isTrashed() -> thumbnailTrashFile(file) else -> thumbnailFile(file) } @@ -132,17 +132,17 @@ object ApiRoutes { fun getImagePreviewUrl(file: File): String = with(file) { val url = if (isPublicShared()) { - getPublicShareFilePreview(driveId, publicShareUuid, id) + getPublicShareFilePreview(driveId, publicShareUuid, id, publicShareAuthToken) } else { imagePreviewFile(file) } - return "$url?width=2500&height=1500&quality=80" + return url.appendQuery("width=2500&height=1500&quality=80") } fun getOnlyOfficeUrl(file: File) = with(file) { if (isPublicShared()) { - showPublicShareOfficeFile(driveId, publicShareUuid, id) + showPublicShareOfficeFile(driveId, publicShareUuid, id, publicShareAuthToken) } else { "$AUTOLOG_URL?url=${showOffice(file)}" } @@ -313,7 +313,7 @@ object ApiRoutes { /** Public Share */ //region Public share - fun getPublicShareInfo(driveId: Int, linkUuid: String) = "${getPublicShareUrlV2(driveId, linkUuid)}/init" + fun getPublicShareInfo(driveId: Int, linkUuid: String): String = "${getPublicShareUrlV2(driveId, linkUuid)}/init" fun submitPublicSharePassword(driveId: Int, linkUuid: String) = "${getPublicShareUrlV2(driveId, linkUuid)}/auth" @@ -321,30 +321,40 @@ object ApiRoutes { return "$SHARE_URL_V3/$driveId/share/$linkUuid/files/$fileId?$sharedFileWithQuery" } - fun getPublicShareChildrenFiles(driveId: Int, linkUuid: String, fileId: Int, sortType: SortType): String { + fun getPublicShareChildrenFiles( + driveId: Int, + linkUuid: String, + fileId: Int, + sortType: SortType, + authToken: String?, + ): String { val orderQuery = "order_by=${sortType.orderBy}&order=${sortType.order}" - return "$SHARE_URL_V3/$driveId/share/$linkUuid/files/$fileId/files?$sharedFileWithQuery&$orderQuery" + val authParam = authToken?.let { "&sharelink_token=$it" } ?: "" + return "$SHARE_URL_V3/$driveId/share/$linkUuid/files/$fileId/files?$sharedFileWithQuery&$orderQuery$authParam" } fun getPublicShareFileCount(driveId: Int, linkUuid: String, fileId: Int): String { return "${publicShareFile(driveId, linkUuid, fileId)}/count" } - private fun getPublicShareFileThumbnail(driveId: Int, linkUuid: String, fileId: Int): String { - return "${publicShareFile(driveId, linkUuid, fileId)}/thumbnail" + private fun getPublicShareFileThumbnail(driveId: Int, linkUuid: String, fileId: Int, authToken: String? = null): String { + val authParam = authToken?.let { "?sharelink_token=$it" } ?: "" + return "${publicShareFile(driveId, linkUuid, fileId)}/thumbnail$authParam" } - private fun getPublicShareFilePreview(driveId: Int, linkUuid: String, fileId: Int): String { - return "${publicShareFile(driveId, linkUuid, fileId)}/preview" + private fun getPublicShareFilePreview(driveId: Int, linkUuid: String, fileId: Int, authToken: String? = null): String { + val authParam = authToken?.let { "?sharelink_token=$it" } ?: "" + return "${publicShareFile(driveId, linkUuid, fileId)}/preview$authParam" } - private fun downloadPublicShareFile(driveId: Int, linkUuid: String, fileId: Int): String { - return "${publicShareFile(driveId, linkUuid, fileId)}/download" + private fun downloadPublicShareFile(driveId: Int, linkUuid: String, fileId: Int, authToken: String? = null): String { + val authParam = authToken?.let { "?sharelink_token=$it" } ?: "" + return "${publicShareFile(driveId, linkUuid, fileId)}/download$authParam" } - private fun showPublicShareOfficeFile(driveId: Int, linkUuid: String, fileId: Int): String { - // For now, this call fails because the back hasn't dev the conversion of office files to pdf for mobile - return "$SHARE_URL_V1/share/$driveId/$linkUuid/preview/text/$fileId" + private fun showPublicShareOfficeFile(driveId: Int, linkUuid: String, fileId: Int, authToken: String? = null): String { + val authParam = authToken?.let { "?sharelink_token=$it" } ?: "" + return "$SHARE_URL_V1/share/$driveId/$linkUuid/preview/text/$fileId$authParam" } fun importPublicShareFiles(driveId: Int) = "${driveURLV2(driveId)}/imports/sharelink" @@ -353,8 +363,14 @@ object ApiRoutes { return "${getPublicShareUrlV2(driveId, linkUuid)}/archive" } - fun downloadPublicShareArchive(driveId: Int, publicShareUuid: String, archiveUuid: String): String { - return "${buildPublicShareArchive(driveId, publicShareUuid)}/$archiveUuid/download" + fun downloadPublicShareArchive( + driveId: Int, + publicShareUuid: String, + archiveUuid: String, + authToken: String? = null, + ): String { + val authParam = authToken?.let { "?sharelink_token=$it" } ?: "" + return "${buildPublicShareArchive(driveId, publicShareUuid)}/$archiveUuid/download$authParam" } private fun publicShareFile(driveId: Int, linkUuid: String, fileId: Int): String { @@ -446,4 +462,9 @@ object ApiRoutes { private fun showOffice(file: File) = "${OFFICE_URL}/${file.driveId}/${file.id}" //endregion + + fun String.appendQuery(query: String): String { + val querySeparator = if (contains("?")) "&" else "?" + return this + querySeparator + query + } } diff --git a/app/src/main/java/com/infomaniak/drive/data/api/publicshare/PublicShareApiRepository.kt b/app/src/main/java/com/infomaniak/drive/data/api/publicshare/PublicShareApiRepository.kt index 8fc80ded5d..4c2ab785ff 100644 --- a/app/src/main/java/com/infomaniak/drive/data/api/publicshare/PublicShareApiRepository.kt +++ b/app/src/main/java/com/infomaniak/drive/data/api/publicshare/PublicShareApiRepository.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2025 Infomaniak Network SA + * Copyright (C) 2025-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,6 +24,7 @@ import com.infomaniak.core.network.models.ApiResponse import com.infomaniak.core.network.models.ApiResponseStatus import com.infomaniak.core.network.networking.HttpClient import com.infomaniak.drive.data.api.ApiRoutes +import com.infomaniak.drive.data.api.ApiRoutes.appendQuery import com.infomaniak.drive.data.api.ApiRoutes.loadCursor import com.infomaniak.drive.data.api.CursorApiResponse import com.infomaniak.drive.data.models.ArchiveUUID @@ -39,26 +40,33 @@ import okhttp3.OkHttpClient object PublicShareApiRepository { - suspend fun getPublicShareInfo(driveId: Int, linkUuid: String): ApiResponse { - return callApi( + suspend fun getPublicShareInfo(driveId: Int, linkUuid: String, authToken: String? = null): ApiResponse { + return callPublicShareApi( url = ApiRoutes.getPublicShareInfo(driveId, linkUuid), method = GET, + authToken = authToken, okHttpClient = PublicShareHttpClient.okHttpClientWithTokenInterceptor, ) } - suspend fun submitPublicSharePassword(driveId: Int, linkUuid: String, password: String): ApiResponse { - return callApi( + suspend fun submitPublicSharePassword(driveId: Int, linkUuid: String, password: String): ApiResponse { + return callPublicShareApi( url = ApiRoutes.submitPublicSharePassword(driveId, linkUuid), method = POST, body = mapOf("password" to password), ) } - suspend fun getPublicShareRootFile(driveId: Int, linkUuid: String, fileId: FileId): ApiResponse { - return callApi( + suspend fun getPublicShareRootFile( + driveId: Int, + linkUuid: String, + fileId: FileId, + authToken: String? = null, + ): ApiResponse { + return callPublicShareApi( url = ApiRoutes.getPublicShareRootFile(driveId, linkUuid, fileId), method = GET, + authToken = authToken, ) } @@ -68,23 +76,38 @@ object PublicShareApiRepository { folderId: FileId, sortType: SortType, cursor: String?, + authToken: String? = null, ): CursorApiResponse> { - val url = ApiRoutes.getPublicShareChildrenFiles(driveId, linkUuid, folderId, sortType) + "&${loadCursor(cursor)}" + val baseUrl = ApiRoutes.getPublicShareChildrenFiles(driveId, linkUuid, folderId, sortType, authToken) + val url = baseUrl + "&${loadCursor(cursor)}" + return callApiWithCursor(url, GET) } - suspend fun getPublicShareFileCount(driveId: Int, linkUuid: String, fileId: Int): ApiResponse { - return callApi( + suspend fun getPublicShareFileCount( + driveId: Int, + linkUuid: String, + fileId: Int, + authToken: String? = null, + ): ApiResponse { + return callPublicShareApi( url = ApiRoutes.getPublicShareFileCount(driveId, linkUuid, fileId), method = GET, + authToken = authToken, ) } - suspend fun buildPublicShareArchive(driveId: Int, linkUuid: String, archiveBody: ArchiveBody): ApiResponse { - return callApi( + suspend fun buildPublicShareArchive( + driveId: Int, + linkUuid: String, + archiveBody: ArchiveBody, + authToken: String? = null, + ): ApiResponse { + return callPublicShareApi( url = ApiRoutes.buildPublicShareArchive(driveId, linkUuid), method = POST, body = archiveBody, + authToken = authToken, ) } @@ -97,6 +120,7 @@ object PublicShareApiRepository { fileIds: List, exceptedFileIds: List, password: String = "", + authToken: String? = null, ): ApiResponse> { val body: MutableMap = mutableMapOf( @@ -109,9 +133,10 @@ object PublicShareApiRepository { if (fileIds.isNotEmpty()) body["file_ids"] = fileIds.toTypedArray() if (exceptedFileIds.isNotEmpty()) body["except_file_ids"] = exceptedFileIds.toTypedArray() - return callApi( + return callPublicShareApi( url = ApiRoutes.importPublicShareFiles(destinationDriveId), method = POST, + authToken = authToken, body = body, okHttpClient = AccountUtils.getHttpClient( userId = destinationUserId, @@ -121,13 +146,15 @@ object PublicShareApiRepository { ) } - private suspend inline fun callApi( + private suspend inline fun callPublicShareApi( url: String, method: ApiController.ApiMethod, + authToken: String? = null, body: Any? = null, okHttpClient: OkHttpClient = HttpClient.okHttpClient, ): T { - return ApiController.callApi(url, method, body, okHttpClient) + val authentifiedUrl = authToken?.let { token -> url.appendQuery("sharelink_token=$token") } ?: url + return ApiController.callApi(authentifiedUrl, method, body, okHttpClient) } private suspend inline fun callApiWithCursor( diff --git a/app/src/main/java/com/infomaniak/drive/data/api/publicshare/PublicShareToken.kt b/app/src/main/java/com/infomaniak/drive/data/api/publicshare/PublicShareToken.kt new file mode 100644 index 0000000000..e4daf93012 --- /dev/null +++ b/app/src/main/java/com/infomaniak/drive/data/api/publicshare/PublicShareToken.kt @@ -0,0 +1,26 @@ +/* + * Infomaniak kDrive - Android + * Copyright (C) 2025-2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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.infomaniak.drive.data.api.publicshare + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class PublicShareToken( + val token: String = "", +) : Parcelable diff --git a/app/src/main/java/com/infomaniak/drive/data/models/File.kt b/app/src/main/java/com/infomaniak/drive/data/models/File.kt index 37a9c70dc8..32f5e8b61d 100644 --- a/app/src/main/java/com/infomaniak/drive/data/models/File.kt +++ b/app/src/main/java/com/infomaniak/drive/data/models/File.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2022-2025 Infomaniak Network SA + * Copyright (C) 2022-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -161,6 +161,11 @@ open class File( @Transient var publicShareUuid: String = "" + @IgnoredOnParcel + @Ignore + @Transient + var publicShareAuthToken: String? = null + val revisedAtInMillis: Long inline get() = revisedAt * 1000 fun initUid() { diff --git a/app/src/main/java/com/infomaniak/drive/ui/LaunchActivity.kt b/app/src/main/java/com/infomaniak/drive/ui/LaunchActivity.kt index dd8671e73d..49a570576a 100644 --- a/app/src/main/java/com/infomaniak/drive/ui/LaunchActivity.kt +++ b/app/src/main/java/com/infomaniak/drive/ui/LaunchActivity.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2022-2025 Infomaniak Network SA + * Copyright (C) 2022-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/app/src/main/java/com/infomaniak/drive/ui/fileList/FileListViewModel.kt b/app/src/main/java/com/infomaniak/drive/ui/fileList/FileListViewModel.kt index 686c1ba1da..104e66c792 100644 --- a/app/src/main/java/com/infomaniak/drive/ui/fileList/FileListViewModel.kt +++ b/app/src/main/java/com/infomaniak/drive/ui/fileList/FileListViewModel.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2022-2025 Infomaniak Network SA + * Copyright (C) 2022-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -228,7 +228,12 @@ class FileListViewModel(application: Application) : AndroidViewModel(application fun getFileCount(folder: File): LiveData = liveData(Dispatchers.IO) { lastItemCount?.let { emit(it) } val apiResponse = if (folder.isPublicShared()) { - PublicShareApiRepository.getPublicShareFileCount(folder.driveId, folder.publicShareUuid, folder.id) + PublicShareApiRepository.getPublicShareFileCount( + driveId = folder.driveId, + linkUuid = folder.publicShareUuid, + fileId = folder.id, + authToken = folder.publicShareAuthToken, + ) } else { ApiRepository.getFileCount(folder) } diff --git a/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareActivity.kt b/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareActivity.kt index efd511120c..7b3a2c943c 100644 --- a/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareActivity.kt +++ b/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareActivity.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2024-2025 Infomaniak Network SA + * Copyright (C) 2024-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,6 +24,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.navigation.NavDestination import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.navArgs import com.infomaniak.core.legacy.utils.setMargins import com.infomaniak.core.twofactorauth.front.TwoFactorAuthApprovalAutoManagedBottomSheet import com.infomaniak.core.twofactorauth.front.addComposeOverlay @@ -39,6 +40,7 @@ import com.infomaniak.drive.utils.IOFile class PublicShareActivity : EdgeToEdgeActivity() { private val binding by lazy { ActivityPublicShareBinding.inflate(layoutInflater) } + private val navigationArgs: PublicShareActivityArgs by navArgs() private val publicShareViewModel: PublicShareViewModel by viewModels() private val navController by lazy { (supportFragmentManager.findFragmentById(R.id.hostFragment) as NavHostFragment).navController @@ -55,6 +57,8 @@ class PublicShareActivity : EdgeToEdgeActivity() { view.setMargins(left = margin + insets.left, right = margin + insets.right, bottom = margin + insets.bottom) } onBackPressedDispatcher.addCallback { finishAndRemoveTask() } + + publicShareViewModel.rootFileId = navigationArgs.fileId } override fun onDestroy() { diff --git a/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareListFragment.kt b/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareListFragment.kt index 013abcf660..418dac2854 100644 --- a/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareListFragment.kt +++ b/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareListFragment.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2024-2025 Infomaniak Network SA + * Copyright (C) 2024-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -31,6 +31,7 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.infomaniak.core.fragmentnavigation.safelyNavigate import com.infomaniak.core.legacy.utils.SnackbarUtils.showSnackbar import com.infomaniak.core.legacy.utils.safeNavigate import com.infomaniak.core.legacy.utils.whenResultIsOk @@ -59,6 +60,7 @@ import com.infomaniak.drive.utils.FilePresenter.openBookmarkIntent import com.infomaniak.drive.utils.FilePresenter.openFolder import com.infomaniak.drive.utils.IOFile import com.infomaniak.drive.utils.PublicShareUtils +import com.infomaniak.drive.utils.Utils import com.infomaniak.drive.views.FileInfoActionsView.OnItemClickListener.Companion.downloadFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.invoke @@ -85,11 +87,11 @@ class PublicShareListFragment : FileListFragment() { super.onCreate(savedInstanceState) if (publicShareViewModel.isPasswordNeeded && !publicShareViewModel.hasBeenAuthenticated) { - safeNavigate(PublicShareListFragmentDirections.actionPublicShareListFragmentToPublicSharePasswordFragment()) + safelyNavigate(PublicShareListFragmentDirections.actionPublicShareListFragmentToPublicSharePasswordFragment()) } if (publicShareViewModel.isExpired) { - safeNavigate(PublicShareListFragmentDirections.actionPublicShareListFragmentToPublicShareOutdatedFragment()) + safelyNavigate(PublicShareListFragmentDirections.actionPublicShareListFragmentToPublicShareOutdatedFragment()) } } @@ -102,6 +104,13 @@ class PublicShareListFragment : FileListFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { folderName = navigationArgs.fileName folderId = navigationArgs.fileId + if (publicShareViewModel.rootFileId == PUBLIC_SHARE_DEFAULT_ID) { + // This means we're coming from the PublicSharePasswordFragment, and the rootFolderId hasn't been set yet + // because we couldn't access the init call before opening the PublicShareActivity + // If the rootFileId is already set, we should not update it again as navigation.fileId change at each folder. + publicShareViewModel.rootFileId = navigationArgs.fileId + } + publicShareViewModel.cancelDownload() downloadFiles = DownloadFiles() @@ -298,7 +307,7 @@ class PublicShareListFragment : FileListFragment() { } else { val rootSharedFileId = publicShareViewModel.rootSharedFile.value?.id val fileIds = multiSelectManager.selectedItemsIds.toList().ifEmpty { - if (folderId == rootSharedFileId || publicShareViewModel.fileId == rootSharedFileId) { + if (folderId == rootSharedFileId || publicShareViewModel.rootFileId == rootSharedFileId) { emptyList() } else { listOf(folderId) @@ -337,7 +346,7 @@ class PublicShareListFragment : FileListFragment() { } companion object { - const val PUBLIC_SHARE_DEFAULT_ID = -1 + const val PUBLIC_SHARE_DEFAULT_ID = Utils.OTHER_ROOT_ID } private inner class DownloadFiles : (Boolean, Boolean) -> Unit { @@ -351,7 +360,7 @@ class PublicShareListFragment : FileListFragment() { childrenLiveData.value = emptyFilesResult cancelDownload() - if (folderId == ROOT_SHARED_FILE_ID || rootSharedFile.value == null) { + if (folderId == ROOT_SHARED_FILE_ID || !isFolder()) { downloadPublicShareRootFile() } else { getFiles(folderId, fileListViewModel.sortType, isNewSort) diff --git a/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareMultiSelectActionsBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareMultiSelectActionsBottomSheetDialog.kt index 8dc047993d..007d5a5e98 100644 --- a/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareMultiSelectActionsBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareMultiSelectActionsBottomSheetDialog.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2024 Infomaniak Network SA + * Copyright (C) 2024-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -63,7 +63,12 @@ class PublicShareMultiSelectActionsBottomSheetDialog : MultiSelectActionsBottomS private fun observeArchiveUuid() = with(publicShareViewModel) { buildArchiveResult.observe(viewLifecycleOwner) { (error, archiveUuid) -> archiveUuid?.let { - val downloadURL = ApiRoutes.downloadPublicShareArchive(driveId, publicShareUuid, it.uuid) + val downloadURL = ApiRoutes.downloadPublicShareArchive( + driveId = driveId, + publicShareUuid = publicShareUuid, + archiveUuid = it.uuid, + authToken = submitPasswordResult.value, + ) val userBearerToken = AccountUtils.currentUser?.apiToken?.accessToken DownloadManagerUtils.scheduleDownload( context = requireContext(), diff --git a/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicSharePasswordFragment.kt b/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicSharePasswordFragment.kt index 75ce711125..53882c76e5 100644 --- a/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicSharePasswordFragment.kt +++ b/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicSharePasswordFragment.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2024-2025 Infomaniak Network SA + * Copyright (C) 2024-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,11 +24,11 @@ import android.view.ViewGroup import androidx.core.widget.addTextChangedListener import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import com.infomaniak.core.fragmentnavigation.safelyNavigate import com.infomaniak.core.legacy.utils.SnackbarUtils.showSnackbar import com.infomaniak.core.legacy.utils.hideProgressCatching import com.infomaniak.core.legacy.utils.initProgress import com.infomaniak.core.legacy.utils.safeBinding -import com.infomaniak.core.legacy.utils.safeNavigate import com.infomaniak.core.legacy.utils.setMargins import com.infomaniak.core.legacy.utils.showProgressCatching import com.infomaniak.core.network.models.ApiError @@ -36,13 +36,12 @@ import com.infomaniak.core.sentry.SentryLog import com.infomaniak.drive.MatomoDrive.MatomoName import com.infomaniak.drive.MatomoDrive.trackPublicShareActionEvent import com.infomaniak.drive.R -import com.infomaniak.drive.SHARE_URL_V1 import com.infomaniak.drive.data.models.ShareLink import com.infomaniak.drive.databinding.FragmentPublicSharePasswordBinding import com.infomaniak.drive.extensions.enableEdgeToEdge import com.infomaniak.drive.ui.publicShare.PublicShareActivity.Companion.PUBLIC_SHARE_TAG import com.infomaniak.drive.ui.publicShare.PublicShareListFragment.Companion.PUBLIC_SHARE_DEFAULT_ID -import com.infomaniak.drive.utils.PublicShareUtils +import handleActionDone import com.infomaniak.core.network.models.exceptions.NetworkException as ApiControllerNetworkException class PublicSharePasswordFragment : Fragment() { @@ -57,13 +56,8 @@ class PublicSharePasswordFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?): Unit = with(binding) { super.onViewCreated(view, savedInstanceState) - // TODO: Remove this and call setupValidationButton instead - // Also change the layout (description, button's title, input visibility) - passwordValidateButton.setOnClickListener { - trackPublicShareActionEvent(MatomoName.OpenInBrowser) - PublicShareUtils.openDeepLinkInBrowser(requireActivity(), getPublicShareUrl()) - } - + setupValidationButton() + publicSharePasswordEditText.handleActionDone(::attemptPasswordValidation) publicSharePasswordEditText.addTextChangedListener { publicSharePasswordLayout.error = null } observeSubmitPasswordResult() observeInitResult() @@ -75,28 +69,25 @@ class PublicSharePasswordFragment : Fragment() { } } - //region Hack TODO: Remove this when the back will support bearer token - private fun getPublicShareUrl(): String { - return "${SHARE_URL_V1}/share/${publicShareViewModel.driveId}/${publicShareViewModel.publicShareUuid}" - } - //endregion - private fun setupValidationButton() = with(binding.passwordValidateButton) { initProgress(viewLifecycleOwner) - setOnClickListener { - if (isFieldBlank()) return@setOnClickListener + setOnClickListener { attemptPasswordValidation() } + } - showProgressCatching() - val password = binding.publicSharePasswordEditText.text?.trim().toString() - publicShareViewModel.submitPublicSharePassword(password) - } + private fun attemptPasswordValidation() { + if (isFieldBlank()) return + + binding.passwordValidateButton.showProgressCatching() + trackPublicShareActionEvent(MatomoName.ValidatePassword) + val password = binding.publicSharePasswordEditText.text?.trim().toString() + publicShareViewModel.submitPublicSharePassword(password) } private fun observeSubmitPasswordResult() = with(binding) { - publicShareViewModel.submitPasswordResult.observe(viewLifecycleOwner) { isAuthorized -> - if (isAuthorized == true) { + publicShareViewModel.submitPasswordResult.observe(viewLifecycleOwner) { authToken -> + if (authToken.isNotEmpty()) { publicShareViewModel.hasBeenAuthenticated = true - publicShareViewModel.initPublicShare() + publicShareViewModel.initPublicShare(authToken) } else { passwordValidateButton.hideProgressCatching(R.string.buttonValid) publicSharePasswordEditText.text = null @@ -114,9 +105,11 @@ class PublicSharePasswordFragment : Fragment() { private fun onInitSuccess(shareLink: ShareLink?) { binding.passwordValidateButton.hideProgressCatching(R.string.buttonValid) publicShareViewModel.canDownloadFiles = shareLink?.capabilities?.canDownload == true - safeNavigate( + val fileId = shareLink?.fileId ?: PUBLIC_SHARE_DEFAULT_ID + safelyNavigate( PublicSharePasswordFragmentDirections.actionPublicSharePasswordFragmentToPublicShareListFragment( - fileId = shareLink?.fileId ?: PUBLIC_SHARE_DEFAULT_ID, + fileId = fileId, + fileName = getString(R.string.sharedWithMeTitle) ) ) } diff --git a/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareViewModel.kt b/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareViewModel.kt index 2122a4a56b..aa9c666435 100644 --- a/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareViewModel.kt +++ b/app/src/main/java/com/infomaniak/drive/ui/publicShare/PublicShareViewModel.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2024-2025 Infomaniak Network SA + * Copyright (C) 2024-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -37,6 +37,7 @@ import com.infomaniak.drive.data.models.File import com.infomaniak.drive.data.models.File.SortType import com.infomaniak.drive.data.models.ShareLink import com.infomaniak.drive.ui.fileList.BaseDownloadProgressDialog.DownloadAction +import com.infomaniak.drive.ui.publicShare.PublicShareListFragment.Companion.PUBLIC_SHARE_DEFAULT_ID import com.infomaniak.drive.utils.IOFile import io.sentry.Sentry import kotlinx.coroutines.Job @@ -58,9 +59,10 @@ class PublicShareViewModel(application: Application, val savedStateHandle: Saved val buildArchiveResult = SingleLiveEvent>() val initPublicShareResult = SingleLiveEvent>() val importPublicShareResult = SingleLiveEvent() - val submitPasswordResult = SingleLiveEvent() + val submitPasswordResult = SingleLiveEvent() var hasBeenAuthenticated = false var canDownloadFiles = canDownload + var rootFileId = PUBLIC_SHARE_DEFAULT_ID private val _fetchCacheFileForActionResult = MutableSharedFlow>( extraBufferCapacity = 1, @@ -71,9 +73,6 @@ class PublicShareViewModel(application: Application, val savedStateHandle: Saved val driveId: Int inline get() = savedStateHandle[PublicShareActivityArgs::driveId.name] ?: ROOT_SHARED_FILE_ID - val fileId: Int - inline get() = savedStateHandle[PublicShareActivityArgs::fileId.name] ?: ROOT_SHARED_FILE_ID - val publicShareUuid: String inline get() = savedStateHandle[PublicShareActivityArgs::publicShareUuid.name] ?: "" @@ -94,33 +93,42 @@ class PublicShareViewModel(application: Application, val savedStateHandle: Saved super.onCleared() } - fun initPublicShare() = viewModelScope.launch { - val apiResponse = PublicShareApiRepository.getPublicShareInfo(driveId, publicShareUuid) + fun initPublicShare(authToken: String? = null) = viewModelScope.launch { + val apiResponse = PublicShareApiRepository.getPublicShareInfo(driveId, publicShareUuid, authToken) val result = if (apiResponse.isSuccess()) null to apiResponse.data else apiResponse.error to null initPublicShareResult.postValue(result) } fun submitPublicSharePassword(password: String) = viewModelScope.launch { - val passwordResult = PublicShareApiRepository.submitPublicSharePassword( + val token = PublicShareApiRepository.submitPublicSharePassword( driveId = driveId, linkUuid = publicShareUuid, password = password, - ).data + ).data?.token ?: "" - submitPasswordResult.postValue(passwordResult) + submitPasswordResult.postValue(token) } fun downloadPublicShareRootFile() = viewModelScope.launch { - val file = if (fileId == ROOT_SHARED_FILE_ID) { + val file = if (rootFileId == ROOT_SHARED_FILE_ID) { rootSharedFile.value } else { - val apiResponse = PublicShareApiRepository.getPublicShareRootFile(driveId, publicShareUuid, fileId) + val apiResponse = PublicShareApiRepository.getPublicShareRootFile( + driveId = driveId, + linkUuid = publicShareUuid, + fileId = rootFileId, + authToken = submitPasswordResult.value, + ) if (!apiResponse.isSuccess()) SentryLog.w(TAG, "downloadSharedFile: ${apiResponse.error?.code}") apiResponse.data } - rootSharedFile.postValue(file?.apply { publicShareUuid = this@PublicShareViewModel.publicShareUuid }) + val publicShareFile = file?.apply { + publicShareUuid = this@PublicShareViewModel.publicShareUuid + publicShareAuthToken = submitPasswordResult.value + } + rootSharedFile.postValue(publicShareFile) } fun getFiles(folderId: Int, sortType: SortType, isNewSort: Boolean) { @@ -140,7 +148,7 @@ class PublicShareViewModel(application: Application, val savedStateHandle: Saved val newFiles = mutableListOf().apply { childrenLiveData.value?.files?.let(::addAll) - addAll(folderFilesProviderResult.folderFiles.addPublicShareUuid()) + addAll(folderFilesProviderResult.folderFiles.addPublicShareInfo()) if (any(File::isFolder)) sortByDescending(File::isFolder) } @@ -175,6 +183,7 @@ class PublicShareViewModel(application: Application, val savedStateHandle: Saved destinationFolderId = destinationFolderId, fileIds = fileIds, exceptedFileIds = exceptedFileIds, + authToken = submitPasswordResult.value, ) val error = if (apiResponse.isSuccess()) null else apiResponse.translateError() val destinationPath = "$SHARE_URL_V1/drive/$destinationDriveId/files/$destinationFolderId" @@ -187,7 +196,12 @@ class PublicShareViewModel(application: Application, val savedStateHandle: Saved } fun buildArchive(archiveBody: ArchiveUUID.ArchiveBody) = viewModelScope.launch { - val apiResponse = PublicShareApiRepository.buildPublicShareArchive(driveId, publicShareUuid, archiveBody) + val apiResponse = PublicShareApiRepository.buildPublicShareArchive( + driveId = driveId, + linkUuid = publicShareUuid, + archiveBody = archiveBody, + authToken = submitPasswordResult.value, + ) val result = apiResponse.data?.let { archiveUuid -> null to archiveUuid } ?: (apiResponse.translateError() to null) buildArchiveResult.postValue(result) @@ -225,6 +239,7 @@ class PublicShareViewModel(application: Application, val savedStateHandle: Saved folderId = folderFilesProviderArgs.folderId, sortType = folderFilesProviderArgs.order, cursor = currentCursor, + authToken = submitPasswordResult.value, ).let { CursorApiResponse( result = it.result, @@ -258,7 +273,14 @@ class PublicShareViewModel(application: Application, val savedStateHandle: Saved } } - private fun List.addPublicShareUuid() = map { it.apply { publicShareUuid = this@PublicShareViewModel.publicShareUuid } } + private fun List.addPublicShareInfo() = map { + it.apply { + publicShareUuid = this@PublicShareViewModel.publicShareUuid + publicShareAuthToken = submitPasswordResult.value + } + } + + fun isFolder(): Boolean = rootSharedFile.value?.isFolder() == true data class PublicShareFilesResult( val files: List, diff --git a/app/src/main/java/com/infomaniak/drive/utils/PreviewUtils.kt b/app/src/main/java/com/infomaniak/drive/utils/PreviewUtils.kt index c5819b0d47..b45220b8a3 100644 --- a/app/src/main/java/com/infomaniak/drive/utils/PreviewUtils.kt +++ b/app/src/main/java/com/infomaniak/drive/utils/PreviewUtils.kt @@ -1,6 +1,6 @@ /* * Infomaniak kDrive - Android - * Copyright (C) 2024-2025 Infomaniak Network SA + * Copyright (C) 2024-2026 Infomaniak Network SA * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -39,6 +39,7 @@ import com.infomaniak.drive.MatomoDrive.trackPdfActivityActionEvent import com.infomaniak.drive.MatomoDrive.trackPublicShareActionEvent import com.infomaniak.drive.R import com.infomaniak.drive.data.api.ApiRoutes +import com.infomaniak.drive.data.api.ApiRoutes.appendQuery import com.infomaniak.drive.data.models.File import com.infomaniak.drive.data.models.UserDrive import com.infomaniak.drive.ui.SaveExternalFilesActivity @@ -183,7 +184,8 @@ suspend fun downloadFile( isPublicShared: Boolean, ) { Dispatchers.IO { if (externalOutputFile.exists()) externalOutputFile.delete() } - val downloadUrl = ApiRoutes.getDownloadFileUrl(file) + if (file.isOnlyOfficePreview()) "?as=pdf" else "" + val baseDownloadUrl = ApiRoutes.getDownloadFileUrl(file) + val downloadUrl = if (file.isOnlyOfficePreview()) baseDownloadUrl.appendQuery("as=pdf") else baseDownloadUrl val downloadProgressInterceptor = DownloadOfflineFileManager.downloadProgressInterceptor(onProgress = onProgress) val okHttpClient = when { isPublicShared -> unauthenticatedHttpClient diff --git a/app/src/main/res/layout/fragment_public_share_password.xml b/app/src/main/res/layout/fragment_public_share_password.xml index 00ffe1e8f5..c057381223 100644 --- a/app/src/main/res/layout/fragment_public_share_password.xml +++ b/app/src/main/res/layout/fragment_public_share_password.xml @@ -1,6 +1,6 @@