diff --git a/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/ToggleLayoutType.kt b/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/ToggleLayoutType.kt index ce6ed66f..55f167d1 100644 --- a/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/ToggleLayoutType.kt +++ b/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/ToggleLayoutType.kt @@ -19,13 +19,14 @@ package me.proton.drive.android.settings.domain.usecase import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.domain.util.coRunCatching import me.proton.drive.android.settings.domain.entity.LayoutType import javax.inject.Inject class ToggleLayoutType @Inject constructor( private val updateLayoutType: UpdateLayoutType ) { - suspend operator fun invoke(userId: UserId, currentLayoutType: LayoutType) = + suspend operator fun invoke(userId: UserId, currentLayoutType: LayoutType) = coRunCatching { updateLayoutType( userId = userId, layoutType = if (currentLayoutType == LayoutType.GRID) { @@ -33,5 +34,6 @@ class ToggleLayoutType @Inject constructor( } else { LayoutType.GRID } - ) + ).getOrThrow() + } } diff --git a/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/UpdateLayoutType.kt b/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/UpdateLayoutType.kt index 6edadbf9..8ea8f3d0 100644 --- a/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/UpdateLayoutType.kt +++ b/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/UpdateLayoutType.kt @@ -19,6 +19,7 @@ package me.proton.drive.android.settings.domain.usecase import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.domain.util.coRunCatching import me.proton.drive.android.settings.domain.UiSettingsRepository import me.proton.drive.android.settings.domain.entity.LayoutType import javax.inject.Inject @@ -26,6 +27,7 @@ import javax.inject.Inject class UpdateLayoutType @Inject constructor( private val repository: UiSettingsRepository ) { - suspend operator fun invoke(userId: UserId, layoutType: LayoutType) = + suspend operator fun invoke(userId: UserId, layoutType: LayoutType) = coRunCatching { repository.updateLayoutType(userId, layoutType) + } } diff --git a/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/UpdateThemeStyle.kt b/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/UpdateThemeStyle.kt index 8ed1aebe..eb3999ab 100644 --- a/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/UpdateThemeStyle.kt +++ b/app-ui-settings/src/main/java/me/proton/drive/android/settings/domain/usecase/UpdateThemeStyle.kt @@ -19,6 +19,7 @@ package me.proton.drive.android.settings.domain.usecase import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.domain.util.coRunCatching import me.proton.drive.android.settings.domain.UiSettingsRepository import me.proton.drive.android.settings.domain.entity.ThemeStyle import javax.inject.Inject @@ -26,6 +27,7 @@ import javax.inject.Inject class UpdateThemeStyle @Inject constructor( private val repository: UiSettingsRepository ) { - suspend operator fun invoke(userId: UserId, themeStyle: ThemeStyle) = + suspend operator fun invoke(userId: UserId, themeStyle: ThemeStyle) = coRunCatching { repository.updateThemeStyle(userId, themeStyle) + } } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9059c98e..ce436457 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,7 +27,6 @@ plugins { id("dagger.hilt.android.plugin") id("kotlin-parcelize") kotlin("android") - kotlin("kapt") id("me.proton.core.gradle-plugins.environment-config") version "1.3.0" } @@ -82,8 +81,7 @@ driveModule( implementation(libs.treessence) androidTestImplementation(libs.dagger.hilt.android.testing) - kapt(libs.dagger.hilt.android.compiler) - kaptAndroidTest(libs.dagger.hilt.android.compiler) + add("kspAndroidTest", libs.dagger.hilt.android.compiler) androidTestUtil(libs.androidx.test.orchestrator) androidTestUtil(libs.androidx.test.services) diff --git a/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt b/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt index a91bbbc2..d0068342 100644 --- a/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt +++ b/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt @@ -48,6 +48,7 @@ import me.proton.android.drive.provider.AppBuildConfigFieldsProvider import me.proton.android.drive.provider.BuildConfigurationProvider import me.proton.android.drive.provider.AppProtonDriveClientProvider import me.proton.android.drive.provider.AppProtonPhotosClientProvider +import me.proton.android.drive.provider.AppProtonSdkClientProvider import me.proton.android.drive.repository.BridgeFindDuplicatesRepository import me.proton.android.drive.repository.ClientUidRepositoryImpl import me.proton.android.drive.settings.DebugSettings @@ -75,6 +76,7 @@ import me.proton.core.drive.base.domain.usecase.DriveUrlBuilder import me.proton.core.drive.documentsprovider.domain.usecase.GetDocumentsProviderRoots import me.proton.core.drive.folder.create.domain.provider.OpenFolderActionProvider import me.proton.core.drive.key.domain.handler.PublicKeyEventHandler +import me.proton.core.drive.link.domain.provider.ProtonSdkClientProvider import me.proton.core.drive.log.domain.handler.LogEventHandler import me.proton.core.drive.messagequeue.domain.ActionProvider import me.proton.core.drive.notification.data.provider.NotificationBuilderProvider @@ -250,6 +252,10 @@ abstract class ApplicationBindsModule { @Singleton abstract fun bindsAppProtonPhotosClientProvider(impl: AppProtonPhotosClientProvider): ProtonPhotosClientProvider + @Binds + @Singleton + abstract fun bindsAppProtonSdkClientProvider(impl: AppProtonSdkClientProvider): ProtonSdkClientProvider + @Binds @IntoSet abstract fun bindsOpenFolderActionProvider(impl: OpenFolderActionProvider): ActionProvider diff --git a/app/src/main/kotlin/me/proton/android/drive/initializer/DownloadInitializer.kt b/app/src/main/kotlin/me/proton/android/drive/initializer/DownloadInitializer.kt index 2f1f1ae1..841b8a68 100644 --- a/app/src/main/kotlin/me/proton/android/drive/initializer/DownloadInitializer.kt +++ b/app/src/main/kotlin/me/proton/android/drive/initializer/DownloadInitializer.kt @@ -107,7 +107,9 @@ class DownloadInitializer : Initializer { downloadErrorHandlers.forEach { handler -> coRunCatching { handler.onError(downloadError) - }.getOrNull(LogTag.DOWNLOAD, "Failed to handle download error") + }.onFailure { error -> + error.log(LogTag.DOWNLOAD, "Failed to handle download error") + } } } diff --git a/app/src/main/kotlin/me/proton/android/drive/initializer/ProtonDriveSdkInitializer.kt b/app/src/main/kotlin/me/proton/android/drive/initializer/ProtonDriveSdkInitializer.kt index d8893e90..0c66c420 100644 --- a/app/src/main/kotlin/me/proton/android/drive/initializer/ProtonDriveSdkInitializer.kt +++ b/app/src/main/kotlin/me/proton/android/drive/initializer/ProtonDriveSdkInitializer.kt @@ -28,8 +28,7 @@ import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import me.proton.android.drive.provider.AppProtonDriveClientProvider -import me.proton.android.drive.provider.AppProtonPhotosClientProvider +import me.proton.android.drive.provider.AppProtonSdkClientProvider import me.proton.core.accountmanager.domain.AccountManager import me.proton.core.accountmanager.presentation.observe import me.proton.core.accountmanager.presentation.onAccountRemoved @@ -49,8 +48,7 @@ class ProtonDriveSdkInitializer : Initializer { ).run { accountManager.observe(appLifecycleProvider.lifecycle, Lifecycle.State.RESUMED) .onAccountRemoved { account -> - appProtonDriveClientProvider.remove(account.userId) - appProtonPhotosClientProvider.remove(account.userId) + appProtonSdkClientProvider.remove(account.userId) } } } @@ -66,6 +64,5 @@ interface SdkInitializerEntryPoint { val configurationProvider: ConfigurationProvider val accountManager: AccountManager val appLifecycleProvider: AppLifecycleProvider - val appProtonDriveClientProvider: AppProtonDriveClientProvider - val appProtonPhotosClientProvider: AppProtonPhotosClientProvider + val appProtonSdkClientProvider: AppProtonSdkClientProvider } diff --git a/app/src/main/kotlin/me/proton/android/drive/initializer/ShortcutInitializer.kt b/app/src/main/kotlin/me/proton/android/drive/initializer/ShortcutInitializer.kt index c027a8e9..f9250280 100644 --- a/app/src/main/kotlin/me/proton/android/drive/initializer/ShortcutInitializer.kt +++ b/app/src/main/kotlin/me/proton/android/drive/initializer/ShortcutInitializer.kt @@ -65,6 +65,12 @@ class ShortcutInitializer : Initializer { } .onAccountRemoved { updateDynamicShortcuts(emptyList()) + .onFailure { error -> + error.log( + tag = LogTag.DEFAULT, + message = "Failed to clear dynamic shortcuts", + ) + } } } } diff --git a/app/src/main/kotlin/me/proton/android/drive/initializer/TagsMigrationInitializer.kt b/app/src/main/kotlin/me/proton/android/drive/initializer/TagsMigrationInitializer.kt index 764a14e0..e8b52a86 100644 --- a/app/src/main/kotlin/me/proton/android/drive/initializer/TagsMigrationInitializer.kt +++ b/app/src/main/kotlin/me/proton/android/drive/initializer/TagsMigrationInitializer.kt @@ -133,7 +133,7 @@ class TagsMigrationInitializer : Initializer { driveLink = getDriveLink(fileId).toResult().getOrThrow(), retryable = true, networkType = NetworkType.UNMETERED, - ) + ).getOrThrow() }.onFailure { error -> error.log( PHOTO, diff --git a/app/src/main/kotlin/me/proton/android/drive/initializer/WebViewInitializer.kt b/app/src/main/kotlin/me/proton/android/drive/initializer/WebViewInitializer.kt index fa81e0da..fc648e84 100644 --- a/app/src/main/kotlin/me/proton/android/drive/initializer/WebViewInitializer.kt +++ b/app/src/main/kotlin/me/proton/android/drive/initializer/WebViewInitializer.kt @@ -29,6 +29,7 @@ import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import me.proton.android.drive.extension.log import me.proton.core.drive.base.domain.extension.getOrNull import me.proton.core.drive.base.domain.log.LogTag import me.proton.core.presentation.app.AppLifecycleProvider @@ -51,7 +52,9 @@ class WebViewInitializer : Initializer { CoreLogger.d(LogTag.WEBVIEW, "Start safe browsing: $isSuccess") } } - }.getOrNull(LogTag.WEBVIEW, "startSafeBrowsing failed") + }.onFailure { error -> + error.log(LogTag.WEBVIEW, "startSafeBrowsing failed") + } } } } diff --git a/app/src/main/kotlin/me/proton/android/drive/notification/AppNotificationBuilderProvider.kt b/app/src/main/kotlin/me/proton/android/drive/notification/AppNotificationBuilderProvider.kt index 8f2e7578..40cbc93a 100644 --- a/app/src/main/kotlin/me/proton/android/drive/notification/AppNotificationBuilderProvider.kt +++ b/app/src/main/kotlin/me/proton/android/drive/notification/AppNotificationBuilderProvider.kt @@ -21,6 +21,7 @@ package me.proton.android.drive.notification import androidx.core.app.NotificationCompat import me.proton.android.drive.usecase.notification.BackupNotificationBuilder import me.proton.android.drive.usecase.notification.DownloadNotificationBuilder +import me.proton.android.drive.usecase.notification.DownloadFileProgressNotificationBuilder import me.proton.android.drive.usecase.notification.ForcedSignOutNotificationBuilder import me.proton.android.drive.usecase.notification.NoSpaceLeftOnDeviceNotificationBuilder import me.proton.android.drive.usecase.notification.StorageFullNotificationBuilder @@ -35,6 +36,7 @@ class AppNotificationBuilderProvider @Inject constructor( private val storageFullBuilder: StorageFullNotificationBuilder, private val uploadNotificationBuilder: UploadNotificationBuilder, private val downloadNotificationBuilder: DownloadNotificationBuilder, + private val downloadFileProgressNotificationBuilder: DownloadFileProgressNotificationBuilder, private val forcedSignOutNotificationBuilder: ForcedSignOutNotificationBuilder, private val noSpaceLeftOnDeviceNotificationBuilder: NoSpaceLeftOnDeviceNotificationBuilder, private val backupNotificationBuilder: BackupNotificationBuilder, @@ -55,10 +57,15 @@ class AppNotificationBuilderProvider @Inject constructor( notificationId = requireIsInstance(notificationId), events = events as List, ) - events.size == 1 && events.first() is Event.Download -> + events.isNotEmpty() && events.all { it is Event.Download } -> downloadNotificationBuilder( notificationId = requireIsInstance(notificationId), - event = events.first() as Event.Download, + events = events as List, + ) + events.isNotEmpty() && events.all { it is Event.DownloadFileProgress } -> + downloadFileProgressNotificationBuilder( + notificationId = requireIsInstance(notificationId), + events = events as List, ) events.size == 1 && events.first() is Event.ForcedSignOut -> forcedSignOutNotificationBuilder( diff --git a/app/src/main/kotlin/me/proton/android/drive/provider/AppProtonSdkClientProvider.kt b/app/src/main/kotlin/me/proton/android/drive/provider/AppProtonSdkClientProvider.kt new file mode 100644 index 00000000..4d90620b --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/provider/AppProtonSdkClientProvider.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive 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. + * + * Proton Drive 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 Proton Drive. If not, see . + */ + +package me.proton.android.drive.provider + +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.domain.extension.toResult +import me.proton.core.drive.base.domain.util.coRunCatching +import me.proton.core.drive.drivelink.domain.usecase.GetVolumeType +import me.proton.core.drive.link.domain.entity.LinkId +import me.proton.core.drive.link.domain.extension.userId +import me.proton.core.drive.link.domain.provider.ProtonSdkClientProvider +import me.proton.core.drive.volume.domain.entity.Volume +import me.proton.core.drive.volume.domain.entity.VolumeId +import me.proton.core.drive.volume.domain.usecase.GetVolume +import me.proton.drive.sdk.ProtonSdkClient +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AppProtonSdkClientProvider @Inject constructor( + private val getVolumeType: GetVolumeType, + private val getVolume: GetVolume, + private val driveClientProvider: AppProtonDriveClientProvider, + private val photosClientProvider: AppProtonPhotosClientProvider, +) : ProtonSdkClientProvider { + + override suspend fun getOrCreate( + linkId: LinkId, + ): Result = coRunCatching { + getOrCreate( + userId = linkId.userId, + volumeType = getVolumeType(linkId).getOrThrow(), + ).getOrThrow() + } + + override suspend fun getOrCreate( + userId: UserId, + volumeId: VolumeId + ): Result = coRunCatching { + getOrCreate( + userId = userId, + volumeType = getVolume(userId, volumeId).toResult().getOrThrow().type, + ).getOrThrow() + } + + override suspend fun getOrCreate( + userId: UserId, + volumeType: Volume.Type?, + ): Result = coRunCatching { + when (volumeType) { + null -> error("Cannot create sdk client for null volume type") + Volume.Type.UNKNOWN -> error("Cannot create sdk client for unknown volume type") + Volume.Type.REGULAR -> driveClientProvider.getOrCreate(userId).getOrThrow() + Volume.Type.PHOTO -> photosClientProvider.getOrCreate(userId).getOrThrow() + } + } + + fun remove(userId: UserId) { + driveClientProvider.remove(userId) + photosClientProvider.remove(userId) + } + + +} diff --git a/app/src/main/kotlin/me/proton/android/drive/receiver/NotificationBroadcastReceiver.kt b/app/src/main/kotlin/me/proton/android/drive/receiver/NotificationBroadcastReceiver.kt index dfd1966c..6bfb9d71 100644 --- a/app/src/main/kotlin/me/proton/android/drive/receiver/NotificationBroadcastReceiver.kt +++ b/app/src/main/kotlin/me/proton/android/drive/receiver/NotificationBroadcastReceiver.kt @@ -47,5 +47,6 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { private const val BASE_ACTION = "proton.android.intent.action" const val ACTION_DELETE = "$BASE_ACTION.DELETE" const val ACTION_CANCEL_ALL = "$BASE_ACTION.CANCEL_ALL" + const val ACTION_CANCEL_ALL_DOWNLOADS = "$BASE_ACTION.CANCEL_ALL_DOWNLOADS" } } diff --git a/app/src/main/kotlin/me/proton/android/drive/settings/DebugSettings.kt b/app/src/main/kotlin/me/proton/android/drive/settings/DebugSettings.kt index a9c21b0e..57867011 100644 --- a/app/src/main/kotlin/me/proton/android/drive/settings/DebugSettings.kt +++ b/app/src/main/kotlin/me/proton/android/drive/settings/DebugSettings.kt @@ -155,6 +155,7 @@ class DebugSettings( override val preferSdkForDownload: Boolean = true override val preferSdkForThumbnail: Boolean = true override val preferSdkForNodeOperation: Boolean = true + override val preferSdkForTrash: Boolean = true fun reset(coroutineScope: CoroutineScope) { coroutineScope.launch { diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/options/Option.kt b/app/src/main/kotlin/me/proton/android/drive/ui/options/Option.kt index 3b1e2516..b04479bd 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/options/Option.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/options/Option.kt @@ -650,7 +650,7 @@ fun Iterable