mirror of
https://github.com/ProtonDriveApps/android-drive.git
synced 2026-05-15 09:50:34 +00:00
2.15.0
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.kotlin
|
||||
local.properties
|
||||
private.properties
|
||||
.idea/*
|
||||
|
||||
@@ -402,6 +402,13 @@ test:firebase:e2e:details:
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.details"
|
||||
|
||||
test:firebase:e2e:document:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:document/'
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.document"
|
||||
|
||||
test:firebase:e2e:move:
|
||||
extends: .app-firebase-tests
|
||||
resource_group: "shard-3-$CI_COMMIT_SHORT_SHA"
|
||||
@@ -458,6 +465,26 @@ test:firebase:e2e:settings:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.settings"
|
||||
DEVICE_CONFIG: "quickTest-3"
|
||||
|
||||
test:firebase:e2e:share-link:
|
||||
extends: .app-firebase-tests
|
||||
resource_group: "shard-4-$CI_COMMIT_SHORT_SHA"
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:share-link/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.share.link"
|
||||
DEVICE_CONFIG: "quickTest-2"
|
||||
|
||||
test:firebase:e2e:share-list:
|
||||
extends: .app-firebase-tests
|
||||
resource_group: "shard-4-$CI_COMMIT_SHORT_SHA"
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:share-list/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.share.list"
|
||||
DEVICE_CONFIG: "quickTest-2"
|
||||
|
||||
test:firebase:e2e:share-user:
|
||||
extends: .app-firebase-tests
|
||||
resource_group: "shard-4-$CI_COMMIT_SHORT_SHA"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"project": "android-drive",
|
||||
"locale": "d2c34154477243fc633dfe636a8ecfe604cad707"
|
||||
"locale": "edd85262380858d323198b1112cf4c6b7548c1e1"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# ownership: loose
|
||||
* @rbac/android/drive
|
||||
/.idea/ @rbac/android/drive
|
||||
/app-lock/ @rbac/android/drive
|
||||
/app-ui-settings/ @rbac/android/drive
|
||||
/app/ @rbac/android/drive
|
||||
/buildSrc/ @rbac/android/drive
|
||||
/drive/ @rbac/android/drive
|
||||
/gradle/ @rbac/android/drive
|
||||
/photos/ @rbac/android/drive
|
||||
/screenshot-tests/ @rbac/android/drive
|
||||
/verifier/ @rbac/android/drive
|
||||
+1
-1
@@ -21,6 +21,6 @@ package me.proton.drive.android.settings.domain.entity
|
||||
import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
|
||||
enum class WhatsNewKey(val limit: TimestampS) {
|
||||
PROTON_DOCS(limit = TimestampS(1735646400)) // end of 2024
|
||||
PUBLIC_SHARING(limit = TimestampS(1743465600)), // end of march 2025
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ class DebugSettings(
|
||||
key = prefsPhotosUpsellPhotoCount,
|
||||
default = buildConfig.photosUpsellPhotoCount,
|
||||
)
|
||||
override val albumsFeatureFlag: Boolean = true
|
||||
|
||||
fun reset(coroutineScope: CoroutineScope) {
|
||||
coroutineScope.launch {
|
||||
|
||||
@@ -95,7 +95,6 @@ import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.thumbnail.presentation.coil.ThumbnailEnabled
|
||||
import me.proton.core.notification.presentation.deeplink.DeeplinkManager
|
||||
import me.proton.core.notification.presentation.deeplink.onActivityCreate
|
||||
import me.proton.core.usersettings.presentation.compose.view.SecurityKeysActivity
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import me.proton.drive.android.settings.domain.entity.ThemeStyle
|
||||
import me.proton.drive.android.settings.domain.usecase.GetHomeTab
|
||||
@@ -175,7 +174,7 @@ class MainActivity : FragmentActivity() {
|
||||
exitApp = { finish() },
|
||||
navigateToPasswordManagement = accountViewModel::startPasswordManagement,
|
||||
navigateToRecoveryEmail = accountViewModel::startUpdateRecoveryEmail,
|
||||
navigateToSecurityKeys = { SecurityKeysActivity.start(this@MainActivity) },
|
||||
navigateToSecurityKeys = accountViewModel::startSecurityKeys,
|
||||
navigateToBugReport = bugReportViewModel::sendBugReport,
|
||||
navigateToSubscription = plansViewModel::showCurrentPlans,
|
||||
navigateToRatingBooster = { showRatingBooster(this@MainActivity) },
|
||||
|
||||
@@ -28,4 +28,5 @@ fun DriveLink.onClick(
|
||||
) = when (this) {
|
||||
is DriveLink.Folder -> navigateToFolder(id, if (isNameEncrypted) null else name)
|
||||
is DriveLink.File -> navigateToPreview(id)
|
||||
is DriveLink.Album -> error("TODO") // navigateToAlbum?
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ fun FileOrFolderOptions(
|
||||
.testTag(FileFolderOptionsDialogTestTag.folderOptions),
|
||||
)
|
||||
}
|
||||
is DriveLink.Album -> error("TODO")// AlbumOptions
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -180,10 +180,10 @@ private fun WhatsNewPreview() {
|
||||
Surface(color = ProtonTheme.colors.backgroundNorm) {
|
||||
WhatsNew(
|
||||
viewState = WhatsNewViewState(
|
||||
title = stringResource(id = I18N.string.whats_new_docs_title),
|
||||
description = stringResource(id = I18N.string.whats_new_docs_description),
|
||||
action = stringResource(id = I18N.string.whats_new_docs_action),
|
||||
image = BasePresentation.drawable.img_whats_new_proton_docs
|
||||
title = "Title",
|
||||
description = "Very very long description that can go on two lines",
|
||||
action = "Action",
|
||||
image = BasePresentation.drawable.img_onboarding
|
||||
),
|
||||
viewEvent = object : WhatsNewViewEvent {}
|
||||
)
|
||||
|
||||
@@ -114,6 +114,7 @@ import me.proton.android.drive.ui.screen.SettingsScreen
|
||||
import me.proton.android.drive.ui.screen.SigningOutScreen
|
||||
import me.proton.android.drive.ui.screen.TrashScreen
|
||||
import me.proton.android.drive.ui.screen.UploadToScreen
|
||||
import me.proton.android.drive.ui.screen.UserInvitationScreen
|
||||
import me.proton.android.drive.ui.viewmodel.ConfirmStopSyncFolderDialogViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.PreviewViewModel
|
||||
import me.proton.core.account.domain.entity.Account
|
||||
@@ -157,7 +158,7 @@ fun AppNavGraph(
|
||||
exitApp: () -> Unit,
|
||||
navigateToPasswordManagement: (UserId) -> Unit,
|
||||
navigateToRecoveryEmail: (UserId) -> Unit,
|
||||
navigateToSecurityKeys: () -> Unit,
|
||||
navigateToSecurityKeys: (UserId) -> Unit,
|
||||
navigateToBugReport: () -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToRatingBooster: () -> Unit,
|
||||
@@ -242,7 +243,7 @@ fun AppNavGraph(
|
||||
exitApp: () -> Unit,
|
||||
navigateToPasswordManagement: (UserId) -> Unit,
|
||||
navigateToRecoveryEmail: (UserId) -> Unit,
|
||||
navigateToSecurityKeys: () -> Unit,
|
||||
navigateToSecurityKeys: (UserId) -> Unit,
|
||||
navigateToBugReport: () -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToRatingBooster: () -> Unit,
|
||||
@@ -340,6 +341,7 @@ fun AppNavGraph(
|
||||
addInvitationOptions(navController)
|
||||
addExternalInvitationOptions(navController)
|
||||
addMemberOptions(navController)
|
||||
addUserInvitation(navController)
|
||||
addShareLinkPermissions(navController)
|
||||
addDiscardShareViaInvitationsChanges(navController)
|
||||
addShareViaLink(navController)
|
||||
@@ -662,6 +664,22 @@ fun NavGraphBuilder.addMemberOptions(
|
||||
)
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.addUserInvitation(
|
||||
navController : NavHostController,
|
||||
) = composable(
|
||||
route = Screen.UserInvitation.route,
|
||||
arguments = listOf(
|
||||
navArgument(Screen.UserInvitation.USER_ID) { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
UserInvitationScreen (onBack = {
|
||||
navController.popBackStack(
|
||||
route = Screen.UserInvitation.route,
|
||||
inclusive = true,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.addShareLinkPermissions(
|
||||
navController: NavHostController,
|
||||
) = modalBottomSheet(
|
||||
@@ -910,6 +928,9 @@ internal fun NavGraphBuilder.addHome(
|
||||
)
|
||||
)
|
||||
},
|
||||
navigateToUserInvitation = {
|
||||
navController.navigate(Screen.UserInvitation(userId))
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
@@ -1334,7 +1355,7 @@ fun NavGraphBuilder.addAccountSettings(
|
||||
navController: NavHostController,
|
||||
navigateToPasswordManagement: (UserId) -> Unit,
|
||||
navigateToRecoveryEmail: (UserId) -> Unit,
|
||||
navigateToSecurityKeys: () -> Unit
|
||||
navigateToSecurityKeys: (UserId) -> Unit
|
||||
) = composable(
|
||||
route = Screen.Settings.AccountSettings.route,
|
||||
enterTransition = defaultEnterSlideTransition { true },
|
||||
@@ -1349,7 +1370,7 @@ fun NavGraphBuilder.addAccountSettings(
|
||||
AccountSettingsScreen(
|
||||
navigateToPasswordManagement = { navigateToPasswordManagement(userId) },
|
||||
navigateToRecoveryEmail = { navigateToRecoveryEmail(userId) },
|
||||
navigateToSecurityKeys = navigateToSecurityKeys,
|
||||
navigateToSecurityKeys = { navigateToSecurityKeys(userId) },
|
||||
navigateBack = {
|
||||
navController.popBackStack(
|
||||
route = Screen.Settings.AccountSettings.route,
|
||||
|
||||
@@ -72,6 +72,7 @@ fun HomeNavGraph(
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
navigateToComputerOptions: (deviceId: DeviceId) -> Unit,
|
||||
navigateToNotificationPermissionRationale: () -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
) = DriveNavHost(
|
||||
navController = homeNavController,
|
||||
startDestination = startDestination
|
||||
@@ -129,6 +130,7 @@ fun HomeNavGraph(
|
||||
navigateToMultipleFileOrFolderOptions(selectionId, OptionsFilter.FILES)
|
||||
},
|
||||
navigateToParentFolderOptions = navigateToParentFolderOptions,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -410,6 +412,7 @@ fun NavGraphBuilder.addSharedTabs(
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateToMultipleFileOrFolderOptions: (SelectionId) -> Unit,
|
||||
navigateToParentFolderOptions: (folderId: FolderId) -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
) = composable(
|
||||
route = Screen.SharedTabs.route,
|
||||
enterTransition = defaultEnterSlideTransition {
|
||||
@@ -484,6 +487,7 @@ fun NavGraphBuilder.addSharedTabs(
|
||||
},
|
||||
navigateToPreview = navigateToSinglePreview,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
)
|
||||
}
|
||||
} ?: let {
|
||||
|
||||
@@ -45,6 +45,7 @@ import me.proton.core.drive.drivelink.rename.presentation.viewmodel.RenameViewMo
|
||||
import me.proton.core.drive.drivelink.shared.presentation.viewmodel.LinkSettingsViewModel
|
||||
import me.proton.core.drive.drivelink.shared.presentation.viewmodel.SharedDriveLinkViewModel
|
||||
import me.proton.core.drive.folder.create.presentation.CreateFolderViewModel
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
@@ -198,13 +199,14 @@ sealed class Screen(val route: String) {
|
||||
operator fun invoke(userId: UserId) = "delete/${userId.id}/trash"
|
||||
}
|
||||
|
||||
data object Rename : Screen("rename/{userId}/shares/{shareId}/files?fileId={fileId}&folderId={folderId}") {
|
||||
data object Rename : Screen("rename/{userId}/shares/{shareId}/files?fileId={fileId}&folderId={folderId}&albumId={albumId}") {
|
||||
operator fun invoke(
|
||||
userId: UserId,
|
||||
linkId: LinkId,
|
||||
) = when (linkId) {
|
||||
is FileId -> "rename/${userId.id}/shares/${linkId.shareId.id}/files?fileId=${linkId.id}"
|
||||
is FolderId -> "rename/${userId.id}/shares/${linkId.shareId.id}/files?folderId=${linkId.id}"
|
||||
is AlbumId -> "rename/${userId.id}/shares/${linkId.shareId.id}/files?albumId=${linkId.id}"
|
||||
}
|
||||
|
||||
const val FILE_ID = RenameViewModel.KEY_FILE_ID
|
||||
@@ -553,6 +555,14 @@ sealed class Screen(val route: String) {
|
||||
const val MEMBER_ID = ShareMemberOptionsViewModel.KEY_MEMBER_ID
|
||||
}
|
||||
|
||||
data object UserInvitation : Screen("shareViaInvitations/{userId}/invitations") {
|
||||
operator fun invoke(
|
||||
userId: UserId,
|
||||
) = "shareViaInvitations/${userId.id}/invitations"
|
||||
|
||||
const val USER_ID = Screen.USER_ID
|
||||
}
|
||||
|
||||
data object ShareLinkPermissions : Screen("shareViaLink/{userId}/shares/{shareId}/linkId/{linkId}/permissions") {
|
||||
operator fun invoke(
|
||||
linkId: LinkId,
|
||||
|
||||
@@ -313,8 +313,8 @@ sealed class Option(
|
||||
}
|
||||
|
||||
sealed class ApplicableQuantity(open val quantity: Long) {
|
||||
object Single : ApplicableQuantity(1L)
|
||||
object All : ApplicableQuantity(Long.MAX_VALUE)
|
||||
data object Single : ApplicableQuantity(1L)
|
||||
data object All : ApplicableQuantity(Long.MAX_VALUE)
|
||||
}
|
||||
|
||||
enum class ApplicableTo {
|
||||
@@ -322,7 +322,8 @@ enum class ApplicableTo {
|
||||
FILE_PHOTO,
|
||||
FILE_DEVICE,
|
||||
FILE_PROTON_CLOUD,
|
||||
FOLDER;
|
||||
FOLDER,
|
||||
ALBUM;
|
||||
|
||||
companion object {
|
||||
val ANY_FILE: Set<ApplicableTo> get() = setOf(FILE_MAIN, FILE_PHOTO, FILE_DEVICE, FILE_PROTON_CLOUD)
|
||||
@@ -359,6 +360,7 @@ fun DriveLink.isApplicableTo(applicableTo: Set<ApplicableTo>): Boolean = when (t
|
||||
else -> applicableTo.any { it in setOf(ApplicableTo.FILE_MAIN, ApplicableTo.FILE_DEVICE) }
|
||||
}
|
||||
is DriveLink.Folder -> applicableTo.contains(ApplicableTo.FOLDER)
|
||||
is DriveLink.Album -> applicableTo.contains(ApplicableTo.ALBUM)
|
||||
}
|
||||
|
||||
fun Set<Option>.filter(driveLink: DriveLink) =
|
||||
|
||||
@@ -104,6 +104,7 @@ fun HomeScreen(
|
||||
navigateToWhatsNew: (WhatsNewKey) -> Unit,
|
||||
navigateToRatingBooster: () -> Unit,
|
||||
navigateToNotificationPermissionRationale: () -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
setLocalSnackbarPadding(BottomNavigationHeight)
|
||||
@@ -157,6 +158,7 @@ fun HomeScreen(
|
||||
navigateToBackupSettings = navigateToBackupSettings,
|
||||
navigateToComputerOptions = navigateToComputerOptions,
|
||||
navigateToNotificationPermissionRationale = navigateToNotificationPermissionRationale,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
arguments = arguments,
|
||||
viewState = currentViewState,
|
||||
viewEvent = viewEvent,
|
||||
@@ -195,6 +197,7 @@ internal fun Home(
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
navigateToComputerOptions: (deviceId: DeviceId) -> Unit,
|
||||
navigateToNotificationPermissionRationale: () -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
) {
|
||||
val homeScaffoldState = rememberHomeScaffoldState()
|
||||
val isDrawerOpen = with(homeScaffoldState.scaffoldState.drawerState) {
|
||||
@@ -290,6 +293,7 @@ internal fun Home(
|
||||
navigateToBackupSettings,
|
||||
navigateToComputerOptions,
|
||||
navigateToNotificationPermissionRationale,
|
||||
navigateToUserInvitation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ fun SharedTabsScreen(
|
||||
navigateToFiles: (FolderId, String?) -> Unit,
|
||||
navigateToPreview: (FileId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (LinkId) -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = hiltViewModel<SharedTabsViewModel>()
|
||||
@@ -94,6 +95,7 @@ fun SharedTabsScreen(
|
||||
navigateToFiles = navigateToFiles,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,6 +107,7 @@ fun SharedTabs(
|
||||
navigateToFiles: (FolderId, String?) -> Unit,
|
||||
navigateToPreview: (FileId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (LinkId) -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
@@ -134,6 +137,7 @@ fun SharedTabs(
|
||||
navigateToFiles = navigateToFiles,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
)
|
||||
SharedTab.Type.SHARED_BY_ME -> SharedByMeScreen(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
|
||||
@@ -18,19 +18,24 @@
|
||||
|
||||
package me.proton.android.drive.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.android.drive.ui.effect.HandleHomeEffect
|
||||
import me.proton.android.drive.ui.viewmodel.SharedWithMeViewModel
|
||||
import me.proton.android.drive.ui.viewstate.HomeScaffoldState
|
||||
import me.proton.core.compose.flow.rememberFlowWithLifecycle
|
||||
import me.proton.core.drive.drivelink.shared.presentation.component.Shared
|
||||
import me.proton.core.drive.drivelink.shared.presentation.component.UserInvitationBanner
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
@@ -41,10 +46,14 @@ fun SharedWithMeScreen(
|
||||
navigateToFiles: (FolderId, String?) -> Unit,
|
||||
navigateToPreview: (FileId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val viewModel = hiltViewModel<SharedWithMeViewModel>()
|
||||
val viewState by viewModel.viewState.collectAsStateWithLifecycle(initialValue = viewModel.initialViewState)
|
||||
val userInvitationViewState by viewModel.userInvitationBannerViewState.collectAsStateWithLifecycle(
|
||||
initialValue = null
|
||||
)
|
||||
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||
val viewEvent = remember(lifecycle) {
|
||||
viewModel.viewEvent(
|
||||
@@ -65,6 +74,17 @@ fun SharedWithMeScreen(
|
||||
driveLinksFlow = viewModel.driveLinksMap,
|
||||
modifier = modifier
|
||||
.testTag(SharedWithMeTestTag.content),
|
||||
headerContent = {
|
||||
Box(Modifier.defaultMinSize(minHeight = 1.dp)) {
|
||||
// minHeight: always draw to have the header visible in the lazy list
|
||||
userInvitationViewState?.let { viewState ->
|
||||
UserInvitationBanner(
|
||||
description = viewState.description,
|
||||
onClick = navigateToUserInvitation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 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.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.android.drive.ui.viewmodel.UserInvitationViewModel
|
||||
import me.proton.core.drive.drivelink.shared.presentation.component.UserInvitation
|
||||
|
||||
|
||||
@Composable
|
||||
fun UserInvitationScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val viewModel = hiltViewModel<UserInvitationViewModel>()
|
||||
val viewState by viewModel.viewState.collectAsStateWithLifecycle(viewModel.initialViewState)
|
||||
val viewEvent = remember { viewModel.viewEvent(onBack) }
|
||||
val invitations by viewModel.userInvitations.collectAsStateWithLifecycle()
|
||||
UserInvitation(
|
||||
viewState = viewState,
|
||||
viewEvent = viewEvent,
|
||||
invitations = invitations.orEmpty(),
|
||||
modifier = modifier
|
||||
.statusBarsPadding()
|
||||
.fillMaxSize()
|
||||
.testTag(UserInvitationTestFlag.content),
|
||||
)
|
||||
}
|
||||
|
||||
object UserInvitationTestFlag {
|
||||
const val content = "user invitation content"
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class UiTestHelper @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun doNotShowWhatsNewAfterLogin() {
|
||||
appUiSettingsDataStore.WhatsNew(WhatsNewKey.PROTON_DOCS.name).shown = 1L
|
||||
appUiSettingsDataStore.WhatsNew(WhatsNewKey.PUBLIC_SHARING.name).shown = 1L
|
||||
}
|
||||
|
||||
suspend fun doNotShowRatingBoosterAfterLogin() {
|
||||
|
||||
@@ -113,6 +113,10 @@ class AccountViewModel @Inject constructor(
|
||||
userSettingsOrchestrator.startUpdateRecoveryEmailWorkflow(userId)
|
||||
}
|
||||
|
||||
fun startSecurityKeys(userId: UserId) {
|
||||
userSettingsOrchestrator.startSecurityKeysWorkflow(userId)
|
||||
}
|
||||
|
||||
sealed class State {
|
||||
object PrimaryNeeded : State()
|
||||
object AccountReady : State()
|
||||
|
||||
@@ -30,11 +30,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.BuildConfig
|
||||
import me.proton.android.drive.ui.navigation.HomeTab
|
||||
@@ -44,15 +47,22 @@ import me.proton.android.drive.ui.viewstate.HomeViewState
|
||||
import me.proton.android.drive.usecase.CanGetMoreFreeStorage
|
||||
import me.proton.android.drive.usecase.GetDynamicHomeTabsFlow
|
||||
import me.proton.android.drive.usecase.ShouldShowOverlay
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.domain.entity.SessionUserId
|
||||
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.log.LogTag.VIEW_MODEL
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.base.presentation.component.NavigationTab
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveMobileSharingInvitationsAcceptReject
|
||||
import me.proton.core.drive.feature.flag.domain.extension.on
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.navigationdrawer.presentation.NavigationDrawerViewEvent
|
||||
import me.proton.core.drive.navigationdrawer.presentation.NavigationDrawerViewState
|
||||
import me.proton.core.drive.share.user.domain.usecase.GetUserInvitationCountFlow
|
||||
import me.proton.core.drive.share.user.domain.usecase.HasUserInvitationFlow
|
||||
import me.proton.core.user.domain.UserManager
|
||||
import me.proton.core.user.domain.entity.User
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
@@ -70,24 +80,38 @@ class HomeViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val canGetMoreFreeStorage: CanGetMoreFreeStorage,
|
||||
getDynamicHomeTabsFlow: GetDynamicHomeTabsFlow,
|
||||
hasUserInvitationFlow: HasUserInvitationFlow,
|
||||
getFeatureFlagFlow: GetFeatureFlagFlow,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val shouldShowOverlay: ShouldShowOverlay,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
) : ViewModel(), NotificationDotViewModel, UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private var navigateToTab: ((route: String) -> Unit)? = null
|
||||
|
||||
private val tabs: StateFlow<Map<out HomeTab, NavigationTab>> = getDynamicHomeTabsFlow(userId)
|
||||
.map { dynamicHomeTabs ->
|
||||
dynamicHomeTabs
|
||||
.filter { dynamicHomeTab -> dynamicHomeTab.isEnabled }
|
||||
.sortedBy { dynamicHomeTab -> dynamicHomeTab.order }
|
||||
.map { dynamicHomeTab ->
|
||||
dynamicHomeTab.screen to NavigationTab(
|
||||
iconResId = dynamicHomeTab.iconResId,
|
||||
titleResId = dynamicHomeTab.titleResId,
|
||||
)
|
||||
}
|
||||
.associateBy({ tab -> tab.first }, { tab -> tab.second })
|
||||
override val notificationDotRequested = getFeatureFlagFlow(
|
||||
driveMobileSharingInvitationsAcceptReject(userId),
|
||||
).transform { featureFlag ->
|
||||
if (featureFlag.on) {
|
||||
emitAll(hasUserInvitationFlow(userId))
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||
|
||||
val tabs: StateFlow<Map<out HomeTab, NavigationTab>> = combine(
|
||||
getDynamicHomeTabsFlow(userId),
|
||||
notificationDotRequested,
|
||||
) { dynamicHomeTabs, notificationDotRequested ->
|
||||
dynamicHomeTabs
|
||||
.filter { dynamicHomeTab -> dynamicHomeTab.isEnabled }
|
||||
.sortedBy { dynamicHomeTab -> dynamicHomeTab.order }
|
||||
.map { dynamicHomeTab ->
|
||||
dynamicHomeTab.screen to NavigationTab(
|
||||
iconResId = dynamicHomeTab.iconResId,
|
||||
titleResId = dynamicHomeTab.titleResId,
|
||||
notificationDotVisible = dynamicHomeTab.screen is Screen.SharedTabs
|
||||
&& notificationDotRequested,
|
||||
)
|
||||
}
|
||||
.associateBy({ tab -> tab.first }, { tab -> tab.second })
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
private val currentDestination = MutableStateFlow<String?>(null)
|
||||
@@ -155,13 +179,14 @@ class HomeViewModel @Inject constructor(
|
||||
private val NavigationTab.screen: HomeTab
|
||||
get() = tabs.value.firstNotNullOf { (screen, value) -> screen.takeIf { value == this } }
|
||||
|
||||
private val DynamicHomeTab.screen: HomeTab get() = when (route) {
|
||||
Screen.Files.route -> Screen.Files
|
||||
Screen.Photos.route -> Screen.Photos
|
||||
Screen.Computers.route -> Screen.Computers
|
||||
Screen.SharedTabs.route -> Screen.SharedTabs
|
||||
else -> error("Unhandled tab item route: $route")
|
||||
}
|
||||
private val DynamicHomeTab.screen: HomeTab
|
||||
get() = when (route) {
|
||||
Screen.Files.route -> Screen.Files
|
||||
Screen.Photos.route -> Screen.Photos
|
||||
Screen.Computers.route -> Screen.Computers
|
||||
Screen.SharedTabs.route -> Screen.SharedTabs
|
||||
else -> error("Unhandled tab item route: $route")
|
||||
}
|
||||
|
||||
private fun getViewState(
|
||||
user: User?,
|
||||
@@ -181,7 +206,10 @@ class HomeViewModel @Inject constructor(
|
||||
)
|
||||
)
|
||||
|
||||
private fun handleInvalidDestination(destinationRoute: String, tabs: Map<out HomeTab, NavigationTab>) {
|
||||
private fun handleInvalidDestination(
|
||||
destinationRoute: String,
|
||||
tabs: Map<out HomeTab, NavigationTab>
|
||||
) {
|
||||
val routes = tabs.values.map { navigationTab -> navigationTab.screen.route }
|
||||
if (destinationRoute !in routes) {
|
||||
CoreLogger.w(VIEW_MODEL, "Invalid destination route: $destinationRoute")
|
||||
|
||||
@@ -98,6 +98,7 @@ import me.proton.core.drive.base.presentation.viewmodel.onLoadState
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
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.selection.domain.usecase.GetSelectedDriveLinks
|
||||
import me.proton.core.drive.drivelink.selection.domain.usecase.SelectAll
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
@@ -108,7 +109,6 @@ import me.proton.core.drive.link.selection.domain.usecase.DeselectLinks
|
||||
import me.proton.core.drive.link.selection.domain.usecase.SelectLinks
|
||||
import me.proton.core.drive.linkupload.domain.entity.UploadFileLink.Companion.RECENT_BACKUP_PRIORITY
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.photo.domain.usecase.GetPhotoCount
|
||||
import me.proton.core.drive.share.domain.entity.Share
|
||||
import me.proton.core.drive.user.domain.entity.UserMessage
|
||||
import me.proton.core.drive.user.domain.usecase.CancelUserMessage
|
||||
|
||||
@@ -761,7 +761,7 @@ class PhotoContentProvider(
|
||||
nodeKey = "",
|
||||
nodePassphrase = "",
|
||||
nodePassphraseSignature = "",
|
||||
signatureAddress = "",
|
||||
signatureEmail = "",
|
||||
creationTime = TimestampS(),
|
||||
trashedTime = null,
|
||||
hasThumbnail = false,
|
||||
|
||||
@@ -30,22 +30,33 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
|
||||
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.extension.quantityString
|
||||
import me.proton.core.drive.base.presentation.state.ListContentState
|
||||
import me.proton.core.drive.drivelink.shared.domain.usecase.GetPagedSharedWithMeLinkIds
|
||||
import me.proton.core.drive.drivelink.shared.domain.usecase.SharedDriveLinks
|
||||
import me.proton.core.drive.drivelink.shared.presentation.entity.SharedItem
|
||||
import me.proton.core.drive.drivelink.shared.presentation.viewstate.UserInvitationBannerViewState
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State.ENABLED
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State.NOT_FOUND
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveMobileSharingInvitationsAcceptReject
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveSharingDisabled
|
||||
import me.proton.core.drive.feature.flag.domain.extension.on
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.share.user.domain.entity.SharedLinkId
|
||||
import me.proton.core.drive.share.user.domain.usecase.GetAllSharedWithMeIds
|
||||
import me.proton.core.drive.share.user.domain.usecase.GetUserInvitationCountFlow
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.base.presentation.R as BasePresentation
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
@@ -59,6 +70,7 @@ class SharedWithMeViewModel @Inject constructor(
|
||||
getPagedSharedWithMeLinkIds: GetPagedSharedWithMeLinkIds,
|
||||
getFeatureFlagFlow: GetFeatureFlagFlow,
|
||||
private val getAllSharedWithMeIds: GetAllSharedWithMeIds,
|
||||
getUserInvitationCountFlow: GetUserInvitationCountFlow,
|
||||
) : CommonSharedViewModel(
|
||||
savedStateHandle = savedStateHandle,
|
||||
appContext = appContext,
|
||||
@@ -82,24 +94,46 @@ class SharedWithMeViewModel @Inject constructor(
|
||||
descriptionResId = I18N.string.shared_with_me_empty_description,
|
||||
actionResId = null,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val driveLinks: Flow<PagingData<SharedItem>> = killSwitch.transformLatest { featureFlag ->
|
||||
if (featureFlag.state == ENABLED) {
|
||||
emit(PagingData.empty())
|
||||
} else {
|
||||
emitAll(
|
||||
getPagedSharedWithMeLinkIds(userId)
|
||||
.map { pagingData ->
|
||||
pagingData.map { sharedLinkId ->
|
||||
SharedItem.Listing(
|
||||
linkId = sharedLinkId.linkId,
|
||||
) as SharedItem
|
||||
}
|
||||
}.cachedIn(viewModelScope)
|
||||
)
|
||||
override val driveLinks: Flow<PagingData<SharedItem>> =
|
||||
killSwitch.transformLatest { featureFlag ->
|
||||
if (featureFlag.state == ENABLED) {
|
||||
emit(PagingData.empty())
|
||||
} else {
|
||||
emitAll(
|
||||
getPagedSharedWithMeLinkIds(userId)
|
||||
.map { pagingData ->
|
||||
pagingData.map { sharedLinkId ->
|
||||
SharedItem.Listing(
|
||||
linkId = sharedLinkId.linkId,
|
||||
) as SharedItem
|
||||
}
|
||||
}.cachedIn(viewModelScope)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAllIds(): Result<List<SharedLinkId>> = getAllSharedWithMeIds(userId)
|
||||
|
||||
val userInvitationBannerViewState =
|
||||
getFeatureFlagFlow(driveMobileSharingInvitationsAcceptReject(userId)).transform { featureFlag ->
|
||||
if (featureFlag.on) {
|
||||
emitAll(getUserInvitationCountFlow(userId, refresh = flowOf(true))
|
||||
.filterSuccessOrError()
|
||||
.mapSuccessValueOrNull()
|
||||
.filterNotNull()
|
||||
.filter { count -> count > 0 }
|
||||
.map { count ->
|
||||
UserInvitationBannerViewState(
|
||||
appContext.quantityString(
|
||||
I18N.plurals.shared_by_me_invitation_banner_description,
|
||||
count
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 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.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.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
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.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
|
||||
import me.proton.core.drive.base.presentation.R
|
||||
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.drivelink.shared.presentation.viewevent.UserInvitationViewEvent
|
||||
import me.proton.core.drive.drivelink.shared.presentation.viewstate.UserInvitationViewState
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.share.user.domain.entity.UserInvitation
|
||||
import me.proton.core.drive.share.user.domain.entity.UserInvitationId
|
||||
import me.proton.core.drive.share.user.domain.usecase.AcceptUserInvitation
|
||||
import me.proton.core.drive.share.user.domain.usecase.GetDecryptedUserInvitationsFlow
|
||||
import me.proton.core.drive.share.user.domain.usecase.RejectUserInvitation
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
import me.proton.core.presentation.R as CorePresentation
|
||||
|
||||
@HiltViewModel
|
||||
class UserInvitationViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val acceptUserInvitation: AcceptUserInvitation,
|
||||
private val rejectUserInvitation: RejectUserInvitation,
|
||||
getDecryptedUserInvitationsFlow: GetDecryptedUserInvitationsFlow,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
|
||||
private val emptyState = ListContentState.Empty(
|
||||
imageResId = getThemeDrawableId(
|
||||
light = R.drawable.empty_shared_with_me_light,
|
||||
dark = R.drawable.empty_shared_with_me_dark,
|
||||
dayNight = R.drawable.empty_shared_with_me_daynight,
|
||||
),
|
||||
titleId = I18N.string.shared_user_invitations_title_empty,
|
||||
descriptionResId = I18N.string.shared_user_invitations_description_empty
|
||||
)
|
||||
|
||||
private val listContentState: MutableStateFlow<ListContentState> =
|
||||
MutableStateFlow(ListContentState.Loading)
|
||||
|
||||
val userInvitations: StateFlow<List<UserInvitation>?> =
|
||||
getDecryptedUserInvitationsFlow(userId).transform { result ->
|
||||
when (result) {
|
||||
is DataResult.Processing -> listContentState.value = ListContentState.Loading
|
||||
is DataResult.Error -> {
|
||||
result.log(VIEW_MODEL)
|
||||
listContentState.value = ListContentState.Error(result.logDefaultMessage(
|
||||
context = appContext,
|
||||
useExceptionMessage = configurationProvider.useExceptionMessage,
|
||||
tag = VIEW_MODEL,
|
||||
))
|
||||
}
|
||||
|
||||
is DataResult.Success -> {
|
||||
val invitations = result.value
|
||||
|
||||
listContentState.value = if (invitations.isEmpty()) {
|
||||
emptyState
|
||||
} else {
|
||||
ListContentState.Content()
|
||||
}
|
||||
emit(invitations)
|
||||
}
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
val initialViewState = UserInvitationViewState(
|
||||
title = appContext.getString(I18N.string.shared_user_invitations_title).format(0),
|
||||
navigationIconResId = CorePresentation.drawable.ic_proton_arrow_back,
|
||||
listContentState = listContentState.value,
|
||||
)
|
||||
|
||||
val viewState: Flow<UserInvitationViewState> = combine(
|
||||
listContentState,
|
||||
userInvitations,
|
||||
) { state, userInvitations ->
|
||||
initialViewState.copy(
|
||||
title = appContext.getString(I18N.string.shared_user_invitations_title)
|
||||
.format(userInvitations.orEmpty().size),
|
||||
listContentState = state,
|
||||
)
|
||||
}
|
||||
|
||||
fun viewEvent(onBack: () -> Unit): UserInvitationViewEvent =
|
||||
object : UserInvitationViewEvent {
|
||||
override val onTopAppBarNavigation: () -> Unit = onBack
|
||||
override val onAccept: (UserInvitationId) -> Unit = this@UserInvitationViewModel::onAccept
|
||||
override val onDecline: (UserInvitationId) -> Unit = this@UserInvitationViewModel::onDecline
|
||||
}
|
||||
|
||||
private fun onAccept(invitationId: UserInvitationId) {
|
||||
viewModelScope.launch {
|
||||
acceptUserInvitation(invitationId).filterSuccessOrError()
|
||||
.last()
|
||||
.onFailure { error ->
|
||||
error.log(SHARING, "Cannot accept invitation: $invitationId")
|
||||
broadcastMessages(
|
||||
userId,
|
||||
error.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage
|
||||
),
|
||||
BroadcastMessage.Type.ERROR
|
||||
)
|
||||
}.onSuccess {
|
||||
broadcastMessages(
|
||||
userId,
|
||||
appContext.getString(I18N.string.shared_user_invitations_accept_success),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDecline(invitationId: UserInvitationId) {
|
||||
viewModelScope.launch {
|
||||
rejectUserInvitation(invitationId)
|
||||
.filterSuccessOrError()
|
||||
.last()
|
||||
.onFailure { error ->
|
||||
error.log(SHARING, "Cannot delete invitation: $invitationId")
|
||||
broadcastMessages(
|
||||
userId,
|
||||
error.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage
|
||||
),
|
||||
BroadcastMessage.Type.ERROR
|
||||
)
|
||||
}.onSuccess {
|
||||
broadcastMessages(
|
||||
userId,
|
||||
appContext.getString(I18N.string.shared_user_invitations_decline_success),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,11 +49,11 @@ class WhatsNewViewModel @Inject constructor(
|
||||
|
||||
val viewState: Flow<WhatsNewViewState?> = flowOf {
|
||||
when (key) {
|
||||
WhatsNewKey.PROTON_DOCS -> WhatsNewViewState(
|
||||
title = appContext.getString(I18N.string.whats_new_docs_title),
|
||||
description = appContext.getString(I18N.string.whats_new_docs_description),
|
||||
action = appContext.getString(I18N.string.whats_new_docs_action),
|
||||
image = BasePresentation.drawable.img_whats_new_proton_docs
|
||||
WhatsNewKey.PUBLIC_SHARING -> WhatsNewViewState(
|
||||
title = appContext.getString(I18N.string.whats_new_public_sharing_title),
|
||||
description = appContext.getString(I18N.string.whats_new_public_sharing_description),
|
||||
action = appContext.getString(I18N.string.whats_new_public_sharing_action),
|
||||
image = BasePresentation.drawable.img_whats_new_public_sharing,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +25,18 @@ import me.proton.android.drive.extension.getDefaultMessage
|
||||
import me.proton.core.domain.arch.DataResult
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.data.extension.isRetryable
|
||||
import me.proton.core.drive.base.domain.api.ProtonApiCode
|
||||
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.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.base.presentation.state.ListContentState
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.share.domain.entity.Share
|
||||
import me.proton.core.drive.share.domain.exception.ShareException
|
||||
import me.proton.core.network.domain.isApiProtonError
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.base.presentation.R as BasePresentation
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
class OnFilesDriveLinkError @Inject constructor(
|
||||
@@ -68,10 +72,23 @@ class OnFilesDriveLinkError @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun DataResult.Error.toListContentState() = ListContentState.Error(
|
||||
message = errorMessage,
|
||||
actionResId = if (this.isRetryable) I18N.string.common_retry else null
|
||||
)
|
||||
private fun DataResult.Error.toListContentState() = if(cause?.isApiProtonError(ProtonApiCode.PHOTO_MIGRATION) == true) {
|
||||
ListContentState.Error(
|
||||
message = errorMessage,
|
||||
titleId = I18N.string.photos_error_backup_migration_title,
|
||||
descriptionResId = I18N.string.photos_error_backup_migration_description,
|
||||
imageResId = getThemeDrawableId(
|
||||
light = BasePresentation.drawable.img_update_required_light,
|
||||
dark = BasePresentation.drawable.img_update_required_dark,
|
||||
dayNight = BasePresentation.drawable.img_update_required_daynight,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
ListContentState.Error(
|
||||
message = errorMessage,
|
||||
actionResId = if (this.isRetryable) I18N.string.common_retry else null
|
||||
)
|
||||
}
|
||||
|
||||
private val DataResult.Error.broadcastMessage: String? get() =
|
||||
when (val cause = this.cause) {
|
||||
|
||||
@@ -42,7 +42,7 @@ class ShouldShowWhatsNew @Inject constructor(
|
||||
).first().on
|
||||
) {
|
||||
when {
|
||||
canShow(WhatsNewKey.PROTON_DOCS) -> WhatsNewKey.PROTON_DOCS
|
||||
canShow(WhatsNewKey.PUBLIC_SHARING) -> WhatsNewKey.PUBLIC_SHARING
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
|
||||
+3
@@ -114,6 +114,9 @@ class BackupNotificationBuilder @Inject constructor(
|
||||
|
||||
Event.Backup.BackupState.PREPARING ->
|
||||
appContext.getString(I18N.string.notification_content_text_backup_preparing)
|
||||
|
||||
Event.Backup.BackupState.FAILED_DUE_PHOTO_SHARE_MIGRATION ->
|
||||
appContext.getString(I18N.string.notification_content_text_backup_preparing)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="core_app_scheme" translatable="false">protondrive</string>
|
||||
<bool name="core_feature_key_transparency_enabled">false</bool>
|
||||
<bool name="core_feature_account_recovery_enabled">true</bool>
|
||||
<bool name="core_feature_auth_signin_two_step_enabled">true</bool>
|
||||
<string name="core_feature_auth_sso_redirect_scheme" translatable="false">protondrive</string>
|
||||
<bool name="core_feature_event_manager_worker_repeat_internal_background_by_bucket">true</bool>
|
||||
<bool name="core_feature_notifications_enabled">true</bool>
|
||||
<!-- Show system Notification permission request dialog after login. -->
|
||||
|
||||
+1
-1
@@ -380,7 +380,7 @@ class FileOrFolderOptionsViewModelTest {
|
||||
nodeKey = "",
|
||||
nodePassphrase = "",
|
||||
nodePassphraseSignature = "",
|
||||
signatureAddress = "",
|
||||
signatureEmail = "",
|
||||
creationTime = TimestampS(0),
|
||||
trashedTime = null,
|
||||
shareUrlExpirationTime = null,
|
||||
|
||||
@@ -29,6 +29,7 @@ object FileFolderOptionsRobot : Robot {
|
||||
private val moveToTrashButton get() = node.withText(I18N.string.files_send_to_trash_action)
|
||||
private val restoreFromTrashButton get() = node.withText(I18N.string.files_restore_from_trash_action)
|
||||
private val makeAvailableOfflineButton get() = node.withText(I18N.string.common_make_available_offline_action)
|
||||
private val openInBrowserButton get() = node.withText(I18N.string.common_open_in_browser_action)
|
||||
private val removeAvailableOfflineButton get() = node
|
||||
.withText(I18N.string.common_remove_from_offline_available_action)
|
||||
private val manageLinkButton get() = node.withText(I18N.string.common_manage_link_action)
|
||||
@@ -91,6 +92,8 @@ object FileFolderOptionsRobot : Robot {
|
||||
makeAvailableOfflineButton.scrollTo().click()
|
||||
}
|
||||
|
||||
fun clickOpenInBrowserButton() = openInBrowserButton.scrollTo().clickTo(FilesTabRobot)
|
||||
|
||||
override fun robotDisplayed() {
|
||||
fileFolderOptionsScreen.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
@@ -50,12 +50,15 @@ object PhotosTabRobot :
|
||||
NavigationBarRobot {
|
||||
private val enableBackupButton
|
||||
get() = allNodes.withText(I18N.string.photos_permissions_action).onFirst()
|
||||
private val enableErrorBackupButton
|
||||
get() = node.withText(I18N.string.photos_error_backup_disabled_action)
|
||||
|
||||
private val backupCompleted get() = node.withText(I18N.string.photos_backup_state_completed)
|
||||
private val backupPreparing get() = node.withText(I18N.string.photos_backup_state_preparing)
|
||||
private val backupFailed get() = node.withText(I18N.string.photos_error_backup_failed)
|
||||
private val noBackupsYet get() = node.withText(I18N.string.photos_empty_title)
|
||||
private val missingFolder get() = node.withText(I18N.string.photos_error_backup_missing_folder)
|
||||
private val updateRequired get() = node.withText(I18N.string.photos_error_backup_migration_title)
|
||||
private val noConnectivityBanner get() = node.withText(I18N.string.photos_error_waiting_wifi_connectivity)
|
||||
private val getMoreStorageButton get() = node.withText(I18N.string.storage_quotas_get_storage_action)
|
||||
private val storageFullTitle get() =
|
||||
@@ -80,6 +83,7 @@ object PhotosTabRobot :
|
||||
.withLayoutType(LayoutType.Grid)
|
||||
|
||||
fun enableBackup() = enableBackupButton.clickTo(PhotosTabRobot)
|
||||
fun enableBackupWhenDisabled() = enableErrorBackupButton.clickTo(PhotosTabRobot)
|
||||
fun clickOnMore() = missingFolder.clickTo(PhotosBackupRobot)
|
||||
|
||||
fun clickGetMoreStorage() = SubscriptionRobot.apply { getMoreStorageButton.click() }
|
||||
@@ -109,6 +113,14 @@ object PhotosTabRobot :
|
||||
missingFolder.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
fun assertMigrationInProgressDisplayed() {
|
||||
assertUpdateRequired()
|
||||
}
|
||||
|
||||
fun assertUpdateRequired() {
|
||||
updateRequired.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
fun assertPhotoCountEquals(count: Int) {
|
||||
allNodes
|
||||
.withItemType(ItemType.File)
|
||||
|
||||
@@ -43,6 +43,7 @@ object PreviewRobot : NavigationBarRobot {
|
||||
private val preview get() = allNodes.withTag(ImagePreviewComponentTestTag.image).onFirst()
|
||||
private val pager get() = node.withTag(PreviewComponentTestTag.pager)
|
||||
private val mediaPreview get() = node.withTag(PreviewComponentTestTag.mediaPreview)
|
||||
private val openInBrowserButton get() = node.withText(I18N.string.common_open_in_browser_action)
|
||||
|
||||
fun clickOnContextualButton() = contextualButton.clickTo(FileFolderOptionsRobot)
|
||||
|
||||
@@ -72,6 +73,8 @@ object PreviewRobot : NavigationBarRobot {
|
||||
pager.scrollTo(index)
|
||||
}
|
||||
|
||||
fun clickOpenInBrowserButton() = openInBrowserButton.scrollTo().clickTo(FilesTabRobot)
|
||||
|
||||
override fun robotDisplayed() {
|
||||
previewScreen.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ object ShareUserRobot : NavigationBarRobot, Robot {
|
||||
.hasAncestor(node.withTag(SharedDriveInvitationsTestTags.emailField))
|
||||
private val sendButton get() = node.withContentDescription(I18N.string.common_send_action)
|
||||
private val viewerPermissionButton get() = node.withText(I18N.string.common_permission_viewer)
|
||||
private val sendMessageAndNameLabel get() = node.withText(I18N.string.share_via_invitations_send_message_and_name_label)
|
||||
private val messageTextField get() = node.isSetText()
|
||||
.hasAncestor(node.withTag(SharedDriveInvitationsTestTags.messageField))
|
||||
private val editorPermissionButton get() = node.withText(I18N.string.common_permission_editor)
|
||||
@@ -45,8 +44,6 @@ object ShareUserRobot : NavigationBarRobot, Robot {
|
||||
|
||||
fun clickOnViewerPermission() = viewerPermissionButton.clickTo(this)
|
||||
|
||||
fun clickOnSendMessageAndName() = sendMessageAndNameLabel.scrollTo().clickTo(this)
|
||||
|
||||
fun typeMessage(text: String) = apply {
|
||||
messageTextField.typeText(text)
|
||||
}
|
||||
|
||||
@@ -19,12 +19,21 @@
|
||||
package me.proton.android.drive.ui.robot
|
||||
|
||||
import me.proton.android.drive.ui.screen.SharedWithMeTestTag
|
||||
import me.proton.core.test.android.instrumented.utils.StringUtils
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object SharedWithMeRobot : LinksRobot {
|
||||
override val filesContent get() = node.withTag(SharedWithMeTestTag.content)
|
||||
|
||||
fun clickUserInvitation(count: Int) = node.withText(
|
||||
StringUtils.pluralStringFromResource(
|
||||
I18N.plurals.shared_by_me_invitation_banner_description,
|
||||
count,
|
||||
count
|
||||
)
|
||||
).clickTo(UserInvitationRobot)
|
||||
|
||||
fun assertEmpty() {
|
||||
node.withText(I18N.string.shared_with_me_empty_title).await { assertIsDisplayed() }
|
||||
node.withText(I18N.string.shared_with_me_empty_description).await { assertIsDisplayed() }
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.ui.robot
|
||||
|
||||
import me.proton.android.drive.ui.screen.UserInvitationTestFlag
|
||||
import me.proton.core.drive.drivelink.shared.presentation.component.UserInvitationContentTestFlag
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object UserInvitationRobot : Robot, NavigationBarRobot {
|
||||
private val content get() = node.withTag(UserInvitationTestFlag.content)
|
||||
|
||||
fun clickAccept(name: String) = node.withText(
|
||||
I18N.string.shared_user_invitations_accept_button
|
||||
).hasAncestor(node
|
||||
.withTag(UserInvitationContentTestFlag.item)
|
||||
.hasDescendant(node.withText(name))
|
||||
).clickTo(UserInvitationRobot)
|
||||
|
||||
fun clickDecline(name: String) = node.withText(
|
||||
I18N.string.shared_user_invitations_decline_button
|
||||
).hasAncestor(node
|
||||
.withTag(UserInvitationContentTestFlag.item)
|
||||
.hasDescendant(node.withText(name))
|
||||
).clickTo(UserInvitationRobot)
|
||||
|
||||
fun assertAcceptSucceed() = node.withText(
|
||||
I18N.string.shared_user_invitations_accept_success
|
||||
).await { assertIsDisplayed() }
|
||||
|
||||
fun assertDeclineSucceed() = node.withText(
|
||||
I18N.string.shared_user_invitations_decline_success
|
||||
).await { assertIsDisplayed() }
|
||||
|
||||
fun assertEmpty() {
|
||||
node.withText(I18N.string.shared_user_invitations_title_empty).await { assertIsDisplayed() }
|
||||
node.withText(I18N.string.shared_user_invitations_description_empty).await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
override fun robotDisplayed() {
|
||||
content.await { assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object WhatsNewRobot : Robot {
|
||||
private val gotItButton get() = node.withText(I18N.string.whats_new_docs_action)
|
||||
private val gotItButton get() = node.withText(I18N.string.whats_new_public_sharing_action)
|
||||
private val whatsNewScreen get() = node.withTag(WhatsNewTestTag.screen)
|
||||
|
||||
fun <T : Robot> clickGotIt(goesTo: T) = gotItButton.clickTo(goesTo)
|
||||
|
||||
@@ -24,13 +24,18 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import me.proton.android.drive.log.DriveLogTag
|
||||
import me.proton.android.drive.log.DriveLogTag.UI_TEST
|
||||
import me.proton.core.drive.backup.domain.manager.BackupConnectivityManager
|
||||
import me.proton.core.drive.backup.domain.manager.BackupConnectivityManager.Connectivity.NONE
|
||||
import me.proton.core.drive.backup.domain.manager.BackupConnectivityManager.Connectivity.UNMETERED
|
||||
import me.proton.core.drive.base.domain.api.ProtonApiCode.PHOTO_MIGRATION
|
||||
import me.proton.core.network.data.ProtonErrorException
|
||||
import me.proton.core.network.domain.ApiResult
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import me.proton.test.fusion.FusionConfig
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import org.junit.rules.ExternalResource
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
@@ -46,7 +51,7 @@ object NetworkSimulator: ExternalResource() {
|
||||
get() = Interceptor {
|
||||
if (responseDelay > 0.milliseconds) {
|
||||
CoreLogger.d(
|
||||
DriveLogTag.UI_TEST,
|
||||
UI_TEST,
|
||||
"Delaying response by ${responseDelay.inWholeMilliseconds}ms"
|
||||
)
|
||||
runBlocking {
|
||||
@@ -63,9 +68,39 @@ object NetworkSimulator: ExternalResource() {
|
||||
it.proceed(it.request())
|
||||
}
|
||||
|
||||
private val interceptors = mutableListOf(testNetworkInterceptor)
|
||||
private val photosMigrationInterceptor
|
||||
get() = Interceptor { chain ->
|
||||
if (enabledPhotosMigration) {
|
||||
if (chain.request().url.toString().contains("photo")) {
|
||||
CoreLogger.d(UI_TEST, "Throw migration error for photo request")
|
||||
|
||||
throw ProtonErrorException(
|
||||
response = Response.Builder()
|
||||
.code(422)
|
||||
.message("Migration in progress")
|
||||
.request(chain.request())
|
||||
.protocol(chain.connection()?.protocol() ?: okhttp3.Protocol.HTTP_1_1)
|
||||
.build(),
|
||||
protonData = ApiResult.Error.ProtonData(
|
||||
code = PHOTO_MIGRATION,
|
||||
error = "Migration in progress",
|
||||
)
|
||||
)
|
||||
} else {
|
||||
chain.proceed(chain.request())
|
||||
}
|
||||
} else {
|
||||
chain.proceed(chain.request())
|
||||
}
|
||||
}
|
||||
|
||||
private val interceptors = mutableListOf(
|
||||
testNetworkInterceptor,
|
||||
photosMigrationInterceptor,
|
||||
)
|
||||
private var isNetworkTimeout: Boolean = false
|
||||
private var responseDelay: Duration = 0.milliseconds
|
||||
private var enabledPhotosMigration: Boolean = false
|
||||
|
||||
val client: OkHttpClient = OkHttpClient
|
||||
.Builder()
|
||||
@@ -102,6 +137,10 @@ object NetworkSimulator: ExternalResource() {
|
||||
NetworkSimulator.isNetworkTimeout = isNetworkTimeout
|
||||
}
|
||||
|
||||
fun enabledPhotosMigration() {
|
||||
enabledPhotosMigration = true
|
||||
}
|
||||
|
||||
private fun setConnectivity(connectivity: BackupConnectivityManager.Connectivity) = apply {
|
||||
_connectivity.value = connectivity
|
||||
}
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.ui.test.flow.document
|
||||
|
||||
import android.app.Instrumentation
|
||||
import android.content.Intent
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.Intents.intending
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
|
||||
import androidx.test.espresso.intent.matcher.UriMatchers.hasHost
|
||||
import androidx.test.espresso.intent.matcher.UriMatchers.hasParamWithName
|
||||
import androidx.test.espresso.intent.matcher.UriMatchers.hasPath
|
||||
import androidx.test.espresso.intent.matcher.UriMatchers.hasScheme
|
||||
import androidx.test.espresso.intent.rule.IntentsRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.annotation.Scenario
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.MoveToFolderRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.test.BaseTest
|
||||
import me.proton.core.drive.files.presentation.extension.SemanticsDownloadState.Downloaded
|
||||
import me.proton.core.test.rule.annotation.PrepareUser
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class DocumentFlowTest : BaseTest() {
|
||||
|
||||
private val expectedIntent = allOf(
|
||||
hasAction(Intent.ACTION_VIEW),
|
||||
hasData(
|
||||
allOf(
|
||||
hasScheme("https"),
|
||||
hasHost("docs.${envConfig.host}"),
|
||||
hasPath("/doc"),
|
||||
hasParamWithName("volumeId"),
|
||||
hasParamWithName("linkId"),
|
||||
hasParamWithName("email"),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 8)
|
||||
fun openDocumentFromFiles() {
|
||||
val file = "doc9.protondoc"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickOnFile(file)
|
||||
.verify {
|
||||
nodeWithTextDisplayed(file)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 8)
|
||||
fun openDocumentFromOffline() {
|
||||
val file = "doc1.protondoc"
|
||||
val folder = "folder1"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMakeAvailableOffline(FilesTabRobot)
|
||||
.verify {
|
||||
itemIsDisplayed(folder, downloadState = Downloaded)
|
||||
}
|
||||
.openSidebarBySwipe()
|
||||
.clickOffline()
|
||||
.clickOnFolder(folder)
|
||||
.clickOnFile(file)
|
||||
.verify {
|
||||
nodeWithTextDisplayed(file)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 8)
|
||||
fun previewDocumentFromFiles() {
|
||||
|
||||
intending(expectedIntent).respondWith(Instrumentation.ActivityResult(0, null));
|
||||
|
||||
val file = "doc9.protondoc"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickOnFile(file)
|
||||
.clickOnContextualButton()
|
||||
.clickOpenInBrowserButton()
|
||||
.verify {
|
||||
intended(expectedIntent)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 8)
|
||||
fun openDocumentFromMenu() {
|
||||
|
||||
intending(expectedIntent).respondWith(Instrumentation.ActivityResult(0, null));
|
||||
|
||||
val file = "doc9.protondoc"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickOpenInBrowserButton()
|
||||
.verify {
|
||||
intended(expectedIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -36,7 +36,6 @@ class WhatsNewFlowTest : BaseTest() {
|
||||
@Test
|
||||
@FeatureFlag(DRIVE_ANDROID_WHATS_NEW, ENABLED)
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Ignore("PROTON_DOCS was limited to 2024, enable the test for a new whats new")
|
||||
fun whatsNewShouldShow() {
|
||||
WhatsNewRobot
|
||||
.verify {
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.ui.test.flow.photos
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import dagger.hilt.android.testing.UninstallModules
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import me.proton.android.drive.extension.debug
|
||||
import me.proton.android.drive.photos.data.di.PhotosConfigurationModule
|
||||
import me.proton.android.drive.photos.domain.provider.PhotosDefaultConfigurationProvider
|
||||
import me.proton.android.drive.provider.PhotosConnectedDefaultConfigurationProvider
|
||||
import me.proton.android.drive.ui.annotation.Scenario
|
||||
import me.proton.android.drive.ui.data.ImageName
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.robot.SettingsRobot
|
||||
import me.proton.android.drive.ui.robot.settings.PhotosBackupRobot
|
||||
import me.proton.android.drive.ui.rules.NetworkSimulator
|
||||
import me.proton.android.drive.ui.test.PhotosBaseTest
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.test.rule.annotation.PrepareUser
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@HiltAndroidTest
|
||||
@UninstallModules(PhotosConfigurationModule::class)
|
||||
class PhotosMigrationFlowTest : PhotosBaseTest() {
|
||||
|
||||
@Inject
|
||||
lateinit var configurationProvider: ConfigurationProvider
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
configurationProvider.debug.photosUpsellPhotoCount = Int.MAX_VALUE
|
||||
dcimCameraFolder.copyFileFromAssets("boat.jpg")
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 2)
|
||||
fun creatingShareShouldFailed() {
|
||||
NetworkSimulator.enabledPhotosMigration()
|
||||
|
||||
PhotosTabRobot
|
||||
.verify { assertUpdateRequired() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 2, isPhotos = true)
|
||||
fun loadingPhotosListingShouldFailed() {
|
||||
NetworkSimulator.enabledPhotosMigration()
|
||||
|
||||
PhotosTabRobot
|
||||
.verify { nodeWithTextDisplayed("Migration in progress") }
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 2, isPhotos = true)
|
||||
fun startingBackupOnCreatedShareShouldFailed() {
|
||||
PhotosTabRobot.verify {
|
||||
assertPhotoDisplayed(ImageName.Main.fileName)
|
||||
}
|
||||
|
||||
NetworkSimulator.enabledPhotosMigration()
|
||||
|
||||
PhotosTabRobot
|
||||
.enableBackupWhenDisabled()
|
||||
.verify { assertUpdateRequired() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 2)
|
||||
fun startingBackupFromSettingShouldFailed() {
|
||||
NetworkSimulator.enabledPhotosMigration()
|
||||
|
||||
PhotosTabRobot
|
||||
.openSidebarBySwipe()
|
||||
.clickSettings()
|
||||
.clickPhotosBackup()
|
||||
.clickBackupToggle(PhotosBackupRobot)
|
||||
.verify { nodeWithTextDisplayed("Migration in progress") }
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 2, isPhotos = true)
|
||||
fun startingBackupOnCreatedShareFromSettingShouldFailed() {
|
||||
PhotosTabRobot.verify {
|
||||
assertPhotoDisplayed(ImageName.Main.fileName)
|
||||
}
|
||||
|
||||
NetworkSimulator.enabledPhotosMigration()
|
||||
|
||||
PhotosTabRobot
|
||||
.openSidebarBySwipe()
|
||||
.clickSettings()
|
||||
.clickPhotosBackup()
|
||||
.clickBackupToggle(PhotosBackupRobot)
|
||||
.verify {
|
||||
assertPhotosBackupTurnOn()
|
||||
}
|
||||
.clickBack(SettingsRobot)
|
||||
.clickBack(PhotosTabRobot)
|
||||
.verify { assertMigrationInProgressDisplayed() }
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Suppress("Unused")
|
||||
interface TestPhotosConfigurationModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindPhotosDefaultConfigurationProvider(
|
||||
impl: PhotosConnectedDefaultConfigurationProvider,
|
||||
): PhotosDefaultConfigurationProvider
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.ui.test.flow.share.link
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.annotation.Scenario
|
||||
import me.proton.android.drive.ui.extension.tomorrow
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.ManageAccessRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.test.BaseTest
|
||||
import me.proton.core.test.rule.annotation.PrepareUser
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class ChangeExpirationDateManageAccessTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 4)
|
||||
fun setExpirationDate() {
|
||||
val fileName = "image.jpg"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(fileName)
|
||||
.clickMoreOnItem(fileName)
|
||||
.clickManageAccess()
|
||||
.clickAllowToAnyone()
|
||||
.clickSettings()
|
||||
.clickExpirationDateTextField()
|
||||
.verify { robotDisplayed() }
|
||||
.selectDate(tomorrow)
|
||||
.clickOk()
|
||||
.clickSave()
|
||||
.clickUpdateSuccessfulGrowler()
|
||||
.clickBack(ManageAccessRobot)
|
||||
.clickBack(FilesTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
.scrollToItemWithName(fileName)
|
||||
.clickMoreOnItem(fileName)
|
||||
.clickManageAccess()
|
||||
.clickSettings()
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
expirationDateToggleIsOn()
|
||||
expirationDateIsShown(tomorrow)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 4)
|
||||
fun changeExpirationDateOfExpiredLink() {
|
||||
val fileName = "expiredSharedFile.jpg"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(fileName)
|
||||
.clickMoreOnItem(fileName)
|
||||
.clickManageAccess()
|
||||
.clickSettings()
|
||||
.clickExpirationDateTextField()
|
||||
.verify { robotDisplayed() }
|
||||
.selectDate(tomorrow)
|
||||
.clickOk()
|
||||
.clickSave()
|
||||
.clickUpdateSuccessfulGrowler()
|
||||
.clickBack(ManageAccessRobot)
|
||||
.clickBack(FilesTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
.scrollToItemWithName(fileName)
|
||||
.clickMoreOnItem(fileName)
|
||||
.clickManageAccess()
|
||||
.clickSettings()
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
expirationDateToggleIsOn()
|
||||
expirationDateIsShown(tomorrow)
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2025 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.ui.test.flow.share.link
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.annotation.Scenario
|
||||
import me.proton.android.drive.ui.robot.ConfirmStopSharingRobot
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.ManageAccessRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.test.BaseTest
|
||||
import me.proton.core.test.rule.annotation.PrepareUser
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@HiltAndroidTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DeleteSharedLinkManageAccessFlowTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 4)
|
||||
fun stopSharingActiveLinkViaManageAccess() {
|
||||
val file = "shared.jpg"
|
||||
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickManageAccess()
|
||||
.clickAllowToAnyone(ConfirmStopSharingRobot)
|
||||
.confirmStopSharing(ManageAccessRobot)
|
||||
.verify {
|
||||
assertLinkIsNotShareWithAnyonePublic()
|
||||
nodeWithTextDisplayed(I18N.string.description_files_stop_sharing_action_success)
|
||||
robotDisplayed()
|
||||
}
|
||||
.clickBack(FilesTabRobot)
|
||||
.verify { itemIsDisplayed(file, isSharedByLink = false) }
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2025 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.ui.test.flow.share.link
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.annotation.Scenario
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.ManageAccessRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.robot.ShareRobot
|
||||
import me.proton.android.drive.ui.test.BaseTest
|
||||
import me.proton.core.test.rule.annotation.PrepareUser
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class SetCustomPasswordManageAccessFlowTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 4)
|
||||
fun deleteCustomPassword() {
|
||||
val file = FOLDER_SHARED_WITH_PASSWORD
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.verify { robotDisplayed() }
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickManageAccess()
|
||||
.clickSettings()
|
||||
.verify { robotDisplayed() }
|
||||
.clickPasswordToggle()
|
||||
.clickSave()
|
||||
.clickUpdateSuccessfulGrowler()
|
||||
.clickBack(ManageAccessRobot)
|
||||
.clickBack(FilesTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickManageAccess()
|
||||
.clickSettings()
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
passwordToggleIsOff()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 4)
|
||||
fun setCustomPassword() {
|
||||
val file = FILE_SHARED
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.verify { robotDisplayed() }
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickManageAccess()
|
||||
.clickSettings()
|
||||
.verify { robotDisplayed() }
|
||||
.typePassword(FILE_SHARE_PASSWORD)
|
||||
.clickPasswordToggle()
|
||||
.clickSave()
|
||||
.clickUpdateSuccessfulGrowler()
|
||||
.clickBack(ManageAccessRobot)
|
||||
.clickBack(FilesTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickManageAccess()
|
||||
.verify { assertLinkIsShareWithAnyonePasswordProtected() }
|
||||
.clickSettings()
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
passwordToggleIsOn()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 4)
|
||||
fun discardPasswordChanges() {
|
||||
val file = FILE
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.verify { robotDisplayed() }
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickManageAccess()
|
||||
.clickAllowToAnyone()
|
||||
.clickSettings()
|
||||
.verify { robotDisplayed() }
|
||||
.typePassword(FILE_SHARE_PASSWORD)
|
||||
.clickPasswordToggle()
|
||||
.clickBack(ShareRobot.DiscardChanges)
|
||||
.verify { robotDisplayed() }
|
||||
.clickDiscard(ManageAccessRobot)
|
||||
.clickBack(FilesTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickManageAccess()
|
||||
.clickSettings()
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
passwordToggleIsOff()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FILE_SHARED = "shared.jpg"
|
||||
private const val FOLDER_SHARED_WITH_PASSWORD = "sharedFolderWithPassword"
|
||||
private const val FILE = "image.jpg"
|
||||
private const val FILE_SHARE_PASSWORD = "1234"
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.ui.test.flow.share.link
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.annotation.FeatureFlag
|
||||
import me.proton.android.drive.ui.annotation.FeatureFlags
|
||||
import me.proton.android.drive.ui.annotation.Scenario
|
||||
import me.proton.android.drive.ui.data.ImageName
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.test.BaseTest
|
||||
import me.proton.core.drive.base.domain.entity.Permissions
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State.ENABLED
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.DRIVE_DYNAMIC_ENTITLEMENT_CONFIGURATION
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.DRIVE_PUBLIC_SHARE_EDIT_MODE
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.DRIVE_PUBLIC_SHARE_EDIT_MODE_DISABLED
|
||||
import me.proton.core.test.quark.data.Plan
|
||||
import me.proton.core.test.rule.annotation.PrepareUser
|
||||
import me.proton.core.test.rule.annotation.payments.TestSubscriptionData
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class SharingManageAccessFlowTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 2)
|
||||
fun shareFile() {
|
||||
val file = "image.jpg"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(file)
|
||||
.clickManageAccess()
|
||||
.clickAllowToAnyone()
|
||||
.verify { assertLinkIsShareWithAnyonePublic() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 2, isPhotos = true)
|
||||
fun sharePhoto() {
|
||||
val image = ImageName.Main
|
||||
PhotosTabRobot.waitUntilLoaded()
|
||||
PhotosTabRobot
|
||||
.longClickOnPhoto(image.fileName)
|
||||
.clickOptions()
|
||||
.clickManageAccess()
|
||||
.clickAllowToAnyone()
|
||||
.verify { assertLinkIsShareWithAnyonePublic() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@FeatureFlags(
|
||||
[
|
||||
FeatureFlag(DRIVE_PUBLIC_SHARE_EDIT_MODE, ENABLED),
|
||||
FeatureFlag(DRIVE_DYNAMIC_ENTITLEMENT_CONFIGURATION, ENABLED)
|
||||
]
|
||||
)
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 2)
|
||||
fun shareFolderAsEditor() {
|
||||
val folder = "folder1"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickManageAccess()
|
||||
.clickAllowToAnyone()
|
||||
.verify { assertLinkIsShareWithAnyonePublic(Permissions.viewer) }
|
||||
.clickViewerPermissions()
|
||||
.clickEditor()
|
||||
.verify { assertLinkIsShareWithAnyonePublic(Permissions.editor) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@FeatureFlags(
|
||||
[
|
||||
FeatureFlag(DRIVE_PUBLIC_SHARE_EDIT_MODE, ENABLED),
|
||||
FeatureFlag(DRIVE_PUBLIC_SHARE_EDIT_MODE_DISABLED, ENABLED)
|
||||
]
|
||||
)
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 2)
|
||||
fun shareFolderAsEditorKillSwitch() {
|
||||
val folder = "folder1"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickManageAccess()
|
||||
.clickAllowToAnyone()
|
||||
.verify {
|
||||
assertLinkIsShareWithAnyonePublic(Permissions.viewer)
|
||||
assertViewerPermissionsIsNotClickable()
|
||||
}
|
||||
}
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 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.ui.test.flow.share.list
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.annotation.Scenario
|
||||
import me.proton.android.drive.ui.robot.ConfirmStopSharingRobot
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.robot.PreviewRobot
|
||||
import me.proton.android.drive.ui.robot.SharedByMeRobot
|
||||
import me.proton.android.drive.ui.robot.SharedTabRobot
|
||||
import me.proton.android.drive.ui.test.BaseTest
|
||||
import me.proton.android.drive.ui.test.flow.details.DetailsFlowTest.LinkDetails
|
||||
import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
import me.proton.core.drive.base.domain.extension.bytes
|
||||
import me.proton.core.drive.base.presentation.extension.asHumanReadableString
|
||||
import me.proton.core.drive.files.presentation.extension.SemanticsDownloadState
|
||||
import me.proton.core.test.rule.annotation.PrepareUser
|
||||
import me.proton.test.fusion.FusionConfig
|
||||
import org.junit.Test
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@HiltAndroidTest
|
||||
class SharedByMeTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 2)
|
||||
fun emptySharedByMe() {
|
||||
PhotosTabRobot
|
||||
.navigateToSharedByMeTab()
|
||||
.verify {
|
||||
assertEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 6)
|
||||
fun previewTextFile() {
|
||||
val file = "newShare.txt"
|
||||
PhotosTabRobot
|
||||
.navigateToSharedByMeTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickOnFile(file)
|
||||
.verify {
|
||||
PreviewRobot.nodeWithTextDisplayed("Hello World!")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 6)
|
||||
fun renameFile() {
|
||||
val itemToBeRenamed = "newShare.txt"
|
||||
val newItemName = "renamedShare.txt"
|
||||
|
||||
PhotosTabRobot
|
||||
.navigateToSharedByMeTab()
|
||||
.scrollToItemWithName(itemToBeRenamed)
|
||||
.clickMoreOnItem(itemToBeRenamed)
|
||||
.clickRename()
|
||||
.clearName()
|
||||
.typeName(newItemName)
|
||||
.clickRename()
|
||||
.scrollToItemWithName(newItemName)
|
||||
.verify {
|
||||
itemIsDisplayed(newItemName)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 6)
|
||||
fun moveFileToTrashAndRestoreIt() {
|
||||
val file = "newShare.txt"
|
||||
PhotosTabRobot
|
||||
.navigateToSharedByMeTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMoveToTrash()
|
||||
.dismissMoveToTrashSuccessGrowler(1, FilesTabRobot)
|
||||
.verify {
|
||||
//itemIsNotDisplayed(file)//TODO: uncomment this once fixed on BE
|
||||
}
|
||||
.openSidebarBySwipe()
|
||||
.clickTrash()
|
||||
.verify {
|
||||
itemIsDisplayed(file)
|
||||
}
|
||||
.clickMoreOnItem(file)
|
||||
.clickRestoreTrash()
|
||||
.clickBack(SharedTabRobot)
|
||||
.clickSharedByMeTab()
|
||||
.verify {
|
||||
itemIsDisplayed(file)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 6)
|
||||
fun makeAvailableOffline() {
|
||||
val file = "newShare.txt"
|
||||
PhotosTabRobot
|
||||
.navigateToSharedByMeTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMakeAvailableOffline()
|
||||
.verify {
|
||||
itemIsDisplayed(file, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
.openSidebarBySwipe()
|
||||
.clickOffline()
|
||||
.verify {
|
||||
itemIsDisplayed(file, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 6)
|
||||
fun fileDetails() {
|
||||
PhotosTabRobot
|
||||
.navigateToSharedByMeTab()
|
||||
.scrollToItemWithName(newShareText.name)
|
||||
.clickMoreOnItem(newShareText.name)
|
||||
.clickFileDetails()
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
hasHeaderTitle(newShareText.name)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_name_entry),
|
||||
value = newShareText.name,
|
||||
)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_uploaded_by_entry),
|
||||
value = newShareText.uploadedBy,
|
||||
)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_location_entry),
|
||||
value = requireNotNull(newShareText.location),
|
||||
)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_last_modified_entry),
|
||||
value = newShareText.modified,
|
||||
)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_mime_type_entry),
|
||||
value = requireNotNull(newShareText.mimeType),
|
||||
)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_size_entry),
|
||||
value = requireNotNull(newShareText.size),
|
||||
)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_shared_entry),
|
||||
value = newShareText.isShared,
|
||||
)
|
||||
}
|
||||
.clickBack(SharedTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 6)
|
||||
fun stopSharing() {
|
||||
val file = "newShare.txt"
|
||||
PhotosTabRobot
|
||||
.navigateToSharedByMeTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickManageAccess()
|
||||
.clickStopSharing(ConfirmStopSharingRobot)
|
||||
.confirmStopSharing(SharedTabRobot)
|
||||
.pullToRefresh(SharedTabRobot)
|
||||
.verify {
|
||||
itemIsNotDisplayed(file)
|
||||
}
|
||||
}
|
||||
|
||||
private val newShareText
|
||||
get() = LinkDetails(
|
||||
name = "newShare.txt",
|
||||
uploadedBy = "${protonRule.testDataRule.mainTestUser!!.name}@${envConfig.host}",
|
||||
location = "/" + FusionConfig.targetContext.getString(I18N.string.title_my_files),
|
||||
modified = TimestampS().asHumanReadableString(),
|
||||
isShared = FusionConfig.targetContext.getString(I18N.string.common_yes),
|
||||
mimeType = "text/plain",
|
||||
size = 63.bytes.asHumanReadableString(FusionConfig.targetContext),
|
||||
)
|
||||
|
||||
private fun PhotosTabRobot.navigateToSharedByMeTab(): SharedByMeRobot =
|
||||
this
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
}
|
||||
.clickSharedTab()
|
||||
.clickSharedByMeTab()
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 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.ui.test.flow.share.list
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.annotation.Scenario
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.robot.SharedTabRobot
|
||||
import me.proton.android.drive.ui.robot.SharedWithMeRobot
|
||||
import me.proton.android.drive.ui.test.BaseTest
|
||||
import me.proton.android.drive.ui.test.flow.details.DetailsFlowTest.LinkDetails
|
||||
import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
import me.proton.core.drive.base.domain.extension.bytes
|
||||
import me.proton.core.drive.base.presentation.extension.asHumanReadableString
|
||||
import me.proton.core.drive.files.presentation.extension.SemanticsDownloadState
|
||||
import me.proton.core.test.rule.annotation.PrepareUser
|
||||
import me.proton.test.fusion.FusionConfig
|
||||
import org.junit.Test
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@HiltAndroidTest
|
||||
class SharedWithMeTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
@PrepareUser(loginBefore = true)
|
||||
@Scenario(forTag = "main", value = 2)
|
||||
fun emptyList() {
|
||||
PhotosTabRobot
|
||||
.navigateToSharedWithMeTab()
|
||||
.verify {
|
||||
assertEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(withTag = "main", loginBefore = true)
|
||||
@PrepareUser(withTag = "sharingUser")
|
||||
@Scenario(forTag = "sharingUser", value = 6, sharedWithUserTag = "main")
|
||||
fun browseSharedFolder() {
|
||||
val folder = "ReadWriteFolder"
|
||||
val fileInFolder = "EditableFile.txt"
|
||||
PhotosTabRobot
|
||||
.navigateToSharedWithMeTab()
|
||||
.scrollToItemWithName(folder)
|
||||
.clickOnFolder(folder)
|
||||
.verify {
|
||||
nodeWithTextDisplayed(fileInFolder)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(withTag = "main", loginBefore = true)
|
||||
@PrepareUser(withTag = "sharingUser")
|
||||
@Scenario(forTag = "sharingUser", value = 6, sharedWithUserTag = "main")
|
||||
fun previewTextFile() {
|
||||
val file = "newShareInsideLegacy.txt"
|
||||
PhotosTabRobot
|
||||
.navigateToSharedWithMeTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickOnFile(file)
|
||||
.verify {
|
||||
nodeWithTextDisplayed("Hello World!")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(withTag = "main", loginBefore = true)
|
||||
@PrepareUser(withTag = "sharingUser")
|
||||
@Scenario(forTag = "sharingUser", value = 6, sharedWithUserTag = "main")
|
||||
fun makeAvailableOffline() {
|
||||
val file = "newShareInsideLegacy.txt"
|
||||
PhotosTabRobot
|
||||
.navigateToSharedWithMeTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMakeAvailableOffline()
|
||||
.verify {
|
||||
itemIsDisplayed(file, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
.openSidebarBySwipe()
|
||||
.clickOffline()
|
||||
.verify {
|
||||
itemIsDisplayed(file, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(withTag = "main", loginBefore = true)
|
||||
@PrepareUser(withTag = "sharingUser")
|
||||
@Scenario(forTag = "sharingUser", value = 6, sharedWithUserTag = "main")
|
||||
fun fileDetails() {
|
||||
val sharingUser = protonRule.testDataRule.preparedUsers["sharingUser"]!!
|
||||
PhotosTabRobot
|
||||
.navigateToSharedWithMeTab()
|
||||
.scrollToItemWithName(newShareInsideLegacy(sharingUser.name).name)
|
||||
.clickMoreOnItem(newShareInsideLegacy(sharingUser.name).name)
|
||||
.clickFileDetails()
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
hasHeaderTitle(newShareInsideLegacy(sharingUser.name).name)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_name_entry),
|
||||
value = newShareInsideLegacy(sharingUser.name).name,
|
||||
)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_uploaded_by_entry),
|
||||
value = newShareInsideLegacy(sharingUser.name).uploadedBy,
|
||||
)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_last_modified_entry),
|
||||
value = newShareInsideLegacy(sharingUser.name).modified,
|
||||
)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_size_entry),
|
||||
value = requireNotNull(newShareInsideLegacy(sharingUser.name).size),
|
||||
)
|
||||
hasInfoItem(
|
||||
name = FusionConfig.targetContext.getString(I18N.string.file_info_shared_entry),
|
||||
value = newShareInsideLegacy(sharingUser.name).isShared,
|
||||
)
|
||||
}
|
||||
.clickBack(SharedTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(withTag = "main", loginBefore = true)
|
||||
@PrepareUser(withTag = "sharingUser")
|
||||
@Scenario(forTag = "sharingUser", value = 6, sharedWithUserTag = "main")
|
||||
fun leaveShare() {
|
||||
val newShare = "newShareInsideLegacy.txt"
|
||||
PhotosTabRobot
|
||||
.navigateToSharedWithMeTab()
|
||||
.scrollToItemWithName(newShare)
|
||||
.clickMoreOnItem(newShare)
|
||||
.clickRemoveMe(SharedWithMeRobot)
|
||||
.verify {
|
||||
itemIsNotDisplayed(newShare)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newShareInsideLegacy(name: String) = LinkDetails(
|
||||
name = "newShareInsideLegacy.txt",
|
||||
uploadedBy = "$name@${envConfig.host}",
|
||||
modified = TimestampS().asHumanReadableString(),
|
||||
isShared = FusionConfig.targetContext.getString(I18N.string.common_yes),
|
||||
mimeType = "text/plain",
|
||||
size = 63.bytes.asHumanReadableString(FusionConfig.targetContext),
|
||||
)
|
||||
|
||||
private fun PhotosTabRobot.navigateToSharedWithMeTab(): SharedWithMeRobot =
|
||||
this
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
}
|
||||
.clickSharedTab()
|
||||
.clickSharedWithMeTab()
|
||||
}
|
||||
-2
@@ -61,7 +61,6 @@ class SharingUserFlowTest : BaseTest() {
|
||||
.typeEmail(email)
|
||||
.clickOnEditorPermission()
|
||||
.clickOnViewerPermission()
|
||||
.clickOnSendMessageAndName()
|
||||
.typeMessage("Hello! You can view this file")
|
||||
.clickSend()
|
||||
.verify {
|
||||
@@ -212,7 +211,6 @@ class SharingUserFlowTest : BaseTest() {
|
||||
assertShareFile(image)
|
||||
}
|
||||
.typeEmail(email)
|
||||
.clickOnSendMessageAndName()
|
||||
.typeMessage("Hello! I hope you will receive this invite")
|
||||
.clickSend()
|
||||
.verify {
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.ui.test.flow.share.user
|
||||
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.MainActivity
|
||||
import me.proton.android.drive.ui.annotation.FeatureFlag
|
||||
import me.proton.android.drive.ui.annotation.Scenario
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.robot.SharedWithMeRobot
|
||||
import me.proton.android.drive.ui.test.BaseTest
|
||||
import me.proton.android.drive.utils.getRandomString
|
||||
import me.proton.android.drive.utils.replaceEmailPrefix
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State.ENABLED
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.DRIVE_MOBILE_SHARING_INVITATIONS_ACCEPT_REJECT
|
||||
import me.proton.core.test.quark.v2.command.populate
|
||||
import me.proton.core.test.quark.v2.command.userCreateAddress
|
||||
import me.proton.core.test.quark.v2.command.volumeCreate
|
||||
import me.proton.core.test.rule.annotation.PrepareUser
|
||||
import me.proton.core.test.rule.annotation.mapToUser
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class UserInvitationIdFlowTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
@PrepareUser(withTag = "main", loginBefore = true)
|
||||
@PrepareUser(withTag = "sharingUser")
|
||||
@Scenario(forTag = "sharingUser", value = 6, sharedWithUserTag = "main")
|
||||
@FeatureFlag(DRIVE_MOBILE_SHARING_INVITATIONS_ACCEPT_REJECT, ENABLED)
|
||||
fun acceptUserInvitationFileAndPreview() {
|
||||
val name = "newShare.txt"
|
||||
PhotosTabRobot
|
||||
.navigateToSharedWithMeTab()
|
||||
.clickUserInvitation(2)
|
||||
.clickAccept(name)
|
||||
.verify {
|
||||
assertAcceptSucceed()
|
||||
}
|
||||
.clickBack(SharedWithMeRobot)
|
||||
.clickOnFile(name)
|
||||
.verify {
|
||||
nodeWithTextDisplayed("Hello World!")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(withTag = "main")
|
||||
@PrepareUser(withTag = "sharingUser")
|
||||
@FeatureFlag(DRIVE_MOBILE_SHARING_INVITATIONS_ACCEPT_REJECT, ENABLED)
|
||||
fun acceptUserInvitationWithMultipleAddress() {
|
||||
val user = protonRule.testDataRule.mainTestUser!!
|
||||
val primaryName = getRandomString(10)
|
||||
val primaryEmail = user.email.replaceEmailPrefix(primaryName)
|
||||
|
||||
// Create another address before login
|
||||
protonRule.testDataRule.quarkCommand.userCreateAddress(
|
||||
decryptedUserId = user.decryptedUserId,
|
||||
password = user.password,
|
||||
email = primaryEmail,
|
||||
isPrimary = true
|
||||
)
|
||||
|
||||
val sharingUser = requireNotNull(protonRule.testDataRule.preparedUsers["sharingUser"])
|
||||
quarkRule.quarkCommands.populate(sharingUser.mapToUser(), scenario = 6, sharingUser = user.mapToUser())
|
||||
|
||||
loginTestHelper.login(user.name, user.password)
|
||||
ActivityScenario.launch(MainActivity::class.java)
|
||||
|
||||
val name = "newShare.txt"
|
||||
PhotosTabRobot
|
||||
.navigateToSharedWithMeTab()
|
||||
.clickUserInvitation(2)
|
||||
.clickAccept(name)
|
||||
.verify {
|
||||
assertAcceptSucceed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(withTag = "main", loginBefore = true)
|
||||
@PrepareUser(withTag = "sharingUser")
|
||||
@Scenario(forTag = "sharingUser", value = 6, sharedWithUserTag = "main")
|
||||
@FeatureFlag(DRIVE_MOBILE_SHARING_INVITATIONS_ACCEPT_REJECT, ENABLED)
|
||||
fun acceptUserInvitationFolderAndOpen() {
|
||||
val name = "newShare"
|
||||
PhotosTabRobot
|
||||
.navigateToSharedWithMeTab()
|
||||
.clickUserInvitation(2)
|
||||
.clickAccept(name)
|
||||
.verify {
|
||||
assertAcceptSucceed()
|
||||
}
|
||||
.clickBack(SharedWithMeRobot)
|
||||
.clickOnFolder(name)
|
||||
.verify {
|
||||
itemIsDisplayed("legacyShareInsideNew.txt")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(withTag = "main", loginBefore = true)
|
||||
@PrepareUser(withTag = "sharingUser")
|
||||
@Scenario(forTag = "sharingUser", value = 6, sharedWithUserTag = "main")
|
||||
@FeatureFlag(DRIVE_MOBILE_SHARING_INVITATIONS_ACCEPT_REJECT, ENABLED)
|
||||
fun declineUserInvitation() {
|
||||
PhotosTabRobot
|
||||
.navigateToSharedWithMeTab()
|
||||
.clickUserInvitation(2)
|
||||
.clickDecline("newShare")
|
||||
.verify {
|
||||
assertDeclineSucceed()
|
||||
}
|
||||
.clickBack(SharedWithMeRobot)
|
||||
.verify {
|
||||
itemIsNotDisplayed("newShare")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PrepareUser(withTag = "main", loginBefore = true)
|
||||
@PrepareUser(withTag = "sharingUser")
|
||||
@Scenario(forTag = "sharingUser", value = 6, sharedWithUserTag = "main")
|
||||
@FeatureFlag(DRIVE_MOBILE_SHARING_INVITATIONS_ACCEPT_REJECT, ENABLED)
|
||||
fun emptyUserInvitation() {
|
||||
PhotosTabRobot
|
||||
.navigateToSharedWithMeTab()
|
||||
.clickUserInvitation(2)
|
||||
.clickDecline("newShare")
|
||||
.verify {
|
||||
assertDeclineSucceed()
|
||||
}
|
||||
.clickDecline("newShare.txt")
|
||||
.verify {
|
||||
assertDeclineSucceed()
|
||||
assertEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun PhotosTabRobot.navigateToSharedWithMeTab(): SharedWithMeRobot =
|
||||
this
|
||||
.verify {
|
||||
waitUntilLoaded()
|
||||
robotDisplayed()
|
||||
}
|
||||
.clickSharedTab()
|
||||
.clickSharedWithMeTab()
|
||||
}
|
||||
@@ -22,7 +22,7 @@ object Config {
|
||||
const val minSdk = 26
|
||||
const val targetSdk = 34
|
||||
const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
const val versionName = "2.14.0"
|
||||
const val versionName = "2.15.0"
|
||||
const val archivesBaseName = "ProtonDrive-$versionName"
|
||||
val supportedResourceConfigurations = listOf(
|
||||
"b+es+419",
|
||||
|
||||
+6
@@ -90,6 +90,7 @@ sealed class Event {
|
||||
UNCOMPLETED,
|
||||
PAUSE_BACKGROUND_RESTRICTIONS,
|
||||
PREPARING,
|
||||
FAILED_DUE_PHOTO_SHARE_MIGRATION,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +119,11 @@ sealed class Event {
|
||||
override val occurredAt: TimestampMs = TimestampMs()
|
||||
}
|
||||
|
||||
data class BackupSync(val folderId: FolderId, val bucketId: Int) : Event() {
|
||||
override val id: String = "$EVENT_ID_PREFIX${this.javaClass.simpleName.uppercase()}_1"
|
||||
override val occurredAt: TimestampMs = TimestampMs()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Download(val downloadId: String, val downloadedFiles: Int, val totalFiles: Int) :
|
||||
Event() {
|
||||
|
||||
+5
-3
@@ -22,19 +22,21 @@ import android.system.OsConstants
|
||||
import me.proton.core.drive.backup.domain.entity.BackupError
|
||||
import me.proton.core.drive.base.domain.api.ProtonApiCode.EXCEEDED_QUOTA
|
||||
import me.proton.core.drive.base.data.extension.isErrno
|
||||
import me.proton.core.drive.base.domain.api.ProtonApiCode.PHOTO_MIGRATION
|
||||
import me.proton.core.network.domain.ApiException
|
||||
import me.proton.core.network.domain.hasProtonErrorCode
|
||||
|
||||
fun Throwable.toBackupError(): BackupError = when (this) {
|
||||
fun Throwable.toBackupError(retryable: Boolean = true): BackupError = when (this) {
|
||||
is SecurityException -> BackupError.Permissions()
|
||||
is ApiException -> when {
|
||||
hasProtonErrorCode(EXCEEDED_QUOTA) -> BackupError.DriveStorage()
|
||||
else -> BackupError.Other()
|
||||
hasProtonErrorCode(PHOTO_MIGRATION) -> BackupError.Migration()
|
||||
else -> BackupError.Other(retryable)
|
||||
}
|
||||
|
||||
else -> if (isErrno(OsConstants.ENOSPC)) {
|
||||
BackupError.LocalStorage()
|
||||
} else {
|
||||
BackupError.Other()
|
||||
BackupError.Other(retryable)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -73,6 +73,7 @@ class BackupUploadErrorHandler @Inject constructor(
|
||||
BackupErrorType.LOCAL_STORAGE,
|
||||
BackupErrorType.DRIVE_STORAGE,
|
||||
BackupErrorType.PHOTOS_UPLOAD_NOT_ALLOWED,
|
||||
BackupErrorType.MIGRATION,
|
||||
-> {
|
||||
BackupStopException("Backup must stop: ${backupError.type}", throwable)
|
||||
.log(BACKUP, "Stopping backup")
|
||||
|
||||
+3
-2
@@ -38,6 +38,7 @@ import me.proton.core.drive.backup.data.worker.WorkerKeys.KEY_USER_ID
|
||||
import me.proton.core.drive.backup.domain.entity.BackupFolder
|
||||
import me.proton.core.drive.backup.domain.usecase.AddBackupError
|
||||
import me.proton.core.drive.backup.domain.usecase.CheckDuplicates
|
||||
import me.proton.core.drive.backup.domain.usecase.HandleBackupError
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
import me.proton.core.drive.base.data.workmanager.addTags
|
||||
import me.proton.core.drive.base.domain.log.LogTag
|
||||
@@ -51,7 +52,7 @@ class BackupCheckDuplicatesWorker @AssistedInject constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
private val checkDuplicates: CheckDuplicates,
|
||||
private val addBackupError: AddBackupError,
|
||||
private val handleBackupError: HandleBackupError,
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
|
||||
|
||||
@@ -66,7 +67,7 @@ class BackupCheckDuplicatesWorker @AssistedInject constructor(
|
||||
.onSuccess { folderId ->
|
||||
checkDuplicates(BackupFolder(bucketId, folderId)).onFailure { error ->
|
||||
error.log(LogTag.BACKUP, "Cannot check duplicates")
|
||||
addBackupError(folderId, error.toBackupError())
|
||||
handleBackupError(folderId, error.toBackupError())
|
||||
return Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -29,13 +29,14 @@ import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.backup.data.extension.toBackupError
|
||||
import me.proton.core.drive.backup.data.manager.BackupManagerImpl
|
||||
import me.proton.core.drive.backup.data.worker.WorkerKeys.KEY_FOLDER_ID
|
||||
import me.proton.core.drive.backup.data.worker.WorkerKeys.KEY_SHARE_ID
|
||||
import me.proton.core.drive.backup.data.worker.WorkerKeys.KEY_USER_ID
|
||||
import me.proton.core.drive.backup.domain.entity.BackupError
|
||||
import me.proton.core.drive.backup.domain.usecase.AddBackupError
|
||||
import me.proton.core.drive.backup.domain.usecase.CleanRevisions
|
||||
import me.proton.core.drive.backup.domain.usecase.HandleBackupError
|
||||
import me.proton.core.drive.base.data.entity.LoggerLevel.WARNING
|
||||
import me.proton.core.drive.base.data.extension.isRetryable
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
@@ -52,7 +53,7 @@ class BackupCleanRevisionsWorker @AssistedInject constructor(
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
private val cleanRevisions: CleanRevisions,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
private val addBackupError: AddBackupError,
|
||||
private val handleBackupError: HandleBackupError,
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val userId = UserId(requireNotNull(inputData.getString(KEY_USER_ID)))
|
||||
@@ -76,7 +77,7 @@ class BackupCleanRevisionsWorker @AssistedInject constructor(
|
||||
tag = BACKUP,
|
||||
message = "Cannot clean revisions for: ${folderId.id} retryable $retryable, max retries reached $canRetry"
|
||||
)
|
||||
addBackupError(folderId, BackupError.Other(retryable))
|
||||
handleBackupError(folderId, error.toBackupError(retryable))
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -29,14 +29,14 @@ import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.backup.data.extension.toBackupError
|
||||
import me.proton.core.drive.backup.data.manager.BackupManagerImpl
|
||||
import me.proton.core.drive.backup.data.worker.WorkerKeys.KEY_FOLDER_ID
|
||||
import me.proton.core.drive.backup.data.worker.WorkerKeys.KEY_SHARE_ID
|
||||
import me.proton.core.drive.backup.data.worker.WorkerKeys.KEY_USER_ID
|
||||
import me.proton.core.drive.backup.domain.entity.BackupError
|
||||
import me.proton.core.drive.backup.domain.entity.BackupFolder
|
||||
import me.proton.core.drive.backup.domain.usecase.AddBackupError
|
||||
import me.proton.core.drive.backup.domain.usecase.FindDuplicates
|
||||
import me.proton.core.drive.backup.domain.usecase.HandleBackupError
|
||||
import me.proton.core.drive.base.data.entity.LoggerLevel.WARNING
|
||||
import me.proton.core.drive.base.data.extension.isRetryable
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
@@ -54,7 +54,7 @@ class BackupFindDuplicatesWorker @AssistedInject constructor(
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
private val findDuplicates: FindDuplicates,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
private val addBackupError: AddBackupError,
|
||||
private val handleBackupError: HandleBackupError,
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val userId = UserId(requireNotNull(inputData.getString(KEY_USER_ID)))
|
||||
@@ -85,7 +85,7 @@ class BackupFindDuplicatesWorker @AssistedInject constructor(
|
||||
tag = BACKUP,
|
||||
message = "Cannot find duplicates for: ${folderId.id} retryable $retryable, max retries reached $canRetry"
|
||||
)
|
||||
addBackupError(folderId, BackupError.Other(retryable))
|
||||
handleBackupError(folderId, error.toBackupError(retryable))
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -32,9 +32,9 @@ import me.proton.core.drive.backup.domain.entity.BackupFileState
|
||||
import me.proton.core.drive.backup.domain.entity.BackupFolder
|
||||
import me.proton.core.drive.backup.domain.repository.BackupDuplicateRepository
|
||||
import me.proton.core.drive.backup.domain.repository.BackupFileRepository
|
||||
import me.proton.core.drive.backup.domain.usecase.AddBackupError
|
||||
import me.proton.core.drive.backup.domain.usecase.AddFolder
|
||||
import me.proton.core.drive.backup.domain.usecase.CheckDuplicates
|
||||
import me.proton.core.drive.backup.domain.usecase.HandleBackupError
|
||||
import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
import me.proton.core.drive.base.domain.extension.bytes
|
||||
import me.proton.core.drive.db.test.myFiles
|
||||
@@ -64,7 +64,7 @@ class BackupCheckDuplicatesWorkerTest {
|
||||
lateinit var checkDuplicates: CheckDuplicates
|
||||
|
||||
@Inject
|
||||
lateinit var addBackupError: AddBackupError
|
||||
lateinit var handleBackupError: HandleBackupError
|
||||
|
||||
@Inject
|
||||
lateinit var addFolder: AddFolder
|
||||
@@ -130,7 +130,7 @@ class BackupCheckDuplicatesWorkerTest {
|
||||
context = appContext,
|
||||
workerParams = workerParameters,
|
||||
checkDuplicates = checkDuplicates,
|
||||
addBackupError = addBackupError,
|
||||
handleBackupError = handleBackupError,
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
+3
-3
@@ -31,8 +31,8 @@ import me.proton.core.drive.backup.domain.entity.BackupDuplicate
|
||||
import me.proton.core.drive.backup.domain.entity.BackupError
|
||||
import me.proton.core.drive.backup.domain.repository.BackupDuplicateRepository
|
||||
import me.proton.core.drive.backup.domain.repository.BackupErrorRepository
|
||||
import me.proton.core.drive.backup.domain.usecase.AddBackupError
|
||||
import me.proton.core.drive.backup.domain.usecase.CleanRevisions
|
||||
import me.proton.core.drive.backup.domain.usecase.HandleBackupError
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.db.test.myFiles
|
||||
import me.proton.core.drive.folder.domain.repository.FolderRepository
|
||||
@@ -65,7 +65,7 @@ class BackupCleanRevisionsWorkerTest {
|
||||
lateinit var configurationProvider: ConfigurationProvider
|
||||
|
||||
@Inject
|
||||
lateinit var addBackupError: AddBackupError
|
||||
lateinit var handleBackupError: HandleBackupError
|
||||
|
||||
@Inject
|
||||
lateinit var backupDuplicateRepository: BackupDuplicateRepository
|
||||
@@ -167,7 +167,7 @@ class BackupCleanRevisionsWorkerTest {
|
||||
workerParams = workerParameters,
|
||||
cleanRevisions = cleanRevisions,
|
||||
configurationProvider = configurationProvider,
|
||||
addBackupError = addBackupError,
|
||||
handleBackupError = handleBackupError,
|
||||
)
|
||||
})
|
||||
.setInputData(
|
||||
|
||||
+3
-3
@@ -34,8 +34,8 @@ import me.proton.core.drive.backup.domain.entity.BackupFolder
|
||||
import me.proton.core.drive.backup.domain.repository.BackupErrorRepository
|
||||
import me.proton.core.drive.backup.domain.repository.BackupFileRepository
|
||||
import me.proton.core.drive.backup.domain.repository.BackupFolderRepository
|
||||
import me.proton.core.drive.backup.domain.usecase.AddBackupError
|
||||
import me.proton.core.drive.backup.domain.usecase.FindDuplicates
|
||||
import me.proton.core.drive.backup.domain.usecase.HandleBackupError
|
||||
import me.proton.core.drive.base.domain.api.ProtonApiCode
|
||||
import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
import me.proton.core.drive.base.domain.extension.bytes
|
||||
@@ -81,7 +81,7 @@ class BackupFindDuplicatesWorkerTest {
|
||||
lateinit var configurationProvider: ConfigurationProvider
|
||||
|
||||
@Inject
|
||||
lateinit var addBackupError: AddBackupError
|
||||
lateinit var handleBackupError: HandleBackupError
|
||||
private lateinit var backupFolder: BackupFolder
|
||||
private lateinit var backupFiles: List<BackupFile>
|
||||
|
||||
@@ -187,7 +187,7 @@ class BackupFindDuplicatesWorkerTest {
|
||||
workerParams = workerParameters,
|
||||
findDuplicates = findDuplicates,
|
||||
configurationProvider = configurationProvider,
|
||||
addBackupError = addBackupError,
|
||||
handleBackupError = handleBackupError,
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
+5
@@ -63,5 +63,10 @@ data class BackupError(
|
||||
type = BackupErrorType.BACKGROUND_RESTRICTIONS,
|
||||
retryable = true,
|
||||
)
|
||||
|
||||
fun Migration() = BackupError(
|
||||
type = BackupErrorType.MIGRATION,
|
||||
retryable = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -27,4 +27,5 @@ enum class BackupErrorType {
|
||||
DRIVE_STORAGE,
|
||||
PHOTOS_UPLOAD_NOT_ALLOWED,
|
||||
BACKGROUND_RESTRICTIONS,
|
||||
MIGRATION,
|
||||
}
|
||||
|
||||
+1
@@ -32,4 +32,5 @@ fun BackupErrorType.toEventBackupState() = when (this) {
|
||||
-> Event.Backup.BackupState.FAILED_PHOTOS_UPLOAD_NOT_ALLOWED
|
||||
|
||||
BackupErrorType.BACKGROUND_RESTRICTIONS -> Event.Backup.BackupState.PAUSE_BACKGROUND_RESTRICTIONS
|
||||
BackupErrorType.MIGRATION -> Event.Backup.BackupState.FAILED_DUE_PHOTO_SHARE_MIGRATION
|
||||
}
|
||||
|
||||
+3
-4
@@ -19,7 +19,6 @@
|
||||
package me.proton.core.drive.backup.domain.usecase
|
||||
|
||||
import me.proton.core.drive.backup.domain.entity.BackupFolder
|
||||
import me.proton.core.drive.backup.domain.manager.BackupManager
|
||||
import me.proton.core.drive.base.domain.log.LogTag
|
||||
import me.proton.core.drive.base.domain.util.coRunCatching
|
||||
import me.proton.core.drive.linkupload.domain.entity.UploadFileLink
|
||||
@@ -28,14 +27,14 @@ import javax.inject.Inject
|
||||
|
||||
class EnableBackupForFolder @Inject constructor(
|
||||
private val addFolder: AddFolder,
|
||||
private val backupManager: BackupManager,
|
||||
private val syncFolders: SyncFolders,
|
||||
) {
|
||||
suspend operator fun invoke(backupFolder: BackupFolder) = coRunCatching {
|
||||
CoreLogger.d(LogTag.BACKUP, "Adding folder: ${backupFolder.bucketId}")
|
||||
addFolder(backupFolder).getOrThrow()
|
||||
backupManager.sync(
|
||||
syncFolders(
|
||||
backupFolder = backupFolder,
|
||||
uploadPriority = UploadFileLink.BACKUP_PRIORITY
|
||||
)
|
||||
).getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Core.
|
||||
*
|
||||
* Proton Core 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 Core 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 Core. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.drive.backup.domain.usecase
|
||||
|
||||
import me.proton.core.drive.backup.domain.entity.BackupError
|
||||
import me.proton.core.drive.backup.domain.entity.BackupErrorType
|
||||
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.BACKUP
|
||||
import me.proton.core.drive.base.domain.log.logId
|
||||
import me.proton.core.drive.base.domain.util.coRunCatching
|
||||
import me.proton.core.drive.folder.domain.usecase.DeleteLocalContent
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.share.domain.usecase.GetShare
|
||||
import javax.inject.Inject
|
||||
|
||||
class HandleBackupError @Inject constructor(
|
||||
private val stopBackup: StopBackup,
|
||||
private val addBackupError: AddBackupError,
|
||||
private val getShare: GetShare,
|
||||
private val deleteLocalContent: DeleteLocalContent,
|
||||
) {
|
||||
suspend operator fun invoke(folderId: FolderId, backupError: BackupError) = coRunCatching {
|
||||
if (backupError.type == BackupErrorType.MIGRATION) {
|
||||
stopBackup(folderId, backupError).getOrThrow()
|
||||
deleteLocalContent(
|
||||
volumeId = getShare(folderId.shareId).toResult().getOrThrow().volumeId,
|
||||
linkId = folderId,
|
||||
).getOrNull(BACKUP, "Cannot delete local content: ${folderId.id.logId()}")
|
||||
} else {
|
||||
addBackupError(folderId, backupError).getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-4
@@ -19,27 +19,39 @@
|
||||
package me.proton.core.drive.backup.domain.usecase
|
||||
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.announce.event.domain.entity.Event
|
||||
import me.proton.core.drive.announce.event.domain.usecase.AnnounceEvent
|
||||
import me.proton.core.drive.backup.domain.entity.BackupFolder
|
||||
import me.proton.core.drive.backup.domain.entity.BucketUpdate
|
||||
import me.proton.core.drive.backup.domain.manager.BackupManager
|
||||
import me.proton.core.drive.base.domain.util.coRunCatching
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.extension.userId
|
||||
import me.proton.core.util.kotlin.filterNotNullValues
|
||||
import me.proton.core.util.kotlin.filterNullValues
|
||||
import javax.inject.Inject
|
||||
|
||||
class SyncFolders @Inject constructor(
|
||||
private val getAllFolders: GetAllFolders,
|
||||
private val backupManager: BackupManager,
|
||||
private val announceEvent: AnnounceEvent,
|
||||
) {
|
||||
suspend operator fun invoke(backupFolder: BackupFolder, uploadPriority: Long) = coRunCatching {
|
||||
backupManager.sync(backupFolder, uploadPriority)
|
||||
announceEvent(backupFolder.folderId.userId, Event.BackupSync(
|
||||
folderId = backupFolder.folderId,
|
||||
bucketId = backupFolder.bucketId,
|
||||
))
|
||||
}
|
||||
|
||||
suspend operator fun invoke(folderId: FolderId, uploadPriority: Long) = coRunCatching {
|
||||
getAllFolders(folderId).getOrThrow().onEach { backupFolder ->
|
||||
backupManager.sync(backupFolder, uploadPriority)
|
||||
invoke(backupFolder, uploadPriority)
|
||||
}
|
||||
}
|
||||
|
||||
suspend operator fun invoke(userId: UserId, uploadPriority: Long) = coRunCatching {
|
||||
getAllFolders(userId).getOrThrow().onEach { backupFolder ->
|
||||
backupManager.sync(backupFolder, uploadPriority)
|
||||
invoke(backupFolder, uploadPriority)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +83,7 @@ class SyncFolders @Inject constructor(
|
||||
backupFolder
|
||||
}
|
||||
}.onEach { backupFolder ->
|
||||
backupManager.sync(backupFolder, uploadPriority)
|
||||
invoke(backupFolder, uploadPriority)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -30,7 +30,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class SyncStaleFolders(
|
||||
private val getAllFolders: GetAllFolders,
|
||||
private val backupManager: BackupManager,
|
||||
private val syncFolders: SyncFolders,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
private val clock: () -> TimestampS,
|
||||
) {
|
||||
@@ -38,9 +38,9 @@ class SyncStaleFolders(
|
||||
@Inject
|
||||
constructor(
|
||||
getAllFolders: GetAllFolders,
|
||||
backupManager: BackupManager,
|
||||
syncFolders: SyncFolders,
|
||||
configurationProvider: ConfigurationProvider,
|
||||
) : this(getAllFolders, backupManager, configurationProvider, ::TimestampS)
|
||||
) : this(getAllFolders, syncFolders, configurationProvider, ::TimestampS)
|
||||
|
||||
private val stale = { backupFolder: BackupFolder ->
|
||||
backupFolder.syncTime?.let { syncTime ->
|
||||
@@ -57,9 +57,9 @@ class SyncStaleFolders(
|
||||
getAllFolders(userId).sync(uploadPriority)
|
||||
}
|
||||
|
||||
private fun Result<List<BackupFolder>>.sync(
|
||||
private suspend fun Result<List<BackupFolder>>.sync(
|
||||
uploadPriority: Long,
|
||||
) = getOrThrow().filter(stale).onEach { backupFolder ->
|
||||
backupManager.sync(backupFolder, uploadPriority)
|
||||
syncFolders(backupFolder, uploadPriority)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ object Dto {
|
||||
const val ACTIVE_REVISION = "ActiveRevision"
|
||||
const val ACTIVE_URLS = "ActiveUrls"
|
||||
const val ADDRESS_ID = "AddressID"
|
||||
const val ADDRESS_KEY_ID = "AddressKeyID"
|
||||
const val ALBUM = "Album"
|
||||
const val ALBUM_PROPERTIES = "AlbumProperties"
|
||||
const val ALBUMS = "Albums"
|
||||
const val ANCHOR_ID = "AnchorID"
|
||||
const val ATTRIBUTES = "Attributes"
|
||||
const val AVAILABLE_HASHES = "AvailableHashes"
|
||||
@@ -39,18 +43,18 @@ object Dto {
|
||||
const val CONTENT_KEY_PACKET = "ContentKeyPacket"
|
||||
const val CONTENT_KEY_PACKET_SIGNATURE = "ContentKeyPacketSignature"
|
||||
const val CONTEXT_SHARE_ID = "ContextShareID"
|
||||
const val COVER_LINK_ID = "CoverLinkID"
|
||||
const val CREATION_TIME = "CreationTime"
|
||||
const val CREATE_TIME = "CreateTime"
|
||||
const val CREATOR = "Creator"
|
||||
const val CREATOR_EMAIL = "CreatorEmail"
|
||||
const val DATA = "Data"
|
||||
const val DELETED_URL_ID = "DeletedURLID"
|
||||
const val DOCUMENT = "Document"
|
||||
const val EXTERNAL_INVITATION_ID = "ExternalInvitationID"
|
||||
const val EXTERNAL_INVITATION_SIGNUP = "ExternalInvitationSignup"
|
||||
const val DEVICE = "Device"
|
||||
const val DEVICES = "Devices"
|
||||
const val DEVICE_ID = "DeviceID"
|
||||
const val DOCUMENT = "Document"
|
||||
const val DOWNLOADED_BYTES = "DownloadedBytes"
|
||||
const val DUPLICATE_HASHES = "DuplicateHashes"
|
||||
const val EMAIL = "Email"
|
||||
const val EMAIL_DETAILS = "EmailDetails"
|
||||
@@ -63,10 +67,12 @@ object Dto {
|
||||
const val EXIF = "Exif"
|
||||
const val EXPIRATION_DURATION = "ExpirationDuration"
|
||||
const val EXPIRATION_TIME = "ExpirationTime"
|
||||
const val EXTERNAL_INVITATION_IDS = "ExternalInvitationIDs"
|
||||
const val EXTERNAL_INVITATION = "ExternalInvitation"
|
||||
const val EXTERNAL_INVITATION_ID = "ExternalInvitationID"
|
||||
const val EXTERNAL_INVITATION_IDS = "ExternalInvitationIDs"
|
||||
const val EXTERNAL_INVITATIONS = "ExternalInvitations"
|
||||
const val EXTERNAL_INVITATION_SIGNATURE = "ExternalInvitationSignature"
|
||||
const val EXTERNAL_INVITATION_SIGNUP = "ExternalInvitationSignup"
|
||||
const val FILE = "File"
|
||||
const val FILE_PROPERTIES = "FileProperties"
|
||||
const val FLAGS = "Flags"
|
||||
@@ -94,6 +100,7 @@ object Dto {
|
||||
const val KEY_PACKET = "KeyPacket"
|
||||
const val KEY_PACKET_SIGNATURE = "KeyPacketSignature"
|
||||
const val LAST_ACCESS_TIME = "LastAccessTime"
|
||||
const val LAST_ACTIVITY_TIME = "LastActivityTime"
|
||||
const val LAST_ANCHOR_ID = "LastAnchorID"
|
||||
const val LAST_SYNC_TIME = "LastSyncTime"
|
||||
const val LINK = "Link"
|
||||
@@ -138,11 +145,13 @@ object Dto {
|
||||
const val PERMISSIONS = "Permissions"
|
||||
const val PHOTO = "Photo"
|
||||
const val PHOTOS = "Photos"
|
||||
const val PHOTO_COUNT = "PhotoCount"
|
||||
const val PUBLIC_URL = "PublicUrl"
|
||||
const val REVISION = "Revision"
|
||||
const val REVISION_ID = "RevisionID"
|
||||
const val RESPONSE = "Response"
|
||||
const val RESPONSES = "Responses"
|
||||
const val RESTORE_STATUS = "RestoreStatus"
|
||||
const val ROOT_LINK_ID = "RootLinkID"
|
||||
const val SESSION_KEY_SIGNATURE = "SessionKeySignature"
|
||||
const val SHARE = "Share"
|
||||
@@ -184,8 +193,9 @@ object Dto {
|
||||
const val TRASH = "Trash"
|
||||
const val TRASHED = "Trashed"
|
||||
const val TYPE = "Type"
|
||||
const val UPLOAD_LINKS = "UploadLinks"
|
||||
const val UNREADABLE_SHARE_IDS = "UnreadableShareIDs"
|
||||
const val UPLOAD_LINKS = "UploadLinks"
|
||||
const val UPLOADED_BYTES = "UploadedBytes"
|
||||
const val URL = "URL"
|
||||
const val URL_PASSWORD_SALT = "UrlPasswordSalt"
|
||||
const val URLS_EXPIRED = "UrlsExpired"
|
||||
|
||||
@@ -33,9 +33,11 @@ object Column {
|
||||
const val CONTENT_KEY_PACKET = "content_key_packet"
|
||||
const val CONTENT_KEY_PACKET_SIGNATURE = "content_key_packet_signature"
|
||||
const val COUNT = "count"
|
||||
const val COVER_LINK_ID = "cover_link_id"
|
||||
const val CREATE_TIME = "create_time"
|
||||
const val CREATION_TIME = "creation_time"
|
||||
const val CREATOR_EMAIL = "creator_email"
|
||||
const val DETAILS = "details"
|
||||
const val DIGESTS = "digests"
|
||||
const val DURATION = "duration"
|
||||
const val ENCRYPTED = "encrypted"
|
||||
@@ -59,6 +61,7 @@ object Column {
|
||||
const val KEY_PACKET = "key_packet"
|
||||
const val KEY_PACKET_SIGNATURE = "key_packet_signature"
|
||||
const val LAST_ACCESS_TIME = "last_access_time"
|
||||
const val LAST_ACTIVITY_TIME = "last_activity_time"
|
||||
const val LAST_FETCH_CHILDREN_TIMESTAMP = "last_fetch_children_timestamp"
|
||||
const val LAST_FETCH_TIMESTAMP = "last_fetch_timestamp"
|
||||
const val LAST_FETCH_TRASH_TIMESTAMP = "last_fetch_trash_timestamp"
|
||||
@@ -114,6 +117,7 @@ object Column {
|
||||
const val SIZE = "size"
|
||||
const val SHARE_ID = "share_id"
|
||||
const val SHARE_INVITATION_COUNT = "share_invitation_count"
|
||||
const val SHARE_KEY = "share_key"
|
||||
const val SHARE_PASSPHRASE_KEY_PACKET = "share_passphrase_key_packet"
|
||||
const val SHARE_PASSWORD_SALT = "share_password_salt"
|
||||
const val SHARE_MEMBER_COUNT = "share_member_count"
|
||||
|
||||
+4
-1
@@ -58,7 +58,10 @@ fun Throwable.getDefaultMessage(
|
||||
is UnsupportedOperationException -> getDefaultMessage(context)
|
||||
is IOException -> getDefaultMessage(context)
|
||||
is SecurityException -> getDefaultMessage(context)
|
||||
is RuntimeException -> getDefaultMessage(context)
|
||||
is RuntimeException -> {
|
||||
cause?.getDefaultMessage(context, useExceptionMessage)
|
||||
?: getDefaultMessage(context)
|
||||
}
|
||||
else -> unhandled
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ object ProtonApiCode {
|
||||
const val INSUFFICIENT_QUOTA = 200001
|
||||
const val EXCEEDED_QUOTA = 200002
|
||||
const val TOO_MANY_CHILDREN = 200300
|
||||
const val PHOTO_MIGRATION = 201100
|
||||
const val ENCRYPTION_VERIFICATION_FAILED = 200501
|
||||
const val KEY_GET_INPUT_INVALID = 33101;
|
||||
const val KEY_GET_ADDRESS_MISSING = 33102
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ interface ConfigurationProvider {
|
||||
val minimumOrganizationFetchInterval: Duration get() = 1.days
|
||||
val observeWorkManagerInterval: Duration get() = 1.minutes
|
||||
val cacheInternalStorageLimit: Bytes get() = 512.MiB
|
||||
val drivePublicShareEditMode: Boolean get() = false // implement entitlements PublicCollaboration
|
||||
val albumsFeatureFlag: Boolean get() = false
|
||||
|
||||
data class Thumbnail(
|
||||
val maxWidth: Int,
|
||||
|
||||
+13
-4
@@ -18,6 +18,7 @@
|
||||
package me.proton.core.drive.base.presentation.component
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.BottomNavigation
|
||||
import androidx.compose.material.BottomNavigationItem
|
||||
import androidx.compose.material.Divider
|
||||
@@ -30,11 +31,13 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
|
||||
data class NavigationTab(
|
||||
val iconResId: Int,
|
||||
val titleResId: Int,
|
||||
val notificationDotVisible: Boolean = false
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -53,10 +56,16 @@ fun BottomNavigation(
|
||||
val isSelected = selectedTab == tab
|
||||
BottomNavigationItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(id = tab.iconResId),
|
||||
contentDescription = stringResource(id = tab.titleResId)
|
||||
)
|
||||
BoxWithNotificationDot(
|
||||
modifier = Modifier.padding(horizontal = ProtonDimens.SmallSpacing),
|
||||
notificationDotVisible = tab.notificationDotVisible,
|
||||
horizontalOffset = ProtonDimens.SmallSpacing,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = tab.iconResId),
|
||||
contentDescription = stringResource(id = tab.titleResId)
|
||||
)
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
|
||||
+5
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Icon
|
||||
@@ -34,6 +35,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.presentation.R as CorePresentation
|
||||
@@ -42,6 +44,8 @@ import me.proton.core.presentation.R as CorePresentation
|
||||
fun BoxWithNotificationDot(
|
||||
modifier: Modifier = Modifier,
|
||||
notificationDotVisible: Boolean = false,
|
||||
horizontalOffset: Dp = 0.dp,
|
||||
verticalOffset: Dp = 0.dp,
|
||||
content: @Composable (Modifier) -> Unit
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
@@ -50,6 +54,7 @@ fun BoxWithNotificationDot(
|
||||
NotificationDot(
|
||||
Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.offset(x = horizontalOffset, y = verticalOffset)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Core.
|
||||
*
|
||||
* Proton Core 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 Core 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 Core. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.drive.base.presentation.component
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import me.proton.core.compose.theme.ProtonDimens.DefaultButtonMinHeight
|
||||
import me.proton.core.compose.theme.ProtonDimens.DefaultCornerRadius
|
||||
import me.proton.core.compose.theme.ProtonDimens.ExtraSmallSpacing
|
||||
import me.proton.core.compose.theme.ProtonDimens.MediumSpacing
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.compose.theme.captionNorm
|
||||
import me.proton.core.drive.base.domain.entity.FileTypeCategory
|
||||
import me.proton.core.drive.base.domain.extension.firstCodePointAsStringOrNull
|
||||
import me.proton.core.drive.base.presentation.extension.iconResId
|
||||
|
||||
@Composable
|
||||
fun BoxScope.LetterBadge(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.offset(x = ExtraSmallSpacing)
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(MediumSpacing)
|
||||
.clip(CircleShape)
|
||||
.background(ProtonTheme.colors.shade40),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text.firstCodePointAsStringOrNull?.uppercase() ?: "?",
|
||||
style = ProtonTheme.typography.captionNorm,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LetterBadge() {
|
||||
ProtonTheme {
|
||||
Box {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(DefaultButtonMinHeight)
|
||||
.clip(RoundedCornerShape(DefaultCornerRadius)),
|
||||
painter = painterResource(FileTypeCategory.Unknown.iconResId),
|
||||
contentDescription = null,
|
||||
)
|
||||
LetterBadge("Name")
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -41,5 +41,8 @@ sealed class ListContentState {
|
||||
val message: String,
|
||||
@StringRes val actionResId: Int? = null,
|
||||
override val isRefreshing: Boolean = false,
|
||||
@StringRes val titleId: Int? = null,
|
||||
@StringRes val descriptionResId: Int? = null,
|
||||
@DrawableRes val imageResId: Int? = null,
|
||||
) : ListContentState()
|
||||
}
|
||||
|
||||
+4
@@ -49,6 +49,10 @@ fun onLoadState(
|
||||
flow
|
||||
//.debounce(500L)
|
||||
.onEach { (loadState, itemCount) ->
|
||||
val append = loadState.append
|
||||
if (append is LoadState.Error) {
|
||||
append.error.getDefaultMessage(appContext, useExceptionMessage).let(onError)
|
||||
}
|
||||
listContentState.processRefreshState(
|
||||
appContext = appContext,
|
||||
useExceptionMessage = useExceptionMessage,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -0,0 +1,37 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="163dp"
|
||||
android:height="159dp"
|
||||
android:viewportWidth="163"
|
||||
android:viewportHeight="159">
|
||||
<path
|
||||
android:pathData="M33,0.95L126,0.95A3,3 0,0 1,129 3.95L129,155.95A3,3 0,0 1,126 158.95L33,158.95A3,3 0,0 1,30 155.95L30,3.95A3,3 0,0 1,33 0.95z"
|
||||
android:fillColor="#45495D"/>
|
||||
<path
|
||||
android:pathData="M39,5.95L120,5.95A3,3 0,0 1,123 8.95L123,149.95A3,3 0,0 1,120 152.95L39,152.95A3,3 0,0 1,36 149.95L36,8.95A3,3 0,0 1,39 5.95z"
|
||||
android:fillColor="#2D3142"/>
|
||||
<path
|
||||
android:pathData="M70.89,56.24H86.25V76.72H98.64L78.57,96.79L58.5,76.72H70.89V56.24Z"
|
||||
android:fillColor="#7A85B3"/>
|
||||
<path
|
||||
android:pathData="M55.5,103h45v4h-45z"
|
||||
android:fillColor="#7A85B3"/>
|
||||
<path
|
||||
android:pathData="M61,12.95m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
|
||||
android:fillColor="#EC3E7C"
|
||||
android:fillAlpha="0.2"/>
|
||||
<path
|
||||
android:pathData="M61,12.95m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
|
||||
android:fillColor="#EC3E7C"/>
|
||||
<path
|
||||
android:pathData="M70,9.95L99,9.95A3,3 0,0 1,102 12.95L102,12.95A3,3 0,0 1,99 15.95L70,15.95A3,3 0,0 1,67 12.95L67,12.95A3,3 0,0 1,70 9.95z"
|
||||
android:fillColor="#60698E"/>
|
||||
<path
|
||||
android:pathData="M116.47,15.95L150.54,15.95A12.47,12.47 0,0 1,163.01 28.42L163.01,62.48A12.47,12.47 0,0 1,150.54 74.95L116.47,74.95A12.47,12.47 0,0 1,104 62.48L104,28.42A12.47,12.47 0,0 1,116.47 15.95z"
|
||||
android:fillColor="#45495D"/>
|
||||
<path
|
||||
android:pathData="M112.45,58.24C110.73,58.24 109.73,56.34 110.74,54.98L122.57,39.08C123.08,38.4 124.11,38.4 124.62,39.08L133.22,50.64L138.01,44.21C138.51,43.53 139.55,43.53 140.05,44.21L148.06,54.98C149.07,56.34 148.08,58.24 146.36,58.24H112.45Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M131.31,34.63C131.31,36.81 133.02,38.57 135.12,38.57C137.23,38.57 138.93,36.81 138.93,34.63C138.93,32.46 137.23,30.7 135.12,30.7C133.02,30.7 131.31,32.46 131.31,34.63Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,37 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="163dp"
|
||||
android:height="159dp"
|
||||
android:viewportWidth="163"
|
||||
android:viewportHeight="159">
|
||||
<path
|
||||
android:pathData="M33,0.95L126,0.95A3,3 0,0 1,129 3.95L129,155.95A3,3 0,0 1,126 158.95L33,158.95A3,3 0,0 1,30 155.95L30,3.95A3,3 0,0 1,33 0.95z"
|
||||
android:fillColor="#EBE7FF"/>
|
||||
<path
|
||||
android:pathData="M39,5.95L120,5.95A3,3 0,0 1,123 8.95L123,149.95A3,3 0,0 1,120 152.95L39,152.95A3,3 0,0 1,36 149.95L36,8.95A3,3 0,0 1,39 5.95z"
|
||||
android:fillColor="#E0D9FF"/>
|
||||
<path
|
||||
android:pathData="M70.89,59.24H86.25V79.72H98.64L78.57,99.79L58.5,79.72H70.89V59.24Z"
|
||||
android:fillColor="#645B8A"/>
|
||||
<path
|
||||
android:pathData="M55.5,106h45v4h-45z"
|
||||
android:fillColor="#645B8A"/>
|
||||
<path
|
||||
android:pathData="M60,12.95m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
|
||||
android:fillColor="#EC3E7C"
|
||||
android:fillAlpha="0.2"/>
|
||||
<path
|
||||
android:pathData="M60,12.95m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
|
||||
android:fillColor="#EC3E7C"/>
|
||||
<path
|
||||
android:pathData="M69,9.95L98,9.95A3,3 0,0 1,101 12.95L101,12.95A3,3 0,0 1,98 15.95L69,15.95A3,3 0,0 1,66 12.95L66,12.95A3,3 0,0 1,69 9.95z"
|
||||
android:fillColor="#EBE7FF"/>
|
||||
<path
|
||||
android:pathData="M116.47,15.95L150.54,15.95A12.47,12.47 0,0 1,163.01 28.42L163.01,62.48A12.47,12.47 0,0 1,150.54 74.95L116.47,74.95A12.47,12.47 0,0 1,104 62.48L104,28.42A12.47,12.47 0,0 1,116.47 15.95z"
|
||||
android:fillColor="#EBE7FF"/>
|
||||
<path
|
||||
android:pathData="M112.45,58.24C110.73,58.24 109.73,56.34 110.74,54.98L122.57,39.08C123.08,38.4 124.11,38.4 124.62,39.08L133.22,50.64L138.01,44.21C138.51,43.53 139.55,43.53 140.05,44.21L148.06,54.98C149.07,56.34 148.08,58.24 146.36,58.24H112.45Z"
|
||||
android:fillColor="#6D4AFF"/>
|
||||
<path
|
||||
android:pathData="M131.31,34.63C131.31,36.81 133.02,38.57 135.12,38.57C137.23,38.57 138.93,36.81 138.93,34.63C138.93,32.46 137.23,30.7 135.12,30.7C133.02,30.7 131.31,32.46 131.31,34.63Z"
|
||||
android:fillColor="#6D4AFF"/>
|
||||
</vector>
|
||||
@@ -24,5 +24,6 @@
|
||||
<item name="empty_trash_daynight" type="drawable" format="reference">@drawable/empty_trash_dark</item>
|
||||
<item name="empty_offline_daynight" type="drawable" format="reference">@drawable/empty_offline_dark</item>
|
||||
<item name="img_upsell_drive_daynight" type="drawable" format="reference">@drawable/img_upsell_drive_dark</item>
|
||||
<item name="img_update_required_daynight" type="drawable" format="reference">@drawable/img_update_required_dark</item>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
<item name="empty_trash_daynight" type="drawable" format="reference">@drawable/empty_trash_light</item>
|
||||
<item name="empty_offline_daynight" type="drawable" format="reference">@drawable/empty_offline_light</item>
|
||||
<item name="img_upsell_drive_daynight" type="drawable" format="reference">@drawable/img_upsell_drive_light</item>
|
||||
<item name="img_update_required_daynight" type="drawable" format="reference">@drawable/img_update_required_light</item>
|
||||
</resources>
|
||||
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2022-2023 Proton AG.
|
||||
* This file is part of Proton Core.
|
||||
*
|
||||
* Proton Core 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 Core 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 Core. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.drive.cryptobase.domain.usecase
|
||||
|
||||
import me.proton.core.crypto.common.context.CryptoContext
|
||||
import me.proton.core.crypto.common.pgp.SessionKey
|
||||
import me.proton.core.crypto.common.pgp.Unarmored
|
||||
import me.proton.core.drive.base.domain.util.coRunCatching
|
||||
import me.proton.core.drive.cryptobase.domain.CryptoScope
|
||||
import me.proton.core.key.domain.decryptSessionKey
|
||||
import me.proton.core.key.domain.entity.keyholder.KeyHolder
|
||||
import me.proton.core.key.domain.useKeys
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class GetSessionKey @Inject constructor(
|
||||
private val cryptoContext: CryptoContext,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
decryptKey: KeyHolder,
|
||||
message: Unarmored,
|
||||
coroutineContext: CoroutineContext = CryptoScope.EncryptAndDecrypt.coroutineContext,
|
||||
): Result<SessionKey> = coRunCatching(coroutineContext) {
|
||||
decryptKey.useKeys(cryptoContext) {
|
||||
decryptSessionKey(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -29,4 +29,8 @@ object SignatureContexts {
|
||||
value = "drive.share-member.external-invitation",
|
||||
isCritical = true
|
||||
)
|
||||
val DRIVE_SHARE_MEMBER_MEMBER = SignatureContext(
|
||||
value = "drive.share-member.member",
|
||||
isCritical = true
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -51,6 +51,7 @@ class DecryptAncestorsName @Inject constructor(
|
||||
when (link) {
|
||||
is Link.Folder -> link.copy(name = decryptedName.text)
|
||||
is Link.File -> link.copy(name = decryptedName.text)
|
||||
is Link.Album -> link.copy(name = decryptedName.text)
|
||||
}
|
||||
} else {
|
||||
link
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ class DecryptLinkName @Inject constructor(
|
||||
coroutineContext: CoroutineContext = CryptoScope.EncryptAndDecrypt.coroutineContext,
|
||||
): Result<DecryptedText> = coRunCatching(coroutineContext) {
|
||||
val link = getLink(linkId).toResult().getOrThrow()
|
||||
val email = link.nameSignatureEmail ?: link.signatureAddress
|
||||
val email = link.nameSignatureEmail ?: link.signatureEmail
|
||||
val verificationKeys = getVerificationKeys(
|
||||
link = link,
|
||||
email = email,
|
||||
|
||||
+7
-3
@@ -25,6 +25,7 @@ import me.proton.core.drive.cryptobase.domain.usecase.UnlockKey
|
||||
import me.proton.core.drive.key.domain.extension.keyHolder
|
||||
import me.proton.core.drive.key.domain.usecase.GetNodeKey
|
||||
import me.proton.core.drive.key.domain.usecase.GetVerificationKeys
|
||||
import me.proton.core.drive.link.domain.entity.Album
|
||||
import me.proton.core.drive.link.domain.entity.BaseLink
|
||||
import me.proton.core.drive.link.domain.entity.File
|
||||
import me.proton.core.drive.link.domain.entity.Folder
|
||||
@@ -40,11 +41,14 @@ class DecryptLinkXAttr @Inject constructor(
|
||||
private val getPublicKeyRing: GetPublicKeyRing,
|
||||
private val unlockKey: UnlockKey,
|
||||
) {
|
||||
suspend operator fun invoke(link: BaseLink): Result<DecryptedText> = getLinkKey(link.id).mapCatching { decryptKey ->
|
||||
suspend operator fun invoke(
|
||||
link: BaseLink,
|
||||
): Result<DecryptedText> = getLinkKey(link.id).mapCatching { decryptKey ->
|
||||
val signatureAddress = when (link) {
|
||||
is File -> link.uploadedBy
|
||||
is Folder -> link.signatureAddress
|
||||
else -> throw IllegalStateException("Link must be either file or folder and it was not")
|
||||
is Folder -> link.signatureEmail
|
||||
is Album -> link.signatureEmail
|
||||
else -> error("Link must be either file, folder or album and it was not")
|
||||
}
|
||||
val verificationKeys = getVerificationKeys(link.id, signatureAddress).getOrThrow().keyHolder
|
||||
unlockKey(decryptKey.keyHolder) { unlockedKey ->
|
||||
|
||||
+2
-2
@@ -81,13 +81,13 @@ class CreateMoveInfo @Inject constructor(
|
||||
hash = hmacSha256(newParentFolderHashKey, decryptedLinkName).getOrThrow(),// calculate with new parent,
|
||||
previousHash = link.hash,
|
||||
parentLinkId = newParentFolder.id.id,
|
||||
signatureEmail = if (link.signatureAddress.isEmpty()) {
|
||||
signatureEmail = if (link.signatureEmail.isEmpty()) {
|
||||
signatureAddress
|
||||
} else {
|
||||
null
|
||||
},
|
||||
nodePassphrase = newLinkKey.nodePassphrase,
|
||||
nodePassphraseSignature = if (link.signatureAddress.isEmpty() || link.nameSignatureEmail.isNullOrEmpty()) {
|
||||
nodePassphraseSignature = if (link.signatureEmail.isEmpty() || link.nameSignatureEmail.isNullOrEmpty()) {
|
||||
newLinkKey.nodePassphraseSignature
|
||||
} else {
|
||||
null
|
||||
|
||||
+6
-1
@@ -49,8 +49,13 @@ class CreateRenameInfo @Inject constructor(
|
||||
link: Link,
|
||||
name: String,
|
||||
mimeType: String,
|
||||
shouldValidateName: Boolean = true,
|
||||
): Result<RenameInfo> = coRunCatching {
|
||||
val linkName = validateLinkName(name).getOrThrow()
|
||||
val linkName = if (shouldValidateName) {
|
||||
validateLinkName(name).getOrThrow()
|
||||
} else {
|
||||
name
|
||||
}
|
||||
val parentFolderKey = getNodeKey(parentFolder).getOrThrow()
|
||||
val parentFolderHashKey = getNodeHashKey(parentFolder, parentFolderKey).getOrThrow()
|
||||
val signatureAddress = getSignatureAddress(link.shareId).getOrThrow()
|
||||
|
||||
+44
-16
@@ -28,8 +28,11 @@ import me.proton.core.drive.key.domain.extension.nodePassphrase
|
||||
import me.proton.core.drive.key.domain.extension.nodePassphraseSignature
|
||||
import me.proton.core.drive.key.domain.usecase.GenerateNodeKey
|
||||
import me.proton.core.drive.key.domain.usecase.GenerateShareKey
|
||||
import me.proton.core.drive.key.domain.usecase.GetAddressKeyId
|
||||
import me.proton.core.drive.key.domain.usecase.GetAddressKeys
|
||||
import me.proton.core.drive.volume.domain.entity.Volume
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeInfo
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeInfo.Companion.DEFAULT_PHOTOS_ROOT_FOLDER_NAME
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeInfo.Companion.DEFAULT_ROOT_FOLDER_NAME
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -40,25 +43,50 @@ class CreateVolumeInfo @Inject constructor(
|
||||
private val generateHashKey: GenerateHashKey,
|
||||
private val encryptText: EncryptText,
|
||||
private val getAddressKeys: GetAddressKeys,
|
||||
private val getAddressKeyId: GetAddressKeyId,
|
||||
) {
|
||||
suspend operator fun invoke(userId: UserId, signatureAddress: String): Result<VolumeInfo> =
|
||||
suspend operator fun invoke(
|
||||
userId: UserId,
|
||||
signatureAddress: String,
|
||||
type: Volume.Type,
|
||||
): Result<VolumeInfo> =
|
||||
generateShareAndFolderKey(userId, signatureAddress).mapCatching { (shareKey, folderKey) ->
|
||||
val addressId = getAddressId(userId)
|
||||
VolumeInfo(
|
||||
addressId = addressId,
|
||||
shareKey = shareKey.nodeKey,
|
||||
sharePassphrase = shareKey.nodePassphrase,
|
||||
sharePassphraseSignature = shareKey.nodePassphraseSignature,
|
||||
folderName = encryptText(
|
||||
encryptKey = shareKey.keyHolder,
|
||||
text = DEFAULT_ROOT_FOLDER_NAME,
|
||||
signKey = getAddressKeys(userId, addressId).keyHolder
|
||||
).getOrThrow(),
|
||||
folderKey = folderKey.nodeKey,
|
||||
folderPassphrase = folderKey.nodePassphrase,
|
||||
folderPassphraseSignature = folderKey.nodePassphraseSignature,
|
||||
folderHashKey = generateHashKey(folderKey.keyHolder).getOrThrow()
|
||||
)
|
||||
val addressKeyId = getAddressKeyId(userId, addressId).getOrThrow().id
|
||||
when (type) {
|
||||
Volume.Type.PHOTO -> VolumeInfo.Photo(
|
||||
addressId = addressId,
|
||||
shareKey = shareKey.nodeKey,
|
||||
sharePassphrase = shareKey.nodePassphrase,
|
||||
sharePassphraseSignature = shareKey.nodePassphraseSignature,
|
||||
folderName = encryptText(
|
||||
encryptKey = shareKey.keyHolder,
|
||||
text = DEFAULT_PHOTOS_ROOT_FOLDER_NAME,
|
||||
signKey = getAddressKeys(userId, addressId).keyHolder
|
||||
).getOrThrow(),
|
||||
folderKey = folderKey.nodeKey,
|
||||
folderPassphrase = folderKey.nodePassphrase,
|
||||
folderPassphraseSignature = folderKey.nodePassphraseSignature,
|
||||
folderHashKey = generateHashKey(folderKey.keyHolder).getOrThrow(),
|
||||
addressKeyId = addressKeyId,
|
||||
)
|
||||
else -> VolumeInfo.Regular(
|
||||
addressId = addressId,
|
||||
shareKey = shareKey.nodeKey,
|
||||
sharePassphrase = shareKey.nodePassphrase,
|
||||
sharePassphraseSignature = shareKey.nodePassphraseSignature,
|
||||
folderName = encryptText(
|
||||
encryptKey = shareKey.keyHolder,
|
||||
text = DEFAULT_ROOT_FOLDER_NAME,
|
||||
signKey = getAddressKeys(userId, addressId).keyHolder
|
||||
).getOrThrow(),
|
||||
folderKey = folderKey.nodeKey,
|
||||
folderPassphrase = folderKey.nodePassphrase,
|
||||
folderPassphraseSignature = folderKey.nodePassphraseSignature,
|
||||
folderHashKey = generateHashKey(folderKey.keyHolder).getOrThrow(),
|
||||
addressKeyId = addressKeyId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun generateShareAndFolderKey(userId: UserId, signatureAddress: String): Result<Pair<Key.Node, Key.Node>> =
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Core.
|
||||
*
|
||||
* Proton Core 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 Core 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 Core. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.drive.db.test
|
||||
|
||||
import me.proton.core.drive.photo.data.db.entity.AlbumListingEntity
|
||||
|
||||
suspend fun FolderContext.albumListings(
|
||||
vararg albumListings: AlbumListingEntity,
|
||||
) {
|
||||
db.albumListingDao.insertOrUpdate(*albumListings)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton AG.
|
||||
* This file is part of Proton Core.
|
||||
*
|
||||
* Proton Core 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 Core 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 Core. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.drive.db.test
|
||||
|
||||
import me.proton.core.drive.base.domain.entity.Permissions
|
||||
import me.proton.core.drive.share.user.data.db.entity.UserInvitationDetailsEntity
|
||||
import me.proton.core.drive.share.user.data.db.entity.UserInvitationIdEntity
|
||||
|
||||
suspend fun ShareContext.userInvitation(id: String) {
|
||||
userInvitationId(NullableUserInvitationIdEntity(id))
|
||||
userInvitationDetails(NullableUserInvitationDetailsEntity(id))
|
||||
}
|
||||
|
||||
suspend fun ShareContext.userInvitationId(userInvitationIdEntity: UserInvitationIdEntity) {
|
||||
db.userInvitationIdDao.insertOrIgnore(userInvitationIdEntity)
|
||||
}
|
||||
|
||||
|
||||
suspend fun ShareContext.userInvitationDetails(userInvitationDetailsEntity: UserInvitationDetailsEntity) {
|
||||
db.userInvitationDetailsDao.insertOrIgnore(userInvitationDetailsEntity)
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun ShareContext.NullableUserInvitationIdEntity(
|
||||
id: String,
|
||||
) = UserInvitationIdEntity(
|
||||
id = id,
|
||||
userId = user.userId,
|
||||
volumeId = volume.id,
|
||||
shareId = share.id,
|
||||
)
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun ShareContext.NullableUserInvitationDetailsEntity(
|
||||
id: String,
|
||||
) = UserInvitationDetailsEntity(
|
||||
id = id,
|
||||
userId = user.userId,
|
||||
volumeId = volume.id,
|
||||
shareId = share.id,
|
||||
inviterEmail = "inviterEmail",
|
||||
inviteeEmail = "inviteeEmail",
|
||||
permissions = Permissions.viewer.value,
|
||||
keyPacket = "user-invitation-key-packet",
|
||||
keyPacketSignature = "user-invitation-key-packet-signature",
|
||||
createTime = 0L,
|
||||
passphrase = "passphrase",
|
||||
shareKey = "shareKey",
|
||||
creatorEmail = "creatorEmail",
|
||||
type = 0L,
|
||||
linkId = "linkId",
|
||||
name = "name",
|
||||
mimeType = null,
|
||||
)
|
||||
@@ -22,6 +22,7 @@ package me.proton.core.drive.db.test
|
||||
import me.proton.android.drive.db.DriveDatabase
|
||||
import me.proton.core.account.data.entity.AccountEntity
|
||||
import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import me.proton.core.drive.volume.data.api.entity.VolumeDto
|
||||
import me.proton.core.drive.volume.data.db.VolumeEntity
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeId
|
||||
import me.proton.core.user.data.entity.UserEntity
|
||||
@@ -34,6 +35,7 @@ data class VolumeContext(
|
||||
) : BaseContext()
|
||||
|
||||
val volumeId = VolumeId("volume-id")
|
||||
val photoVolumeId = VolumeId("photo-volume-id")
|
||||
val mainShareId = ShareId(userId, "main-share-id")
|
||||
|
||||
suspend fun UserContext.volume(
|
||||
@@ -52,14 +54,46 @@ suspend fun <T> UserContext.volume(
|
||||
return VolumeContext(db, user, account, volume).block()
|
||||
}
|
||||
|
||||
suspend fun <T> UserContext.photoVolume(
|
||||
volume: VolumeEntity = NullablePhotoVolumeEntity(
|
||||
id = photoVolumeId.id,
|
||||
),
|
||||
block: suspend VolumeContext.() -> T,
|
||||
): T {
|
||||
db.volumeDao.insertOrUpdate(volume)
|
||||
return VolumeContext(db, user, account, volume).block()
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun NullableVolumeEntity(id: String = volumeId.id, state: Long = 1, creationTime: Long = 0) =
|
||||
fun NullableVolumeEntity(
|
||||
id: String = volumeId.id,
|
||||
state: Long = 1,
|
||||
createTime: Long = 0,
|
||||
) =
|
||||
VolumeEntity(
|
||||
id = id,
|
||||
userId = userId,
|
||||
shareId = mainShareId.id,
|
||||
creationTime = creationTime,
|
||||
maxSpace = 0,
|
||||
linkId = mainRootId.id,
|
||||
createTime = createTime,
|
||||
usedSpace = 0,
|
||||
state = state,
|
||||
type = VolumeDto.TYPE_REGULAR,
|
||||
)
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun NullablePhotoVolumeEntity(
|
||||
id: String = photoVolumeId.id,
|
||||
state: Long = 1,
|
||||
createTime: Long = 0,
|
||||
) =
|
||||
VolumeEntity(
|
||||
id = id,
|
||||
userId = userId,
|
||||
shareId = photoShareId.id,
|
||||
linkId = photoRootId.id,
|
||||
createTime = createTime,
|
||||
usedSpace = 0,
|
||||
state = state,
|
||||
type = VolumeDto.TYPE_PHOTO,
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user