This commit is contained in:
Damir Mihaljinec
2026-04-21 17:13:39 +02:00
parent 592ab097d1
commit ffe22e8a48
293 changed files with 25547 additions and 924 deletions
@@ -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()
}
}
@@ -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)
}
}
@@ -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)
}
}
+1 -3
View File
@@ -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)
@@ -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
@@ -107,7 +107,9 @@ class DownloadInitializer : Initializer<Unit> {
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")
}
}
}
@@ -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<Unit> {
).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
}
@@ -65,6 +65,12 @@ class ShortcutInitializer : Initializer<Unit> {
}
.onAccountRemoved {
updateDynamicShortcuts(emptyList())
.onFailure { error ->
error.log(
tag = LogTag.DEFAULT,
message = "Failed to clear dynamic shortcuts",
)
}
}
}
}
@@ -133,7 +133,7 @@ class TagsMigrationInitializer : Initializer<Unit> {
driveLink = getDriveLink(fileId).toResult().getOrThrow(),
retryable = true,
networkType = NetworkType.UNMETERED,
)
).getOrThrow()
}.onFailure { error ->
error.log(
PHOTO,
@@ -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<Unit> {
CoreLogger.d(LogTag.WEBVIEW, "Start safe browsing: $isSuccess")
}
}
}.getOrNull(LogTag.WEBVIEW, "startSafeBrowsing failed")
}.onFailure { error ->
error.log(LogTag.WEBVIEW, "startSafeBrowsing failed")
}
}
}
}
@@ -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<Event.Upload>,
)
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<Event.Download>,
)
events.isNotEmpty() && events.all { it is Event.DownloadFileProgress } ->
downloadFileProgressNotificationBuilder(
notificationId = requireIsInstance(notificationId),
events = events as List<Event.DownloadFileProgress>,
)
events.size == 1 && events.first() is Event.ForcedSignOut ->
forcedSignOutNotificationBuilder(
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<ProtonSdkClient> = coRunCatching {
getOrCreate(
userId = linkId.userId,
volumeType = getVolumeType(linkId).getOrThrow(),
).getOrThrow()
}
override suspend fun getOrCreate(
userId: UserId,
volumeId: VolumeId
): Result<ProtonSdkClient> = coRunCatching {
getOrCreate(
userId = userId,
volumeType = getVolume(userId, volumeId).toResult().getOrThrow().type,
).getOrThrow()
}
override suspend fun getOrCreate(
userId: UserId,
volumeType: Volume.Type?,
): Result<ProtonSdkClient> = 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)
}
}
@@ -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"
}
}
@@ -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 {
@@ -650,7 +650,7 @@ fun Iterable<Option>.filterPermissions(
Option.ShareMultiplePhotos -> permissions.isAdmin
Option.TagPhotoFile -> permissions.isAdmin
Option.TakeAPhoto -> permissions.canWrite
Option.Trash -> permissions.isAdmin
Option.Trash -> permissions.canWrite
Option.UploadFile -> permissions.canWrite
Option.UploadFolder -> permissions.canWrite
}
@@ -31,6 +31,7 @@ import me.proton.core.compose.theme.ProtonTheme
import me.proton.core.drive.base.presentation.component.TopAppBar
import me.proton.core.accountmanager.presentation.R as AccountPresentation
import me.proton.core.presentation.R as CorePresentation
import me.proton.core.drive.i18n.R as I18N
@Composable
@@ -44,6 +45,7 @@ fun AccountSettingsScreen(
Column(modifier = modifier.fillMaxSize()) {
TopAppBar(
navigationIcon = painterResource(id = CorePresentation.drawable.ic_arrow_back),
navigationContentDescription = stringResource(I18N.string.common_back_action),
onNavigationIcon = navigateBack,
title = stringResource(AccountPresentation.string.account_settings_header),
modifier = Modifier.statusBarsPadding()
@@ -77,6 +77,7 @@ fun AppAccess(
Column(modifier = modifier) {
TopAppBar(
navigationIcon = painterResource(id = CorePresentation.drawable.ic_arrow_back),
navigationContentDescription = stringResource(I18N.string.common_back_action),
onNavigationIcon = navigateBack,
title = viewState.title,
modifier = Modifier.statusBarsPadding()
@@ -179,6 +179,7 @@ private fun TopAppBar(
navigationIcon = if (viewState.navigationIconResId != 0) {
painterResource(id = viewState.navigationIconResId)
} else null,
navigationContentDescription = viewState.navigationContentDescription,
notificationDotVisible = viewState.notificationDotVisible,
onNavigationIcon = viewEvent.onTopAppBarNavigation,
title = viewState.title,
@@ -124,6 +124,7 @@ fun TopAppBar(
val focusManager = LocalFocusManager.current
TopAppBar(
navigationIcon = painterResource(id = CorePresentation.drawable.ic_arrow_back),
navigationContentDescription = stringResource(I18N.string.common_back_action),
onNavigationIcon = viewEvent.onBackPressed,
title = "",
modifier = modifier.statusBarsPadding(),
@@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -35,6 +36,7 @@ import me.proton.android.drive.ui.viewstate.FileInfoViewState
import me.proton.core.compose.theme.ProtonDimens.DefaultSpacing
import me.proton.core.drive.base.presentation.component.TopAppBar
import me.proton.core.drive.file.info.presentation.FileInfoContent
import me.proton.core.drive.i18n.R as I18N
import me.proton.core.presentation.R as CorePresentation
@ExperimentalCoroutinesApi
@@ -65,6 +67,7 @@ fun FileInfo(
) {
TopAppBar(
navigationIcon = painterResource(id = CorePresentation.drawable.ic_arrow_back),
navigationContentDescription = stringResource(I18N.string.common_back_action),
onNavigationIcon = navigateBack,
title = "",
modifier = Modifier.statusBarsPadding()
@@ -116,6 +116,7 @@ private fun TopAppBar(
) {
TopAppBar(
navigationIcon = painterResource(id = CorePresentation.drawable.ic_proton_close),
navigationContentDescription = stringResource(I18N.string.common_close_action),
onNavigationIcon = navigateBack,
title = {},
modifier = modifier.statusBarsPadding(),
@@ -90,6 +90,7 @@ private fun LogScreen(
TopAppBar(
title = title,
navigationIcon = painterResource(id = CorePresentation.drawable.ic_arrow_back),
navigationContentDescription = stringResource(I18N.string.common_back_action),
onNavigationIcon = navigateBack,
) {
ActionButton(
@@ -111,6 +111,7 @@ fun MoveToFolder(
Column {
TopAppBar(
navigationIcon = painterResource(id = viewState.navigationIconResId),
navigationContentDescription = viewState.navigationContentDescription,
onNavigationIcon = viewEvent.onTopAppBarNavigation,
title = { modifier ->
Title(
@@ -195,6 +195,7 @@ fun AlbumsTab(
navigationIcon = if (viewState.navigationIconResId != 0) {
painterResource(id = viewState.navigationIconResId)
} else null,
navigationContentDescription = viewState.navigationContentDescription,
onNavigationIcon = viewEvent.onTopAppBarNavigation,
title = defaultTitle,
notificationDotVisible = false,
@@ -269,7 +270,7 @@ fun PhotosTab(
viewModel.HandleHomeEffect(homeScaffoldState)
val photos = rememberFlowWithLifecycle(flow = viewModel.driveLinks)
val photos = rememberFlowWithLifecycle(flow = viewModel.photoItems)
val listEffect = rememberFlowWithLifecycle(flow = viewModel.listEffect)
LaunchedEffect(viewModel, LocalContext.current) {
@@ -339,6 +340,7 @@ private fun PhotosTab(
navigationIcon = if (viewState.navigationIconResId != 0) {
painterResource(id = viewState.navigationIconResId)
} else null,
navigationContentDescription = viewState.navigationContentDescription,
onNavigationIcon = viewEvent.onTopAppBarNavigation,
title = if (viewState.inMultiselect) {
{ Title(viewState.title, false) }
@@ -122,6 +122,7 @@ fun PhotosBackup(
Column(modifier = modifier) {
TopAppBar(
navigationIcon = painterResource(id = CorePresentation.drawable.ic_arrow_back),
navigationContentDescription = stringResource(I18N.string.common_back_action),
onNavigationIcon = navigateBack,
title = viewState.title,
)
@@ -98,7 +98,7 @@ fun PhotosScreen(
lifecycle = lifecycle,
)
}
val photos = rememberFlowWithLifecycle(flow = viewModel.driveLinks)
val photos = rememberFlowWithLifecycle(flow = viewModel.photoItems)
val listEffect = rememberFlowWithLifecycle(flow = viewModel.listEffect)
LaunchedEffect(viewModel, LocalContext.current) {
@@ -203,6 +203,7 @@ fun TopAppBar(
navigationIcon = if (viewState.navigationIconResId != 0) {
painterResource(id = viewState.navigationIconResId)
} else null,
navigationContentDescription = viewState.navigationContentDescription,
notificationDotVisible = viewState.notificationDotVisible,
onNavigationIcon = viewEvent.onTopAppBarNavigation,
title = viewState.title,
@@ -184,6 +184,7 @@ fun TopAppBar(
) {
BaseTopAppBar(
navigationIcon = painterResource(CorePresentation.drawable.ic_proton_close),
navigationContentDescription = stringResource(I18N.string.common_close_action),
onNavigationIcon = onNavigationIcon,
title = title,
modifier = modifier.statusBarsPadding(),
@@ -45,6 +45,7 @@ import me.proton.core.compose.theme.ProtonTheme
import me.proton.core.compose.theme.headlineSmallNorm
import me.proton.core.compose.theme.interactionNorm
import me.proton.core.drive.base.presentation.component.TopAppBar
import me.proton.core.drive.i18n.R as I18N
import me.proton.core.presentation.R as CorePresentation
@Composable
@@ -101,6 +102,7 @@ fun TopAppBar(
val focusManager = LocalFocusManager.current
TopAppBar(
navigationIcon = painterResource(id = CorePresentation.drawable.ic_proton_cross),
navigationContentDescription = stringResource(I18N.string.common_close_action),
onNavigationIcon = viewEvent.onBackPressed,
title = stringResource(id = viewState.titleResId),
modifier = modifier.statusBarsPadding(),
@@ -73,6 +73,7 @@ fun SharedTabsScreen(
TopAppBar(
titleResId = viewState.titleResId,
navigationIconResId = viewState.navigationIconResId,
navigationContentDescription = viewState.navigationContentDescription,
onTopAppBarNavigation = viewEvent.onTopAppBarNavigation,
)
}
@@ -147,11 +148,13 @@ fun SharedTabs(
private fun TopAppBar(
@StringRes titleResId: Int,
@DrawableRes navigationIconResId: Int,
navigationContentDescription: String?,
modifier: Modifier = Modifier,
onTopAppBarNavigation: () -> Unit,
) {
BaseTopAppBar(
navigationIcon = painterResource(id = navigationIconResId),
navigationContentDescription = navigationContentDescription,
onNavigationIcon = onTopAppBarNavigation,
title = stringResource(id = titleResId),
modifier = modifier,
@@ -306,6 +306,7 @@ private fun TopAppBar(
if (isLandscape) {
BaseTopAppBar(
navigationIcon = painterResource((closeAction as Action.Icon).iconResId),
navigationContentDescription = stringResource((closeAction as Action.Icon).contentDescriptionResId),
onNavigationIcon = closeAction.onAction,
title = "",
backgroundColor = backgroundColor,
@@ -114,6 +114,7 @@ private fun TopAppBar(
navigationIcon = if (viewState.navigationIconResId != 0) {
painterResource(id = viewState.navigationIconResId)
} else null,
navigationContentDescription = viewState.navigationContentDescription,
onNavigationIcon = viewEvent.onTopAppBarNavigation,
title = viewState.title ?: "",
isTitleEncrypted = viewState.isTitleEncrypted,
@@ -147,6 +147,7 @@ fun UploadTo(
Column(modifier = Modifier.fillMaxWidth()) {
TopAppBar(
navigationIcon = painterResource(id = viewState.navigationIconResId),
navigationContentDescription = viewState.navigationContentDescription,
onNavigationIcon = viewEvent.onTopAppBarNavigation,
title = viewState.title,
isTitleEncrypted = viewState.isTitleEncrypted,
@@ -23,11 +23,13 @@ import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import me.proton.android.drive.extension.log
import me.proton.android.drive.photos.domain.usecase.AddPhotosToAlbum
import me.proton.android.drive.photos.domain.usecase.AddToAlbumInfo
import me.proton.android.drive.ui.viewevent.AddToAlbumsOptionsViewEvent
import me.proton.android.drive.ui.viewstate.AddToAlbumsOptionsViewState
import me.proton.core.domain.arch.DataResult
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.component.RunAction
@@ -47,8 +49,8 @@ import me.proton.core.drive.photo.domain.entity.PhotoListing
import me.proton.core.drive.share.crypto.domain.usecase.GetPhotoShare
import me.proton.core.drive.share.domain.entity.Share
import javax.inject.Inject
import me.proton.core.presentation.R as CorePresentation
import me.proton.core.drive.i18n.R as I18N
import me.proton.core.presentation.R as CorePresentation
@HiltViewModel
class AddToAlbumsOptionsViewModel @Inject constructor(
@@ -123,7 +125,9 @@ class AddToAlbumsOptionsViewModel @Inject constructor(
}
override suspend fun onSuccessfullyAdded(photoListings: List<PhotoListing.Volume>) {
deselectLinks(selectionId)
deselectLinks(selectionId).onFailure { error ->
error.log(VIEW_MODEL, "Failed to deselect links")
}
}
override fun getAlbumDetails(album: DriveLink.Album?): String? = album.details()
@@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.options.Option
import me.proton.android.drive.ui.options.filter
import me.proton.android.drive.ui.options.filterPermissions
@@ -41,6 +42,7 @@ import me.proton.android.drive.ui.options.filterShareMember
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.drive.base.domain.entity.Permissions
import me.proton.core.drive.base.domain.extension.mapWithPrevious
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.presentation.component.RunAction
import me.proton.core.drive.base.presentation.extension.require
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
@@ -115,7 +117,9 @@ class AlbumOptionsViewModel @Inject constructor(
when (option) {
is Option.OfflineToggle -> option.build(runAction) { driveLink ->
viewModelScope.launch {
toggleOffline(driveLink)
toggleOffline(driveLink).onFailure { error ->
error.log(VIEW_MODEL, "Failed to toggle offline for ${driveLink.id.id}")
}
}
}
is Option.ShareViaInvitations -> option.build(runAction, navigateToShareViaInvitations)
@@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.getDefaultMessage
import me.proton.android.drive.extension.log
import me.proton.android.drive.extension.thumbnailVO
import me.proton.android.drive.photos.domain.usecase.AddPhotosToStream
import me.proton.android.drive.photos.domain.usecase.AddToAlbumInfo
@@ -63,13 +64,12 @@ import me.proton.android.drive.ui.common.onClick
import me.proton.android.drive.usecase.OnFilesDriveLinkError
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.domain.arch.onSuccess
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.data.extension.log as logResult
import me.proton.core.drive.base.domain.entity.Permissions
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
import me.proton.core.drive.base.domain.extension.isViewerOrEditorOnly
import me.proton.core.drive.base.domain.extension.mapWithPrevious
import me.proton.core.drive.base.domain.extension.onFailure
import me.proton.core.drive.base.domain.log.LogTag
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.log.logId
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
@@ -203,7 +203,7 @@ class AlbumViewModel @Inject constructor(
contentState = listContentState,
shareType = Share.Type.PHOTO,
)
error.log(VIEW_MODEL)
error.logResult(VIEW_MODEL)
}
return@mapWithPrevious null
}
@@ -260,6 +260,11 @@ class AlbumViewModel @Inject constructor(
} else {
CorePresentation.drawable.ic_proton_close
},
navigationContentDescription = if (selected.isEmpty() || inPickerMode) {
appContext.getString(I18N.string.common_back_action)
} else {
appContext.getString(I18N.string.common_close_action)
},
title = takeIf { selected.isNotEmpty() && !inPickerMode }
?.let {
appContext.quantityString(
@@ -412,7 +417,7 @@ class AlbumViewModel @Inject constructor(
viewModelScope.launch {
saveAllLoading.value = true
addPhotosToStream(albumId).onFailure { error ->
error.log(LogTag.ALBUM, "Cannot copy photo to stream: ${albumId.id}")
error.log(VIEW_MODEL, "Cannot copy photo to stream: ${albumId.id}")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.photos.domain.usecase.GetPhotosDriveLink
import me.proton.android.drive.photos.presentation.state.AlbumsItem
import me.proton.android.drive.photos.presentation.viewevent.AlbumsViewEvent
@@ -56,7 +57,7 @@ import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.domain.arch.onSuccess
import me.proton.core.drive.base.data.extension.getDefaultMessage
import me.proton.core.drive.base.data.extension.isRetryable
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.data.extension.log as logResult
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
import me.proton.core.drive.base.domain.extension.mapWithPrevious
import me.proton.core.drive.base.domain.extension.onFailure
@@ -137,6 +138,7 @@ class AlbumsViewModel @Inject constructor(
isRefreshEnabled = listContentState.value != ListContentState.Loading,
placeholderImageResId = emptyStateImageResId,
navigationIconResId = CorePresentation.drawable.ic_proton_hamburger,
navigationContentDescription = appContext.getString(I18N.string.common_open_side_menu_action),
filters = listOf(
AlbumsFilter(
AlbumListing.Filter.ALL,
@@ -205,7 +207,7 @@ class AlbumsViewModel @Inject constructor(
contentState = listContentState,
shareType = Share.Type.PHOTO,
)
error.log(VIEW_MODEL, "Cannot get drive link")
error.logResult(VIEW_MODEL, "Cannot get drive link")
}
return@mapWithPrevious null
}
@@ -347,7 +349,7 @@ class AlbumsViewModel @Inject constructor(
)
.onFailure { error ->
isRefreshing.value = false
error.log(VIEW_MODEL)
error.log(VIEW_MODEL, "Cannot get all album listings")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -26,12 +26,14 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.lock.domain.extension.autoLockDefaultDuration
import me.proton.android.drive.lock.domain.usecase.GetAutoLockDuration
import me.proton.android.drive.lock.domain.usecase.UpdateAutoLockDuration
import me.proton.android.drive.ui.viewevent.AutoLockDurationsViewEvent
import me.proton.android.drive.ui.viewstate.AutoLockDurationsViewState
import me.proton.core.compose.component.bottomsheet.RunAction
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import javax.inject.Inject
import kotlin.time.Duration
@@ -62,7 +64,9 @@ class AutoLockDurationsViewModel @Inject constructor(
override val onDuration: (Duration) -> Unit = { duration ->
runAction {
viewModelScope.launch {
updateAutoLockDuration(duration)
updateAutoLockDuration(duration).onFailure { error ->
error.log(VIEW_MODEL, "Cannot update auto lock duration")
}
dismiss()
}
}
@@ -29,14 +29,14 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.navigation.Screen
import me.proton.android.drive.ui.viewevent.BackupIssuesViewEvent
import me.proton.android.drive.ui.viewstate.BackupIssuesViewState
import me.proton.core.drive.backup.domain.usecase.GetAllFailedFiles
import me.proton.core.drive.backup.domain.usecase.RetryBackup
import me.proton.core.drive.base.data.extension.getDefaultMessage
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.log.LogTag.BACKUP
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.extension.require
@@ -70,7 +70,7 @@ class BackupIssuesViewModel @Inject constructor(
backupFile.uriString.toUri()
})
}.catch { error ->
error.log(BACKUP, "Cannot get all failed files")
error.log(VIEW_MODEL, "Cannot get all failed files")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -95,7 +95,7 @@ class BackupIssuesViewModel @Inject constructor(
retryBackup(folderId).onSuccess {
onSuccess()
}.onFailure { error ->
error.log(BACKUP, "Cannot retry on backup")
error.log(VIEW_MODEL, "Cannot retry on backup")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -39,12 +39,13 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.common.onClick
import me.proton.android.drive.ui.effect.HomeEffect
import me.proton.android.drive.ui.effect.HomeTabViewModel
import me.proton.core.drive.base.domain.entity.TimestampMs
import me.proton.core.drive.base.domain.extension.getOrNull
import me.proton.core.drive.base.domain.log.LogTag.SHARING
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.presentation.effect.ListEffect
import me.proton.core.drive.base.presentation.state.ListContentAppendingState
@@ -193,9 +194,10 @@ abstract class CommonSharedViewModel(
private fun onRefresh() {
viewModelScope.launch {
_listEffect.emit(ListEffect.REFRESH)
val linkIds = getAllIds().getOrNull(SHARING, "Cannot get ids")
val linkIds = getAllIds().getOrNull(VIEW_MODEL, "Cannot get ids")
.orEmpty().map { it.linkId }.toSet()
sharedDriveLinks.refresh(linkIds).getOrNull(SHARING, "Cannot refresh drive links")
sharedDriveLinks.refresh(linkIds)
.onFailure { error -> error.log(VIEW_MODEL, "Cannot refresh drive links") }
}
}
@@ -34,12 +34,12 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.effect.HomeEffect
import me.proton.android.drive.ui.effect.HomeTabViewModel
import me.proton.android.drive.ui.viewevent.ComputersViewEvent
import me.proton.android.drive.ui.viewstate.ComputersViewState
import me.proton.core.domain.arch.DataResult
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.data.extension.logDefaultMessage
import me.proton.core.drive.base.domain.entity.CryptoProperty
import me.proton.core.drive.base.domain.entity.TimestampMs
@@ -48,19 +48,20 @@ import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.common.getThemeDrawableId
import me.proton.core.drive.base.presentation.state.ListContentState
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
import me.proton.core.drive.device.domain.entity.Device
import me.proton.core.drive.device.domain.entity.DeviceId
import me.proton.core.drive.device.domain.extension.name
import me.proton.core.drive.device.domain.usecase.RefreshDevices
import me.proton.core.drive.drivelink.device.domain.usecase.GetDecryptedDevicesSortedByName
import me.proton.core.drive.base.presentation.state.ListContentState
import me.proton.core.drive.files.domain.usecase.ToFirstItemMetricsNotifier
import me.proton.core.drive.link.domain.entity.FolderId
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
import me.proton.core.drive.observability.domain.metrics.common.mobile.performance.PageType
import me.proton.core.plan.presentation.compose.usecase.ShouldUpgradeStorage
import javax.inject.Inject
import me.proton.core.drive.base.data.extension.log as logResult
import me.proton.core.drive.drivelink.device.presentation.R as DriveLinkDevicePresentation
import me.proton.core.drive.i18n.R as I18N
@@ -107,6 +108,7 @@ class ComputersViewModel @Inject constructor(
val initialViewState = ComputersViewState(
title = appContext.getString(I18N.string.computers_title),
navigationIconResId = me.proton.core.presentation.R.drawable.ic_proton_hamburger,
navigationContentDescription = appContext.getString(I18N.string.common_open_side_menu_action),
notificationDotVisible = false,
listContentState = listContentState.value,
isRefreshEnabled = listContentState.value != ListContentState.Loading
@@ -136,7 +138,7 @@ class ComputersViewModel @Inject constructor(
when (result) {
is DataResult.Processing -> listContentState.value = ListContentState.Loading
is DataResult.Error -> {
result.log(VIEW_MODEL)
result.logResult(VIEW_MODEL)
broadcastMessages(
userId = userId,
message = result.logDefaultMessage(
@@ -27,11 +27,13 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.navigation.Screen
import me.proton.android.drive.ui.viewevent.ConfirmDeletionViewEvent
import me.proton.android.drive.ui.viewstate.ConfirmDeletionViewState
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.presentation.extension.require
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink
@@ -66,7 +68,9 @@ class ConfirmDeletionDialogViewModel @Inject constructor(
fun viewEvent(onDismiss: () -> Unit) = object : ConfirmDeletionViewEvent {
override val onConfirm = {
viewModelScope.launch {
deleteFromTrash(userId, linkId)
deleteFromTrash(userId, linkId).onFailure{ error ->
error.log(VIEW_MODEL, "Failed to delete from trash ${linkId.id}")
}
onDismiss()
}
Unit
@@ -24,9 +24,9 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.viewevent.ConfirmEmptyTrashViewEvent
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.log.LogTag.TRASH
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.presentation.extension.require
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
import me.proton.core.drive.trash.domain.usecase.EmptyTrash
@@ -46,7 +46,7 @@ class ConfirmEmptyTrashDialogViewModel @Inject constructor(
override val onConfirm = {
viewModelScope.launch {
emptyTrash(userId, volumeId).onFailure { error ->
error.log(TRASH, "Cannot empty trash volumeId: ${volumeId.id}")
error.log(VIEW_MODEL, "Cannot empty trash volumeId: ${volumeId.id}")
}
dismiss()
}
@@ -33,15 +33,15 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.getDefaultMessage
import me.proton.android.drive.extension.log
import me.proton.android.drive.photos.domain.usecase.AddPhotosToStream
import me.proton.android.drive.photos.presentation.extension.processAddToStream
import me.proton.android.drive.photos.presentation.viewevent.ConfirmLeaveAlbumDialogViewEvent
import me.proton.android.drive.photos.presentation.viewstate.ConfirmLeaveAlbumDialogViewState
import me.proton.android.drive.usecase.LeaveShare
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
import me.proton.core.drive.base.domain.log.LogTag
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
@@ -107,12 +107,13 @@ class ConfirmLeaveAlbumDialogViewModel @Inject constructor(
private fun leaveAlbumWithoutSaving(dismiss: () -> Unit) {
viewModelScope.launch {
album.value?.let {
album.value?.let { album ->
isWithoutSavingOperationInProgress.value = true
leaveShare(it)
.onSuccess {
dismiss()
}
leaveShare(album).onSuccess {
dismiss()
}.onFailure { error ->
error.log(VIEW_MODEL, "Failed to leave share for ${album.id.id}")
}
isWithoutSavingOperationInProgress.value = false
}
}
@@ -123,7 +124,7 @@ class ConfirmLeaveAlbumDialogViewModel @Inject constructor(
album.value?.let {
isSavingOperationInProgress.value = true
addPhotosToStream(albumId).onFailure { error ->
error.log(LogTag.ALBUM, "Cannot copy photo to stream: ${albumId.id}")
error.log(VIEW_MODEL, "Cannot copy photo to stream: ${albumId.id}")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -25,12 +25,12 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.navigation.Screen
import me.proton.android.drive.ui.viewevent.ConfirmSkipIssuesViewEvent
import me.proton.core.drive.backup.domain.usecase.DeleteAllFailedFiles
import me.proton.core.drive.base.data.extension.getDefaultMessage
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.log.LogTag.BACKUP
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.extension.require
@@ -65,7 +65,7 @@ class ConfirmSkipIssuesDialogViewModel @Inject constructor(
deleteAllFailedFiles(folderId).onSuccess {
onSuccess()
}.onFailure { error ->
error.log(BACKUP, "Cannot delete failed files")
error.log(VIEW_MODEL, "Cannot delete failed files")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -26,13 +26,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.viewevent.ConfirmStopSharingViewEvent
import me.proton.android.drive.ui.viewstate.ConfirmStopSharingViewState
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.extension.require
@@ -28,10 +28,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.navigation.Screen
import me.proton.android.drive.ui.viewevent.ConfirmStopSharingViewEvent
import me.proton.android.drive.ui.viewstate.ConfirmStopSharingViewState
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.extension.require
@@ -96,7 +96,7 @@ class ConfirmStopLinkSharingDialogViewModel @Inject constructor(
confirm()
}.onFailure { error ->
errorMessage.emit(context.getString(I18N.string.description_files_stop_sharing_action_error))
error.log(VIEW_MODEL)
error.log(VIEW_MODEL, "Failed to delete share url for ${linkId.id}")
}
}
}
@@ -27,6 +27,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.navigation.Screen
import me.proton.android.drive.ui.viewevent.ConfirmStopSyncFolderViewEvent
import me.proton.android.drive.ui.viewstate.ConfirmStopSyncFolderViewState
@@ -34,8 +35,7 @@ import me.proton.core.drive.backup.domain.entity.BackupFolder
import me.proton.core.drive.backup.domain.usecase.DisableBackupForFolder
import me.proton.core.drive.backup.domain.usecase.GetAllBuckets
import me.proton.core.drive.base.data.extension.getDefaultMessage
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.log.LogTag.BACKUP
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.extension.require
@@ -79,7 +79,7 @@ class ConfirmStopSyncFolderDialogViewModel @Inject constructor(
disableBackupForFolder(BackupFolder(id, folderId)).onSuccess {
onSuccess()
}.onFailure { error ->
error.log(BACKUP, "Cannot stop sync for folder: $id")
error.log(VIEW_MODEL, "Cannot stop sync for folder: $id")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.getDefaultMessage
import me.proton.android.drive.extension.log
import me.proton.android.drive.photos.domain.usecase.AddPhotosToStream
import me.proton.android.drive.photos.domain.usecase.RemovePhotosFromAlbum
import me.proton.android.drive.photos.presentation.extension.processAddToStream
@@ -49,10 +50,8 @@ import me.proton.android.drive.usecase.LeaveShare
import me.proton.android.drive.usecase.NotifyActivityNotFound
import me.proton.android.drive.usecase.OpenProtonDocumentInBrowser
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.entity.Permissions
import me.proton.core.drive.base.domain.extension.mapWithPrevious
import me.proton.core.drive.base.domain.log.LogTag
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
@@ -189,7 +188,9 @@ class FileOrFolderOptionsViewModel @Inject constructor(
}
is Option.OfflineToggle -> option.build(runAction) { driveLink ->
viewModelScope.launch {
toggleOffline(driveLink)
toggleOffline(driveLink).onFailure { error ->
error.log(VIEW_MODEL, "Failed to toggle offline for ${driveLink.id.id}")
}
deselectLinks()
}
}
@@ -224,13 +225,17 @@ class FileOrFolderOptionsViewModel @Inject constructor(
}
is Option.RemoveMe -> option.build(runAction) { driveLink ->
viewModelScope.launch {
leaveShare(driveLink)
leaveShare(driveLink).onFailure { error ->
error.log(VIEW_MODEL, "Failed to leave share for ${driveLink.id.id}")
}
deselectLinks()
}
}
is Option.OpenInBrowser -> option.build(runAction) { driveLink ->
viewModelScope.launch {
openProtonDocumentInBrowser(driveLink)
openProtonDocumentInBrowser(driveLink).onFailure { error ->
error.log(VIEW_MODEL, "Failed to open proton document in browser")
}
deselectLinks()
}
}
@@ -259,19 +264,19 @@ class FileOrFolderOptionsViewModel @Inject constructor(
}
}
is Option.AddToAlbums -> option.build(runAction) { driveLink ->
if (selectionId != null) {
navigateToAddToAlbumsOptions(selectionId)
} else {
viewModelScope.launch {
selectLinks(listOf(driveLink.id))
.onFailure { error ->
error.log(VIEW_MODEL, "Failed to select links")
}
.onSuccess { selectionId ->
navigateToAddToAlbumsOptions(selectionId)
}
}
if (selectionId != null) {
navigateToAddToAlbumsOptions(selectionId)
} else {
viewModelScope.launch {
selectLinks(listOf(driveLink.id))
.onFailure { error ->
error.log(VIEW_MODEL, "Failed to select links")
}
.onSuccess { selectionId ->
navigateToAddToAlbumsOptions(selectionId)
}
}
}
}
else -> throw IllegalStateException(
"Option ${option.javaClass.simpleName} is not found. Did you forget to add it?"
@@ -291,7 +296,9 @@ class FileOrFolderOptionsViewModel @Inject constructor(
private fun deselectLinks() {
viewModelScope.launch {
if (selectionId != null) {
deselectLinks(selectionId)
deselectLinks(selectionId).onFailure { error ->
error.log(VIEW_MODEL, "Failed to deselect links")
}
}
}
}
@@ -333,7 +340,7 @@ class FileOrFolderOptionsViewModel @Inject constructor(
albumId = requireNotNull(albumId),
newCoverFileId = driveLink.id
).onFailure { error ->
error.log(LogTag.ALBUM, "Cannot update album cover: ${driveLink.id.id}")
error.log(VIEW_MODEL, "Cannot update album cover: ${driveLink.id.id}")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -359,7 +366,7 @@ class FileOrFolderOptionsViewModel @Inject constructor(
photoIds = listOf(fileId),
albumId = requireNotNull(albumId) { "album id is required to save shared photo"},
).onFailure { error ->
error.log(LogTag.ALBUM, "Cannot copy photo to stream: ${fileId.id}")
error.log(VIEW_MODEL, "Cannot copy photo to stream: ${fileId.id}")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.document.scanner.domain.usecase.IsScannerAvailable
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.common.onClick
import me.proton.android.drive.ui.effect.HomeEffect
import me.proton.android.drive.ui.effect.HomeTabViewModel
@@ -60,7 +61,7 @@ import me.proton.android.drive.usecase.OpenProtonDocumentInBrowser
import me.proton.core.domain.arch.onSuccess
import me.proton.core.drive.base.data.datastore.GetUserDataStore
import me.proton.core.drive.base.data.extension.getDefaultMessage
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.data.extension.log as logResult
import me.proton.core.drive.base.domain.entity.Percentage
import me.proton.core.drive.base.domain.entity.Permissions
import me.proton.core.drive.base.domain.entity.TimestampMs
@@ -104,7 +105,6 @@ import me.proton.core.drive.observability.domain.metrics.common.mobile.performan
import me.proton.core.drive.share.domain.entity.ShareId
import me.proton.core.drive.sorting.domain.entity.Sorting
import me.proton.core.drive.sorting.domain.usecase.GetSorting
import me.proton.core.drive.upload.data.extension.logTag
import me.proton.core.drive.upload.domain.usecase.CancelUploadFile
import me.proton.core.drive.upload.domain.usecase.GetUploadProgress
import me.proton.core.drive.user.domain.extension.isFree
@@ -179,7 +179,7 @@ class FilesViewModel @Inject constructor(
}
.onFailure { error ->
onFilesDriveLinkError(userId, previous, error, listContentState)
error.log(VIEW_MODEL, "Cannot get drive link for ${folderId?.id}")
error.logResult(VIEW_MODEL, "Cannot get drive link for ${folderId?.id}")
toFirstItemMetricsNotifier.reset()
}
return@mapWithPrevious null
@@ -245,6 +245,11 @@ class FilesViewModel @Inject constructor(
} else {
CorePresentation.drawable.ic_arrow_back
},
navigationContentDescription = if (isRootFolder && selected.value.isEmpty()) {
appContext.getString(I18N.string.common_open_side_menu_action)
} else {
appContext.getString(I18N.string.common_back_action)
},
drawerGesturesEnabled = isRootFolder,
listContentState = listContentState.value,
listContentAppendingState = listContentAppendingState.value,
@@ -311,6 +316,13 @@ class FilesViewModel @Inject constructor(
} else {
CorePresentation.drawable.ic_arrow_back
},
navigationContentDescription = if (showHamburgerMenuIcon) {
appContext.getString(I18N.string.common_open_side_menu_action)
} else if (selected.isNotEmpty()) {
appContext.getString(I18N.string.common_close_action)
} else {
appContext.getString(I18N.string.common_back_action)
},
sorting = sorting,
listContentState = listContentState,
listContentAppendingState = appendingState,
@@ -510,7 +522,7 @@ class FilesViewModel @Inject constructor(
private fun onCancelUpload(uploadFileLink: UploadFileLink) {
viewModelScope.launch {
cancelUploadFile(uploadFileLink).onFailure { error ->
error.log(uploadFileLink.logTag(), "Cannot cancel upload")
error.log(VIEW_MODEL, "Cannot cancel upload")
_homeEffect.emit(
HomeEffect.ShowSnackbar(
error.getDefaultMessage(
@@ -38,7 +38,6 @@ import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.extension.asHumanReadableString
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
import javax.inject.Inject
import me.proton.core.drive.base.presentation.R as BasePresentation
@@ -50,7 +49,6 @@ class GetMoreFreeStorageViewModel @Inject constructor(
@ApplicationContext private val appContext: Context,
configurationProvider: ConfigurationProvider,
private val broadcastMessages: BroadcastMessages,
getFeatureFlag: GetFeatureFlagFlow,
savedStateHandle: SavedStateHandle,
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
private val uploadAFile = GetMoreFreeStorageViewState.Action(
@@ -80,6 +80,7 @@ abstract class HostFilesViewModel(
titleResId = I18N.string.title_my_files,
sorting = Sorting.DEFAULT,
navigationIconResId = 0,
navigationContentDescription = null,
drawerGesturesEnabled = false,
listContentState = listContentState.value,
listContentAppendingState = listContentAppendingState.value,
@@ -36,13 +36,15 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.handler.FolderCreatedHandler
import me.proton.android.drive.ui.handler.FolderCreatedHandlerDelegate
import me.proton.android.drive.ui.navigation.Screen
import me.proton.android.drive.ui.viewevent.MoveToFolderViewEvent
import me.proton.android.drive.ui.viewstate.MoveFileViewState
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.android.drive.ui.handler.FolderCreatedHandler
import me.proton.android.drive.ui.handler.FolderCreatedHandlerDelegate
import me.proton.core.drive.base.domain.log.LogTag
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.presentation.common.Action
import me.proton.core.drive.drivelink.crypto.domain.usecase.DecryptDriveLinks
@@ -163,6 +165,11 @@ class MoveToFolderViewModel @Inject constructor(
} else {
CorePresentation.drawable.ic_arrow_back
},
navigationContentDescription = if (parentLink == null || isRoot) {
appContext.getString(I18N.string.common_close_action)
} else {
appContext.getString(I18N.string.common_back_action)
},
driveLinks = decryptDriveLinks(driveLinksToMove)
.map { driveLink -> if (driveLink.isNameEncrypted) "" else driveLink.name },
)
@@ -203,8 +210,19 @@ class MoveToFolderViewModel @Inject constructor(
} else if (folder.id == parentId) {
CoreLogger.w(LogTag.MOVE, "folder same as parent, move aborted")
} else {
moveFile(userId, driveLinksToMove.value.map { driveLink -> driveLink.id }, folder.id)
selectionId?.let { deselectLinks(selectionId) }
val linkIds = driveLinksToMove.value.map { driveLink -> driveLink.id }
moveFile(
userId = userId,
linkIds = linkIds,
parentId = folder.id,
).onFailure { error ->
error.log(VIEW_MODEL, "Failed to move files for ${linkIds.map { it.id }}")
}
selectionId?.let {
deselectLinks(selectionId).onFailure { error ->
error.log(VIEW_MODEL, "Failed to deselect links")
}
}
navigateBack()
}
}
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.getDefaultMessage
import me.proton.android.drive.extension.log
import me.proton.android.drive.photos.domain.usecase.RemovePhotosFromAlbum
import me.proton.android.drive.photos.presentation.extension.processRemove
import me.proton.android.drive.ui.options.Option
@@ -36,7 +37,6 @@ import me.proton.android.drive.ui.options.filterAll
import me.proton.android.drive.ui.options.filterPermissions
import me.proton.android.drive.ui.options.filterPhotoTag
import me.proton.android.drive.ui.options.filterShare
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
@@ -105,8 +105,15 @@ class MultipleFileOrFolderOptionsViewModel @Inject constructor(
runAction = runAction,
moveToTrash = {
viewModelScope.launch {
sendToTrash(userId, driveLinks)
deselectLinks(selectionId)
sendToTrash(userId, driveLinks).onFailure { error ->
error.log(
VIEW_MODEL,
"Failed to send to trash for ${driveLinks.map { it.id.id }}"
)
}
deselectLinks(selectionId).onFailure { error ->
error.log(VIEW_MODEL, "Failed to deselect links")
}
}
},
)
@@ -121,7 +128,9 @@ class MultipleFileOrFolderOptionsViewModel @Inject constructor(
tagPhotos = {
viewModelScope.launch {
scanPhotoForTags(driveLinks)
deselectLinks(selectionId)
deselectLinks(selectionId).onFailure { error ->
error.log(VIEW_MODEL, "Failed to deselect links")
}
}
}
)
@@ -131,7 +140,12 @@ class MultipleFileOrFolderOptionsViewModel @Inject constructor(
viewModelScope.launch {
exportToDownload(
driveLinks.filterIsInstance<DriveLink.File>().map { driveLink -> driveLink.id }
)
).onFailure { error ->
error.log(
VIEW_MODEL,
"Failed to export to download for ${driveLinks.map { it.id.id }}"
)
}
}
}
)
@@ -149,7 +163,9 @@ class MultipleFileOrFolderOptionsViewModel @Inject constructor(
driveLinks
.filterIsInstance<DriveLink.File>()
)
deselectLinks(selectionId)
deselectLinks(selectionId).onFailure { error ->
error.log(VIEW_MODEL, "Failed to deselect links")
}
}
},
)
@@ -155,7 +155,7 @@ abstract class MultiplePhotosOptionsViewModel(
.let { photoListings ->
addToAlbumInfo(albumId, photoListings.toSet())
.onFailure { error ->
error.log(VIEW_MODEL, "Failed to add photos to album info")
error.log(VIEW_MODEL, "Failed to add photos to album info for ${albumId.id}")
error.broadcast()
}
.onSuccess {
@@ -163,7 +163,7 @@ abstract class MultiplePhotosOptionsViewModel(
showAddToAlbumStartMessage()
addPhotosToAlbum(albumId)
.onFailure { error ->
error.log(VIEW_MODEL, "Failed to add photos to album")
error.log(VIEW_MODEL, "Failed to add photos to album for ${albumId.id}")
error.broadcast()
}
.onSuccess { result ->
@@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.common.onClick
import me.proton.android.drive.ui.navigation.PagerType
import me.proton.android.drive.ui.navigation.Screen
@@ -51,9 +52,8 @@ import me.proton.android.drive.ui.viewstate.OfflineViewState
import me.proton.android.drive.usecase.OpenProtonDocumentInBrowser
import me.proton.core.domain.arch.onSuccess
import me.proton.core.drive.base.data.extension.getDefaultMessage
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.data.extension.log as logResult
import me.proton.core.drive.base.domain.entity.Percentage
import me.proton.core.drive.base.domain.entity.onProcessing
import me.proton.core.drive.base.domain.extension.onFailure
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
@@ -78,7 +78,6 @@ import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
import me.proton.core.drive.share.domain.entity.ShareId
import me.proton.core.drive.sorting.domain.entity.Sorting
import me.proton.core.drive.sorting.domain.usecase.GetSorting
import me.proton.core.util.kotlin.CoreLogger
import me.proton.drive.android.settings.domain.entity.LayoutType
import me.proton.drive.android.settings.domain.usecase.GetLayoutType
import me.proton.drive.android.settings.domain.usecase.ToggleLayoutType
@@ -113,7 +112,7 @@ class OfflineViewModel @Inject constructor(
.onSuccess { driveLink ->
return@map driveLink
}
.onFailure { error -> error.log(VIEW_MODEL) }
.onFailure { error -> error.logResult(VIEW_MODEL, "Cannot get drive link for ${folderId?.id}") }
return@map null
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
val driveLinks: Flow<PagingData<DriveLink>> = flowOf(folderId).flatMapLatest { folderId ->
@@ -135,6 +134,7 @@ class OfflineViewModel @Inject constructor(
title = savedStateHandle.get(Screen.Files.FOLDER_NAME),
titleResId = I18N.string.title_offline_available,
navigationIconResId = CorePresentation.drawable.ic_arrow_back,
navigationContentDescription = appContext.getString(I18N.string.common_close_action),
drawerGesturesEnabled = false,
sorting = Sorting.DEFAULT,
listContentState = listContentState.value,
@@ -267,7 +267,14 @@ class OfflineViewModel @Inject constructor(
}
private fun onToggleLayout() {
viewModelScope.launch { toggleLayoutType(userId = userId, currentLayoutType = layoutType.value) }
viewModelScope.launch {
toggleLayoutType(
userId = userId,
currentLayoutType = layoutType.value
).onFailure { error ->
error.log(VIEW_MODEL, "Failed to toggle layout type")
}
}
}
private fun retryList() {
@@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.photos.domain.entity.PhotoBackupState
import me.proton.android.drive.photos.domain.usecase.EnablePhotosBackup
import me.proton.android.drive.photos.domain.usecase.GetPhotosDriveLink
@@ -39,19 +40,16 @@ import me.proton.android.drive.photos.presentation.viewstate.BackupPermissionsVi
import me.proton.android.drive.usecase.MarkOnboardingAsShown
import me.proton.core.drive.backup.domain.entity.BackupPermissions
import me.proton.core.drive.backup.domain.manager.BackupPermissionsManager
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.entity.Bytes
import me.proton.core.drive.base.domain.extension.firstSuccessOrError
import me.proton.core.drive.base.domain.extension.getOrNull
import me.proton.core.drive.base.domain.extension.toResult
import me.proton.core.drive.base.domain.log.LogTag
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.presentation.viewevent.OnboardingViewEvent
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
import me.proton.core.drive.base.presentation.viewstate.OnboardingViewState
import me.proton.core.drive.user.domain.extension.isFree
import me.proton.core.user.domain.usecase.GetUser
import me.proton.core.util.kotlin.CoreLogger
import javax.inject.Inject
import me.proton.core.drive.i18n.R as I18N
@@ -147,7 +145,7 @@ class OnboardingViewModel @Inject constructor(
getPhotosDriveLink(userId)
.firstSuccessOrError()
.toResult()
.getOrNull(LogTag.BACKUP, "Getting photos drive link during onboarding failed")
.getOrNull(VIEW_MODEL, "Getting photos drive link during onboarding failed")
?.id
?.let { photoRootId ->
enablePhotosBackup(photoRootId)
@@ -158,7 +156,7 @@ class OnboardingViewModel @Inject constructor(
}
}
.onFailure { error ->
error.log(LogTag.BACKUP, "Enabling backup during onboarding failed")
error.log(VIEW_MODEL, "Enabling backup during onboarding failed")
dismiss?.invoke()
}
}
@@ -171,8 +169,9 @@ class OnboardingViewModel @Inject constructor(
private fun onboardingShown() {
viewModelScope.launch {
markOnboardingAsShown()
.getOrNull(VIEW_MODEL, "Marking onboarding as shown failed")
markOnboardingAsShown().onFailure { error ->
error.log(VIEW_MODEL, "Marking onboarding as shown failed")
}
}
}
}
@@ -208,14 +208,13 @@ class ParentFolderOptionsViewModel @Inject constructor(
folder = folder,
uploadFileDescriptions = uriStrings.map { uri -> UploadFileDescription(uri) },
priority = UploadFileLink.USER_PRIORITY,
)
.onFailure { error ->
error.log(VIEW_MODEL, "Upload files failed")
if (error is NotEnoughSpaceException) {
navigateToStorageFull()
return@launch
}
).onFailure { error ->
error.log(VIEW_MODEL, "Upload files failed in ${folder.id.id}")
if (error is NotEnoughSpaceException) {
navigateToStorageFull()
return@launch
}
}
}
dismiss()
}
@@ -228,10 +227,9 @@ class ParentFolderOptionsViewModel @Inject constructor(
folderId = folder.id,
uriString = uriString,
shouldBroadcastMessage = true,
)
.onFailure { error ->
error.log(VIEW_MODEL, "Upload folder failed")
}
).onFailure { error ->
error.log(VIEW_MODEL, "Upload folder failed in ${folder.id.id}")
}
}
dismiss()
}
@@ -247,15 +245,14 @@ class ParentFolderOptionsViewModel @Inject constructor(
uploadFileDescriptions = listOf(UploadFileDescription(uri.toString())),
shouldDeleteSource = true,
priority = UploadFileLink.USER_PRIORITY,
)
.onFailure { error ->
error.log(VIEW_MODEL, "Upload file failed")
if (error is NotEnoughSpaceException) {
navigateToStorageFull()
updatePhotoUri(null)
return@launch
}
).onFailure { error ->
error.log(VIEW_MODEL, "Upload file failed in ${folder.id.id}")
if (error is NotEnoughSpaceException) {
navigateToStorageFull()
updatePhotoUri(null)
return@launch
}
}
}
} else {
withContext(Job() + Dispatchers.IO) {
@@ -351,6 +348,7 @@ class ParentFolderOptionsViewModel @Inject constructor(
navigateToPreview(fileId)
}
.onFailure { error ->
error.log(VIEW_MODEL, "Failed to create new document $documentType in ${folderId.id}")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(appContext, configurationProvider.useExceptionMessage),
@@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.getDefaultMessage
import me.proton.android.drive.extension.log
import me.proton.android.drive.photos.domain.entity.PhotoBackupState
import me.proton.android.drive.photos.domain.usecase.GetPhotosConfiguration
import me.proton.android.drive.photos.domain.usecase.GetPhotosDriveLink
@@ -45,11 +46,10 @@ import me.proton.android.drive.ui.viewstate.PhotosBackupViewState
import me.proton.android.drive.ui.viewstate.TagsMigrationProgressState
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.drive.backup.domain.entity.BackupNetworkType
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.exception.DriveException
import me.proton.core.drive.base.domain.extension.firstSuccessOrError
import me.proton.core.drive.base.domain.extension.toResult
import me.proton.core.drive.base.domain.log.LogTag.BACKUP
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.domain.usecase.IsIgnoringBatteryOptimizations
@@ -180,7 +180,7 @@ class PhotosBackupViewModel @Inject constructor(
}
}
}.onFailure { error ->
error.log(BACKUP)
error.log(VIEW_MODEL, "Failed to toggle backup")
val userError = if (error.cause is DriveException) {
error.cause as DriveException
} else {
@@ -201,7 +201,7 @@ class PhotosBackupViewModel @Inject constructor(
override val onToggleMobileData = {
viewModelScope.launch {
togglePhotosNetworkConfiguration(userId).onFailure { error ->
error.log(BACKUP)
error.log(VIEW_MODEL, "Failed to toggle photos network configuration")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -35,7 +35,7 @@ import me.proton.android.drive.ui.viewevent.PhotosExportDataViewEvent
import me.proton.android.drive.ui.viewstate.PhotosExportDataViewState
import me.proton.android.drive.usecase.ExportPhotoData
import me.proton.android.drive.usecase.SendFile
import me.proton.core.drive.base.domain.log.LogTag.BACKUP
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
@@ -82,7 +82,7 @@ class PhotosExportDataViewModel @Inject constructor(
private suspend fun onExportData(context: Context) {
val onFailure: (exception: Throwable) -> Unit = { error ->
error.log(BACKUP)
error.log(VIEW_MODEL, "Failed to export data")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -23,6 +23,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.photos.domain.usecase.AddToAlbumInfo
import me.proton.android.drive.photos.domain.usecase.GetAddToAlbumPhotoListings
import me.proton.android.drive.photos.domain.usecase.GetPhotoListingCount
@@ -118,11 +119,19 @@ open class PhotosPickerAndSelectionViewModel(
private fun addToAlbumAndToSelected(driveLink: DriveLink.File) = viewModelScope.launch {
val photoListings = setOf(driveLink.toVolumePhotoListing())
if (destinationAlbumId == null) {
addToAlbumInfo(photoListings)
.getOrNull(VIEW_MODEL, "Failed to add to album info for new album ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}")
addToAlbumInfo(photoListings).onFailure { error ->
error.log(
VIEW_MODEL,
"Failed to add to album info for new album ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}"
)
}
} else {
addToAlbumInfo(destinationAlbumId, photoListings)
.getOrNull(VIEW_MODEL, "Failed to add to album info ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}")
addToAlbumInfo(destinationAlbumId, photoListings).onFailure { error ->
error.log(
VIEW_MODEL,
"Failed to add to album info ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}"
)
}
}
addSelected(listOf(driveLink.id))
}
@@ -130,11 +139,19 @@ open class PhotosPickerAndSelectionViewModel(
private fun removeFromAlbumAndFromSelected(driveLink: DriveLink.File) = viewModelScope.launch {
val photoListings = setOf(driveLink.toVolumePhotoListing())
if (destinationAlbumId == null) {
removeFromAlbumInfo(photoListings)
.getOrNull(VIEW_MODEL, "Failed to remove from album info for new album ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}")
removeFromAlbumInfo(photoListings).onFailure { error ->
error.log(
VIEW_MODEL,
"Failed to remove from album info for new album ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}"
)
}
} else {
removeFromAlbumInfo(destinationAlbumId, photoListings)
.getOrNull(VIEW_MODEL, "Failed to remove from album info ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}")
removeFromAlbumInfo(destinationAlbumId, photoListings).onFailure { error ->
error.log(
VIEW_MODEL,
"Failed to remove from album info ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}"
)
}
}
removeSelected(listOf(driveLink.id))
}
@@ -23,9 +23,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.photos.presentation.viewevent.PhotosUpsellViewEvent
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.log.LogTag.PHOTO
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.presentation.component.RunAction
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
import me.proton.core.drive.telemetry.domain.event.PhotosEvent.UpsellPhotosAccepted
@@ -60,7 +60,7 @@ class PhotosUpsellViewModel @Inject constructor(
override val onDismiss = {
viewModelScope.launch {
cancelUserMessage(userId, UserMessage.UPSELL_PHOTOS).onFailure { error ->
error.log(PHOTO, "Cannot cancel upsell photos message")
error.log(VIEW_MODEL, "Cannot cancel upsell photos message")
}
}
Unit
@@ -26,9 +26,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewModelScope
import androidx.paging.CombinedLoadStates
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.insertSeparators
import androidx.paging.map
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -38,10 +36,11 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
@@ -53,6 +52,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.extension.thumbnailVO
import me.proton.android.drive.photos.domain.entity.PhotoBackupState
import me.proton.android.drive.photos.domain.usecase.AddToAlbumInfo
@@ -92,7 +92,6 @@ import me.proton.core.drive.backup.domain.usecase.GetDisabledBackupState
import me.proton.core.drive.backup.domain.usecase.RetryBackup
import me.proton.core.drive.backup.domain.usecase.SyncFolders
import me.proton.core.drive.base.data.extension.getDefaultMessage
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.entity.FastScrollAnchor
import me.proton.core.drive.base.domain.entity.TimestampMs
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
@@ -100,7 +99,6 @@ import me.proton.core.drive.base.domain.extension.flowOf
import me.proton.core.drive.base.domain.extension.getOrNull
import me.proton.core.drive.base.domain.extension.mapWithPrevious
import me.proton.core.drive.base.domain.extension.onFailure
import me.proton.core.drive.base.domain.log.LogTag.BACKUP
import me.proton.core.drive.base.domain.log.LogTag.PHOTO
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.log.logId
@@ -117,9 +115,12 @@ import me.proton.core.drive.base.presentation.state.ListContentState
import me.proton.core.drive.base.presentation.viewmodel.onLoadState
import me.proton.core.drive.base.presentation.viewstate.TagViewState
import me.proton.core.drive.drivelink.domain.entity.DriveLink
import me.proton.core.drive.drivelink.photo.domain.entity.PhotoListingAnchor
import me.proton.core.drive.drivelink.photo.domain.entity.PhotoListingsSyncState
import me.proton.core.drive.drivelink.photo.domain.paging.PhotoDriveLinks
import me.proton.core.drive.drivelink.photo.domain.usecase.GetPagedPhotoListingsList
import me.proton.core.drive.drivelink.photo.domain.usecase.GetPhotoCount
import me.proton.core.drive.drivelink.photo.domain.usecase.GetPhotoListingsPagingData
import me.proton.core.drive.drivelink.photo.domain.usecase.PhotoListingsLoader
import me.proton.core.drive.drivelink.selection.domain.usecase.GetSelectedDriveLinks
import me.proton.core.drive.drivelink.selection.domain.usecase.SelectAll
import me.proton.core.drive.feature.flag.domain.usecase.IsSpringSalePromoEnabled
@@ -148,6 +149,7 @@ import java.util.Calendar
import java.util.Locale
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import me.proton.core.drive.base.data.extension.log as logResult
import me.proton.core.drive.base.domain.extension.combine as baseCombine
import me.proton.core.drive.i18n.R as I18N
import me.proton.core.presentation.R as CorePresentation
@@ -165,7 +167,7 @@ class PhotosViewModel @Inject constructor(
removeFromAlbumInfo: RemoveFromAlbumInfo,
getAddToAlbumPhotoListings: GetAddToAlbumPhotoListings,
getPhotoListingCount: GetPhotoListingCount,
getPagedPhotoListingsList: GetPagedPhotoListingsList,
private val getPhotoListingsPagingData: GetPhotoListingsPagingData,
getBackupState: GetBackupState,
getDisabledBackupState: GetDisabledBackupState,
getPhotoCount: GetPhotoCount,
@@ -191,6 +193,7 @@ class PhotosViewModel @Inject constructor(
private val cancelUserMessage: CancelUserMessage,
private val toFirstItemMetricsNotifier: ToFirstItemMetricsNotifier,
private val isSpringSalePromoEnabled: IsSpringSalePromoEnabled,
private val photoListingsLoader: PhotoListingsLoader,
val backupPermissionsViewModel: BackupPermissionsViewModel,
) : PhotosPickerAndSelectionViewModel(
savedStateHandle = savedStateHandle,
@@ -272,6 +275,7 @@ class PhotosViewModel @Inject constructor(
val initialViewState = PhotosViewState(
title = appContext.getString(I18N.string.photos_title),
navigationIconResId = CorePresentation.drawable.ic_proton_hamburger,
navigationContentDescription = appContext.getString(I18N.string.common_open_side_menu_action),
topBarActions = topBarActions,
listContentState = ListContentState.Loading,
showEmptyList = null,
@@ -304,7 +308,7 @@ class PhotosViewModel @Inject constructor(
contentState = listContentState,
shareType = Share.Type.PHOTO,
)
error.log(VIEW_MODEL, "Cannot get drive link")
error.logResult(VIEW_MODEL, "Cannot get drive link")
toFirstItemMetricsNotifier.reset()
if (previous is DataResult.Success) {
retryLoadingPhotosDriveLinkFolder()
@@ -315,28 +319,75 @@ class PhotosViewModel @Inject constructor(
)
}.stateIn(viewModelScope, Eagerly, null)
private fun loaderKey(tag: PhotoTag?) =
tag?.let { PhotoListingAnchor.tagKey(it) } ?: PhotoListingAnchor.mainKey
private val _startLoader = combine(
driveLink.filterNotNull(),
photoListingsFilter,
) { link, tag ->
photoListingsLoader.load(
key = loaderKey(tag),
userId = userId,
volumeId = link.volumeId,
shareId = link.id.shareId,
tag = tag,
)
}.stateIn(viewModelScope, Eagerly, Unit)
val loaderSyncState: StateFlow<PhotoListingsSyncState> = combine(
driveLink.filterNotNull(),
photoListingsFilter,
) { link, tag ->
photoListingsLoader.stateFor(loaderKey(tag))
}.flatMapLatest { it }
.stateIn(viewModelScope, Eagerly, PhotoListingsSyncState.Idle)
private val _loaderErrors = viewModelScope.launch {
loaderSyncState.collect { syncState ->
if (syncState is PhotoListingsSyncState.Error && listContentState.value is ListContentState.Content) {
_homeEffect.emit(
HomeEffect.ShowSnackbar(
syncState.error.getDefaultMessage(appContext, configurationProvider.useExceptionMessage)
)
)
}
}
}
private val effectiveContentState: StateFlow<ListContentState> = combine(
listContentState,
loaderSyncState,
) { contentState, syncState ->
when {
syncState is PhotoListingsSyncState.Loading && contentState is ListContentState.Content ->
contentState.copy(isRefreshing = true)
syncState is PhotoListingsSyncState.Loading && contentState !is ListContentState.Content ->
ListContentState.Loading
syncState is PhotoListingsSyncState.Error && contentState !is ListContentState.Content ->
ListContentState.Error(
message = syncState.error.getDefaultMessage(appContext, configurationProvider.useExceptionMessage),
actionResId = I18N.string.common_retry,
)
else -> contentState
}
}.stateIn(viewModelScope, Eagerly, ListContentState.Loading)
val driveLinksMap: Flow<Map<LinkId, DriveLink>> = photoDriveLinks.getDriveLinksMapFlow(userId)
val driveLinks: Flow<PagingData<PhotosItem>> = combine(
parentId.filterNotNull().distinctUntilChanged(),
photoListingsFilter,
) { _, filter ->
filter
}.transformLatest { filter ->
emit(PagingData.empty())
emitAll(getPagedPhotoListingsList(userId, filter)
.map { pagingData ->
pagingData.map { photoListing ->
PhotosItem.PhotoListing(
id = photoListing.linkId,
captureTime = photoListing.captureTime,
link = null,
thumbnailVO = photoListing.thumbnailVO,
)
}
}
.map {
it.insertSeparators { before: PhotosItem.PhotoListing?, after: PhotosItem.PhotoListing? ->
private val pagingDataCache = mutableMapOf<PhotoTag?, Flow<PagingData<PhotosItem>>>()
private fun pagingDataForTag(link: DriveLink.Folder, tag: PhotoTag?): Flow<PagingData<PhotosItem>> =
pagingDataCache.getOrPut(tag) {
getPhotoListingsPagingData(userId, link.volumeId, tag) { listing ->
PhotosItem.PhotoListing(
id = listing.linkId,
captureTime = listing.captureTime,
link = null,
thumbnailVO = listing.thumbnailVO,
)
}.map { pagingData ->
pagingData.insertSeparators { before: PhotosItem.PhotoListing?, after: PhotosItem.PhotoListing? ->
if (after == null) {
null
} else if (before == null) {
@@ -356,10 +407,8 @@ class PhotosViewModel @Inject constructor(
val afterCalendar = Calendar.getInstance().apply {
timeInMillis = after.captureTime.value * 1000L
}
if (beforeCalendar.get(Calendar.YEAR)
!= afterCalendar.get(Calendar.YEAR) ||
beforeCalendar.get(Calendar.MONTH)
!= afterCalendar.get(Calendar.MONTH)
if (beforeCalendar.get(Calendar.YEAR) != afterCalendar.get(Calendar.YEAR) ||
beforeCalendar.get(Calendar.MONTH) != afterCalendar.get(Calendar.MONTH)
) {
val cal = Calendar.getInstance().apply {
timeInMillis = after.captureTime.value * 1000L
@@ -375,9 +424,17 @@ class PhotosViewModel @Inject constructor(
}
}
}
}
)
}.cachedIn(viewModelScope)
}.shareIn(viewModelScope, WhileSubscribed(5_000), replay = 1)
}
val photoItems: Flow<PagingData<PhotosItem>> = combine(
driveLink.filterNotNull(),
photoListingsFilter,
) { link, filter ->
link to filter
}.flatMapLatest { (link, filter) ->
pagingDataForTag(link, filter)
}
private val listContentAppendingState = MutableStateFlow<ListContentAppendingState>(
ListContentAppendingState.Idle
@@ -413,7 +470,7 @@ class PhotosViewModel @Inject constructor(
val viewState: Flow<PhotosViewState> = baseCombine(
selected,
listContentState,
effectiveContentState,
backupState,
getPhotoCount(userId = userId),
firstVisibleItemIndex,
@@ -476,6 +533,11 @@ class PhotosViewModel @Inject constructor(
} else {
CorePresentation.drawable.ic_proton_cross
},
navigationContentDescription = if (showHamburgerMenuIcon) {
appContext.getString(I18N.string.common_open_side_menu_action)
} else {
appContext.getString(I18N.string.common_close_action)
},
notificationDotVisible = showHamburgerMenuIcon && notificationDotRequested,
inMultiselect = selected.isNotEmpty() || inPickerMode,
isFastScrollEnabled = isFastScrollEnabled,
@@ -660,7 +722,7 @@ class PhotosViewModel @Inject constructor(
enablePhotosBackup(folderId).onSuccess { state ->
onPhotoBackupState(state)
}.onFailure { error ->
error.log(BACKUP, "Cannot enable backup for folder: ${folderId.id}")
error.log(VIEW_MODEL, "Cannot enable backup for folder: ${folderId.id}")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -702,7 +764,7 @@ class PhotosViewModel @Inject constructor(
viewModelScope.launch {
(parentId.value as? FolderId)?.let { folderId ->
retryBackup(folderId).onFailure { error ->
error.log(BACKUP, "Cannot retry on backup")
error.log(VIEW_MODEL, "Cannot retry on backup")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -719,7 +781,7 @@ class PhotosViewModel @Inject constructor(
private fun dismissBackgroundRestrictions() {
viewModelScope.launch {
cancelUserMessage(userId, UserMessage.BACKUP_BATTERY_SETTINGS).onFailure { error ->
error.log(BACKUP, "Cannot dismiss battery settings warning")
error.log(VIEW_MODEL, "Cannot dismiss battery settings warning")
}
}
}
@@ -734,7 +796,16 @@ class PhotosViewModel @Inject constructor(
if (driveLink.value == null) {
retryLoadingPhotosDriveLinkFolder()
} else {
retryList()
driveLink.value?.let { link ->
val tag = photoListingsFilter.value
photoListingsLoader.retry(
key = loaderKey(tag),
userId = userId,
volumeId = link.volumeId,
shareId = link.id.shareId,
tag = tag,
)
}
}
}
}
@@ -744,10 +815,6 @@ class PhotosViewModel @Inject constructor(
listContentState.value = ListContentState.Loading
}
private suspend fun retryList() {
_listEffect.emit(ListEffect.RETRY)
}
private fun onRefresh() {
viewModelScope.launch {
(parentId.value as? FolderId)?.let { folderId ->
@@ -758,6 +825,16 @@ class PhotosViewModel @Inject constructor(
error.log(VIEW_MODEL, "Failed sync folder on manual refresh")
}
}
driveLink.value?.let { link ->
val tag = photoListingsFilter.value
photoListingsLoader.refresh(
key = loaderKey(tag),
userId = userId,
volumeId = link.volumeId,
shareId = link.id.shareId,
tag = tag,
)
}
_listEffect.emit(ListEffect.REFRESH)
}
}
@@ -792,7 +869,10 @@ class PhotosViewModel @Inject constructor(
isFastScrollEnabled.value = isFastScrollThresholdReached(items.size, anchors, anchorsInLabel)
}
private val fastScrollAnchors: MutableMap<Pair<Int, Int>, List<FastScrollAnchor>> = mutableMapOf()
private val fastScrollAnchors: MutableMap<Pair<Int, Int>, List<FastScrollAnchor>> =
object : LinkedHashMap<Pair<Int, Int>, List<FastScrollAnchor>>() {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Pair<Int, Int>, List<FastScrollAnchor>>) = size > 1
}
private val List<PhotosItem>.itemsHash get() = this.fold(1) { acc, item ->
31 * acc + item.hashCode()
@@ -74,6 +74,7 @@ import me.proton.core.drive.base.domain.entity.TimestampS
import me.proton.core.drive.base.domain.entity.toFileTypeCategory
import me.proton.core.drive.base.domain.extension.bytes
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
import me.proton.core.drive.base.domain.extension.getOrNull
import me.proton.core.drive.base.domain.function.pagedList
import me.proton.core.drive.base.domain.log.LogTag
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
@@ -258,6 +259,7 @@ class PreviewViewModel @Inject constructor(
private val renderFailed = MutableStateFlow<Pair<Throwable, Any>?>(null)
val initialViewState = PreviewViewState(
navigationIconResId = CorePresentation.drawable.ic_arrow_back,
navigationContentDescription = appContext.getString(I18N.string.common_back_action),
isFullscreen = isFullscreen,
previewContentState = PreviewContentState.Loading,
items = emptyList(),
@@ -547,7 +549,7 @@ class PreviewViewModel @Inject constructor(
viewModelScope.launch {
openProtonDocumentInBrowser(driveLink)
.onFailure { error ->
error.log(LogTag.DEFAULT, "Open document failed")
error.log(VIEW_MODEL, "Open document failed")
val message = when {
error is ActivityNotFoundException -> appContext.getString(I18N.string.common_error_no_browser_available)
else -> error.getDefaultMessage(
@@ -651,9 +653,9 @@ class FolderContentProvider(
.distinctUntilChanged()
.mapLatest {
getDecryptedDriveLinks(folderId)
.getOrNull()
.getOrNull(LogTag.DEFAULT, "Cannot decrypt drive links for ${folderId.id}")
?.filterIsInstance<DriveLink.File>()
?: emptyList<DriveLink.File>()
?: emptyList()
}
)
} ?: emit(emptyList())
@@ -726,9 +728,9 @@ class OfflineContentProvider(
.distinctUntilChanged()
.mapLatest {
getDecryptedOfflineDriveLinks(userId)
.getOrNull()
.getOrNull(LogTag.DEFAULT, "Cannot decrypt offline drive links")
?.filterIsInstance<DriveLink.File>()
?: emptyList<DriveLink.File>()
?: emptyList()
}
.stateIn(coroutineScope, SharingStarted.Eagerly, emptyList())
@@ -46,7 +46,6 @@ import me.proton.android.drive.extension.getDefaultMessage
import me.proton.android.drive.extension.log
import me.proton.core.drive.base.domain.entity.TimestampMs
import me.proton.core.drive.base.domain.extension.bytes
import me.proton.core.drive.base.domain.extension.getOrNull
import me.proton.core.drive.base.domain.extension.toResult
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
@@ -189,20 +188,18 @@ class ScanDocumentNameViewModel @Inject constructor(
error.showOnDoneError()
return@launch
}
removeScanResult(userId, scanResult.id).getOrNull(
tag = VIEW_MODEL,
message = "Failed to remove scan result"
)
removeScanResult(userId, scanResult.id).onFailure { error ->
error.log(VIEW_MODEL, "Failed to remove scan result")
}
navigateBack()
}
}
private fun onCancel(navigateBack: () -> Unit) {
viewModelScope.launch {
clearScanResult(userId, scanResultId).getOrNull(
tag = VIEW_MODEL,
message = "Failed clearing scan result $scanResultId",
)
clearScanResult(userId, scanResultId).onFailure { error ->
error.log(VIEW_MODEL, "Failed clearing scan result $scanResultId")
}
navigateBack()
}
}
@@ -34,6 +34,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.presentation.common.Action
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
import me.proton.core.drive.drivelink.domain.entity.DriveLink
@@ -99,7 +101,9 @@ open class SelectionViewModel(
super.onCleared()
selectionId.value?.let { selectionId ->
CoroutineScope(Dispatchers.Main).launch {
deselectLinks(selectionId)
deselectLinks(selectionId).onFailure { error ->
error.log(VIEW_MODEL, "Failed to deselect links")
}
}
}
}
@@ -113,7 +117,13 @@ open class SelectionViewModel(
protected open fun onTopAppBarNavigation(nonSelectedBlock: () -> Unit): () -> Unit = {
Unit.also {
if (selected.value.isNotEmpty()) {
selectionId.value?.let { viewModelScope.launch { deselectLinks(it) } }
selectionId.value?.let { selectionId ->
viewModelScope.launch {
deselectLinks(selectionId).onFailure { error ->
error.log(VIEW_MODEL, "Failed to deselect links")
}
}
}
} else {
nonSelectedBlock()
}
@@ -152,16 +162,25 @@ open class SelectionViewModel(
protected open fun addSelected(linkIds: List<LinkId>) {
viewModelScope.launch {
selectionId.value?.let { selectionId ->
selectLinks(selectionId, linkIds)
} ?: setSelectionId(selectLinks(linkIds).getOrNull())
val id = selectionId.value
if (id != null) {
selectLinks(id, linkIds).onFailure { error ->
error.log(VIEW_MODEL, "Failed to select links")
}
} else {
setSelectionId(selectLinks(linkIds).onFailure { error ->
error.log(VIEW_MODEL, "Failed to select links")
}.getOrNull())
}
}
}
protected open fun removeSelected(linkIds: List<LinkId>) {
viewModelScope.launch {
selectionId.value?.let { selectionId ->
deselectLinks(selectionId, linkIds)
deselectLinks(selectionId, linkIds).onFailure { error ->
error.log(VIEW_MODEL, "Failed to deselect links")
}
}
}
}
@@ -169,7 +188,11 @@ open class SelectionViewModel(
protected fun removeAllSelected() {
if (selected.value.isNotEmpty()) {
viewModelScope.launch {
selectionId.value?.let { selectionId -> deselectLinks(selectionId) }
selectionId.value?.let { selectionId ->
deselectLinks(selectionId).onFailure { error ->
error.log(VIEW_MODEL, "Failed to deselect links")
}
}
}
}
}
@@ -39,10 +39,10 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.navigation.Screen
import me.proton.core.domain.arch.DataResult
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.entity.Percentage
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
@@ -183,6 +183,7 @@ class SettingsViewModel @Inject constructor(
) { debugSettings, themeStyle, enabled, autoLockDuration, isBackupEnabled, dynamicHomeTab, userLogKillSwitch ->
SettingsViewState(
navigationIcon = CorePresentation.drawable.ic_arrow_back,
navigationContentDescription = context.getString(I18N.string.common_back_action),
appNameResId = I18N.string.app_name,
appVersion = BuildConfig.VERSION_NAME,
legalLinks = listOf(
@@ -225,7 +226,12 @@ class SettingsViewModel @Inject constructor(
},
onThemeStyleChanged = { newStyle ->
viewModelScope.launch {
updateThemeStyle(userId, enumValues<ThemeStyle>().first { style -> style.resId == newStyle })
updateThemeStyle(
userId = userId,
themeStyle = enumValues<ThemeStyle>().first { style -> style.resId == newStyle }
).onFailure { error ->
error.log(VIEW_MODEL, "Failed to update theme style")
}
}
},
onAccountSettings = {
@@ -32,9 +32,9 @@ import me.proton.core.compose.component.bottomsheet.RunAction
import me.proton.core.domain.arch.DataResult
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.drive.base.data.extension.getDefaultMessage
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.data.extension.log as logResult
import me.proton.core.drive.base.domain.entity.Permissions
import me.proton.core.drive.base.domain.log.LogTag.SHARING
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.extension.require
@@ -130,7 +130,7 @@ abstract class ShareInvitationOptionsViewModel(
error: DataResult.Error,
type: BroadcastMessage.Type = BroadcastMessage.Type.WARNING
) {
error.log(SHARING)
error.logResult(VIEW_MODEL)
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -31,13 +31,13 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.options.ShareLinkPermissionsOption
import me.proton.core.compose.component.bottomsheet.RunAction
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.drive.base.data.extension.getDefaultMessage
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.entity.Permissions
import me.proton.core.drive.base.domain.log.LogTag.SHARING
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.extension.require
@@ -112,7 +112,7 @@ class ShareLinkPermissionsViewModel @Inject constructor(
permissions = permissions,
).onFailure { error ->
error.log(
SHARING,
VIEW_MODEL,
"Cannot update permissions for ${sharedDriveLink.shareUrlId.id}"
)
broadcastMessages(
@@ -33,15 +33,16 @@ import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.options.MemberOption
import me.proton.core.compose.component.bottomsheet.RunAction
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.drive.base.data.extension.getDefaultMessage
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.data.extension.log as logResult
import me.proton.core.drive.base.domain.entity.Permissions
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
import me.proton.core.drive.base.domain.extension.onFailure
import me.proton.core.drive.base.domain.log.LogTag.SHARING
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.extension.require
@@ -145,7 +146,7 @@ class ShareMemberOptionsViewModel @Inject constructor(
).filterSuccessOrError()
.last()
dataResult.onFailure { error ->
error.log(SHARING)
error.logResult(VIEW_MODEL, "Failed to update member permissions for $memberId")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -163,7 +164,7 @@ class ShareMemberOptionsViewModel @Inject constructor(
memberId = memberId,
).filterSuccessOrError().last()
dataResult.onFailure { error ->
error.log(SHARING)
error.logResult(VIEW_MODEL, "Failed to delete member for $memberId")
broadcastMessages(
userId = userId,
message = error.getDefaultMessage(
@@ -24,11 +24,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import me.proton.android.drive.extension.log
import me.proton.android.drive.photos.domain.usecase.AddPhotosToAlbum
import me.proton.android.drive.photos.domain.usecase.AddToAlbumInfo
import me.proton.android.drive.ui.viewevent.ShareMultiplePhotosOptionsViewEvent
import me.proton.android.drive.ui.viewstate.ShareMultiplePhotosOptionsViewState
import me.proton.core.domain.arch.DataResult
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
import me.proton.core.drive.base.presentation.component.RunAction
@@ -117,7 +119,9 @@ class ShareMultiplePhotosOptionsViewModel @Inject constructor(
}
override suspend fun onSuccessfullyAdded(photoListings: List<PhotoListing.Volume>) {
deselectLinks(selectionId)
deselectLinks(selectionId).onFailure { error ->
error.log(VIEW_MODEL, "Failed to deselect links")
}
}
override fun getAlbumDetails(album: DriveLink.Album?): String? = album.details()
@@ -18,16 +18,17 @@
package me.proton.android.drive.ui.viewmodel
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.proton.android.drive.ui.effect.HomeEffect
import me.proton.android.drive.ui.effect.HomeTabViewModel
@@ -42,6 +43,7 @@ import me.proton.core.drive.i18n.R as I18N
@HiltViewModel
class SharedTabsViewModel @Inject constructor(
@param:ApplicationContext private val appContext: Context,
savedStateHandle: SavedStateHandle,
shouldUpgradeStorage: ShouldUpgradeStorage,
) : ViewModel(),
@@ -58,6 +60,7 @@ class SharedTabsViewModel @Inject constructor(
val initialViewState = SharedTabsViewState(
titleResId = I18N.string.title_shared,
navigationIconResId = CorePresentation.drawable.ic_proton_hamburger,
navigationContentDescription = appContext.getString(I18N.string.common_open_side_menu_action),
notificationDotVisible = false,
tabs = listOf(sharedWithMeTab, sharedWithByTab),
selectedTab = selectedTab.value,
@@ -23,7 +23,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.navigation.Screen
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.presentation.extension.require
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
import me.proton.core.drive.sorting.domain.entity.Sorting
@@ -43,7 +45,9 @@ class SortingDialogViewModel @Inject constructor(
suspend fun setSorting(sorting: Sorting) {
viewModelScope.launch {
updateSorting(userId, sorting)
updateSorting(userId, sorting).onFailure { error ->
error.log(VIEW_MODEL, "Failed update sorting")
}
}.join()
}
}
@@ -27,12 +27,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.proton.android.drive.R
import me.proton.android.drive.log.DriveLogTag.DEFAULT
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.viewevent.DriveLitePopupViewEvent
import me.proton.android.drive.ui.viewstate.SubscriptionPromoViewState
import me.proton.android.drive.usecase.MarkSubscriptionPromoAsShown
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.extension.GiB
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.presentation.component.RunAction
import me.proton.core.drive.base.presentation.extension.asHumanReadableString
import me.proton.core.drive.base.presentation.extension.require
@@ -151,7 +151,7 @@ class SubscriptionPromoViewModel @Inject constructor(
override val onDismiss = {
viewModelScope.launch {
markSubscriptionPromoAsShown(userId, key).onFailure { error ->
error.log(DEFAULT, "Cannot mark subscription promo as shown")
error.log(VIEW_MODEL, "Cannot mark subscription promo as shown")
}
}
Unit
@@ -45,6 +45,7 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.common.onClick
import me.proton.android.drive.ui.effect.HomeEffect
import me.proton.android.drive.ui.effect.HomeTabViewModel
@@ -57,18 +58,18 @@ import me.proton.core.drive.base.domain.extension.onFailure
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.presentation.common.getThemeDrawableId
import me.proton.core.drive.base.presentation.effect.ListEffect
import me.proton.core.drive.base.presentation.state.ListContentAppendingState
import me.proton.core.drive.base.presentation.state.ListContentState
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
import me.proton.core.drive.base.presentation.viewmodel.onLoadState
import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink
import me.proton.core.drive.drivelink.domain.entity.DriveLink
import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted
import me.proton.core.drive.drivelink.list.domain.usecase.GetPagedDriveLinksList
import me.proton.core.drive.files.domain.usecase.ToFirstItemMetricsNotifier
import me.proton.core.drive.files.presentation.event.FilesViewEvent
import me.proton.core.drive.files.presentation.state.FilesViewState
import me.proton.core.drive.base.presentation.state.ListContentAppendingState
import me.proton.core.drive.base.presentation.state.ListContentState
import me.proton.core.drive.base.presentation.effect.ListEffect
import me.proton.core.drive.base.presentation.viewmodel.onLoadState
import me.proton.core.drive.files.domain.usecase.ToFirstItemMetricsNotifier
import me.proton.core.drive.i18n.R
import me.proton.core.drive.link.domain.entity.FolderId
import me.proton.core.drive.link.domain.extension.userId
@@ -76,7 +77,6 @@ import me.proton.core.drive.observability.domain.metrics.common.mobile.performan
import me.proton.core.drive.share.domain.entity.ShareId
import me.proton.core.drive.sorting.domain.entity.Sorting
import me.proton.core.drive.sorting.domain.usecase.GetSorting
import me.proton.core.util.kotlin.CoreLogger
import me.proton.drive.android.settings.domain.entity.LayoutType
import me.proton.drive.android.settings.domain.usecase.GetLayoutType
import me.proton.drive.android.settings.domain.usecase.ToggleLayoutType
@@ -114,6 +114,7 @@ class SyncedFoldersViewModel @Inject constructor(
title = folderName,
titleResId = R.string.title_offline_available,
navigationIconResId = me.proton.core.presentation.R.drawable.ic_arrow_back,
navigationContentDescription = appContext.getString(I18N.string.common_back_action),
drawerGesturesEnabled = true,
sorting = Sorting.DEFAULT,
listContentState = listContentState.value,
@@ -254,7 +255,14 @@ class SyncedFoldersViewModel @Inject constructor(
}
private fun onToggleLayout() {
viewModelScope.launch { toggleLayoutType(userId = userId, currentLayoutType = layoutType.value) }
viewModelScope.launch {
toggleLayoutType(
userId = userId,
currentLayoutType = layoutType.value,
).onFailure { error ->
error.log(VIEW_MODEL, "Failed to toggle layout type $layoutType")
}
}
}
private fun retryList() {
@@ -41,12 +41,14 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.ui.effect.TrashEffect
import me.proton.android.drive.ui.navigation.Screen
import me.proton.android.drive.ui.screen.EmptyTrashIconState
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.drive.base.domain.entity.TimestampMs
import me.proton.core.drive.base.domain.extension.flowOf
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.presentation.common.getThemeDrawableId
import me.proton.core.drive.base.presentation.effect.ListEffect
@@ -112,6 +114,7 @@ class TrashViewModel @Inject constructor(
titleResId = I18N.string.common_trash,
sorting = Sorting.DEFAULT,
navigationIconResId = CorePresentation.drawable.ic_arrow_back,
navigationContentDescription = appContext.getString(I18N.string.common_back_action),
drawerGesturesEnabled = true,
listContentState = listContentState.value,
listContentAppendingState = listContentAppendingState.value,
@@ -247,6 +250,13 @@ class TrashViewModel @Inject constructor(
}
private fun onToggleLayout() {
viewModelScope.launch { toggleLayoutType(userId = userId, currentLayoutType = layoutType.value) }
viewModelScope.launch {
toggleLayoutType(
userId = userId,
currentLayoutType = layoutType.value,
).onFailure { error ->
error.log(VIEW_MODEL, "Failed to toggle layout $layoutType")
}
}
}
}
@@ -41,7 +41,7 @@ import me.proton.android.drive.ui.navigation.UploadParameters
import me.proton.android.drive.ui.viewevent.UploadToViewEvent
import me.proton.android.drive.ui.viewstate.UploadToViewState
import me.proton.core.domain.arch.mapSuccessValueOrNull
import me.proton.core.drive.base.domain.log.LogTag
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.util.coRunCatching
import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink
@@ -97,6 +97,7 @@ class UploadToViewModel @Inject constructor(
filesViewState = initialFilesViewState,
title = "",
navigationIconResId = CorePresentation.drawable.ic_proton_cross,
navigationContentDescription = appContext.getString(I18N.string.common_close_action),
fileNames = uploadParameters?.uris?.map { uri -> uri.fileName } ?: emptyList(),
)
val driveLink: StateFlow<DriveLink.Folder?> = getDriveLink(userId, folderId = null)
@@ -122,6 +123,11 @@ class UploadToViewModel @Inject constructor(
} else {
CorePresentation.drawable.ic_arrow_back
},
navigationContentDescription = if (parentLink == null || isRoot) {
appContext.getString(I18N.string.common_close_action)
} else {
appContext.getString(I18N.string.common_back_action)
},
)
}.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
@@ -176,7 +182,7 @@ class UploadToViewModel @Inject constructor(
exitApp()
}
}.onFailure { error ->
error.log(LogTag.UPLOAD, "Failed to upload ${copiedUris.size} files")
error.log(VIEW_MODEL, "Failed to upload ${copiedUris.size} files")
cleanupOnFailure(copiedUris)
when (error) {
is NotEnoughSpaceException -> navigateToStorageFull()
@@ -33,14 +33,14 @@ import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.core.domain.arch.DataResult
import me.proton.core.domain.arch.onSuccess
import me.proton.core.drive.base.data.extension.getDefaultMessage
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.data.extension.log as logResult
import me.proton.core.drive.base.data.extension.logDefaultMessage
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
import me.proton.core.drive.base.domain.extension.onFailure
import me.proton.core.drive.base.domain.log.LogTag.SHARING
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
@@ -94,7 +94,7 @@ class UserInvitationViewModel @Inject constructor(
when (result) {
is DataResult.Processing -> listContentState.value = ListContentState.Loading
is DataResult.Error -> {
result.log(VIEW_MODEL)
result.logResult(VIEW_MODEL)
listContentState.value = ListContentState.Error(result.logDefaultMessage(
context = appContext,
useExceptionMessage = configurationProvider.useExceptionMessage,
@@ -124,6 +124,7 @@ class UserInvitationViewModel @Inject constructor(
}
).format(0),
navigationIconResId = CorePresentation.drawable.ic_proton_arrow_back,
navigationContentDescription = appContext.getString(I18N.string.common_back_action),
listContentState = listContentState.value,
)
@@ -155,7 +156,7 @@ class UserInvitationViewModel @Inject constructor(
acceptUserInvitation(invitationId).filterSuccessOrError()
.last()
.onFailure { error ->
error.log(SHARING, "Cannot accept invitation: $invitationId")
error.logResult(VIEW_MODEL, "Cannot accept invitation: $invitationId")
broadcastMessages(
userId,
error.getDefaultMessage(
@@ -185,7 +186,7 @@ class UserInvitationViewModel @Inject constructor(
.filterSuccessOrError()
.last()
.onFailure { error ->
error.log(SHARING, "Cannot delete invitation: $invitationId")
error.logResult(VIEW_MODEL, "Cannot delete invitation: $invitationId")
broadcastMessages(
userId,
error.getDefaultMessage(
@@ -26,9 +26,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import me.proton.android.drive.extension.log
import me.proton.android.drive.usecase.MarkWhatsNewAsShown
import me.proton.core.drive.base.domain.extension.flowOf
import me.proton.core.drive.base.domain.extension.getOrNull
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
import me.proton.core.drive.base.presentation.common.getThemeDrawableId
import me.proton.core.drive.base.presentation.viewevent.WhatsNewViewEvent
@@ -69,8 +69,9 @@ class WhatsNewViewModel @Inject constructor(
private fun whatsNewShown() {
viewModelScope.launch {
markWhatsNewAsShown(key)
.getOrNull(VIEW_MODEL, "Marking whats new as shown failed for: $key")
markWhatsNewAsShown(key).onFailure { error ->
error.log(VIEW_MODEL, "Marking whats new as shown failed for: $key")
}
}
}
@@ -24,6 +24,7 @@ import me.proton.core.drive.base.presentation.state.ListContentState
data class ComputersViewState(
val title: String,
@DrawableRes val navigationIconResId: Int,
val navigationContentDescription: String?,
val notificationDotVisible: Boolean,
val listContentState: ListContentState,
val isRefreshEnabled: Boolean,
@@ -29,6 +29,7 @@ data class MoveFileViewState(
val title: String,
val isTitleEncrypted: Boolean = false,
val navigationIconResId: Int = 0,
val navigationContentDescription: String? = null,
val driveLinks: List<String> = emptyList(),
val topBarActions: Flow<Set<Action>> = emptyFlow(),
)
@@ -24,6 +24,7 @@ import androidx.annotation.StringRes
data class SharedTabsViewState(
@StringRes val titleResId: Int,
@DrawableRes val navigationIconResId: Int,
val navigationContentDescription: String?,
val notificationDotVisible: Boolean,
val tabs: List<SharedTab>,
val selectedTab: SharedTab,
@@ -28,5 +28,6 @@ data class UploadToViewState(
val isTitleEncrypted: Boolean = false,
val isBackHandlerEnabled: Boolean = false,
val navigationIconResId: Int = 0,
val navigationContentDescription: String? = null,
val fileNames: List<String> = emptyList(),
)
@@ -27,7 +27,6 @@ import me.proton.core.drive.announce.event.domain.usecase.AnnounceEvent
import me.proton.core.drive.base.domain.usecase.ClearCacheFolder
import me.proton.core.drive.base.domain.usecase.GetInternalStorageInfo
import me.proton.core.drive.base.domain.util.coRunCatching
import me.proton.core.drive.photo.data.db.PhotoDatabase
import me.proton.core.util.kotlin.CoreLogger
import java.io.IOException
import javax.inject.Inject
@@ -36,7 +35,6 @@ class HandleUncaughtException @Inject constructor(
private val getInternalStorageInfo: GetInternalStorageInfo,
private val clearCacheFolder: ClearCacheFolder,
private val announceEvent: AnnounceEvent,
private val db: PhotoDatabase,
) {
operator fun invoke(error: Throwable, isFromMainThread: Boolean): Result<Boolean> = coRunCatching {
@@ -46,28 +44,7 @@ class HandleUncaughtException @Inject constructor(
clearCacheFolder()
}
val isCursorWindowError = error.isCursorWindowError
val hasGetPhotoListingsDescFlowString = error.stackTraceToString().contains("getPhotoListingsDescFlow")
CoreLogger.d(DriveLogTag.CRASH, "HandleUncaughtException isCursorWindowError=$isCursorWindowError, hasGetPhotoListingsDescFlowString=$hasGetPhotoListingsDescFlowString, isFromMainThread=$isFromMainThread")
if (error is java.lang.IllegalStateException ||
error.cause is java.lang.IllegalStateException
) {
val invalidCaptureTimes = runBlocking {
db.photoListingDao.getInvalidCaptureTimes()
}
val message = if (invalidCaptureTimes.isNotEmpty()) {
buildString {
append("Invalid capture time found: ")
append(
invalidCaptureTimes.joinToString { (captureTime, type) ->
"(captureTime=$captureTime type=$type)"
}
)
}
} else {
"No invalid capture times found in PhotoListingEntity"
}
CoreLogger.d(DriveLogTag.CRASH, message)
}
CoreLogger.d(DriveLogTag.CRASH, "HandleUncaughtException isCursorWindowError=$isCursorWindowError, isFromMainThread=$isFromMainThread")
when (error) {
is IOException,
is SQLiteDiskIOException,
@@ -26,10 +26,8 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import me.proton.android.drive.log.DriveLogTag.UI
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.domain.entity.UserId
import me.proton.core.drive.base.data.entity.LoggerLevel.WARNING
import me.proton.core.drive.base.data.extension.log
import me.proton.core.drive.base.domain.extension.getOrNull
import me.proton.core.util.kotlin.CoreLogger
import javax.inject.Inject
@@ -48,8 +46,9 @@ class ShowRatingBooster @Inject constructor(
if (task.isSuccessful) {
CoroutineScope(Dispatchers.Main).launch {
accountManager.getPrimaryUserId().firstOrNull()?.let { userId ->
markRatingBoosterAsShown(userId = userId)
.getOrNull(UI, "Marking rating booster as shown failed")
markRatingBoosterAsShown(userId = userId).onFailure { error ->
error.log(UI, "Marking rating booster as shown failed")
}
} ?: {
CoreLogger.w(UI, "User Id is null, please investigate")
}()
@@ -35,6 +35,7 @@ class AcceptNotificationEvent @Inject constructor(
&& (newEvent.state == UploadState.NEW_UPLOAD || newEvent.exists(notificationId))
is Event.Download -> true
is Event.DownloadFileProgress -> true
is Event.ForcedSignOut -> true
is Event.NoSpaceLeftOnDevice -> true
is Event.Backup -> true
@@ -21,7 +21,6 @@ package me.proton.android.drive.usecase.notification
import me.proton.core.domain.entity.UserId
import me.proton.core.drive.announce.event.domain.entity.Event
import me.proton.core.drive.notification.data.extension.createNotificationId
import me.proton.core.drive.notification.data.extension.tag
import me.proton.core.drive.notification.domain.entity.Channel
import me.proton.core.drive.notification.domain.entity.TaglessNotificationId
import me.proton.core.drive.notification.domain.extension.createTaglessNotificationId
@@ -32,9 +31,7 @@ class CreateUserNotificationId @Inject constructor() {
userId: UserId,
event: Event,
) = when (event) {
is Event.Download -> event.createNotificationId(userId).copy(
tag = "${event.tag}_${event.downloadId}"
)
is Event.Download -> event.createNotificationId(userId)
// Foreground service in worker do not use tag
is Event.TransferData -> TaglessNotificationId.UPLOAD.createTaglessNotificationId(
@@ -0,0 +1,93 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package me.proton.android.drive.usecase.notification
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
import me.proton.android.drive.extension.deepLinkBaseUrl
import me.proton.android.drive.receiver.NotificationBroadcastReceiver
import me.proton.android.drive.receiver.NotificationBroadcastReceiver.Companion.ACTION_CANCEL_ALL_DOWNLOADS
import me.proton.android.drive.receiver.NotificationBroadcastReceiver.Companion.EXTRA_NOTIFICATION_ID
import me.proton.android.drive.ui.navigation.Screen
import me.proton.core.drive.announce.event.domain.entity.Event
import me.proton.core.drive.notification.domain.entity.NotificationId
import me.proton.core.util.kotlin.serialize
import javax.inject.Inject
import me.proton.core.drive.i18n.R as I18N
class DownloadFileProgressNotificationBuilder @Inject constructor(
@param:ApplicationContext private val appContext: Context,
private val commonBuilder: CommonNotificationBuilder,
private val contentIntent: CreateContentPendingIntent,
) {
operator fun invoke(
notificationId: NotificationId.User,
events: List<Event.DownloadFileProgress>,
) = events.last().let { event ->
commonBuilder(notificationId, event)
.setContentText(
appContext.resources.getQuantityString(
I18N.plurals.notification_content_text_download_downloading,
event.downloadingCount,
event.downloadingCount,
)
)
.setProgress(100, (event.progress.value * 100).toInt(), false)
.setSilent(true)
.setContentIntent(
contentIntent(
notificationId = notificationId,
uri = "${appContext.deepLinkBaseUrl}/${Screen.Files(notificationId.channel.userId)}".toUri(),
)
)
.addAction(
NotificationCompat.Action.Builder(
0,
appContext.getString(
if (event.downloadingCount > 1) {
I18N.string.common_cancel_all_action
} else {
I18N.string.common_cancel_action
}
),
cancelAllDownloadsIntent(notificationId),
).build()
)
}
@SuppressLint("UnspecifiedImmutableFlag")
private fun cancelAllDownloadsIntent(
notificationId: NotificationId,
requestCode: Int = 8888,
): PendingIntent =
PendingIntent.getBroadcast(
appContext,
requestCode,
Intent(appContext, NotificationBroadcastReceiver::class.java).apply {
action = ACTION_CANCEL_ALL_DOWNLOADS
putExtra(EXTRA_NOTIFICATION_ID, notificationId.serialize())
},
PendingIntent.FLAG_IMMUTABLE
)
}
@@ -35,10 +35,10 @@ class DownloadNotificationBuilder @Inject constructor(
private val commonBuilder: CommonNotificationBuilder,
private val contentIntent: CreateContentPendingIntent,
) {
operator fun invoke(notificationId: NotificationId.User, event: Event.Download) =
commonBuilder(notificationId, event)
operator fun invoke(notificationId: NotificationId.User, events: List<Event.Download>) =
commonBuilder(notificationId, events.last())
.setContentTitle(appContext.getString(I18N.string.notification_content_title_download_complete))
.setContentText(event.text)
.setContentText(events.sumOf { it.downloadedFiles }.text)
.setContentIntent(notificationId)
private fun NotificationCompat.Builder.setContentIntent(
@@ -50,9 +50,9 @@ class DownloadNotificationBuilder @Inject constructor(
)
)
private val Event.Download.text: String get() =
private val Int.text: String get() =
appContext.quantityString(
I18N.plurals.common_in_app_notification_files_download_complete,
downloadedFiles,
this,
)
}
@@ -51,6 +51,7 @@ class ShouldCancelNotification @Inject constructor(
}
is Event.Download -> false
is Event.DownloadFileProgress -> event.downloadingCount == 0
is Event.ForcedSignOut -> false
is Event.NoSpaceLeftOnDevice -> false
is Event.Backup -> false
@@ -28,10 +28,12 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import me.proton.android.drive.extension.log
import me.proton.android.drive.receiver.NotificationBroadcastReceiver.Companion.ACTION_CANCEL_ALL
import me.proton.android.drive.receiver.NotificationBroadcastReceiver.Companion.ACTION_CANCEL_ALL_DOWNLOADS
import me.proton.android.drive.receiver.NotificationBroadcastReceiver.Companion.ACTION_DELETE
import me.proton.core.drive.base.domain.extension.resultValueOrNull
import me.proton.core.drive.base.domain.log.LogTag
import me.proton.core.drive.base.domain.util.coRunCatching
import me.proton.core.drive.drivelink.download.domain.usecase.CancelAllDownload
import me.proton.core.drive.notification.domain.entity.NotificationId
import me.proton.core.drive.notification.domain.usecase.RemoveNotification
import me.proton.core.drive.share.domain.usecase.GetMainShare
@@ -46,6 +48,7 @@ class NotificationActionWorker @AssistedInject constructor(
private val removeNotification: RemoveNotification,
private val getMainShare: GetMainShare,
private val cancelAllUpload: CancelAllUpload,
private val cancelAllDownload: CancelAllDownload,
) : CoroutineWorker(appContext, params) {
private val action = inputData.getString(KEY_ACTION)
private val notificationIdString = inputData.getString(KEY_NOTIFICATION_ID)
@@ -79,6 +82,7 @@ class NotificationActionWorker @AssistedInject constructor(
shareId,
)
}
ACTION_CANCEL_ALL_DOWNLOADS -> cancelAllDownload(notificationId.channel.userId)
else -> RuntimeException("Unknown action '$action'").log(LogTag.NOTIFICATION)
}
}
+5 -1
View File
@@ -64,6 +64,9 @@ allprojects {
resolutionStrategy.dependencySubstitution {
substitute(module("com.google.protobuf:protobuf-lite"))
.using(module("com.google.protobuf:protobuf-javalite:${libs.versions.protobufJavaLite.get()}"))
substitute(module("androidx.navigation:navigation-compose"))
.using(module("androidx.navigation:navigation-compose:${libs.versions.androidx.navigation.get()}"))
.because("androidx-hilt 1.3.0 brings androidx.navigation version 2.9.0 which is more strict that currently used")
}
}
}
@@ -72,11 +75,12 @@ allprojects {
subprojects {
configurations.all {
exclude(group = "me.proton.crypto", module = "android-golib")
exclude(group = "org.jetbrains.kotlin", module = "kotlin-android-extensions-runtime")
}
}
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
delete(rootProject.layout.buildDirectory)
}
tasks.register("deleteTest", Delete::class) {
+1
View File
@@ -23,6 +23,7 @@ plugins {
dependencies {
implementation(libs.gradle.plugin.android)
implementation(libs.gradle.plugin.kotlin)
implementation(libs.gradle.plugin.ksp)
implementation("com.squareup:javapoet:1.13.0") // https://github.com/google/dagger/issues/3068#issuecomment-999118496
}
+1 -1
View File
@@ -22,7 +22,7 @@ object Config {
const val minSdk = 29
const val targetSdk = 35
const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
const val versionName = "2.35.0"
const val versionName = "2.36.0"
const val archivesBaseName = "ProtonDrive-$versionName"
val supportedResourceConfigurations = listOf(
"b+es+419",
+32 -38
View File
@@ -30,7 +30,7 @@ import org.gradle.kotlin.dsl.findByType
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.plugin.KaptExtension
import com.google.devtools.ksp.gradle.KspExtension
import java.io.File
@Suppress("LongMethod")
@@ -43,16 +43,16 @@ fun Project.driveModule(
includeSubmodules: Boolean = false,
i18n: Boolean = false,
socialTest: Boolean = false,
kapt: Boolean = hilt || room || socialTest,
showkase: Boolean = false,
ksp: Boolean = showkase || hilt || socialTest || room,
buildConfig: Boolean = false,
enableTestFixtures: Boolean = false,
dependencies: DependencyHandler.() -> Unit = {},
) {
val catalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
apply(plugin = "kotlin-android")
if (kapt) {
apply(plugin = "kotlin-kapt")
if (ksp) {
apply(plugin = "com.google.devtools.ksp")
}
if (compose) {
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
@@ -72,12 +72,6 @@ fun Project.driveModule(
applicationId = Config.applicationId
versionCode = versionCodeFromGitCommitCount
versionName = Config.versionName
javaCompileOptions {
annotationProcessorOptions {
arguments["room.schemaLocation"] = "$projectDir/schemas"
}
}
}
buildTypes {
@@ -102,6 +96,15 @@ fun Project.driveModule(
this.buildConfig = buildConfig
}
packaging {
resources.excludes.add("META-INF/licenses/**")
resources.excludes.add("META-INF/LICENSE*")
resources.excludes.add("META-INF/AL2.0")
resources.excludes.add("META-INF/LGPL2.1")
resources.excludes.add("licenses/*.txt")
resources.excludes.add("licenses/*.xml")
}
configureJvmTarget()
}
@@ -114,14 +117,6 @@ fun Project.driveModule(
}
}
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments["room.schemaLocation"] = "$projectDir/schemas"
}
}
}
buildTypes {
debug {
enableUnitTestCoverage = true
@@ -136,15 +131,23 @@ fun Project.driveModule(
this.buildConfig = buildConfig
}
packaging {
resources.excludes.add("META-INF/licenses/**")
resources.excludes.add("META-INF/LICENSE*")
resources.excludes.add("META-INF/AL2.0")
resources.excludes.add("META-INF/LGPL2.1")
resources.excludes.add("licenses/*.txt")
resources.excludes.add("licenses/*.xml")
}
configureJvmTarget()
}
extensions.findByType(KaptExtension::class.java)?.let { kaptExt ->
kaptExt.arguments {
if (showkase) {
arg("skipPrivatePreviews", "true")
}
}
if (showkase) {
extensions.findByType<KspExtension>()?.arg("skipPrivatePreviews", "true")
}
if (room) {
extensions.findByType<KspExtension>()?.arg("room.schemaLocation", "$projectDir/schemas")
}
extensions.configure<TestedExtension> {
@@ -170,15 +173,6 @@ fun Project.driveModule(
targetCompatibility = JavaVersion.VERSION_17
}
packagingOptions {
resources.excludes.add("META-INF/licenses/**")
resources.excludes.add("META-INF/LICENSE*")
resources.excludes.add("META-INF/AL2.0")
resources.excludes.add("META-INF/LGPL2.1")
resources.excludes.add("licenses/*.txt")
resources.excludes.add("licenses/*.xml")
}
testOptions {
unitTests {
isIncludeAndroidResources = true
@@ -204,14 +198,14 @@ fun Project.driveModule(
}
}
if (room) {
add("kapt", catalog.findLibrary("androidx.room.compiler").get())
add("ksp", catalog.findLibrary("androidx.room.compiler").get())
add("implementation", catalog.findLibrary("core.dataRoom").get())
add("implementation", catalog.findLibrary("androidx.room.ktx").get())
}
if (hilt) {
add("implementation", catalog.findLibrary("dagger.hilt.android").get())
add("kapt", catalog.findLibrary("dagger.hilt.compiler").get())
add("kapt", catalog.findLibrary("androidx.hilt.compiler").get())
add("ksp", catalog.findLibrary("dagger.hilt.compiler").get())
add("ksp", catalog.findLibrary("androidx.hilt.compiler").get())
}
if (workManager) {
if (hilt) {
@@ -233,7 +227,7 @@ fun Project.driveModule(
}
if (showkase) {
add("debugImplementation", catalog.findLibrary("showkase").get())
add("kaptDebug", catalog.findLibrary("showkaseProcessor").get())
add("kspDebug", catalog.findLibrary("showkaseProcessor").get())
}
// region Test
@@ -242,7 +236,7 @@ fun Project.driveModule(
if (socialTest) {
add("testImplementation", project(":drive:test"))
add("testImplementation", catalog.findLibrary("dagger.hilt.android.testing").get())
add("kaptTest", catalog.findLibrary("dagger.hilt.compiler").get())
add("kspTest", catalog.findLibrary("dagger.hilt.compiler").get())
}
// endregion
}
+5 -5
View File
@@ -70,17 +70,17 @@ fun Project.configureJacoco(flavor: String = "", srcFolder: String = "kotlin") {
"**/ch/protonmail/**",
)
val debugTree = fileTree("$buildDir/tmp/kotlin-classes/$taskName") { exclude(fileFilter) }
val debugTree = fileTree(project.layout.buildDirectory.dir("tmp/kotlin-classes/$taskName").get().asFile) { exclude(fileFilter) }
val mainSrc = "$projectDir/src/main/$srcFolder"
sourceDirectories.setFrom(mainSrc)
classDirectories.setFrom(debugTree)
executionData.setFrom(fileTree(buildDir) { include(listOf("**/*.exec", "**/*.ec")) })
executionData.setFrom(fileTree(project.layout.buildDirectory.get().asFile) { include(listOf("**/*.exec", "**/*.ec")) })
}.dependsOn("test${taskName.capitalize(Locale.ENGLISH)}UnitTest")
tasks.register("coverageReport") {
dependsOn("jacocoTestReport")
val reportFile = project.file("$buildDir/reports/jacoco/jacocoTestReport/jacocoTestReport.xml")
val reportFile = project.layout.buildDirectory.file("reports/jacoco/jacocoTestReport/jacocoTestReport.xml").get().asFile
inputs.files(reportFile).withPropertyName("reportFile")
onlyIf { reportFile.exists() }
doLast {
@@ -111,8 +111,8 @@ fun Project.configureJacoco(flavor: String = "", srcFolder: String = "kotlin") {
tasks.register<Exec>("coberturaCoverageReport") {
dependsOn("coverageReport")
val jacocoFile = project.file("$buildDir/reports/jacoco/jacocoTestReport/jacocoTestReport.xml")
val coberturaFile = project.file( "$buildDir/reports/cobertura-coverage.xml")
val jacocoFile = project.layout.buildDirectory.file("reports/jacoco/jacocoTestReport/jacocoTestReport.xml").get().asFile
val coberturaFile = project.layout.buildDirectory.file("reports/cobertura-coverage.xml").get().asFile
inputs.file(jacocoFile).withPropertyName("jacocoFile")
outputs.file(coberturaFile)
workingDir = File(rootDir, "buildSrc")
@@ -169,6 +169,15 @@ sealed class Event {
override val occurredAt: TimestampMs = TimestampMs()
}
@Serializable
data class DownloadFileProgress(
val downloadingCount: Int,
val progress: Percentage,
) : Event() {
override val id: String = "$EVENT_ID_PREFIX${this.javaClass.simpleName.uppercase()}_1"
override val occurredAt: TimestampMs = TimestampMs()
}
data object ForcedSignOut : Event() {
override val id: String = "$EVENT_ID_PREFIX${this.javaClass.simpleName.uppercase()}_1"
override val occurredAt: TimestampMs = TimestampMs()
@@ -23,7 +23,6 @@ import me.proton.core.drive.announce.event.domain.handler.EventHandler
import me.proton.core.drive.base.domain.log.LogTag.ANNOUNCE_EVENT
import me.proton.core.drive.base.domain.usecase.ReportError
import me.proton.core.drive.base.domain.util.coRunCatching
import me.proton.core.util.kotlin.CoreLogger
import javax.inject.Inject
class AnnounceEvent @Inject constructor(
@@ -103,7 +103,7 @@ class BackupManagerImpl @Inject constructor(
}
override fun sync(backupFolder: BackupFolder, uploadPriority: Long) {
CoreLogger.i(BACKUP, "Sync bucket: ${backupFolder.bucketId.toBase36()}}")
CoreLogger.i(BACKUP, "Sync bucket: ${backupFolder.bucketId.toBase36()}")
workManager
.beginUniqueWork(
backupFolder.uniqueScanWorkName(),
@@ -41,6 +41,7 @@ object Dto {
const val BLOCK_LIST = "BlockList"
const val BLOCK_NUMBER = "BlockNumber"
const val CAPTURE_TIME = "CaptureTime"
const val CHECKSUM_VERIFIED = "ChecksumVerified"
const val CHILD_LINK_IDS = "ChildLinkIDs"
const val CLIENT_UID = "ClientUID"
const val CLIENT_UIDS = "ClientUIDs"

Some files were not shown because too many files have changed in this diff Show More