From 2c8d2dfeaf6ef6cfc565bb48141b11073ff11968 Mon Sep 17 00:00:00 2001 From: Anatoly Rosencrantz Date: Tue, 4 Apr 2023 13:23:14 +0300 Subject: [PATCH] 1.1.0 --- .gitlab-ci.yml | 29 +- .idea/scopes/proton_core.xml | 2 +- app-lock/build.gradle.kts | 23 + app-lock/data/build.gradle.kts | 32 + app-lock/data/src/main/AndroidManifest.xml | 20 + .../drive/lock/data/crypto/KeyStore.kt | 138 + .../lock/data/crypto/KeyStoreSecretKey.kt | 62 + .../drive/lock/data/crypto/PgpSecretKey.kt | 53 + .../drive/lock/data/db/AppLockDatabase.kt | 31 + .../drive/lock/data/db/dao/AppLockDao.kt | 47 + .../lock/data/db/dao/AutoLockDurationDao.kt | 38 + .../lock/data/db/dao/EnableAppLockDao.kt | 34 + .../android/drive/lock/data/db/dao/LockDao.kt | 42 + .../lock/data/db/entity/AppLockEntity.kt | 34 + .../data/db/entity/AutoLockDurationEntity.kt | 34 + .../data/db/entity/EnableAppLockEntity.kt | 34 + .../drive/lock/data/db/entity/LockEntity.kt | 50 + .../drive/lock/data/di/AppLockBindModule.kt | 46 + .../drive/lock/data/di/AppLockModule.kt | 114 + .../drive/lock/data/extension/AppLock.kt | 26 + .../lock/data/extension/AppLockEntity.kt | 26 + .../drive/lock/data/extension/LockEntity.kt | 28 + .../drive/lock/data/extension/LockKey.kt | 28 + .../drive/lock/data/extension/LockState.kt | 34 + .../drive/lock/data/lock/CryptoSystemLock.kt | 109 + .../drive/lock/data/lock/SystemLock.kt | 95 + .../lock/data/manager/AppLockManagerImpl.kt | 78 + .../lock/data/manager/AutoLockManagerImpl.kt | 54 + .../data/provider/BiometricPromptProvider.kt | 33 + .../provider/BiometricPromptProviderImpl.kt | 145 + .../data/repository/AppLockRepositoryImpl.kt | 83 + .../lock/data/usecase/BuildAppKeyImpl.kt | 41 + .../lock/data/usecase/GeneratePgpSecretKey.kt | 47 + .../drive/lock/data/usecase/GetAppLockImpl.kt | 36 + .../drive/lock/data/worker/AppLockWorker.kt | 58 + app-lock/domain/build.gradle.kts | 29 + app-lock/domain/src/main/AndroidManifest.xml | 20 + .../drive/lock/domain/entity/AppLock.kt | 23 + .../drive/lock/domain/entity/AppLockType.kt | 22 + .../drive/lock/domain/entity/LockKey.kt | 44 + .../drive/lock/domain/entity/SecretKey.kt | 44 + .../lock/domain/exception/LockException.kt | 26 + .../android/drive/lock/domain/lock/Lock.kt | 56 + .../lock/domain/manager/AppLockManager.kt | 33 + .../lock/domain/manager/AutoLockManager.kt | 24 + .../domain/repository/AppLockRepository.kt | 44 + .../drive/lock/domain/usecase/BuildAppKey.kt | 25 + .../lock/domain/usecase/DisableAppLock.kt | 40 + .../lock/domain/usecase/EnableAppLock.kt | 49 + .../lock/domain/usecase/GenerateSecretKey.kt | 24 + .../drive/lock/domain/usecase/GetAppLock.kt | 26 + .../domain/usecase/GetAutoLockDuration.kt | 40 + .../drive/lock/domain/usecase/GetLockState.kt | 31 + .../usecase/HasEnableAppLockTimestamp.kt | 29 + .../drive/lock/domain/usecase/LockApp.kt | 31 + .../drive/lock/domain/usecase/UnlockApp.kt | 41 + .../domain/usecase/UpdateAutoLockDuration.kt | 37 + app-lock/presentation/build.gradle.kts | 30 + .../presentation/src/main/AndroidManifest.xml | 20 + .../lock/presentation/component/AppLock.kt | 65 + .../lock/presentation/component/Unlock.kt | 251 + .../presentation/extension/LockException.kt | 31 + .../presentation/viewevent/UnlockViewEvent.kt | 25 + .../presentation/viewmodel/UnlockViewModel.kt | 73 + .../drawable-nodpi/welcome_header_dark.webp | Bin 0 -> 204420 bytes .../welcome_header_dark_land.webp | Bin 0 -> 420748 bytes .../drawable-nodpi/welcome_header_light.webp | Bin 0 -> 6038 bytes .../welcome_header_light_land.webp | Bin 0 -> 70976 bytes .../main/res/values-land-night/drawable.xml | 21 + .../src/main/res/values-land/drawable.xml | 23 + .../src/main/res/values-night/drawable.xml | 21 + .../src/main/res/values/drawable.xml | 21 + app-lock/src/main/AndroidManifest.xml | 20 + app/build.gradle.kts | 5 +- .../1.json | 3231 +------------ app/src/dynamic/AndroidManifest.xml | 28 + app/src/main/AndroidManifest.xml | 33 +- .../kotlin/me/proton/android/drive/App.kt | 8 +- .../me/proton/android/drive/db/AppDatabase.kt | 244 +- .../android/drive/di/AppDatabaseModule.kt | 139 +- .../android/drive/di/ApplicationModule.kt | 16 +- .../android/drive/di/DriveDatabaseModule.kt | 183 + .../android/drive/extension/DriveException.kt | 15 +- .../drive/initializer/AutoLockInitializer.kt | 67 + .../DocumentsProviderInitializer.kt | 61 + .../drive/initializer/MainInitializer.kt | 54 + .../proton/android/drive/log/DriveLogger.kt | 38 +- .../proton/android/drive/ui/MainActivity.kt | 8 +- .../drive/ui/dialog/AutoLockDurations.kt | 122 + .../drive/ui/dialog/SystemAccessDialog.kt | 73 + .../drive/ui/navigation/AppNavGraph.kt | 82 +- .../android/drive/ui/navigation/Screen.kt | 16 + .../drive/ui/screen/AppAccessScreen.kt | 160 + .../android/drive/ui/screen/FilesScreen.kt | 21 +- .../android/drive/ui/screen/MoveToFolder.kt | 3 +- .../android/drive/ui/screen/OfflineScreen.kt | 2 +- .../android/drive/ui/screen/PreviewScreen.kt | 1 - .../android/drive/ui/screen/SettingsScreen.kt | 20 +- .../drive/ui/viewevent/AppAccessViewEvent.kt | 24 + .../viewevent/AutoLockDurationsViewEvent.kt | 25 + .../viewevent/SystemAccessDialogViewEvent.kt | 23 + .../drive/ui/viewmodel/AppAccessViewModel.kt | 132 + .../viewmodel/AutoLockDurationsViewModel.kt | 70 + .../drive/ui/viewmodel/BugReportViewModel.kt | 12 +- .../drive/ui/viewmodel/FilesViewModel.kt | 60 +- .../ui/viewmodel/MoveToFolderViewModel.kt | 4 +- .../drive/ui/viewmodel/OfflineViewModel.kt | 42 +- .../drive/ui/viewmodel/PreviewViewModel.kt | 57 +- .../drive/ui/viewmodel/SettingsViewModel.kt | 43 +- .../drive/ui/viewmodel/SharedViewModel.kt | 27 +- .../viewmodel/SystemAccessDialogViewModel.kt | 62 + .../drive/ui/viewstate/AppAccessViewState.kt | 32 + .../viewstate/AutoLockDurationsViewState.kt | 27 + .../android/drive/usecase/CleanUpAccount.kt | 3 + .../usecase/GetDocumentsProviderRootsImpl.kt | 35 + .../drive/ui/robot/CreateFolderRobot.kt | 34 +- .../drive/ui/robot/FileFolderOptionsRobot.kt | 24 +- .../android/drive/ui/robot/FilesTabRobot.kt | 79 +- .../android/drive/ui/robot/HomeRobot.kt | 18 +- .../drive/ui/robot/MoveToFolderRobot.kt | 37 +- .../ui/robot/ParentFolderOptionsRobot.kt | 11 +- .../android/drive/ui/robot/PreviewRobot.kt | 11 +- .../android/drive/ui/robot/RenameRobot.kt | 26 +- .../me/proton/android/drive/ui/robot/Robot.kt | 180 +- .../android/drive/ui/robot/SharedTabRobot.kt | 10 +- .../android/drive/ui/robot/WelcomeRobot.kt | 38 +- .../android/drive/ui/rules/LogoutAllRule.kt | 6 +- .../proton/android/drive/ui/test/BaseTest.kt | 31 +- .../test/flow/CreatingFolderFlowErrorTest.kt | 13 +- .../flow/CreatingFolderFlowSuccessTest.kt | 10 +- .../test/flow/RenamingFileSuccessFlowTest.kt | 2 +- .../ui/test/flow/RenamingFlowSuccessTest.kt | 4 +- .../test/flow/RenamingFolderFlowErrorTest.kt | 35 +- .../android/drive/ui/toolkits/Helpers.kt | 47 + buildSrc/src/main/kotlin/Config.kt | 2 +- buildSrc/src/main/kotlin/DeleteTestPlugin.kt | 11 +- buildSrc/src/main/kotlin/DriveModule.kt | 13 +- drive/base/data-test/build.gradle.kts | 28 + .../data-test/src/main/AndroidManifest.xml | 20 + .../data/test/manager/StubbedWorkManager.kt | 73 + .../test/manager/StubbedWorkManagerTest.kt | 131 + .../base/data/api/RunCatchingApiException.kt | 4 +- .../proton/core/drive/base/data/db/Column.kt | 2 + .../core/drive/base/data/db/paging/Flow.kt | 123 +- .../core/drive/base/data/di/BaseBindModule.kt | 12 +- .../base/data/usecase/GetMemoryInfoImpl.kt | 41 + .../drive/base/data/db/paging/FlowTest.kt | 252 + drive/base/domain/build.gradle.kts | 8 +- .../drive/base/domain/entity/MemoryInfo.kt | 23 + .../base/domain/extension/ApiException.kt | 37 + .../core/drive/base/domain/extension/Bytes.kt | 1 + .../domain/provider/ConfigurationProvider.kt | 7 + .../drive/base/domain/repository/Fetcher.kt | 6 +- .../base/domain/usecase/GetMemoryInfo.kt | 24 + .../base/domain/repository/FetcherTest.kt | 94 + .../component/OutlinedTextFieldWithError.kt | 63 - .../presentation/component/ProtonListItem.kt | 6 +- .../Composable.kt} | 25 +- .../base/presentation/extension/Modifier.kt | 40 + .../drawable/ic_checkmark_circle_filled.xml | 32 + .../main/res/drawable/ic_proton_lock_open.xml | 13 + .../src/main/res/values/strings.xml | 20 + .../data/repository/BlockRepositoryImpl.kt | 22 +- drive/crypto-base/domain/build.gradle.kts | 6 +- .../domain/extension/UserAddressRepository.kt | 16 +- .../domain/usecase/DecryptNestedPrivateKey.kt | 7 +- .../domain/usecase/GenerateSrpForShareUrl.kt | 8 +- .../share/CreateShareUrlCustomPasswordInfo.kt | 2 +- .../usecase/share/CreateShareUrlInfo.kt | 2 +- .../usecase/upload/EncryptUploadBlocks.kt | 46 +- drive/db/build.gradle.kts | 60 + .../1.json | 3233 +++++++++++++ .../10.json | 0 .../11.json | 0 .../12.json | 0 .../13.json | 0 .../14.json | 0 .../15.json | 0 .../16.json | 0 .../17.json | 0 .../18.json | 0 .../19.json | 0 .../2.json | 0 .../20.json | 0 .../21.json | 0 .../22.json | 4237 ++++++++++++++++ .../23.json | 4244 +++++++++++++++++ .../3.json | 0 .../4.json | 0 .../5.json | 0 .../6.json | 0 .../7.json | 0 .../8.json | 0 .../9.json | 0 drive/db/src/main/AndroidManifest.xml | 19 + .../proton/android/drive/db/DriveDatabase.kt | 273 ++ .../drive/db/DriveDatabaseMigrations.kt | 16 +- .../usecase/GetDocumentsProviderRoots.kt | 12 +- .../domain/usecase/GetDecryptedDriveLinks.kt | 6 + .../list/domain/usecase/GetDriveLinks.kt | 4 + .../usecase/GetFolderChildrenDriveLinks.kt | 21 + .../domain/usecase/GetPagedDriveLinksList.kt | 11 +- drive/drivelink-paged/domain/build.gradle.kts | 2 +- .../domain/usecase/GetObservablePageSize.kt | 43 + .../domain/usecase/GetPagedDriveLinks.kt | 30 + .../data/db/dao/DriveLinkSelectionDao.kt | 3 +- .../DriveLinkSelectionRepositoryImpl.kt | 34 + .../DriveLinkSelectionRepository.kt | 9 + .../selection/domain/usecase/SelectAll.kt | 46 + .../sorting/domain/sorter/LocaleNameSorter.kt | 51 + .../drivelink/sorting/domain/sorter/Sorter.kt | 2 +- .../drivelink/sorting/domain/sorter/Files.kt | 57 + .../domain/sorter/LocaleNameSorterTest.kt | 129 + .../sorting/domain/sorter/SorterTest.kt | 32 +- .../drivelink/data/db/dao/DriveLinkDao.kt | 27 +- .../repository/DriveLinkRepositoryImpl.kt | 7 + .../domain/repository/DriveLinkRepository.kt | 4 + .../domain/usecase/GetDriveLinksCount.kt | 30 + drive/event-manager/data/build.gradle.kts | 1 + drive/file/base/domain/build.gradle.kts | 1 - .../drive/file/base/domain/entity/XAttr.kt | 4 +- .../file/base/domain/usecase/CreateXAttr.kt | 4 +- .../files/presentation/component/Files.kt | 22 +- .../component/files/CircleSelection.kt | 50 + .../component/files/FilesGridItem.kt | 130 +- .../presentation/component/files/FilesList.kt | 13 +- .../component/files/FilesListItem.kt | 161 +- .../presentation/component/files/Previews.kt | 74 + .../create/presentation/CreateFolder.kt | 40 +- .../presentation/CreateFolderViewModel.kt | 5 +- .../presentation/CreateFolderViewState.kt | 2 +- drive/link-trash/data-test/build.gradle.kts | 30 + .../data-test/src/main/AndroidManifest.xml | 20 + .../data/test/di/TestLinkTrashBindModule.kt | 37 + .../repository/StubbedLinkTrashRepository.kt | 132 + .../StubbedLinkTrashRepositoryTest.kt | 181 + .../linkupload/data/db/dao/LinkUploadDao.kt | 6 + .../data/db/entity/LinkUploadEntity.kt | 3 + .../data/extension/LinkUploadEntity.kt | 16 +- .../repository/LinkUploadRepositoryImpl.kt | 7 + .../linkupload/domain/entity/UploadDigests.kt | 21 + .../domain/entity/UploadFileLink.kt | 1 + .../domain/repository/LinkUploadRepository.kt | 3 + .../domain/usecase/UpdateDigests.kt | 35 + drive/link/data-test/build.gradle.kts | 25 + .../data-test/src/main/AndroidManifest.xml | 20 + .../drive/link/data/test/NullableBaseLink.kt | 190 + drive/link/data/build.gradle.kts | 1 + .../link/data/db/LinkWithPropertiesTest.kt | 20 +- .../core/drive/link/data/db/TestDatabase.kt | 61 - .../data/api/extension/GetLinkResponses.kt | 4 +- .../presentation/NavigationDrawer.kt | 1 + .../presentation/component/ImagePreview.kt | 58 +- .../presentation/component/MediaPreview.kt | 3 +- .../presentation/component/PdfPreview.kt | 195 +- .../preview/presentation/component/Preview.kt | 87 +- .../component/TransformationState.kt | 118 + .../component/event/PreviewViewEvent.kt | 1 - .../component/state/PreviewViewState.kt | 4 - .../drive/settings/presentation/Settings.kt | 33 +- .../presentation/component/SettingsEntry.kt | 2 +- .../presentation/event/SettingsViewEvent.kt | 2 + .../presentation/extension/Duration.kt | 32 + .../presentation/state/SettingsViewState.kt | 5 +- .../settings/src/main/res/values/strings.xml | 23 +- drive/share/data-test/build.gradle.kts | 29 + .../data-test/src/main/AndroidManifest.xml | 20 + .../drive/share/data/test/di/TestModule.kt | 36 + .../share/data/test/nullable/NullableShare.kt | 68 + .../test/repository/StubbedShareRepository.kt | 135 + .../repository/StubbedShareRepositoryTest.kt | 129 + .../thumbnail/data/di/ThumbnailModule.kt | 10 + .../data/provider/AudioThumbnailProvider.kt | 48 + .../data/provider/FileThumbnailProvider.kt | 76 + .../data/provider/VideoThumbnailProvider.kt | 53 + drive/trash/data-test/build.gradle.kts | 28 + .../data-test/src/main/AndroidManifest.xml | 20 + .../data/test/di/TestDriveTrashModule.kt | 36 + .../data/test/manager/StubbedTrashManager.kt | 84 + drive/trash/domain/build.gradle.kts | 11 +- .../domain/src/main/res/values/strings.xml | 4 +- .../TrashExtraActionProviderTest.kt | 168 + .../trash/domain/usecase/DefaultValues.kt | 30 + .../domain/usecase/DeleteFromTrashTest.kt | 73 + .../trash/domain/usecase/EmptyTrashTest.kt | 71 + .../trash/domain/usecase/GetEmptyTrashTest.kt | 95 + .../domain/usecase/RestoreFromTrashTest.kt | 73 + .../trash/domain/usecase/SendToTrashTest.kt | 97 + .../src/test/resources/robolectric.properties | 19 + drive/trash/presentation/build.gradle.kts | 26 + .../presentation/src/main/AndroidManifest.xml | 20 + .../src/main/res/values/strings.xml | 22 + .../data/worker/GetBlocksUploadUrlWorker.kt | 15 +- .../upload/domain/extension/InputStream.kt | 20 + .../usecase/SplitFileToBlocksAndEncrypt.kt | 47 +- .../upload/domain/usecase/UpdateRevision.kt | 3 +- .../domain/extension/InputStreamKtTest.kt | 49 + .../user/presentation/storage/Storage.kt | 2 +- gradle/libs.versions.toml | 39 +- 299 files changed, 21811 insertions(+), 4638 deletions(-) create mode 100644 app-lock/build.gradle.kts create mode 100644 app-lock/data/build.gradle.kts create mode 100644 app-lock/data/src/main/AndroidManifest.xml create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStore.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStoreSecretKey.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/PgpSecretKey.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/AppLockDatabase.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AppLockDao.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AutoLockDurationDao.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/EnableAppLockDao.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/LockDao.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AppLockEntity.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AutoLockDurationEntity.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/EnableAppLockEntity.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/LockEntity.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockBindModule.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockModule.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLock.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLockEntity.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockEntity.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockKey.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockState.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/CryptoSystemLock.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/SystemLock.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AppLockManagerImpl.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AutoLockManagerImpl.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProvider.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProviderImpl.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/repository/AppLockRepositoryImpl.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/BuildAppKeyImpl.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GeneratePgpSecretKey.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GetAppLockImpl.kt create mode 100644 app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/worker/AppLockWorker.kt create mode 100644 app-lock/domain/build.gradle.kts create mode 100644 app-lock/domain/src/main/AndroidManifest.xml create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLock.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLockType.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/LockKey.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/SecretKey.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/exception/LockException.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/lock/Lock.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AppLockManager.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AutoLockManager.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/repository/AppLockRepository.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/BuildAppKey.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/DisableAppLock.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/EnableAppLock.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GenerateSecretKey.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAppLock.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAutoLockDuration.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetLockState.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/HasEnableAppLockTimestamp.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/LockApp.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UnlockApp.kt create mode 100644 app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UpdateAutoLockDuration.kt create mode 100644 app-lock/presentation/build.gradle.kts create mode 100644 app-lock/presentation/src/main/AndroidManifest.xml create mode 100644 app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/AppLock.kt create mode 100644 app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/Unlock.kt create mode 100644 app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/extension/LockException.kt create mode 100644 app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewevent/UnlockViewEvent.kt create mode 100644 app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewmodel/UnlockViewModel.kt create mode 100644 app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark.webp create mode 100644 app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark_land.webp create mode 100644 app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_light.webp create mode 100644 app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_light_land.webp create mode 100644 app-lock/presentation/src/main/res/values-land-night/drawable.xml create mode 100644 app-lock/presentation/src/main/res/values-land/drawable.xml create mode 100644 app-lock/presentation/src/main/res/values-night/drawable.xml create mode 100644 app-lock/presentation/src/main/res/values/drawable.xml create mode 100644 app-lock/src/main/AndroidManifest.xml create mode 100644 app/src/dynamic/AndroidManifest.xml create mode 100644 app/src/main/kotlin/me/proton/android/drive/di/DriveDatabaseModule.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/initializer/AutoLockInitializer.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/initializer/DocumentsProviderInitializer.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/initializer/MainInitializer.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/ui/dialog/AutoLockDurations.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/ui/dialog/SystemAccessDialog.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/ui/screen/AppAccessScreen.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AppAccessViewEvent.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AutoLockDurationsViewEvent.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/ui/viewevent/SystemAccessDialogViewEvent.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AppAccessViewModel.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AutoLockDurationsViewModel.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SystemAccessDialogViewModel.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AppAccessViewState.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AutoLockDurationsViewState.kt create mode 100644 app/src/main/kotlin/me/proton/android/drive/usecase/GetDocumentsProviderRootsImpl.kt create mode 100644 drive/base/data-test/build.gradle.kts create mode 100644 drive/base/data-test/src/main/AndroidManifest.xml create mode 100644 drive/base/data-test/src/main/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManager.kt create mode 100644 drive/base/data-test/src/test/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManagerTest.kt create mode 100644 drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/usecase/GetMemoryInfoImpl.kt create mode 100644 drive/base/data/src/test/kotlin/me/proton/core/drive/base/data/db/paging/FlowTest.kt create mode 100644 drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/entity/MemoryInfo.kt create mode 100644 drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/ApiException.kt create mode 100644 drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/usecase/GetMemoryInfo.kt create mode 100644 drive/base/domain/src/test/kotlin/me/proton/core/drive/base/domain/repository/FetcherTest.kt rename drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/{component/checkbox/CheckboxDefaults.kt => extension/Composable.kt} (53%) create mode 100644 drive/base/presentation/src/main/res/drawable/ic_checkmark_circle_filled.xml create mode 100644 drive/base/presentation/src/main/res/drawable/ic_proton_lock_open.xml create mode 100644 drive/db/build.gradle.kts create mode 100644 drive/db/schemas/me.proton.android.drive.db.DriveDatabase/1.json rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/10.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/11.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/12.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/13.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/14.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/15.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/16.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/17.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/18.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/19.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/2.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/20.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/21.json (100%) create mode 100644 drive/db/schemas/me.proton.android.drive.db.DriveDatabase/22.json create mode 100644 drive/db/schemas/me.proton.android.drive.db.DriveDatabase/23.json rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/3.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/4.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/5.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/6.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/7.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/8.json (100%) rename {app/schemas/me.proton.android.drive.db.AppDatabase => drive/db/schemas/me.proton.android.drive.db.DriveDatabase}/9.json (100%) create mode 100644 drive/db/src/main/AndroidManifest.xml create mode 100644 drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt rename app/src/main/kotlin/me/proton/android/drive/db/AppDatabaseMigrations.kt => drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabaseMigrations.kt (87%) create mode 100644 drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetObservablePageSize.kt create mode 100644 drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/usecase/SelectAll.kt create mode 100644 drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorter.kt create mode 100644 drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Files.kt create mode 100644 drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorterTest.kt create mode 100644 drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/usecase/GetDriveLinksCount.kt create mode 100644 drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/CircleSelection.kt create mode 100644 drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/Previews.kt create mode 100644 drive/link-trash/data-test/build.gradle.kts create mode 100644 drive/link-trash/data-test/src/main/AndroidManifest.xml create mode 100644 drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/di/TestLinkTrashBindModule.kt create mode 100644 drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepository.kt create mode 100644 drive/link-trash/data-test/src/test/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepositoryTest.kt create mode 100644 drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadDigests.kt create mode 100644 drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateDigests.kt create mode 100644 drive/link/data-test/build.gradle.kts create mode 100644 drive/link/data-test/src/main/AndroidManifest.xml create mode 100644 drive/link/data-test/src/main/kotlin/me/proton/core/drive/link/data/test/NullableBaseLink.kt delete mode 100644 drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/TestDatabase.kt create mode 100644 drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/TransformationState.kt create mode 100644 drive/settings/src/main/java/me/proton/core/drive/settings/presentation/extension/Duration.kt create mode 100644 drive/share/data-test/build.gradle.kts create mode 100644 drive/share/data-test/src/main/AndroidManifest.xml create mode 100644 drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/di/TestModule.kt create mode 100644 drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/nullable/NullableShare.kt create mode 100644 drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/repository/StubbedShareRepository.kt create mode 100644 drive/share/data-test/src/test/kotlin/me/proton/core/drive/share/data/test/repository/StubbedShareRepositoryTest.kt create mode 100644 drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/AudioThumbnailProvider.kt create mode 100644 drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/FileThumbnailProvider.kt create mode 100644 drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/VideoThumbnailProvider.kt create mode 100644 drive/trash/data-test/build.gradle.kts create mode 100644 drive/trash/data-test/src/main/AndroidManifest.xml create mode 100644 drive/trash/data-test/src/main/kotlin/me/proton/core/drive/trash/data/test/di/TestDriveTrashModule.kt create mode 100644 drive/trash/data-test/src/main/kotlin/me/proton/core/drive/trash/data/test/manager/StubbedTrashManager.kt create mode 100644 drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/notification/TrashExtraActionProviderTest.kt create mode 100644 drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/DefaultValues.kt create mode 100644 drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/DeleteFromTrashTest.kt create mode 100644 drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/EmptyTrashTest.kt create mode 100644 drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/GetEmptyTrashTest.kt create mode 100644 drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/RestoreFromTrashTest.kt create mode 100644 drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/SendToTrashTest.kt create mode 100644 drive/trash/domain/src/test/resources/robolectric.properties create mode 100644 drive/trash/presentation/build.gradle.kts create mode 100644 drive/trash/presentation/src/main/AndroidManifest.xml create mode 100644 drive/trash/presentation/src/main/res/values/strings.xml create mode 100644 drive/upload/domain/src/test/kotlin/me/proton/core/drive/upload/domain/extension/InputStreamKtTest.kt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b96c1f53..e3e3af3f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,5 @@ default: - image: ${CI_REGISTRY}/android/shared/docker-android:v1.0.0 + image: ${CI_REGISTRY}/android/shared/docker-android:v1.0.3 variables: # Use fastzip to improve cache times @@ -111,25 +111,25 @@ deploy:review: build dev debug: extends: [.build] needs: - - job: "detekt analysis" - job: "prepare-environment" + - job: "prepare-build" script: - ./gradlew assembleDevDebug artifacts: paths: - ./app/**/*.apk - rules: - - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH - - if: $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_REF_NAME =~ /^test/ - when: manual - allow_failure: true build dynamic debug: extends: [.build] + needs: + - job: "prepare-environment" + - job: "prepare-build" script: - export $(cat deploy.env) - echo HOST="$DYNAMIC_DOMAIN" >> private.properties - - ./gradlew assembleDynamicDebug assembleDynamicDebugAndroidTest assembleDebugAndroidTest + - ./gradlew assembleDynamicDebug --max-workers=4 + - ./gradlew assembleDynamicDebugAndroidTest --max-workers=4 + - ./gradlew assembleDebugAndroidTest --max-workers=4 rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_REF_NAME =~ /^test/ - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH @@ -253,6 +253,7 @@ upload to firebase: --platform android --job-name $CI_JOB_NAME --slack-channel drive-android-ci-reports + --push-metrics rules: # allow failure so non-run tests don't block pipeline - allow_failure: true @@ -380,13 +381,11 @@ publish to firebase app distribution: - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH startReview: - rules: - - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH - variables: - PRODUCT_FLAVOR: "dev" - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - variables: - PRODUCT_FLAVOR: "dynamic" + needs: + - job: "prepare-build" + - job: "build dev debug" + variables: + PRODUCT_FLAVOR: "dev" before_script: - if [[ -f /load-env.sh ]]; then source /load-env.sh; fi - export REVIEW_APP_ARTIFACT_PATH="app/build/outputs/apk/$PRODUCT_FLAVOR/debug/"${ARCHIVES_BASE_NAME}-${PRODUCT_FLAVOR}-debug.apk diff --git a/.idea/scopes/proton_core.xml b/.idea/scopes/proton_core.xml index f78e65cf..6b9c9132 100644 --- a/.idea/scopes/proton_core.xml +++ b/.idea/scopes/proton_core.xml @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/app-lock/build.gradle.kts b/app-lock/build.gradle.kts new file mode 100644 index 00000000..5f3ee6cd --- /dev/null +++ b/app-lock/build.gradle.kts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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 . + */ + +plugins { + id("com.android.library") +} + +driveModule(includeSubmodules = true) diff --git a/app-lock/data/build.gradle.kts b/app-lock/data/build.gradle.kts new file mode 100644 index 00000000..529fc862 --- /dev/null +++ b/app-lock/data/build.gradle.kts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 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 . + */ + +plugins { + id("com.android.library") +} + +driveModule( + hilt = true, + room = true, + serialization = true, + workManager = true, +) { + api(project(":app-lock:domain")) + api(project(":drive:base")) + api(libs.androidx.biometric) +} diff --git a/app-lock/data/src/main/AndroidManifest.xml b/app-lock/data/src/main/AndroidManifest.xml new file mode 100644 index 00000000..5acdff14 --- /dev/null +++ b/app-lock/data/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStore.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStore.kt new file mode 100644 index 00000000..886cc50d --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStore.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.crypto + +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import me.proton.core.drive.base.domain.log.LogTag +import me.proton.core.util.kotlin.CoreLogger +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec + +object Config { + const val KEY_STORE_TYPE = "AndroidKeyStore" + const val DEFAULT_CIPHER_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + const val DEFAULT_CIPHER_BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM + const val DEFAULT_CIPHER_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE + const val DEFAULT_CIPHER_GCM_TAG_LENGTH = 128 + const val DEFAULT_CIPHER_IV_BYTES = 12 + const val DEFAULT_KEY_SIZE = 256 + const val DEFAULT_USER_AUTHENTICATION_REQUIRED = true +} + +data class SecretKeyProperties( + val keyAlias: String, + val keyStoreType: String = Config.KEY_STORE_TYPE, + val cipherAlgorithm: String = Config.DEFAULT_CIPHER_ALGORITHM, + val cipherBlockMode: String = Config.DEFAULT_CIPHER_BLOCK_MODE, + val cipherPadding: String = Config.DEFAULT_CIPHER_PADDING, + val cipherKeySize: Int = Config.DEFAULT_KEY_SIZE, + val userAuthenticationRequired: Boolean = Config.DEFAULT_USER_AUTHENTICATION_REQUIRED, +) + +val SecretKeyProperties.transformation: String get() = "$cipherAlgorithm/$cipherBlockMode/$cipherPadding" + +fun SecretKeyProperties.getOrCreateSecretKey( + invalidateKeyByBiometricEnrollment: Boolean = true, +): SecretKey { + val keyStore = KeyStore.getInstance(keyStoreType) + keyStore.load(null) + return if (keyStore.containsAlias(keyAlias)) { + keyStore.getKey(keyAlias, null) as SecretKey + } else { + KeyGenerator.getInstance( + cipherAlgorithm, + keyStoreType, + ).let { keyGenerator -> + keyGenerator.init( + KeyGenParameterSpec.Builder( + keyAlias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(cipherBlockMode) + .setEncryptionPaddings(cipherPadding) + .setKeySize(cipherKeySize) + .defaultKeyGenParameterSpecBuilder( + userAuthenticationRequired = userAuthenticationRequired, + invalidateKeyByBiometricEnrollment = invalidateKeyByBiometricEnrollment, + ) + .build() + ) + keyGenerator.generateKey() + } + } +} + +fun KeyGenParameterSpec.Builder.defaultKeyGenParameterSpecBuilder( + userAuthenticationRequired: Boolean, + invalidateKeyByBiometricEnrollment: Boolean, +): KeyGenParameterSpec.Builder { + setUserAuthenticationRequired(userAuthenticationRequired) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + setUserAuthenticationParameters( + 0, + KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL, + ) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + setInvalidatedByBiometricEnrollment(invalidateKeyByBiometricEnrollment) + } + return this +} + +fun SecretKeyProperties.getInitializedCipherForDecryption( + initializationVector: ByteArray? = null, + invalidateKeyByBiometricEnrollment: Boolean = true, + cipherGcmTagLength: Int = Config.DEFAULT_CIPHER_GCM_TAG_LENGTH, +): Cipher = getCipher(transformation).apply { + init( + Cipher.DECRYPT_MODE, + getOrCreateSecretKey( + invalidateKeyByBiometricEnrollment = invalidateKeyByBiometricEnrollment, + ), + GCMParameterSpec(cipherGcmTagLength, initializationVector), + ) +} + +fun SecretKeyProperties.getInitializedCipherForEncryption( + invalidateKeyByBiometricEnrollment: Boolean = true, +): Cipher = getCipher(transformation).apply { + init( + Cipher.ENCRYPT_MODE, + getOrCreateSecretKey( + invalidateKeyByBiometricEnrollment = invalidateKeyByBiometricEnrollment, + ), + ) +} + +private fun getCipher(transformation: String): Cipher = Cipher.getInstance(transformation) + +fun removeKey(keyProperties: SecretKeyProperties) { + try { + KeyStore.getInstance(keyProperties.keyStoreType).run { + load(null) + deleteEntry(keyProperties.keyAlias) + } + } catch (e: Exception) { + CoreLogger.d(LogTag.DEFAULT, e, e.message.orEmpty()) + } +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStoreSecretKey.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStoreSecretKey.kt new file mode 100644 index 00000000..21765617 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStoreSecretKey.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.crypto + +import android.util.Base64 +import me.proton.android.drive.lock.domain.entity.SecretKey +import me.proton.core.crypto.common.keystore.EncryptedByteArray +import me.proton.core.crypto.common.keystore.EncryptedString +import me.proton.core.crypto.common.keystore.PlainByteArray +import me.proton.core.crypto.common.keystore.use + +class KeyStoreSecretKey( + private val keyProperties: SecretKeyProperties, + private val invalidateKeyByBiometricEnrollment: Boolean = true, +) : SecretKey { + + override fun encrypt(value: String): EncryptedString = + value.encodeToByteArray().use { plainByteArray -> + Base64.encodeToString( + encrypt(plainByteArray).array, + Base64.NO_WRAP, + ) + } + + override fun encrypt(value: PlainByteArray): EncryptedByteArray { + val cipher = keyProperties.getInitializedCipherForEncryption(invalidateKeyByBiometricEnrollment) + val cipherByteArray = cipher.doFinal(value.array) + return EncryptedByteArray(cipher.iv + cipherByteArray) + } + + override fun decrypt(value: EncryptedString): String { + val encryptedByteArray = Base64.decode(value, Base64.NO_WRAP) + return decrypt(EncryptedByteArray(encryptedByteArray)).use { plainByteArray -> + plainByteArray.array.decodeToString() + } + } + + override fun decrypt(value: EncryptedByteArray): PlainByteArray { + val initializationVector = value.array.copyOf(Config.DEFAULT_CIPHER_IV_BYTES) + val cipher = keyProperties.getInitializedCipherForDecryption( + initializationVector = initializationVector, + invalidateKeyByBiometricEnrollment = invalidateKeyByBiometricEnrollment, + ) + val cipherByteArray = value.array.copyOfRange(Config.DEFAULT_CIPHER_IV_BYTES, value.array.size) + return PlainByteArray(cipher.doFinal(cipherByteArray)) + } +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/PgpSecretKey.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/PgpSecretKey.kt new file mode 100644 index 00000000..ef95b32a --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/PgpSecretKey.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.crypto + +import me.proton.android.drive.lock.domain.entity.SecretKey +import me.proton.core.crypto.common.context.CryptoContext +import me.proton.core.crypto.common.keystore.EncryptedByteArray +import me.proton.core.crypto.common.keystore.EncryptedString +import me.proton.core.crypto.common.keystore.PlainByteArray +import me.proton.core.crypto.common.pgp.Armored +import me.proton.core.crypto.common.pgp.Unarmored + +class PgpSecretKey( + passphrase: PlainByteArray, + val lockedKey: Armored, + cryptoContext: CryptoContext, +) : SecretKey { + private val pgpCrypto = cryptoContext.pgpCrypto + private val unlockedKey: Unarmored + private val publicKey: Armored + + init { + passphrase.use { + unlockedKey = pgpCrypto.unlock(lockedKey, passphrase.array).value + publicKey = pgpCrypto.getPublicKey(lockedKey) + } + } + + override fun encrypt(value: String): EncryptedString = pgpCrypto.encryptText(value, publicKey) + + override fun decrypt(value: EncryptedString): String = pgpCrypto.decryptText(value, unlockedKey) + + override fun encrypt(value: PlainByteArray): EncryptedByteArray = + EncryptedByteArray(pgpCrypto.encryptData(value.array, publicKey).toByteArray()) + + override fun decrypt(value: EncryptedByteArray): PlainByteArray = + PlainByteArray(pgpCrypto.decryptData(pgpCrypto.getArmored(value.array), unlockedKey)) +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/AppLockDatabase.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/AppLockDatabase.kt new file mode 100644 index 00000000..cc781c47 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/AppLockDatabase.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.db + +import me.proton.android.drive.lock.data.db.dao.AppLockDao +import me.proton.android.drive.lock.data.db.dao.AutoLockDurationDao +import me.proton.android.drive.lock.data.db.dao.EnableAppLockDao +import me.proton.android.drive.lock.data.db.dao.LockDao +import me.proton.core.data.room.db.Database + +interface AppLockDatabase : Database { + val appLockDao: AppLockDao + val lockDao: LockDao + val autoLockDurationDao: AutoLockDurationDao + val enableAppLockDao: EnableAppLockDao +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AppLockDao.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AppLockDao.kt new file mode 100644 index 00000000..75ed3918 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AppLockDao.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import kotlinx.coroutines.flow.Flow +import me.proton.android.drive.lock.data.db.entity.AppLockEntity +import me.proton.core.data.room.db.BaseDao + +@Dao +abstract class AppLockDao : BaseDao() { + @Query(""" + SELECT EXISTS(SELECT * FROM AppLockEntity) + """) + abstract suspend fun hasAppLock(): Boolean + + @Query(""" + SELECT EXISTS(SELECT * FROM AppLockEntity) + """) + abstract fun hasAppLockFlow(): Flow + + @Query(""" + SELECT * FROM AppLockEntity LIMIT 1 + """) + abstract suspend fun getAppLock(): AppLockEntity + + @Query(""" + DELETE FROM AppLockEntity + """) + abstract suspend fun deleteAppLock() +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AutoLockDurationDao.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AutoLockDurationDao.kt new file mode 100644 index 00000000..f3118a68 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AutoLockDurationDao.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import kotlinx.coroutines.flow.Flow +import me.proton.android.drive.lock.data.db.entity.AutoLockDurationEntity +import me.proton.core.data.room.db.BaseDao + +@Dao +abstract class AutoLockDurationDao : BaseDao() { + @Query(""" + SELECT EXISTS(SELECT * FROM AutoLockDurationEntity WHERE `key` = :key) + """) + abstract suspend fun hasAutoLockDuration(key: String): Boolean + + @Query(""" + SELECT * FROM AutoLockDurationEntity WHERE `key` = :key + """) + abstract fun getAutoLockDuration(key: String): Flow +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/EnableAppLockDao.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/EnableAppLockDao.kt new file mode 100644 index 00000000..6fcb8ac5 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/EnableAppLockDao.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import me.proton.android.drive.lock.data.db.entity.EnableAppLockEntity +import me.proton.core.data.room.db.BaseDao + +@Dao +abstract class EnableAppLockDao : BaseDao() { + @Query( + """ + SELECT EXISTS(SELECT * FROM EnableAppLockEntity WHERE `key` = :key) + """ + ) + abstract suspend fun hasEnableAppLock(key: String): Boolean +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/LockDao.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/LockDao.kt new file mode 100644 index 00000000..b9512544 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/LockDao.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import me.proton.android.drive.lock.data.db.entity.LockEntity +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.core.data.room.db.BaseDao + +@Dao +abstract class LockDao : BaseDao() { + @Query(""" + SELECT EXISTS(SELECT * FROM LockEntity WHERE type = :type) + """) + abstract suspend fun hasLock(type: AppLockType): Boolean + + @Query(""" + SELECT * FROM LockEntity WHERE type = :type LIMIT 1 + """) + abstract suspend fun getLock(type: AppLockType): LockEntity + + @Query(""" + DELETE FROM LockEntity WHERE type = :type + """) + abstract suspend fun deleteLock(type: AppLockType) +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AppLockEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AppLockEntity.kt new file mode 100644 index 00000000..6db81871 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AppLockEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.core.drive.base.data.db.Column.KEY +import me.proton.core.drive.base.data.db.Column.TYPE + +@Entity( + primaryKeys = [KEY], +) +data class AppLockEntity( + @ColumnInfo(name = KEY) + val key: String, + @ColumnInfo(name = TYPE) + val type: AppLockType, +) diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AutoLockDurationEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AutoLockDurationEntity.kt new file mode 100644 index 00000000..a88d6c3b --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AutoLockDurationEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import me.proton.core.drive.base.data.db.Column.DURATION +import me.proton.core.drive.base.data.db.Column.KEY + +@Entity( + primaryKeys = [KEY], +) +data class AutoLockDurationEntity( + @ColumnInfo(name = KEY) + val key: String, + @ColumnInfo(name = DURATION) + val durationInSeconds: Long, +) diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/EnableAppLockEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/EnableAppLockEntity.kt new file mode 100644 index 00000000..05512d10 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/EnableAppLockEntity.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import me.proton.core.drive.base.data.db.Column.KEY +import me.proton.core.drive.base.data.db.Column.LAST_ACCESS_TIME + +@Entity( + primaryKeys = [KEY], +) +data class EnableAppLockEntity( + @ColumnInfo(name = KEY) + val key: String, + @ColumnInfo(name = LAST_ACCESS_TIME) + val timestamp: Long, +) diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/LockEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/LockEntity.kt new file mode 100644 index 00000000..d148e91f --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/LockEntity.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.core.drive.base.data.db.Column.KEY +import me.proton.core.drive.base.data.db.Column.PASSPHRASE +import me.proton.core.drive.base.data.db.Column.TYPE + +@Entity( + primaryKeys = [PASSPHRASE], + foreignKeys = [ + ForeignKey( + entity = AppLockEntity::class, + parentColumns = [KEY], + childColumns = [KEY], + onDelete = ForeignKey.CASCADE + ) + ], + indices = [ + Index(value = [KEY]), + ], +) +data class LockEntity( + @ColumnInfo(name = PASSPHRASE) + val appKeyPassphrase: String, + @ColumnInfo(name = KEY) + val appKey: String, + @ColumnInfo(name = TYPE) + val type: AppLockType, +) diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockBindModule.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockBindModule.kt new file mode 100644 index 00000000..8a89a3d5 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockBindModule.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import me.proton.android.drive.lock.data.usecase.BuildAppKeyImpl +import me.proton.android.drive.lock.data.usecase.GeneratePgpSecretKey +import me.proton.android.drive.lock.data.usecase.GetAppLockImpl +import me.proton.android.drive.lock.domain.usecase.BuildAppKey +import me.proton.android.drive.lock.domain.usecase.GenerateSecretKey +import me.proton.android.drive.lock.domain.usecase.GetAppLock +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +interface AppLockBindModule { + @Binds + @Singleton + fun bindsGetAppLockImpl(impl: GetAppLockImpl): GetAppLock + + @Binds + @Singleton + fun bindsGeneratePgpSecretKey(impl: GeneratePgpSecretKey): GenerateSecretKey + + @Binds + @Singleton + fun bindsBuildAppKeyImpl(impl: BuildAppKeyImpl): BuildAppKey +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockModule.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockModule.kt new file mode 100644 index 00000000..443ff579 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockModule.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2021 Proton Technologies AG + * This file is part of Proton Technologies AG and ProtonCore. + * + * ProtonCore 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. + * + * ProtonCore 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 ProtonCore. If not, see . + */ +package me.proton.android.drive.lock.data.di + +import android.content.Context +import android.os.Build +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import androidx.work.WorkManager +import dagger.MapKey +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoMap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import me.proton.android.drive.lock.data.db.AppLockDatabase +import me.proton.android.drive.lock.data.lock.CryptoSystemLock +import me.proton.android.drive.lock.data.lock.SystemLock +import me.proton.android.drive.lock.data.manager.AppLockManagerImpl +import me.proton.android.drive.lock.data.manager.AutoLockManagerImpl +import me.proton.android.drive.lock.data.provider.BiometricPromptProvider +import me.proton.android.drive.lock.data.provider.BiometricPromptProviderImpl +import me.proton.android.drive.lock.data.repository.AppLockRepositoryImpl +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.lock.Lock +import me.proton.android.drive.lock.domain.manager.AppLockManager +import me.proton.android.drive.lock.domain.manager.AutoLockManager +import me.proton.android.drive.lock.domain.repository.AppLockRepository +import me.proton.android.drive.lock.domain.usecase.GetAutoLockDuration +import me.proton.android.drive.lock.domain.usecase.LockApp +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppLockModule { + @Provides + @Singleton + fun provideBiometricManager(@ApplicationContext context: Context): BiometricManager = + BiometricManager.from(context) + + @Provides + @Singleton + fun provideBiometricPromptProvider(biometricManager: BiometricManager): BiometricPromptProvider = + BiometricPromptProviderImpl(biometricManager) + + @Singleton + @Provides + fun provideAppLockKeyRepository( + appLockDatabase: AppLockDatabase, + ): AppLockRepository = + AppLockRepositoryImpl(appLockDatabase) + + @Singleton + @Provides + fun provideAppLockManager( + appLockRepository: AppLockRepository, + ): AppLockManager = + AppLockManagerImpl(appLockRepository, Dispatchers.Main + Job()) + + @Singleton + @Provides + fun provideAutoLockManager( + workManager: WorkManager, + lockApp: LockApp, + getAutoLockDuration: GetAutoLockDuration, + ): AutoLockManager = + AutoLockManagerImpl(workManager, lockApp, getAutoLockDuration) + + @MapKey + annotation class AppLockTypeKey(val value: AppLockType) + + @Singleton + @Provides @IntoMap + @AppLockTypeKey(AppLockType.SYSTEM) + fun provideSystemLock( + @ApplicationContext appContext: Context, + appLockRepository: AppLockRepository, + biometricPromptProvider: BiometricPromptProvider, + biometricManager: BiometricManager, + ): Lock = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && biometricManager.hasBiometricHardware) { + CryptoSystemLock( + appContext = appContext, + appLockRepository = appLockRepository, + biometricPromptProvider = biometricPromptProvider, + ) + } else { + SystemLock( + appContext = appContext, + appLockRepository = appLockRepository, + biometricPromptProvider = biometricPromptProvider, + ) + } + + private val BiometricManager.hasBiometricHardware: Boolean get() = + canAuthenticate(BIOMETRIC_WEAK) != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLock.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLock.kt new file mode 100644 index 00000000..f0d71385 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLock.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.extension + +import me.proton.android.drive.lock.data.db.entity.AppLockEntity +import me.proton.android.drive.lock.domain.entity.AppLock + +fun AppLock.toAppLockEntity(): AppLockEntity = AppLockEntity( + key = this.key, + type = this.type, +) diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLockEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLockEntity.kt new file mode 100644 index 00000000..aaf9696a --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLockEntity.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.extension + +import me.proton.android.drive.lock.data.db.entity.AppLockEntity +import me.proton.android.drive.lock.domain.entity.AppLock + +fun AppLockEntity.toAppLock() = AppLock( + key = this.key, + type = this.type, +) diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockEntity.kt new file mode 100644 index 00000000..094eb3b3 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockEntity.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.extension + +import android.util.Base64 +import me.proton.android.drive.lock.data.db.entity.LockEntity +import me.proton.android.drive.lock.domain.entity.LockKey + +fun LockEntity.toLock(): LockKey = LockKey( + appKeyPassphrase = Base64.decode(this.appKeyPassphrase, Base64.NO_WRAP), + appKey = this.appKey, + type = this.type, +) diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockKey.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockKey.kt new file mode 100644 index 00000000..6721681b --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockKey.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.extension + +import android.util.Base64 +import me.proton.android.drive.lock.data.db.entity.LockEntity +import me.proton.android.drive.lock.domain.entity.LockKey + +fun LockKey.toLockEntity(): LockEntity = LockEntity( + appKeyPassphrase = Base64.encodeToString(this.appKeyPassphrase, Base64.NO_WRAP), + appKey = this.appKey, + type = this.type, +) diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockState.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockState.kt new file mode 100644 index 00000000..025af919 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockState.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.data.extension + +import me.proton.android.drive.lock.domain.lock.LockState + + +fun LockState.onNotAvailable(action: () -> Unit): LockState = this.also { + if (this is LockState.NotAvailable) action() +} + +fun LockState.onSetupRequired(action: () -> Unit): LockState = this.also { + if (this is LockState.SetupRequired) action() +} + +fun LockState.onReady(action: () -> Unit): LockState = this.also { + if (this is LockState.Ready) action() +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/CryptoSystemLock.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/CryptoSystemLock.kt new file mode 100644 index 00000000..699f685c --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/CryptoSystemLock.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.lock + +import android.content.Context +import androidx.biometric.BiometricPrompt +import dagger.hilt.android.qualifiers.ApplicationContext +import me.proton.android.drive.lock.data.crypto.Config +import me.proton.android.drive.lock.data.crypto.SecretKeyProperties +import me.proton.android.drive.lock.data.crypto.getInitializedCipherForDecryption +import me.proton.android.drive.lock.data.crypto.getInitializedCipherForEncryption +import me.proton.android.drive.lock.data.crypto.removeKey +import me.proton.android.drive.lock.data.provider.BiometricPromptProvider +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.entity.LockKey +import me.proton.android.drive.lock.domain.lock.Lock +import me.proton.android.drive.lock.domain.lock.LockState +import me.proton.android.drive.lock.domain.repository.AppLockRepository +import me.proton.core.drive.base.domain.util.coRunCatching +import javax.inject.Inject +import me.proton.core.drive.base.presentation.R as BasePresentation + +class CryptoSystemLock @Inject constructor( + @ApplicationContext private val appContext: Context, + private val appLockRepository: AppLockRepository, + private val biometricPromptProvider: BiometricPromptProvider, +) : Lock { + private val keyProperties = SecretKeyProperties(keyAlias = SYSTEM_KEY_ALIAS) + + override suspend fun unlock( + key: String, + block: suspend (passphrase: ByteArray) -> T, + ): Result = coRunCatching { + val systemLockKey = appLockRepository.getLockKey(AppLockType.SYSTEM) + require(systemLockKey.appKey == key) + val initializationVector = systemLockKey.appKeyPassphrase.copyOf(Config.DEFAULT_CIPHER_IV_BYTES) + val cipher = biometricPromptProvider.authenticate( + title = appContext.getString(BasePresentation.string.app_lock_biometric_title_app_locked), + subtitle = appContext.getString(BasePresentation.string.app_lock_biometric_subtitle_app_locked), + cryptoObject = BiometricPrompt.CryptoObject( + keyProperties.getInitializedCipherForDecryption( + initializationVector = initializationVector, + ) + ) + ).getOrThrow().cryptoObject?.cipher + block( + requireNotNull(cipher).doFinal( + systemLockKey.appKeyPassphrase.copyOfRange( + Config.DEFAULT_CIPHER_IV_BYTES, + systemLockKey.appKeyPassphrase.size, + ) + ) + ) + } + + override suspend fun lock(passphrase: ByteArray): Result = coRunCatching { + val cipher = requireNotNull( + biometricPromptProvider.authenticate( + title = appContext.getString(BasePresentation.string.app_lock_biometric_title_confirmation), + subtitle = appContext.getString( + BasePresentation.string.app_lock_biometric_subtitle_confirmation_enable + ), + cryptoObject = BiometricPrompt.CryptoObject( + keyProperties.getInitializedCipherForEncryption() + ) + ).getOrThrow().cryptoObject?.cipher + ) + cipher.iv + cipher.doFinal(passphrase) + } + + override fun getLockState(): LockState = biometricPromptProvider.getLockState() + + override suspend fun disable(userAuthenticationRequired: Boolean) { + if (userAuthenticationRequired) { + biometricPromptProvider.authenticate( + title = appContext.getString(BasePresentation.string.app_lock_biometric_title_confirmation), + subtitle = appContext.getString( + BasePresentation.string.app_lock_biometric_subtitle_confirmation_disable + ), + cryptoObject = null, + ).getOrThrow() + } + appLockRepository.deleteLockKey(AppLockType.SYSTEM) + removeKey(keyProperties) + } + + override suspend fun enable(lockKey: LockKey) { + appLockRepository.insertLockKey(lockKey) + } + + companion object { + private const val SYSTEM_KEY_ALIAS = "system_lock_key" + } +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/SystemLock.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/SystemLock.kt new file mode 100644 index 00000000..4371cc1f --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/SystemLock.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.lock + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import me.proton.android.drive.lock.data.crypto.KeyStoreSecretKey +import me.proton.android.drive.lock.data.crypto.SecretKeyProperties +import me.proton.android.drive.lock.data.crypto.removeKey +import me.proton.android.drive.lock.data.provider.BiometricPromptProvider +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.entity.LockKey +import me.proton.android.drive.lock.domain.entity.SecretKey +import me.proton.android.drive.lock.domain.lock.Lock +import me.proton.android.drive.lock.domain.lock.LockState +import me.proton.android.drive.lock.domain.repository.AppLockRepository +import me.proton.core.crypto.common.keystore.EncryptedByteArray +import me.proton.core.crypto.common.keystore.PlainByteArray +import me.proton.core.drive.base.domain.util.coRunCatching +import javax.inject.Inject +import me.proton.core.drive.base.presentation.R as BasePresentation + +class SystemLock @Inject constructor( + @ApplicationContext private val appContext: Context, + private val appLockRepository: AppLockRepository, + private val biometricPromptProvider: BiometricPromptProvider, +) : Lock { + private val keyProperties = SecretKeyProperties( + keyAlias = SYSTEM_KEY_ALIAS, + userAuthenticationRequired = false, + ) + private val secretKey: SecretKey = KeyStoreSecretKey(keyProperties) + + override suspend fun unlock( + key: String, + block: suspend (passphrase: ByteArray) -> T, + ): Result = coRunCatching { + val lockKey = appLockRepository.getLockKey(AppLockType.SYSTEM) + require(lockKey.appKey == key) + biometricPromptProvider.authenticate( + title = appContext.getString(BasePresentation.string.app_lock_biometric_title_app_locked), + subtitle = appContext.getString(BasePresentation.string.app_lock_biometric_subtitle_app_locked), + cryptoObject = null, + ).getOrThrow() + block(secretKey.decrypt(EncryptedByteArray(lockKey.appKeyPassphrase)).array) + } + + override suspend fun lock(passphrase: ByteArray): Result = coRunCatching { + biometricPromptProvider.authenticate( + title = appContext.getString(BasePresentation.string.app_lock_biometric_title_confirmation), + subtitle = appContext.getString(BasePresentation.string.app_lock_biometric_subtitle_confirmation_enable), + cryptoObject = null, + ).getOrThrow() + secretKey.encrypt(PlainByteArray(passphrase)).array + } + + override suspend fun disable(userAuthenticationRequired: Boolean) { + if (userAuthenticationRequired) { + biometricPromptProvider.authenticate( + title = appContext.getString(BasePresentation.string.app_lock_biometric_title_confirmation), + subtitle = appContext.getString( + BasePresentation.string.app_lock_biometric_subtitle_confirmation_disable + ), + cryptoObject = null, + ).getOrThrow() + } + appLockRepository.deleteLockKey(AppLockType.SYSTEM) + removeKey(keyProperties) + } + + override suspend fun enable(lockKey: LockKey) { + appLockRepository.insertLockKey(lockKey) + } + + override fun getLockState(): LockState = biometricPromptProvider.getLockState() + + companion object { + private const val SYSTEM_KEY_ALIAS = "system_lock_key" + } +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AppLockManagerImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AppLockManagerImpl.kt new file mode 100644 index 00000000..4dcc43f1 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AppLockManagerImpl.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.manager + +import kotlinx.coroutines.CoroutineScope +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.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import me.proton.android.drive.lock.domain.entity.AppLock +import me.proton.android.drive.lock.domain.entity.SecretKey +import me.proton.android.drive.lock.domain.manager.AppLockManager +import me.proton.android.drive.lock.domain.repository.AppLockRepository +import me.proton.core.drive.base.domain.util.coRunCatching +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext + +class AppLockManagerImpl @Inject constructor( + private val appLockRepository: AppLockRepository, + coroutineContext: CoroutineContext, +) : AppLockManager { + private val coroutineScope = CoroutineScope(coroutineContext) + override val enabled: StateFlow = appLockRepository.hasAppLockKeyFlow() + .stateIn(coroutineScope, SharingStarted.Eagerly, false) + private val appKey = MutableStateFlow(null) + private val _locked: Flow = appKey.map { secretKey -> secretKey == null } + override val locked: StateFlow = combine(enabled, _locked) { + _, isLocked -> + appLockRepository.hasAppLockKey() && isLocked + }.distinctUntilChanged().stateIn(coroutineScope, SharingStarted.Eagerly, false) + + override suspend fun isLocked(): Boolean = isEnabled() && appKey.value == null + + override suspend fun isEnabled(): Boolean = appLockRepository.hasAppLockKey() + + override suspend fun unlock(appKey: SecretKey) { + this.appKey.value = appKey + } + + override suspend fun lock() { + appKey.value = null + } + + override suspend fun enable( + secretKey: SecretKey, + appLock: AppLock, + ): Result = coRunCatching { + unlock(secretKey) + appLockRepository.insertAppLockKey(appLock) + appLockRepository.insertOrUpdateEnableAppLockTimestamp(System.currentTimeMillis()) + true + } + + override suspend fun disable(): Result = coRunCatching { + appLockRepository.deleteAppLockKey() + lock() + true + } +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AutoLockManagerImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AutoLockManagerImpl.kt new file mode 100644 index 00000000..96f079a4 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AutoLockManagerImpl.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.data.manager + +import android.os.Build +import androidx.work.WorkManager +import kotlinx.coroutines.flow.first +import me.proton.android.drive.lock.data.worker.AppLockWorker +import me.proton.android.drive.lock.domain.manager.AutoLockManager +import me.proton.android.drive.lock.domain.usecase.GetAutoLockDuration +import me.proton.android.drive.lock.domain.usecase.LockApp +import javax.inject.Inject + +class AutoLockManagerImpl @Inject constructor( + private val workManager: WorkManager, + private val lockApp: LockApp, + private val getAutoLockDuration: GetAutoLockDuration, +) : AutoLockManager { + + override suspend fun autoLock() { + val lockAfter = getAutoLockDuration().first() + if (lockAfter.inWholeSeconds == 0L || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + lockApp() + } else { + workManager.enqueue( + AppLockWorker.getWorkRequest(lockAfter, listOf(TAG)) + ) + } + } + + override fun cancelAutoLock() { + workManager.cancelAllWorkByTag(TAG) + } + + companion object { + private const val TAG = "auto-lock" + } +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProvider.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProvider.kt new file mode 100644 index 00000000..7cb9f5a7 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProvider.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.provider + +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.CryptoObject +import androidx.fragment.app.FragmentActivity +import me.proton.android.drive.lock.domain.lock.LockState + +interface BiometricPromptProvider { + fun bindToActivity(activity: FragmentActivity) + suspend fun authenticate( + title: String, + subtitle: String, + cryptoObject: CryptoObject?, + ): Result + fun getLockState(): LockState +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProviderImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProviderImpl.kt new file mode 100644 index 00000000..f4b33514 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProviderImpl.kt @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.provider + +import android.os.Build +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE +import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED +import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE +import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED +import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED +import androidx.biometric.BiometricManager.BIOMETRIC_STATUS_UNKNOWN +import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.CryptoObject +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import kotlin.coroutines.resume +import kotlinx.coroutines.suspendCancellableCoroutine +import me.proton.android.drive.lock.domain.exception.LockException +import me.proton.android.drive.lock.domain.lock.LockState +import me.proton.core.drive.base.domain.util.coRunCatching +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +class BiometricPromptProviderImpl @Inject constructor( + private val biometricManager: BiometricManager, +) : BiometricPromptProvider { + private val listeners = ConcurrentHashMap() + private var activity: WeakReference = WeakReference(null) + private val callback = object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + listeners.keys().toList().forEach { listener -> + listener.onError(LockException.BiometricAuthenticationError(errString.toString())) + } + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + listeners.keys().toList().forEach { listener -> + listener.onError(LockException.BiometricAuthenticationFailed) + } + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + listeners.keys().toList().forEach { listener -> listener.onSuccess(result) } + } + } + + interface AuthenticationListener { + fun onSuccess(result: BiometricPrompt.AuthenticationResult) + fun onError(error: LockException) + } + + override fun bindToActivity(activity: FragmentActivity) { + this.activity = WeakReference(activity) + } + + private fun buildBiometricPrompt(): BiometricPrompt { + val activity = this.activity.get() + requireNotNull(activity) + val executor = ContextCompat.getMainExecutor(activity) + return BiometricPrompt( + activity, + executor, + callback, + ) + } + + override suspend fun authenticate( + title: String, + subtitle: String, + cryptoObject: CryptoObject? + ): Result = coRunCatching { + cryptoObject?.let { + buildBiometricPrompt().authenticate(biometricPromptInfo(title, subtitle), cryptoObject) + } ?: buildBiometricPrompt().authenticate(biometricPromptInfo(title, subtitle)) + + // await result + suspendCancellableCoroutine> { continuation -> + val listener = object : AuthenticationListener { + override fun onSuccess(result: BiometricPrompt.AuthenticationResult) { + listeners.remove(this) + continuation.resume(Result.success(result)) + } + + override fun onError(error: LockException) { + listeners.remove(this) + continuation.resume(Result.failure(error)) + } + + } + listeners[listener] = Unit + continuation.invokeOnCancellation { + listeners.remove(listener) + } + }.getOrThrow() + } + + override fun getLockState(): LockState = + when (val result = biometricManager.canAuthenticate(allowedAuthenticators)) { + BIOMETRIC_SUCCESS -> LockState.Ready + BIOMETRIC_STATUS_UNKNOWN -> LockState.Ready + BIOMETRIC_ERROR_UNSUPPORTED -> LockState.NotAvailable + BIOMETRIC_ERROR_HW_UNAVAILABLE -> LockState.Ready + BIOMETRIC_ERROR_NONE_ENROLLED -> LockState.SetupRequired + BIOMETRIC_ERROR_NO_HARDWARE -> LockState.NotAvailable + BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> LockState.NotAvailable + else -> error("Unhandled BiometricManager.canAuthenticate result $result") + } + + private fun biometricPromptInfo(title: String, subtitle: String): BiometricPrompt.PromptInfo { + return BiometricPrompt.PromptInfo.Builder() + .setTitle(title) + .setSubtitle(subtitle) + .setAllowedAuthenticators(allowedAuthenticators) + .build() + } + + private val allowedAuthenticators: Int get() = when (Build.VERSION.SDK_INT) { + Build.VERSION_CODES.P, Build.VERSION_CODES.Q -> BIOMETRIC_WEAK or DEVICE_CREDENTIAL + else -> BIOMETRIC_STRONG or DEVICE_CREDENTIAL + } +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/repository/AppLockRepositoryImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/repository/AppLockRepositoryImpl.kt new file mode 100644 index 00000000..887ab049 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/repository/AppLockRepositoryImpl.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import me.proton.android.drive.lock.data.db.AppLockDatabase +import me.proton.android.drive.lock.data.db.entity.AutoLockDurationEntity +import me.proton.android.drive.lock.data.db.entity.EnableAppLockEntity +import me.proton.android.drive.lock.data.extension.toAppLock +import me.proton.android.drive.lock.data.extension.toAppLockEntity +import me.proton.android.drive.lock.data.extension.toLock +import me.proton.android.drive.lock.data.extension.toLockEntity +import me.proton.android.drive.lock.domain.entity.AppLock +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.entity.LockKey +import me.proton.android.drive.lock.domain.repository.AppLockRepository +import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +class AppLockRepositoryImpl @Inject constructor( + private val db: AppLockDatabase, +) : AppLockRepository { + + override fun hasAppLockKeyFlow(): Flow = db.appLockDao.hasAppLockFlow() + override suspend fun hasAppLockKey(): Boolean = db.appLockDao.hasAppLock() + override suspend fun getAppLockKey(): AppLock = db.appLockDao.getAppLock().toAppLock() + override suspend fun insertAppLockKey(appLock: AppLock) = db.appLockDao.insertOrUpdate(appLock.toAppLockEntity()) + override suspend fun deleteAppLockKey() = db.appLockDao.deleteAppLock() + + override suspend fun hasLockKey(lockType: AppLockType): Boolean = db.lockDao.hasLock(lockType) + override suspend fun getLockKey(lockType: AppLockType): LockKey = db.lockDao.getLock(lockType).toLock() + override suspend fun insertLockKey(lockKey: LockKey) = db.lockDao.insertOrUpdate(lockKey.toLockEntity()) + override suspend fun deleteLockKey(lockType: AppLockType) = db.lockDao.deleteLock(lockType) + + override suspend fun hasAutoLockDuration(): Boolean = db.autoLockDurationDao.hasAutoLockDuration( + AUTO_LOCK_DURATION_KEY + ) + + override fun getAutoLockDuration(): Flow = db.autoLockDurationDao.getAutoLockDuration( + AUTO_LOCK_DURATION_KEY + ).filterNotNull().map { autoLockDurationEntity -> + autoLockDurationEntity.durationInSeconds.seconds + } + + override suspend fun insertOrUpdateAutoLockDuration(duration: Duration) = db.autoLockDurationDao.insertOrUpdate( + AutoLockDurationEntity( + key = AUTO_LOCK_DURATION_KEY, + durationInSeconds = duration.inWholeSeconds, + ) + ) + + override suspend fun hasEnableAppLockTimestamp(): Boolean = db.enableAppLockDao.hasEnableAppLock(ENABLE_LOCK_KEY) + + override suspend fun insertOrUpdateEnableAppLockTimestamp(timestamp: Long) = db.enableAppLockDao.insertOrUpdate( + EnableAppLockEntity( + key = ENABLE_LOCK_KEY, + timestamp = timestamp, + ) + ) + + companion object { + private const val AUTO_LOCK_DURATION_KEY = "auto-lock" + private const val ENABLE_LOCK_KEY = "enable-lock" + } +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/BuildAppKeyImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/BuildAppKeyImpl.kt new file mode 100644 index 00000000..905e65f9 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/BuildAppKeyImpl.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.usecase + +import me.proton.android.drive.lock.data.crypto.PgpSecretKey +import me.proton.android.drive.lock.domain.entity.SecretKey +import me.proton.android.drive.lock.domain.lock.Lock +import me.proton.android.drive.lock.domain.usecase.BuildAppKey +import me.proton.core.crypto.common.context.CryptoContext +import me.proton.core.crypto.common.keystore.PlainByteArray +import me.proton.core.drive.base.domain.util.coRunCatching +import javax.inject.Inject + +class BuildAppKeyImpl @Inject constructor( + private val cryptoContext: CryptoContext, +) : BuildAppKey { + override suspend operator fun invoke(key: String, lock: Lock): Result = coRunCatching { + lock.unlock(key) { passphrase -> + PgpSecretKey( + passphrase = PlainByteArray(passphrase), + lockedKey = key, + cryptoContext = cryptoContext, + ) + }.getOrThrow() + } +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GeneratePgpSecretKey.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GeneratePgpSecretKey.kt new file mode 100644 index 00000000..ebbd82a9 --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GeneratePgpSecretKey.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.usecase + +import me.proton.android.drive.lock.data.crypto.PgpSecretKey +import me.proton.android.drive.lock.domain.entity.SecretKey +import me.proton.android.drive.lock.domain.usecase.GenerateSecretKey +import me.proton.core.crypto.common.context.CryptoContext +import me.proton.core.crypto.common.keystore.PlainByteArray +import me.proton.core.drive.base.domain.util.coRunCatching +import javax.inject.Inject + +class GeneratePgpSecretKey @Inject constructor( + private val cryptoContext: CryptoContext, +) : GenerateSecretKey { + + override suspend fun invoke( + passphrase: ByteArray, + ): Result = coRunCatching { + cryptoContext.pgpCrypto.generateRandomBytes() + PgpSecretKey( + passphrase = PlainByteArray(passphrase.clone()), + lockedKey = cryptoContext.pgpCrypto.generateNewPrivateKey(DEFAULT_USERNAME, DEFAULT_DOMAIN, passphrase), + cryptoContext = cryptoContext, + ) + } + + companion object { + private const val DEFAULT_USERNAME = "drive-app-lock-key" + private const val DEFAULT_DOMAIN = "proton.me" + } +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GetAppLockImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GetAppLockImpl.kt new file mode 100644 index 00000000..fdaf52ee --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GetAppLockImpl.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.data.usecase + +import me.proton.android.drive.lock.data.crypto.PgpSecretKey +import me.proton.android.drive.lock.domain.entity.AppLock +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.entity.SecretKey +import me.proton.android.drive.lock.domain.usecase.GetAppLock +import me.proton.core.drive.base.domain.util.coRunCatching +import javax.inject.Inject + +class GetAppLockImpl @Inject constructor() : GetAppLock { + override operator fun invoke(secretKey: SecretKey, appLockType: AppLockType): Result = coRunCatching { + require(secretKey is PgpSecretKey) + AppLock( + key = secretKey.lockedKey, + type = appLockType, + ) + } +} diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/worker/AppLockWorker.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/worker/AppLockWorker.kt new file mode 100644 index 00000000..4768f64d --- /dev/null +++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/worker/AppLockWorker.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.data.worker + +import android.annotation.TargetApi +import android.content.Context +import android.os.Build +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import me.proton.android.drive.lock.domain.usecase.LockApp +import me.proton.core.drive.base.data.workmanager.addTags +import java.util.concurrent.TimeUnit +import kotlin.time.Duration + +@HiltWorker +@TargetApi(Build.VERSION_CODES.O) +class AppLockWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters, + private val lockApp: LockApp, +) : CoroutineWorker(appContext, workerParams) { + + override suspend fun doWork(): Result { + lockApp() + return Result.success() + } + + companion object { + fun getWorkRequest( + runAfter: Duration, + tags: List = emptyList(), + ): OneTimeWorkRequest = + OneTimeWorkRequest.Builder(AppLockWorker::class.java) + .setInitialDelay(runAfter.inWholeSeconds, TimeUnit.SECONDS) + .addTags(tags) + .build() + } +} diff --git a/app-lock/domain/build.gradle.kts b/app-lock/domain/build.gradle.kts new file mode 100644 index 00000000..07d41c2e --- /dev/null +++ b/app-lock/domain/build.gradle.kts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 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 . + */ + +plugins { + id("com.android.library") +} + +driveModule( + hilt = true, + serialization = true, +) { + api(project(":drive:link:domain")) + api(project(":drive:crypto-base:domain")) +} diff --git a/app-lock/domain/src/main/AndroidManifest.xml b/app-lock/domain/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4a0a4237 --- /dev/null +++ b/app-lock/domain/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLock.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLock.kt new file mode 100644 index 00000000..927ed0b4 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLock.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.entity + +data class AppLock( + val key: String, + val type: AppLockType, +) diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLockType.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLockType.kt new file mode 100644 index 00000000..c80f72fa --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLockType.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.entity + +enum class AppLockType { + SYSTEM, +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/LockKey.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/LockKey.kt new file mode 100644 index 00000000..2d2b77a1 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/LockKey.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.entity + +data class LockKey( + val appKeyPassphrase: ByteArray, + val appKey: String, + val type: AppLockType, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LockKey + + if (!appKeyPassphrase.contentEquals(other.appKeyPassphrase)) return false + if (appKey != other.appKey) return false + if (type != other.type) return false + + return true + } + + override fun hashCode(): Int { + var result = appKeyPassphrase.contentHashCode() + result = 31 * result + appKey.hashCode() + result = 31 * result + type.hashCode() + return result + } +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/SecretKey.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/SecretKey.kt new file mode 100644 index 00000000..4e64ad34 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/SecretKey.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.entity + +import me.proton.core.crypto.common.keystore.EncryptedByteArray +import me.proton.core.crypto.common.keystore.EncryptedString +import me.proton.core.crypto.common.keystore.PlainByteArray + +interface SecretKey { + /** + * Encrypt a [String] [value] and return an [EncryptedString]. + */ + fun encrypt(value: String): EncryptedString + + /** + * Decrypt an [EncryptedString] [value] and return a [String]. + */ + fun decrypt(value: EncryptedString): String + + /** + * Encrypt a [PlainByteArray] [value] and return an [EncryptedByteArray]. + */ + fun encrypt(value: PlainByteArray): EncryptedByteArray + + /** + * Decrypt an [EncryptedByteArray] [value] and return a [PlainByteArray]. + */ + fun decrypt(value: EncryptedByteArray): PlainByteArray +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/exception/LockException.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/exception/LockException.kt new file mode 100644 index 00000000..19066bf3 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/exception/LockException.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.domain.exception + +import me.proton.core.drive.base.domain.exception.DriveException + +sealed class LockException : DriveException() { + object BiometricAuthenticationFailed : LockException() + data class BiometricAuthenticationError(val errorMessage: String) : LockException() +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/lock/Lock.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/lock/Lock.kt new file mode 100644 index 00000000..9ca9a8c4 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/lock/Lock.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.domain.lock + +import me.proton.android.drive.lock.domain.entity.LockKey + +interface Lock { + /** + * Unlocks passphrase for a given [key] and provides it to the [block]. + * After [block] is done, passphrase should not be available anymore. + */ + suspend fun unlock(key: String, block: suspend (passphrase: ByteArray) -> T): Result + + /** + * Locks [passphrase] so that it's safe to store. + */ + suspend fun lock(passphrase: ByteArray): Result + + /** + * Called when [Lock] should not be used anymore. If [userAuthenticationRequired] is true then user authentication + * is required before disabling can be done. + */ + suspend fun disable(userAuthenticationRequired: Boolean) + + /** + * Called when [Lock] should protect [lockKey] + */ + suspend fun enable(lockKey: LockKey) + + /** + * Provides current [Lock] state. See [LockState]. + */ + fun getLockState(): LockState +} + +sealed interface LockState { + object NotAvailable : LockState + object SetupRequired : LockState + object Ready : LockState +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AppLockManager.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AppLockManager.kt new file mode 100644 index 00000000..c7ff6b97 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AppLockManager.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.manager + +import kotlinx.coroutines.flow.StateFlow +import me.proton.android.drive.lock.domain.entity.AppLock +import me.proton.android.drive.lock.domain.entity.SecretKey + +interface AppLockManager { + val locked: StateFlow + val enabled: StateFlow + suspend fun isLocked(): Boolean + suspend fun isEnabled(): Boolean + suspend fun unlock(appKey: SecretKey) + suspend fun lock() + suspend fun enable(secretKey: SecretKey, appLock: AppLock): Result + suspend fun disable(): Result +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AutoLockManager.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AutoLockManager.kt new file mode 100644 index 00000000..fd26a9ea --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AutoLockManager.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.domain.manager + +interface AutoLockManager { + suspend fun autoLock() + fun cancelAutoLock() +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/repository/AppLockRepository.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/repository/AppLockRepository.kt new file mode 100644 index 00000000..7bf6ba5a --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/repository/AppLockRepository.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.repository + +import kotlinx.coroutines.flow.Flow +import me.proton.android.drive.lock.domain.entity.AppLock +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.entity.LockKey +import kotlin.time.Duration + +interface AppLockRepository { + fun hasAppLockKeyFlow(): Flow + suspend fun hasAppLockKey(): Boolean + suspend fun getAppLockKey(): AppLock + suspend fun insertAppLockKey(appLock: AppLock) + suspend fun deleteAppLockKey() + + suspend fun hasLockKey(lockType: AppLockType): Boolean + suspend fun getLockKey(lockType: AppLockType): LockKey + suspend fun insertLockKey(lockKey: LockKey) + suspend fun deleteLockKey(lockType: AppLockType) + + suspend fun hasAutoLockDuration(): Boolean + fun getAutoLockDuration(): Flow + suspend fun insertOrUpdateAutoLockDuration(duration: Duration) + + suspend fun hasEnableAppLockTimestamp(): Boolean + suspend fun insertOrUpdateEnableAppLockTimestamp(timestamp: Long) +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/BuildAppKey.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/BuildAppKey.kt new file mode 100644 index 00000000..558b66a0 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/BuildAppKey.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.usecase + +import me.proton.android.drive.lock.domain.entity.SecretKey +import me.proton.android.drive.lock.domain.lock.Lock + +interface BuildAppKey { + suspend operator fun invoke(key: String, lock: Lock): Result +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/DisableAppLock.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/DisableAppLock.kt new file mode 100644 index 00000000..ad61ae86 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/DisableAppLock.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.usecase + +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.lock.Lock +import me.proton.android.drive.lock.domain.manager.AppLockManager +import me.proton.core.drive.base.domain.util.coRunCatching +import me.proton.core.util.kotlin.CoreLogger +import javax.inject.Inject + +class DisableAppLock @Inject constructor( + private val appLockManager: AppLockManager, + private val locks: @JvmSuppressWildcards Map, +) { + suspend operator fun invoke( + lockType: AppLockType = AppLockType.SYSTEM, + userAuthenticationRequired: Boolean = true, + ): Result = coRunCatching { + if (appLockManager.isEnabled()) { + requireNotNull(locks[lockType]).disable(userAuthenticationRequired) + appLockManager.disable().getOrThrow() + } + } +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/EnableAppLock.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/EnableAppLock.kt new file mode 100644 index 00000000..4fe0a1af --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/EnableAppLock.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.usecase + +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.entity.LockKey +import me.proton.android.drive.lock.domain.lock.Lock +import me.proton.android.drive.lock.domain.manager.AppLockManager +import me.proton.core.drive.base.domain.util.coRunCatching +import me.proton.core.drive.cryptobase.domain.usecase.GeneratePassphrase +import javax.inject.Inject + +class EnableAppLock @Inject constructor( + private val generatePassphrase: GeneratePassphrase, + private val generateSecretKey: GenerateSecretKey, + private val getAppLock: GetAppLock, + private val appLockManager: AppLockManager, + private val locks: @JvmSuppressWildcards Map, +) { + + suspend operator fun invoke(lockType: AppLockType = AppLockType.SYSTEM) = coRunCatching { + val passphrase = generatePassphrase() + val secretKey = generateSecretKey(passphrase).getOrThrow() + val appLock = getAppLock(secretKey, lockType).getOrThrow() + val lock = requireNotNull(locks[lockType]) + val lockKey = LockKey( + appKeyPassphrase = lock.lock(passphrase).getOrThrow(), + appKey = appLock.key, + type = appLock.type, + ) + appLockManager.enable(secretKey, appLock).getOrThrow() + lock.enable(lockKey) + } +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GenerateSecretKey.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GenerateSecretKey.kt new file mode 100644 index 00000000..7764419a --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GenerateSecretKey.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.usecase + +import me.proton.android.drive.lock.domain.entity.SecretKey + +interface GenerateSecretKey { + suspend operator fun invoke(passphrase: ByteArray): Result +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAppLock.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAppLock.kt new file mode 100644 index 00000000..ce57ae45 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAppLock.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.usecase + +import me.proton.android.drive.lock.domain.entity.AppLock +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.entity.SecretKey + +interface GetAppLock { + operator fun invoke(secretKey: SecretKey, appLockType: AppLockType): Result +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAutoLockDuration.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAutoLockDuration.kt new file mode 100644 index 00000000..55bb8617 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAutoLockDuration.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.domain.usecase + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flow +import me.proton.android.drive.lock.domain.repository.AppLockRepository +import me.proton.core.drive.base.domain.provider.ConfigurationProvider +import javax.inject.Inject +import kotlin.time.Duration + +class GetAutoLockDuration @Inject constructor( + private val appLockRepository: AppLockRepository, + private val configurationProvider: ConfigurationProvider, +) { + operator fun invoke(): Flow = flow { + if (appLockRepository.hasAutoLockDuration().not()) { + emit(configurationProvider.autoLockDurations.first()) + } + emitAll(appLockRepository.getAutoLockDuration()) + } +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetLockState.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetLockState.kt new file mode 100644 index 00000000..6644041b --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetLockState.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.domain.usecase + +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.lock.Lock +import me.proton.android.drive.lock.domain.lock.LockState +import javax.inject.Inject + +class GetLockState @Inject constructor( + private val locks: @JvmSuppressWildcards Map, +) { + operator fun invoke(appLockType: AppLockType = AppLockType.SYSTEM): LockState = + requireNotNull(locks[appLockType]).getLockState() +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/HasEnableAppLockTimestamp.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/HasEnableAppLockTimestamp.kt new file mode 100644 index 00000000..690fa471 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/HasEnableAppLockTimestamp.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.domain.usecase + +import me.proton.android.drive.lock.domain.repository.AppLockRepository +import javax.inject.Inject + +class HasEnableAppLockTimestamp @Inject constructor( + private val appLockRepository: AppLockRepository, +) { + suspend operator fun invoke(): Boolean = + appLockRepository.hasEnableAppLockTimestamp() +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/LockApp.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/LockApp.kt new file mode 100644 index 00000000..642f8da9 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/LockApp.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.usecase + +import me.proton.android.drive.lock.domain.manager.AppLockManager +import javax.inject.Inject + +class LockApp @Inject constructor( + private val appLockManager: AppLockManager, +) { + suspend operator fun invoke() { + if (appLockManager.isEnabled() && appLockManager.isLocked().not()) { + appLockManager.lock() + } + } +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UnlockApp.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UnlockApp.kt new file mode 100644 index 00000000..d440cb54 --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UnlockApp.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.domain.usecase + +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.lock.Lock +import me.proton.android.drive.lock.domain.manager.AppLockManager +import me.proton.android.drive.lock.domain.repository.AppLockRepository +import me.proton.core.drive.base.domain.util.coRunCatching +import javax.inject.Inject + +class UnlockApp @Inject constructor( + private val appLockManager: AppLockManager, + private val appLockRepository: AppLockRepository, + private val locks: @JvmSuppressWildcards Map, + private val buildAppKey: BuildAppKey, +) { + suspend operator fun invoke(): Result = coRunCatching { + if (appLockManager.isEnabled() && appLockManager.isLocked()) { + val appLockKey = appLockRepository.getAppLockKey() + appLockManager.unlock( + buildAppKey(appLockKey.key, requireNotNull(locks[appLockKey.type])).getOrThrow() + ) + } + } +} diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UpdateAutoLockDuration.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UpdateAutoLockDuration.kt new file mode 100644 index 00000000..eab69a4e --- /dev/null +++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UpdateAutoLockDuration.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.domain.usecase + +import me.proton.android.drive.lock.domain.repository.AppLockRepository +import me.proton.core.drive.base.domain.provider.ConfigurationProvider +import me.proton.core.drive.base.domain.util.coRunCatching +import javax.inject.Inject +import kotlin.time.Duration + +class UpdateAutoLockDuration @Inject constructor( + private val appLockRepository: AppLockRepository, + private val configurationProvider: ConfigurationProvider, +) { + suspend operator fun invoke(duration: Duration): Result = coRunCatching { + require(configurationProvider.autoLockDurations.contains(duration)) { + "Only values from ConfigurationProvider#autoLockDurations are allowed" + } + appLockRepository.insertOrUpdateAutoLockDuration(duration) + } +} diff --git a/app-lock/presentation/build.gradle.kts b/app-lock/presentation/build.gradle.kts new file mode 100644 index 00000000..559b9a0d --- /dev/null +++ b/app-lock/presentation/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 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 . + */ + +plugins { + id("com.android.library") +} + +driveModule( + hilt = true, + compose = true, +) { + api(project(":app-lock:domain")) + implementation(project(":drive:base:presentation")) + implementation(libs.accompanist.drawablepainter) +} diff --git a/app-lock/presentation/src/main/AndroidManifest.xml b/app-lock/presentation/src/main/AndroidManifest.xml new file mode 100644 index 00000000..80f095b1 --- /dev/null +++ b/app-lock/presentation/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/AppLock.kt b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/AppLock.kt new file mode 100644 index 00000000..a662cac0 --- /dev/null +++ b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/AppLock.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.presentation.component + +import androidx.compose.animation.Crossfade +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import me.proton.core.account.domain.entity.Account +import me.proton.core.domain.entity.UserId + +@Composable +fun AppLock( + locked: Flow, + primaryAccount: Flow, + content: @Composable () -> Unit, +) { + var isLocked by remember { mutableStateOf(false) } + var userId by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + locked + .onEach { locked -> + isLocked = locked + } + .launchIn(this) + primaryAccount + .onEach { account -> + userId = account?.userId + } + .launchIn(this) + } + Crossfade(targetState = isLocked) { appLocked -> + if (appLocked) { + Unlock( + userId = userId, + modifier = Modifier, + ) + } else { + content() + } + } +} diff --git a/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/Unlock.kt b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/Unlock.kt new file mode 100644 index 00000000..73cac280 --- /dev/null +++ b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/Unlock.kt @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.presentation.component + +import android.graphics.drawable.Drawable +import androidx.annotation.DrawableRes +import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.drawablepainter.rememberDrawablePainter +import me.proton.android.drive.lock.presentation.R +import me.proton.android.drive.lock.presentation.viewevent.UnlockViewEvent +import me.proton.android.drive.lock.presentation.viewmodel.UnlockViewModel +import me.proton.core.compose.component.ProtonSolidButton +import me.proton.core.compose.component.ProtonTextButton +import me.proton.core.compose.theme.ProtonDimens.LargeSpacing +import me.proton.core.compose.theme.ProtonDimens.ListItemHeight +import me.proton.core.compose.theme.ProtonDimens.SmallSpacing +import me.proton.core.compose.theme.ProtonTheme +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.presentation.extension.conditional +import me.proton.core.drive.base.presentation.extension.isLandscape +import me.proton.core.drive.base.presentation.extension.isPortrait +import me.proton.core.drive.base.presentation.extension.shadow +import me.proton.core.drive.base.presentation.R as BasePresentation +import me.proton.core.presentation.R as CorePresentation + +@Composable +fun Unlock( + userId: UserId?, + modifier: Modifier = Modifier, +) { + val viewModel = hiltViewModel() + Unlock( + userId = userId, + viewEvent = viewModel.viewEvent, + modifier = modifier, + ) +} + +@Composable +fun Unlock( + userId: UserId?, + viewEvent: UnlockViewEvent, + modifier: Modifier = Modifier, +) { + LaunchedEffect(Unit) { + viewEvent.onShowBiometric() + } + Column( + modifier = modifier + .fillMaxSize() + .conditional(isPortrait) { + navigationBarsPadding() + } + ) { + LogoHeader( + modifier = Modifier + .weight(LogoHeaderWeight) + ) + Actions( + modifier = Modifier + .weight(1f - LogoHeaderWeight), + onUnlock = { viewEvent.onShowBiometric() }, + onSignOut = { viewEvent.onSignOut(userId) }, + ) + } + +} + +@Composable +private fun LogoHeader( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + ) { + Image( + painter = rememberDrawablePainter( + drawable = getDrawable( + light = R.drawable.welcome_header_light, + dark = R.drawable.welcome_header_dark, + dayNight = R.drawable.welcome_header, + ) + ), + contentDescription = null, + contentScale = ContentScale.None, + ) + val y = LogoTranslationY + Image( + painter = painterResource(id = CorePresentation.drawable.ic_logo_drive), + contentDescription = null, + modifier = Modifier + .size(LogoSize) + .align(Alignment.BottomCenter) + .graphicsLayer { + translationX = 0f + translationY = y + } + .shadow( + color = ShadowColor, + alpha = DriveLogoShadowAlpha, + cornersRadius = DriveLogoShadowCornerRadius, + blurRadius = DriveLogoShadowBlurRadius, + offsetY = DriveLogoShadowOffsetY, + ) + ) + } +} + +@Composable +private fun Actions( + modifier: Modifier = Modifier, + onUnlock: () -> Unit, + onSignOut: () -> Unit, +) { + Box( + modifier = modifier + .fillMaxWidth(), + ) { + Image( + painter = rememberDrawablePainter( + drawable = getDrawable( + light = CorePresentation.drawable.logo_drive_dark, + dark = CorePresentation.drawable.logo_drive_light, + dayNight = CorePresentation.drawable.logo_drive_daylight, + ) + ), + contentDescription = null, + modifier = Modifier + .padding(top = DriveLogoTopPadding) + .heightIn(max = DriveLogoHeight) + .align(Alignment.TopCenter) + ) + val buttonModifier = Modifier + .conditional(isPortrait) { + fillMaxWidth() + } + .conditional(isLandscape) { + widthIn(min = ButtonMinWidth) + } + .heightIn(min = ListItemHeight) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(all = LargeSpacing) + .align(Alignment.BottomCenter) + ) { + ProtonSolidButton( + onClick = onUnlock, + modifier = buttonModifier, + ) { + Text(text = stringResource(id = BasePresentation.string.app_lock_unlock_the_app)) + } + Spacer( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = SmallSpacing) + ) + ProtonTextButton( + onClick = onSignOut, + modifier = buttonModifier, + ) { + Text(text = stringResource(id = BasePresentation.string.title_sign_out)) + } + } + } +} + +@Composable +private fun getDrawable(@DrawableRes light: Int, @DrawableRes dark: Int, @DrawableRes dayNight: Int): Drawable? = + AppCompatResources.getDrawable( + LocalContext.current, + when (AppCompatDelegate.getDefaultNightMode()) { + AppCompatDelegate.MODE_NIGHT_YES -> dark + AppCompatDelegate.MODE_NIGHT_NO -> light + else -> dayNight + } + ) + +@Preview +@Composable +private fun UnlockPreview() { + ProtonTheme { + Unlock( + userId = null, + viewEvent = object : UnlockViewEvent { + override val onShowBiometric = {} + override val onSignOut: (UserId?) -> Unit = {} + } + ) + } +} + +private val ShadowColor = Color(0xFF0D052E) +private val LogoSize = 106.dp +private val LogoTranslationY: Float @Composable get() = if (isPortrait) { + LocalDensity.current.run { 44.dp.toPx() } +} else { + LocalDensity.current.run { 42.dp.toPx() } +} +private val LogoHeaderWeight: Float @Composable get() = if (isPortrait) 0.3f else 0.25f +private val ButtonMinWidth = 300.dp +private val DriveLogoHeight = 32.dp +private val DriveLogoTopPadding = 60.dp +private const val DriveLogoShadowAlpha = 0.07f +private val DriveLogoShadowCornerRadius = 24.dp +private val DriveLogoShadowBlurRadius = 16.dp +private val DriveLogoShadowOffsetY = 8.dp diff --git a/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/extension/LockException.kt b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/extension/LockException.kt new file mode 100644 index 00000000..79fc8a0d --- /dev/null +++ b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/extension/LockException.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.lock.presentation.extension + +import android.content.Context +import me.proton.android.drive.lock.domain.exception.LockException +import me.proton.core.drive.base.presentation.R as BasePresentation + +fun LockException.getDefaultMessage(context: Context): String = when (this) { + is LockException.BiometricAuthenticationFailed -> context.getString( + BasePresentation.string.app_lock_system_biometrics_authentication_failed + ) + is LockException.BiometricAuthenticationError -> errorMessage + else -> error("Default message for exception is missing") +} diff --git a/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewevent/UnlockViewEvent.kt b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewevent/UnlockViewEvent.kt new file mode 100644 index 00000000..a106cb27 --- /dev/null +++ b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewevent/UnlockViewEvent.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.presentation.viewevent + +import me.proton.core.domain.entity.UserId + +interface UnlockViewEvent { + val onShowBiometric: () -> Unit + val onSignOut: (UserId?) -> Unit +} diff --git a/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewmodel/UnlockViewModel.kt b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewmodel/UnlockViewModel.kt new file mode 100644 index 00000000..c1a85e23 --- /dev/null +++ b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewmodel/UnlockViewModel.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.lock.presentation.viewmodel + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import me.proton.android.drive.lock.domain.exception.LockException +import me.proton.android.drive.lock.domain.usecase.UnlockApp +import me.proton.android.drive.lock.presentation.viewevent.UnlockViewEvent +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.domain.usecase.SignOut +import me.proton.android.drive.lock.presentation.extension.getDefaultMessage +import me.proton.core.accountmanager.domain.AccountManager +import me.proton.core.drive.base.domain.provider.ConfigurationProvider +import me.proton.core.drive.base.domain.usecase.BroadcastMessages +import me.proton.core.drive.base.presentation.extension.getDefaultMessage +import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage +import javax.inject.Inject + +@Suppress("StaticFieldLeak") +@HiltViewModel +class UnlockViewModel @Inject constructor( + @ApplicationContext private val appContext: Context, + private val signOut: SignOut, + private val unlockApp: UnlockApp, + private val configurationProvider: ConfigurationProvider, + private val broadcastMessages: BroadcastMessages, + private val accountManager: AccountManager, +) : ViewModel() { + + val viewEvent = object : UnlockViewEvent { + override val onShowBiometric: () -> Unit = { showBiometrics() } + override val onSignOut: (userId: UserId?) -> Unit = { userId -> userId?.let { doSignOut(userId) }} + } + + private fun showBiometrics() = viewModelScope.launch { + unlockApp() + .onFailure { error -> + broadcastMessages( + userId = accountManager.getAccounts().first().first().userId, + message = when (error) { + is LockException -> error.getDefaultMessage(appContext) + else -> error.getDefaultMessage(appContext, configurationProvider.useExceptionMessage) + }, + type = BroadcastMessage.Type.WARNING, + ) + } + } + + private fun doSignOut(userId: UserId) = viewModelScope.launch { + signOut(userId) + } +} diff --git a/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark.webp b/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..ff4acd8d659d028e29f76afce896a4865111f0ad GIT binary patch literal 204420 zcmWif`8ykE_y1=;Q?0FPFIu&mh}f5zDD4Qb1XEGcglNSU1f3@6DB5SzP-_rLY^f4U zEwPI>s6MDA39%Hh_nB!DR53%_OslPLzdzjnz`3sbI_JD!uM^$+fV*C{rj<-^6atPQ$5b= z(dWyr&iZvrzy64`d$)0PBfb-quyLgKc^nvL`|3#V?YKU1|4Ylw$FuB~*U#=9ej$&> zuXIVC-TUWDTPKQ6*2>u5&v?GE7^BmlkTvl9{g*69`so~sDL3uj$o_3!-=w{tV=0@T zcJJMn>)kqwqs#VP4wj-TgOk6lvv~2DDK97cUg48Re-7=5I$OW;GMb5Ld*U$0ttR+= zhzO>H{s$wmIaQa3i{H(CvE(2BhrVhP_hCUiKR;I0qbO%tMahF+qP!H=A0=nMxxhgh# zcjI;RjuJWOSMBQanIo_Lmp5JhPc8AK(NoRHo$mvcoEN7zU3%Trb55k)`RzhLyHK0E zdH8YQ_uqd`j33kcKNx3Sw+eUf-+@T|Q|^|xPw7B__&?u#d3$)_R8hQ%E@?OA?ayPn z{Nuyp{d!k9Fz~dt}|Cg6T~=aTc)Wl*`TWKf266wqAMRYO4LaS9R%*Xqfdw zO@XoZ+|QKDZ}!U5l-*yP5DjiV(ro^4Goa&M9pM&+$hlePbo4P^Y=| zweQ?7Y~>8xbj^6~?D68nzjR|R#hBas-G}3QhvM@=>Wc!RL-6%Qblge0jgE&vgOLYd$oG}{CL%oQv6%i>+*Mi zj9%%N*V356PZy@(`_JmRuv^VPxf)iSdL)&O$I3&$JN1+M!N(s?f4*QUH&YY_$L#o=%ZH3dyp6pDWIBG}i%y5QttGA5>H)YcyNwS9`+xmdAdJGHEdt2nY1 z#@d!|RuC{2ZnfNp(#m`D-Qg|tCW_Z%N?faDz;>QQz`8xjfMPMo1p-qAS#4 zx)It3x(AYGYA`E+_-ghytVE_h<$rlZ5hfZgE-^>OE_<@3&*`{zRk!a@qqp7a6sAh!j9x%Xjn zLycbaEef!?`svg5|6M!6G@TI-GU_>P@*K^NNUreXpy70M<2f~=P&%C@w{8ZjiG{O` z*KZdY$-|%>9hs6UV6xe?yNT+eDus}GY&-7oW^Sr^D?9hP_;ph9CgyB=+6O+92d0HT z%kee~UvRqJh;E@l;Ji)ylQ+~{&c1E~<0gN+PAHV`B>RuTymeEyyGn#EhxKz4 zPToM4nYUQAelAztiB1W0W%84PE>i>AcHVq6cL3WpMbs(iHW{G;Ih%}b=8f4N81MX# z@sr^8JBxX=ww?fs+9$AUNxwe+b&hHFXr~kCwpYJqz6d*IvLER#<1id<*|?akbn$Dj z>z$Q9EZ9D>?P+6tRdC5xGm=jz&TU#$rv&iEMqtvUr>%jiBbb+JHLpd> z1XHRH>;OXAN};Ze4e3*B`H@lWw@ZuD1VN#7S7Woe;ZChIa)91R@3Sg@T?5G#wdJi;4R}jUC2P?_Z7f5y+0%e z0`~(v>u+rT{;KGONsdZv4YT7BB*`b$>Qej*YUL_p6)lhphCTLRN7e=6Qt6OB%jqCg zfM?)=&7&Vn?Wlzz5g*tM&eFRe)*+dM(gQ-yB5DIRit4xu#qY;UPLMs>CFQ^37Ff$Ev{ zJS8@xewz{r7E*Bv&))!E`Rv6Scs=e@Cpz~a3peL-oUh?q-%ez_KfHn+S)}ju(ysKe z*IHQS=0R_=cs4=y@|let^yD{gV*2e3g`XZCbXHq8`*190TRjD^DmhH3X#gQ!0Z&LX zog_hR%xE-FU^C18Hr&Y}30j5m+w(Elr${TNq4?7aw{BRvLj`2lrTwmILs8#V`jwR? zw-7!I1})Bo!Tc>A>U0Hyf1FOvRW`r=@Zi>XIUul%( z&DX%HDyKRoKym9cOChCL;AnFXGA1s-1ceNQyKDS|@vE@Z9I3zSA-DMP&j%ES+aNtn zK-;-&P$fcW`Vxod+-B?(7SC{d$!&@3sArMCP)C> zB;ygCJWi1>vhPjMNI+>=DaeTI`X5pPu*YNQIupZ=v85A+sv%we1)^ZVx|iz%Qwjm@ z8L1I0=j23B{YtJ^RcJNZY|IMtOPhT5$zaw$F2~wp$PQt;DRGIWgHk5&32h-3R1Fqm z8~K;eWMRNuB|MYU5Kk!h*hg>gj1e^EpdUys8G!Q0=uMZYr;JK?Ra4(-zPio`C8jE5 z^+69qE?H3}xhY&QI)J+v?Qfpc1MU}Xb?MhdpsG+Wd4!@1aeLz{jElK|g@C}^VA@XA z!+^L_f@Z;_3zNDz3BCuyTY}1AMd@hBf@@3J4HMTY&Xb5ZL5!zB{WL$f+6URGza8MH zrg-&y$8vt?yGixM20zb9M8odVWd1rVxa7@Try&(bP}=_FrF*}4B|refTjnD_f4)U! zD46DH#);7^aN%gyl!M+qzuy96FjcLTc?0uIWwyj+(=o4%+%3ARo=Gg@{Hh(Nu8Nlv zD|Ru!7yG<*d^Y|G(Q;7x^VU@NRvX`o`_H9b=IncIv!CN7iqVdaO|>Jwzyzbn%Zpno zRAa=TM2FZqsm&wmc2`K#(V&9tOkHdn_#P66@q0_bCmve%E`J%)I;YU`KsOw%|PFWMvdMqWKykl-bo6CITJfp+sN)MIjPZ)?O=Trp5(R46uS%DMjweEmeQm zP9Ymsl^zWB8hhC2*a*M4^ZJSQ&GDBO9m_VE23DYy7vXUSHoNRawA;%koF`}_&;*sB z<=RL}`XyB}&eb}mtZi!gLEm2M_nML#HqND6xTq#^1j_Mh;KzW} zLRo_^Bytnyf%{M8?MD`=n=VjPZ=QpUib?PXRtZY9*3iL-B1~oUE+9W7u<<8d-!e^4 zh8EhGNN+fZbvPf&;@bH3cc_wDA&TShew~VVO~<;f*C38k9~=-zOZx(lR*Np}t2Z%%;^)&7 z1#D0xSJmOjrrZ)MQS`y@ZiThMPsJuoIe-&qofVRo(52elr=!|x_+XDnoxhM?E$8IJ zJ-BuO@%SOM5aS8W6x@>8Pl%NMj!upeo~RK<-r zxx`IG`MzwzYnpRz$MYPfW}v)`35URDdtD!TuY2eHQQP36$>qEJwX+JLgFmT?muIVu zS27=>(SBLwJnI(dIiu#8GHXc3U%m699iZE7aHXxWde6ay+Fe7wL)oEfMO;;$mUrKb z2{WV|H|Fc9%}s4skl7|m!VQh>uzA&sYiC5hld67R5U;`t2BDP{kf3ba2~csWTf1lt zxPhxGiPIdm)SYlXs_D0`tT z=fTq#Wp=Qs&w85Y5+HrSEiwjhXs)U3v!I7quebpYjq zJ?9vatsgb3suDiv|4abufFxYi{lXKnk|Mmb6o>v@r`rKu;dG8LJVLM^A@tpEtO!DU zLyT&~4C5!Yrpf<9`@k|8oL?(B(ee3yHjg$H`GNi|6k^z$AlOPPV+bET5mkkYGT1?? zyUupb$E3d6Fe7&5GRlEbvr&B&QHWF34_{1jid|NWC;iFccOnrc*j8K3QP~my!&{R? z>ul!0;b%h&QjhG=#MqN04o2IL-t^*{(Un)aHzZw=aAfEN7cZ6N%b*PYuXDvK zns_>4X3QB2Qd>@HT4)BzXp6FmMb=merntgRJ%_?pVvXa^ycr2ToMdAzN6rT$nB(z? zst6h(uHWr>rWwp&iJfd7j-XNIzOU9_Ie7<4q@vyD;Kn91@&hfQlW zUE!odwzq>Y2a1bILfNR!)n(0ibrTn4@|>@lKYH@Ft%0N5x$pn5Qb{^jzJta#WX?sQ zqMu04Q$-+!8sm?bG|C~w^*6RoL_h6)B( zsxogIaL(M_XG2XJ)YIvnMY$c&MTHeknC1Y5UDjAZ(1C+J8bMfeZI9v-46q!~6tlkm z-fcB%ns=+2xd(lSabG!~W=}|*$+}S1-MQW2d#>a*-vvS41VR#gX$+&$+yy#r5!P+3 zt?J0KDxV&bMFqEYDmSf?+xgEN$5()nIlug8>sg8KiK&dePKvyYSVsg7F4YUpLI_NS z_^D^?MPmesQC!rW1a1*)lPw^Gd78H`f^Zc({4KPpijgT7J1~=e*Pa z>&nyq9M0p&znq=@RBXBfV>Q);3MvRuyZEo@LW1WW!4k%t_oIzM>D8)J&-fmta@>AZ z*YSWF+hIHicPH8l+3()&h>drgN+cxo%bM@|mK@z|)p$`DlVPoUVAI3zpVW>EC`zlk zjHRVT1!kSr&0^NdOI9#`6CC0X3*m`N_V4nG%;aPY8b|72v~H)3%UC8Kwd|*Gwkt-N zzpTX1#sBI)?!Tpfz)A5gQk3|{3sfPlZ2WPMs6rj@=Y&#l*inzgOrKPlw2qeaVdA39 z5#fKg@Yhc6AX~v_`FCb1xVY17hKi{W2v{u&rDrA^WsWw3`Q+@ll>TY9N_1TW5?U41 z_5}zDZwrm7I<4aoVQ6Di!19fk#jKJ?skprwBl=|}i0_8Qp(Q_g+#kX5I55~R4^TX9k%DGCBIGBNXb27@)6Qs6FcQ{y^ zW>-1+X(;t)s*jQM$FBFse>}9jpG^+M`&H0s zFG!HqsxLP2OclohxvL`{rG%n;zj>PVg!v8MO!56mZKTj;QQ37Xv&TO`PzT`9?E*@6 z0OE2MBe|RbC#|)35rg2osgL0kGe6T&i5$n{ripF+% z9f8lrX4BJ@TYUtJOJ`nAJTp0Dq>|ym6JR-oxGMPC-IRR1#Q`HykYi}3d!zyuhAH^W zXDPFK(-fDrdNHaVjjF3D`Po3M5vX-hQ~}-i*f-f-e0i+TS0g82QpyzJKTOOFS*d;a zMK8EYV{tpJibm0?z-U}G7i&MtY|?Eul}7hY`D0*tLxB^^ck|O*`H-|1jh-`!e5Pwr z!g!j|Gedak8%?;tI&e0M?MbtnUNNQDLngFUPr+~}Uy4wV&QZA;He<$KWjDHH-rL12 zn;<)0@u&_ypzU9bLBdtu@&JxUV2MKaa<~_;ocN{+1EJ)ZS#F|XQYc{B+hFgpO^#(n zO3XiA@x9^N28Ka>8j=WLYm-dPBBchbrdIe*_el$!EQY&juWMF8INPE?7oic$6#SWC z{qPd6>?N6@+`14Nt(E+_GaVhSOL8uq)yzsbt9pRGO`{d3we?3DO*c`L6kiqt zfzVo>hDvQq&!0LFhKG6)RYiBKmWM1cGlF7Q&ZHgWL3g!c$|Oa}*9cKWVh?+{ zMdEbssSl-?@J7)iC@z?!taEcXf|)jCRg93*AIBQ#S~r4`o0hE zzh@(Vf0;o$%9^=p+~ZlJV%%@MNFvRhHfrTBr=K>wAH4Lu0zbu`KOM`J9{7r`&$|oY zgV%X+CYcwOM}OrvtEmp9ZHJj=g_X?aT2Ip3QKEczFN1WgAIpfU_t|mY|LhIgslbL(WCvAu$KQs{m52(9>v@Ka)wPxmOFgl;vAGR@)tO-% z_xOsM=+)1uX(V16I4`-!X|dB*$U+{ny#?+O7IgGZ>0M>synAAW*Iud;-mz@JEsklc z*KNp|hVMn-lx!RfiVL0IWrgcq{0Xppct!eD(Qq!ho31N&wQkHB ze3c`iqajr5TtD~5agr^mh@?E45JUt7lclqU=29fg>=$N%5pj9oN6rU;lx%}-gW{Dc zh{2amr9Me?wSQDe2>&|3P|{Xq;fGo}Zng4=x4jKDYI53!+E5T?|9m0sfGW zU)CA}1$p{@(4QQ@#c???)h~sB`NKN)``b2k)>{2<%golxE zKU0{r?TJuIa^h>3;^N8`D%{W8#jiGkOEx>g7XL7*)897%Q3wSM+lH)B$bB?_xl@T$ zKj#lhHbayj1(X*CjaUm4zl66DLhF_3c1U9+DEnkfXo|HO=izATi9g#gIueFK+^YRs zueQ}&nwoq7#}t1(>+^7rdd8BCl+oEU32uC=MWB<12TJMPGd!V6rvD21!TcY|hQL|l z+{Q5{zz4a+lg+>Md%%;MhPA}O{>kqydQ_?eU%GO*p{m$)u%Xoxd{Pe5(1ieSgz@fGa(XMb{EZuG z@T`TL&pI_&XS7lHM)k9fyBDjXgE4*)Wmq;KtQfd<)w`8D@Z-t*BPJ*Om=B_-C(Gc6 zmbw{b>M7fR&zc^BUw2Co2Vhe+{Pwu@MxgSwERk&p3=1ypK4zg|U}Ho)|Y~ zPF(!rYgOa6bW0Dx*6%N^D(-Anp;^Sgs~_v>LSZ_xEc8!Q8dHpw_ zGvwHVs>@2MFVNk{?cSo;9ml}FxGz3Me4MbHr?&0M z62^7jSsiRj`rLIU^JCc98>C3)O8g^8KgnBHu4drJ)OO92a;xZ;sq$$b?_fB%b|P3- z+^0GkcE>NtRa}lOS%S9*0f>#!;;OZguhy!aY+FT*%6HXO$u+*_xf2-=D21GxU^`YK zth>y<(GBt(<0~M0KL9BiCqbI1WEcDU%?Te@{zNXyTTDy(&R6jPe8SN5viQ>G{!1M? zP7AhFmyhJj?3^!<(`A>#%U#WQD5F?Yw`0?ZK#qqi_4Dh0J5y47!@4a*Y7HS?eb&- zQaKt2RX3RkBN*h(mTviv2j>`qw5YD1!XSQkEisnmRPjgPTDjWz_hA5l|wtXNaa4D|a62BXrUt^EbAu&J5-|5Tp zSvX0&(Df{J}a(> zD=p%T+q>qmO?!|I@Q9?}S~^1n(E#%kMsI)qsbvapktJqTU(p4d`*uz|3kK;4sRM`K zez&(Kb*D_DlmGjd*SMe!i!IyWL!0TRPak`Wbli2!g(iS=-?wS(ywK;Ol;1RVr9TAg zDK5?ifTXF{Bo`t(jPFZOvkBW?dG5uXPX1!n>%qk3>6Lm<+ee4&xjs;tB{DAgqqK}E z^D;{_KStVL^jDb=6jMK;nVN#!?lD)lXTj2oSYt50rAMg!REIR*kaa7R^D*~_n!#I+UBizWt zM`HxEWzA&L?1PU_8)W$v=Y{N|H3eOsK0xR{Za;hLikc$VC6lbQ3m2Fm{0~{;Lz{>m ziUX9quoe0Evu6-*NwU+BF0iB_l1TPa&ET_52iG6tA8ora8Lf5t)XSE@iDh|P;Gs=w zd($L}%CY39hZ1fss``LHF4^QSWkmGt%a)yQNe6WWe_ie_#|bRCs3)hLv~YwQFXuBG zhk}b_F?d~-B)2I}uqYh9K5RUZ9Oy4^Gz_`ew=i(chZF-Z{TF`Lylc!*F=1$!_==u_ zzG@ddyLJuyJOsFLyjoiUkpY`eDmiGt-Aiddgf_U65@d$ryfylo`0k#jSqFSOVVsGU zns)S_3b@iv>iYyvkW{QpIiEFt2a*Nj>^8Ns;%=+IMycqSHi8;`IsNnXvNYe&+_&yELmC`MLQx5$K&GW;lfPkfR*xp8h8(jy z2UiP}-|=#UZ&Yfl^V#I6A&o|&$FtRPbA}ETB&71<@c%9tT}8$;=>A#|SNbm- z<7gA?I3L#Ew91qXAj=K+Cln`Jt(fl4&Y5zMa%cysP>&RSVHaSttURNWXD4)u5o~QvvKdjK!)0k@-6P@2 zD88+pR8|+4+U|ht@DhXZHxk^U>hZ7YQ>>@)t+c{cWFHbIC{DhJO<6`lMOF1Eg+nywWGoQxmzmtd z?QorOq9Z+I^9(UjhKq3X z*?821L=9RdvF=L1K;Oq*0UPF`O>DiTm@)6TAWD-knCMex*vy@S@kL7e%C2vAsE(_*W-sJ$|+LnKD4BqP$aXPL<$)B#&4-g;8OPnm7^4}?DAO9~e zI|cqkRAqdyG55kh7I`+9sKbQUe1 z@<=gOHfZm1PSIU4?t^(IlLzTFGQ(e?ZBpzCY_O4j;Iop$x?4w2$z5lrb+bPPbH^7D z@#5R1i+1{VpFwD7^MGPJ+4bUN>0E&kb}NfvBEAkQvc+2hXvEL|jPo`Z3|!+pb!{TX ztI7BJCQ#v94$}@8gYe4dr3B^3#(yvq^1#z`-50(}^j_4mUFGP8`}Hd3wdoJ-R6*%c0OY#6$eHM0L)h%Vi{OHsLM zOq$zH%gCviXjOs-R+Z1asB1Z@BQpGE|M9(V&|^qg@AM(0xB2eI?7ajcwLX?z7P|XV z-`P2+GPCjR26YXjrIA30Z(8)o(T(nG=RCZXVix6SF#r9K{nWGkiDYyZM*LQ2yXnrB zJX4>%j7AzZT0WogzI6M44=4mw+iq|5vo2;t@XLX-gjVpyy@#@Dct1pC1lM1)KHr<_@%rev$T0SLi$0b{-CXiLo^mzm zTnv7BH?5$WDt=9MzGgCMt7)B5tXMECo8?K#tO+;GA^{%*MI81pu~Jg7&06N5E*oq@}vQ6-q$xz{9Fs5vbq| zRxt=)&igpksX?|@{Z|PBHB}fnv5T`|?l>wUNZIXl(T=$|rw)@*wGpryHQ4l28G z7UP*(sP4G=7Sf5gLyX?|iO>zXd^v|)Z;bJ*sag6bxo5>fc)_dI?T)^;zj-kMYOK+~ zb&1;gx%9HM8Sn9UrGsBogew_&b#^-jHS163b&onL{{H}5@K zi9I*TLGz_VZI>Ps6?Kaq>sbq4nuB5PfWp-KQmAgs!?_6N1%8y@(!s4tlN07g@J5mY64L`@AG-+^67r`4k0oHaL4A^wiJYC_@ z`G~F47bQC~!CEl^yqr@`SbG8eH&GuSwlmB>9nKQ>-*4Z zA4aHl>TlpJ!Sc@W+2}RyI1!>@(mH9fsTYmBy}hFO;FWrXTapt)A4|4UhGWt-$(my6yv7j zl;@b02fq%J;{+AUSw(amQG~abg)H_xV2hZszb zUvMjLiWt0Y(hoRbICn2nHvv<2K2{AiMauQ>U(TMcAvKxl91_ltxZy*)R)$a1@ml{U zy?0rnMT(p$hB-(hiQsGEOYzGlLT7^MBfajROLT1DhK&r=bI*)G$L;oZE-QZGGHPnG zEPp0udP~rg%kHf4WP?qX-=&7v*9)=?@k{!J3kBD~xqN;<8 zdfPyz7szb93jFuiV=?&~VCGva9Go9p>+XDAo~9)q+RFn2f*=uD>7dY#$?~Gb|6y(B z{`Y5N*>57ZU2y~tTvx~N zQK5PRg^%JT!A?m{0iK>B)a9T{O6?2oMcoXes5WgW-O@d+_rF%hAt1MqGD{+LYCOz+ zy=>F6eq5N?OSWsy#>^)~H&{Uh9l@?FF6Wz^97V|voJ#9~$`JOva8?ipy(3!TD)}gW zS95;$RrJI&eCPH_loMp*Es@ zh|FpxPgq~AZ{qy+m$a1s>K%W%7o8a0j}CF8LSNWIb>*9q45CZ!mKEX3d5RJ9D$$i` z0!9#|G+x!cVzCkYz{Cj>0IMf=_crD&mG1b3Pj%-u@fmW&d2?~Z*Yh7&MET$y1pZ@R zo}sym-T#jNeKna9fwQ)=lu{t`pKande3ZSpgp;)}leb|9oA5R{jljM&p^7HLt6M$X zyQI330e)PBk*EW;IS-+?Q z#i9HehJmVu{Kw#`*xZvw&^AnkC#}b=Ic)mGzprmjMK6sZ8@Ou^KB9d#LOz{JX)+U6 zZNh!(tUY95nV1u1-RjRII<2%s#L6fGY*o@+^GyN)gmwU>N|9m66%q7y4(^Tg zht<5Yl2qBf`f2s)k5e`~sPCRH;^SLKv$$Ks2(5(4@pzJDH$cPVZ*wipgyGh%?RTovebc9L@1W$ZKADcne`T%?&SpP zr4t$vrP04@{`WXG@_~b7Eq;#y_3t}<^+_nu)-J@+E1K(qs4?oLV8L$M?BR4Jze|$y zDUAV&^z>ir{EL`Mlft+nUwG9yvSD1TfH-+E7s=v2Gue8OV+)Q@+s?*YYR)A5XYIti8vT>J)&&?c0^kFSEtxfeJ0A2FdRNMj0cJ zRD_;pGQ9bu8@qVN_nYRmQk>?G_{Qu{YSc?uKGY+!$Egt1Y3bY8=f@+G4?SUuCun*XlUn>dCzoC4+uHi454mUpjG#HtsJBcKB!+rTE&3b3^)CbmI z{55qFBu)uMX^k_JWzAp=+#mU|<4Y51=FG6YS&wz)66+go zvCvv2(d%3%v@hY2q>p0i-r}JgZXc`$H2>GkU{pR;@(^fXFGQOMbs!PLxex-Pe8;K` zk`l;Up6yAD#I$9`|NW(+yk{)z@glyvqTK=0)U^A9-=$uKs|K-os!VI&lV3<--=GjG zVI{NX(rjliH<{fC0s9D?f6_aQr(X8x6ob}_V5N3y&jA%ol1Xr8)5Np$pUUl?bn8nW zV-Vq2d088!211MR(P3qzPn&&Ti2xitpn zuJPIG;Ff%{wc9BR>=TDz`79$}b3QrP;$A)jZt{ge8-i$?$+Jf-{m5<}6mah!=Tnal z9w}$VRt_l@Ec*)-yP+ahpCUpzuW?G7T4+9@xFR8(c53^$meA&NR+0FngYHq~v!~!P zDGlN!pTNvsG0-c;ZD!p#<@dfmy~!?6gOIp~sXuEAP1GEnIsW^e_*3WWn90tm zP@yXvZzaqm9e|_*<0;05*e^`^n!Jxc_meuIo+wR{3RGe0FY@i@%|?Zovua&m4oZR2>(*X>TZ={&A{Q+m3sc9tIi9$6E@5gn0grh>Hsd~ngHO`1 z7!$1TVvEPz6-V)P|79}xY9*Ga>o$1yTNHQcB>xIbD-%f6r4_cUvg;Dfv4ZV4gbuvl zfiGtaGCw@;Ji;`Pk4xspMCWJ!_q&SZoBKAAbn(xT_-=*))=Gy=yVoU&&|dfO+Z!cg zB;#wi$a5vUWS(;x%UhOKXzs1SY7{-});n2aq!%8E;%ALp2r== ziw23Iu)t7dzRh%_XEy%I;kfgY?O(i8v^dLwZ5cA^ zj*^YR|D6A+Vsf{0xp8-7el2gReCMlm$WO=TdO2sxxrFFK??UqI0eFHPgWTwhu-EgH zX<-F@0A>!+aksb*08xG}$J4bIsYBoKqC-o&k(U~55gf~AH}NyWDVqu1SI`;8(R(^U znUHp|dil+L$sx0&48K1?Xy93nHp_CZL2mDx%tw{%&yL!dfZY6C<`^>I7HtnV|9~p< zx}l%v4UKh!Dk1ScNSPA1`ndN$AHLNG?ho)u%TFKI{m@x+pU1J=yJuvpGu3f!3ZL+z z!*koFhJ$E`Sjvk00z3q-TIUCxBRtf#>+9+ay4n~2Ev($9N*?B}T|EUP5%nJ(D%RJ2 z)F!xO@wA+Zi< zJb%f?{x9m($USK4G#BJWZo@^jmd;EzbU@j{qG28xTq5rY@^LwgeS{k{VoOHDoz7V=LU|G)Vrv|kuQKA zp$BVo)_}lM0qyk@c3aAvX{6b@n8L4m>j*-Q;iC){*d^62# zSY9#sUpG?EU_4Rb9n0yh^@bW2yN1%yiuBg%JV?fWlg=4dP@n_^#qJFSg)yFn1>5f0 zBH8|>Md@2b%bmmkbD`*2-r6;kRPe|58LlUko*ci=lV2$SCw57kUiu0e{o?b7RY_&o zG08@%Iw<@JxNunFdl`_?6@FDv0rE1Jf}oAe z;1fQb@1VJX%C4W&T?~_kvX*knzWZLQp!bQDV`LV4Nv1M$*7uOH(@5<`xA2!Sr1f}# zHViuU=JzWkj+Ic$R@zlwp~YP8RBk}hdi;Gl$;PKoPk|FOF_*d3ctmYrU`h6Y5$r8R zf)a?1`W|wAMLgM3h}hGSNywJ}zwg8QzBv7~8qL|OW52u;w!t>j`)OtOt&c(WHz^~& zEMVf3HP#I&2J6{jW7=fk<-G+9RtTe?O?5&SolDu|Z2XzLgH+6(+IWVJrm=-krueqX zsDE3{OGmA%;~iz!O*SWqj@rd+m#GgaDLdB%HOgi&-J!$4)r7;fz5=pcZV|&q7@!QT zU6mzfx&u((>QMDO8*N^Lx5&2%F&09%z8#oEdV<|{R82)FpEe_}wgS^UmbXiQ^+K7G z=zMY7D%>B&W-~ZI3Fhy-@K^8&^-dk~7Y)@$s}N~<_(&GB*+l29OMiA=s=IHYKc;^SSZrK6~`C@gJ(^~46!E&2|bd&yOb%0 z-AkW^uqU^SLu!1yLMh!JNp>!+A9hFzI6}e`7!3AUPHo}V!@|mTHta!si_jL)ysjDP zmn8a;YNsd@8d=p;cV^k9F^l2qAT7u0zBCrP3uH?Q7j)bVQqYeff+#^zHjAm~P={|t zB@pNao7T(-xQ*~e9&>#wfd4kON3Yi#3^=zvoX^dWRhtB#OE&)GE&&1GbOqRA$2Zq_ zgiD%N2PICMD>YxXcARa;Lf+#eEn*=8iGluTP3Nz1s09UEGEdEbFb8yWio3WQ8FAHY4e`5g+yw6YxVPBDzN=#!#*mzo|^EH!V!bj%}6i$Eya!MIcML2UV zYSR6Qomvfl?s<+$$%zuFj;bbnBP3x##lQz8TZ1auq8$7iT$OswQR;|&mNY0))Cnm+ z==l^9Z})<}Qx<4&-_=XSrPp>i&;YdN#5{iki`R84%g4>LS&8?+YmwnoT>BHgEl@*^3SC?WmaBlWgY0sBXTNge zYf+(3nYF3padch0xm&*P|6}P*+>+kc_5ZWKTgy4;)WQLW<$&abEu121jPIZYv5r&1-HajQdf7!&e-7Q05an^nD8_WV<+J-Z#ZDM@jr9CWZL$C|iH{0Y z=ukKV;X|e+XirEGK%;BJ%e*f)H&=bSpGB;}`r)J8OO2Xu)|JLg$bH}*T`$=g{H@CZ z+fk2Yxa(@;FG-%ejJ3f8UZO_HdS!bv7^Z54jkwrc^)V)i8rKN^`NuhliZRu>NH)$n zUp|T%QcT^BFwMHEF^z1DX*j$pYQzWtg`4&UxT4KG$Ge00!qKo*8>}01Nv-1q-I?B$ zVQ9|Y-XF5-qgzB@H#f+`aP93S=s8u2iU|3suW2D|V6{;_n}6_+^0kU(OYde^{~fJq zS2f-_Z&s^^rpFz%NY@{9qhp{j#H(mMhgsFX`xhex77{oz-jY~kKSm{ZFe|ihH#Vu9Jl`$ey!m9zn}-b>uvHDN(EY3X|KT5I8GD# z^7fFEuZTXh91N6Va0Ug^nkTyPulYYuC%{qwey(@+0t`3)CeKi?Zvn9xvJ4S#}mrtVhR zo3&bLJ@PEp^bXAMWI)_G{*9)y!Q_61o3w3XXVztZpvte967GY>fvDVq4!>Avc2J|* zN)cg!m3XM13$=fuO0gH$w>sddu&BNvWZFZr^r@Xv*+aAtHq>%BC3$kSPz561lOS!# zrzuQl6eVYW(m^(6AW)r%IWyikm?hbM4*bgmm!MSzF z)u>gJuf;_S_R_5q6(6C;w4cYbOr4Ty!C4Qjsrt6HVi1jN$bM+DlAe!ZYp1{P66g;U z%!%INwFiM6r9oR|O&fRXeJpu{CYjv9P1b)t{fBcdF_C#;lDe03vEPO=MO@$fon=}a z@^wz+6b}4GHvcOxur-5=zcFo1nM=K3daS#PPkTX?setm?g}+j1IDr}>!WUmb+p}>l zL|JXyk*k_7S&4M(6!y?Mh{Ic;h}x#CG_VKNL25yN`s07cD*cF2Y1*21I$`Q5&GZ@% zKv|rc#u^MzU~7>Gm^qetIan^|4IU}JO*wRp#+p!5x!e8+Jr3A8!|%V1i}cFQK(Pas ztUy6>Vg&{XJCN#_qP1(9#)CmEKm`rU@L?Nht&v>I3OQp`XYue@oc9V%fvyt-Rue%dn^ z>wYGy)+V+fDuUJIwU~3mC?S+{$mS80I+Dx-5>IGeQyCLC6XdVLcIIxMuAsP|wqiw2 zR39HdoulUp%Q_xgaaH8QtAgOfbfkt;uS33fp{6(Kw94iq7Xnv}st67{_u;ZI_OeAF z&ZLgKt!{A~mQMOuopa-$YrQbHK(a}s7jXNN1^l}v_#O-83;4ByF!lNAuK}nFq{8L$ zkd#Yeeb`#uvI}lXnEC}nOMG#xb6TP*W`l_?)s-b*230KD0)Xx9iGo*6e8-_(bvS#n zpb&KgK8}CS&FngPI(?(Me*f54M)`!MWs#q@ebH9v1e}2T*eF$u7KcsCjMsf?*4=PR z=za5gVP)TF``ItA`m9H6)+^qV0byU%1NQ-f5wF^U z$4Z}T?&;`_Z~me)5~hLocGjEU8A3G&@aK|BC+QpI``ZjtVKTF>FR~dgxj`$J+|`+) zNZWmZIf^+UvEvwiQUAw{=RuRb$ZD`&A5fUWLyn^(dF-wl?piMpA)8T-(yY_+6SQwQ zN!~3CYxQ~P7HUpZ(bj;6wx5C<`U7!HWUtc~mhT?-R|LzOM#}cDp}$JT4>2+pXQ@}r z3<9cJxRAkj_jx0@fAm>$I@fyvqs<`@8^}Mnc1ARMqz% zsa>yI{5>+iBt+H`Xg1!p9$){I<9o!AD!`nIw_z6$_fFzg6O>jaZx;5tV7VDzZQ|nu zmXu&F;W`O;yc!lgIm`t{|Ki>phPR$9ByzXkdt$aW%&}+t#(}t|BV#d7?)qWNuo0p5hQ-lRCc3hjwzY^hho>rDPKwrrsF6X^h9V(5)eEAFN=qE6d&G99Qj67haVtDs zsvm_O>VA03rePMD1;$PEQBNY8&Hw7&d_vAnGF>%2%ozSv{gFJs#lj!f^z3iy%|@SE z&;Qe5I>X*BC^jJF|Fo|p!Qr_MbS?aei4eeN1KQ?HwNGk09I~+CQcKvj({@OhKx=-! zIi;xGWuyY+)k?oo+yqG~mDINczqc)Z?f{{5?EXDgr(n4qF2^b<7)897yPxmEb;V!t zQsUbLtKO6mbe=Egf;k^zt03}Ys(kjh=QkQo;oZ&V?3E6OZ%!TFvQL9Xe{QrY=&H1__u1EjV{=VcYHp(_pdS}>I#yr??u3bkrc<>Rj{)1j9= zYQvap@T|kE&P=x1g@K!q0$|D>9GP!PxYYNoJ7u#l=YdGi*djlP(ZikEpc-m^yV(i8$GwS7sk$ z`@98acxK5apTii6WFDbc{Mm>Wux7?B>S>_3F&h(`ZfEaTo5N^T@Zfli1RYe+<=sO* z^da8&QHJF$*((xU^Ixbu`jk*|VsFjYRWJCkuz2-SLY$`}M$1;#?}xNXp*sso$+$xC zIwRV1a(ZE4*}LSm!zh?&vqT0vYXj2*GgBG}dUpH&&IeD-iJ!}t+LqS)GB2pb3OwTU z7`=ygDkkx>gqq6mX(7cgR((W?blR7LUK|FgW10sx^R(^7yW8ikWL6mItjjd(R)zyG z=7}b=hSAktoLyvCR$&Win_ghl+@u+$O+3Y)X?F3I+9p0u8sW^;3*qHn^fDqM!kzq^z`^S7A zDCo27;4sKvD^@4N(9tQki?-+GMm;4i-xTQJSn-GGLU^xIW+GtzUufs^8Zw@nGDGTDu|-IoR}? zDR(tKJ~o*qx=3J3|12AD-?%QmtFU|}C(Wd2PTyAk)Xx;K^K9Sj)#JD$*ZNbCf_wdt z0?y%!E{k2LxogpW{t``>go}CPqvXNR23hajY$wzC4!%}#2e9VVlo#iF_|KT#WctLj zvQ567vgNeU2q@$|AJ_aEQjZ5yu_PSy-l46y5z_kJJfI z=f{6&H&=oPqbR@k?Ppf1)c0=`vws`Qx24qY$vHk+IvZZ9+(H(iE@KuF-F2TAOOQBx z&=L5afw(RQfB@j;ZZYNY%-uu!N9Bah3Wk2)!>YS%&)4gi6E|DAQGDu&o(TSSDB}0y zL&1p3`I+=J+hZw#2AUnR7-upICH&M{FkM3j)XA0r$^H9$3F@FV=VRf5q0-bENu<~D z2}QV8x`YYgW$)Pf4;gE{r>?Wo$ce1ZIaY^cRiZPq$jM8Y@n&b1r_~$}`NmkA)ev}- z9u_>v217ZtHo=}16S5*TKxa(3VofT^?h&O#Sh}va7sxtyH3)jEjCPV2?Fg zesu@Y^rkWdQTTun?u(pep?HrsUjTn5Jd@`+9Yb9oiP(G(AZRj9ZfJK^+T zVOC|XZOUOIU8|web}EPARG6G~xvnyXlaYjfz<4p>fVBXBAF3-z%QIq#8uvQ$09&}% zPF~B_eQNXV4~uW)aNTaG=bl9ZvAq>(9nb7%S9Ym?CopsF!==!r0Iy#BRce1&QJbA+ z{5e}}Fz06ekV=$e^M;AArK_dHpM!i4>)N%QX)3Qt_o~~DElE>ew?5497ii1p!YQX7 z>?f-%1JEHc{2o%ztXz9kS|5m$TDHeGH{}BG%s8=AM_?2cT-M93r8ud#1*AlBxs=Uq zqX@wp*$vxrF-lOc0&1ViAmM{(SeYpkC#m7yUh<-;s}^cdL+;m9j77qHikr*khNY&s zTT2wGzk+E{u78b`CZ9T;adNwwN`Mkl$>{2-Y$G9c)?R*v#HCNUQKZg$6SEqjg1ex;vZ1+QWs`8f z6326;W?eN7>|B7y55+Kb*~DhCQ%Qhe`p&%Ur%PEX+HBX)n)7An?o59l&6h1Ra);)9 z%dnQSlnbZ!zp=FrOI^Z$u!tH6D9X^S6(cNUM@Y#W4=SNK(kIC$*P_#Nh4puqBI=;q zAcb)0Jr_#R4?h(2XV$rKx2Mp*)bu}?hez~UhH)tUof*9<%JnG1heW&4SQ+`@v<-`$ z7LA>;Cu4aVN4Ab}Y+^lK`F6U)%$*sjBz4vUPvjNK#5Jsewb@r2_kuZfcuj9iy()k6 z(g^wqxpM9BqS0O3+;ochF`OCG>e7l(Iw3ri`c$f4CX6}-$!aNEcptE_=k0OOJd30% z57kn7P=JR0YRc|pe!OzilM3xRs5Xhv4fPc4^fh&C{O0TY=u%gtLSd|pSIq;=cxn4< zgwgHQs_Z!VS8bo3-q4FQB4G}1<>GrjJWUh)5x_-zrnuR-KX+Ooo#?dlA9aDtpp~&D zAZ!7>AZ2c5!M&jP55f>;U^I7Pr%4)hq6L_%=^Pyw;0Pf5#LN=^sk$l23mfQ|FJZv`U4j$On&ceO}w{7 zpTP>ewg!j9r$$^U*0c53Y?JS4uYdnkfaEwTkS7+rEHuQ^Z}wKrtYphN?SI{CJqskaavrE0mI| z>iuYAvSZ(lpbWS3GS{9qOdxecC3iKsNu$nS|AP&1-c5K30Lz%q^(JNSpBtNV z<(n)iwEyc&RXVaFWZH3&$kK5L+xZX>(h z?qXcM$!CYyu(i8;KagQdAp&0e2?~Ozu20NzoT^TS6>>|`qLy%QW=#}4LG~m<@EwFz z4b(GZWXM;CD0$~kcsX)plJ}pPUWgAOq_U_uO?-mmAp-)KSLx`=Lr3QQWrIM5tp7_3 z_sU^9c2Ga$g&;mVsN}|;`=tL90Q*c(QM2Dcs0040$+q_cM^hP+s#FW>OvKezwgYIZ-pxR{A_2j7x_$~QB^=ab3luVt%=3(f zM`E;HgXq3fX|ls#|BWY6)OCCK2>8k)@2&3T0hQJO#-e`u?ysR4P5$H5%wLPzZr;8B z7mq^VNwST&Z!Q?2@RjPh=vvK3Cfkp=cYJwwnosK4zb~p~?y3byCGWJ3+d9Md-g-`= zx8oXYDqw2TIgX@0>QJ~-ENfvh3nu(fpFXvV{^iIvFfU}(%Ms4dIU#_|9ZzmB8}{_$ zYj<>zN!Igtw4UHA4mOXft7^tx@I_|SMwFFdw7XF<-imKm? z+~MMb{H)&pk5_^5wk$+Q;sMUyO0j#rg9o4T?T*Ex;F{iEFZ}DAMqHGcP#?z)OJ&Bv zll;L_fRlDf%nOxyVsjkR7@|=6UNu~F<5r0KmXyhY?K6DK%F&l+V}aWy1Ngn65Rsl_ zjk}N*@lX&W7<6?>TyO!vl;J= zc+OFoNDa*kq)x;Sv}iqkwBG!=13}wT-kUXZr<~2`J9!%9=51hh;^tm^YK-*>=&8Ym zjhK>SW2~`|%sN_|BJv={(tF7W!XGRo#^WxV83q2wh*;48OflBsMYKT!+=w9rWq*RB zjD@EvjJXJk#u8LQ_99+nwzAKwH|N}-f{tr8#}e3%YOlBnOcv=BVUDfXk-9J4ogcLt zcbp8T32oRdyRb%6*1|vtoCO`xr~4;zq@;q{w`4yfRg3>woJ@QAe62Sn z1wykg*R06(8*4jx1{9+E!-8Gma$-6=p)R+8n%%WI=4*Q^=7U|(mWA;^coJe#?91G2VDrM002-;IS?x3XM=H+J-dModk{soCVR}z zTD)#|Z?%16`co~z4}Z>r0<@UsJx6rBTOZDu6%x}EpX3;{0E&4YUkdyg#$M#?v;4Ys z51%Kjd^rRz3w(ia@q^*zRuIuKDaBm)YSl2s+0_|gNsK|Vx68H7LTtn*iv4s_4_-O+ z{LG(!{zp2ww9Syg^>hRT31r1_pazp! zz)NIH%F$y)v&3(X|z;MD`_z<95h5 ze(&^&eW;lT{bZs~>Ckoi5M7mecF20Mi8o^2c9%taY;A2@uTW^2XA|BSA`L)LXR|sw z_|WFo6z06Sg0Vv*xvSS&iHuV%V9HqWkRE+m=O$lzmx{>}oEPVDF8F);S6UjVoSOYe zA0M4Hn{fpj-|HVy^q2cniXBW~Sq;UJ=Io?D()IILeWIE>v-)jquKN=$GB)%tb6|N} zGrS7jotiODBfLsJ`PDL$GIJ-wui0$4f_5e^TybOZ5+SH6Tw1~?A3-0{*__QHo>WqK zUv=e|d(Tq(Q&-36Wf4Dy1IsX%q$iHO2(+|$4>_f7#mIW>QX~&83S?GhQ^Pu0oBh-- zm(~-y6yZ`#!Wr{(dW@ovw&cjjAoqfDIsS7c{QZ`T#_!7vHATbh&W8z;CL(AxIeA!9 z%XEi?oe>%(F)#+y!Dmn2lRAUuy#$@@tLU0= z_&jjTg82SH;Pf4NCMZ+t+$(`b~PG zB5XsB|shJ(c<7-$gJOPsQEUk^B?p)QpJhX4Q*e4XIgwFU-3TLI-^xe#FWte{1g#X!rR=OeVx*TXtQHq+)SKcr#5_VY?bm& zWuZ1>$BsfTDCy1Fr!Lsk$o?5n^R!9o_0Xn*EI-^9xowd3`Tg^D+hO?1;7p4vvVV?Q z28ug3=_A`oD-i9^y)lFesHZs=A5Eltb}FB?!zZgAw{DCMXOytPaXxM3M1u7?DG@HH zI;}?&u0=WZ@eil#U|eY3teAf4a$fteF=r+xr6*k}@1}sXoh;wV_Z?`}vyNwaDonKa zWvLvY%T)tVi?H4<{xPyINxT_b>$`BZxiQuVeoQoC=!iBQ-{81)CV{V3X{Z^<5uh?vR8WP*Xz7)h)746%W`XV$(rSLl7r@uX?atAxW z^S=prv827nHYZO(8YVkHtMJgo_Ne;a+leH*_;&pLLe;`Yt)-iK!i?S)BSO^M3W5GbUgDzZ%b$`PQ2luRz3*UF?R2q^EJ{~Ngm~`r6~Dj zaU8Mb6@TSjHL$nGr&?WOdDrbZ4!$x>uY(8pn8I}Lj~1%jR$@JX{F%%`AD6*tEEuOr z+^7}s)DmD=vB9ucBsgCee&IUh6-4edY9 z%!%1_q@SYViU`{5RNAG(Q^_5O2T%@gXp8EfI~WU%2`jDKJ3+SExY5n)TQ!wF3d7=h zt@=^d8Oy%Pg9qTXSG0FaRi`FGbwIut1qxtp81CMbuut?f+UarH8g^JFh6y`)8gZ2I z{_PV`v6RXaV{(kBw(n%0Ezk)|_iI0FEH{i16ePXi_5zNZXs|hwFGsHe9D1Ok5mXyq z3oQbVQ!2~Q_qcMUrXvpJ!|BqgpfAAJ6)Yuxd7(Z&Tal;=WD^P4g8}Yadb=by*Y4{w_|Eh+u32_o$E1gtLbD`Jyhh#VMiSr zUaN6T!W*~;xQFZ(gO<%e{s6@;85pd@BGgv=IB4Kobxrcu@TBLD()f0}uN-nC#B@zZ zVS-n}6Id-wLn)vfie(v@fBL1;`!VFcocsqJyV!%)bU)MAUI(W6vNJ`*4^OX6J^z+f zE7Oy-CF^Wn-LHv%V0l+J+_@Mu7@8YZSvzM`7AG>UG>+}c+MDJb5REEt*BwM8i zRto}Qy>~*G#@L>iE5g?;HM0S|+KPIzo*a4|d6)!#iD4IBH8C+2+UkLf*MqD?f#YEg ze!hOp38lL6c=1{ZCd((*?O@cld7ea=FB&w_^AdndqqAYF4nS`kEK7GjDb{n*q;N-) zP&LlrM+k_9%M+G9-zC>&yV?SM4g=+S#s(zyQYDm8rJxa1QjiuX819jv<}3~R!7#{S z2GsM=HbD}Xbw)!CnRTfeAYRV_&7SC6<}$v%G98p~FD;YgLt8upe48nxLd_Q0cIoY> zTv#&PSz`|$eGDj=dn~yd`Ec_WQ}b&T+M4$IE;=)icK`CZTNNUZCbdc@)R_`NAtWDk zX(?JCe^(D^M9%0A?hlpDuLqHj53>RqS+R>OgfkNd;OnZf*3vx%F2Xc-)%uB?2d9?( zORvgZ8Y)x+wZy{)rF2MQ1P=VMpp9tig-hDPu@eHK8#j86%26mKzzZ#=?PH+717E5gg+>nb; z#e;t>+{jUh9wMKj;EhOu+(KN|Pp_KFm`fH01bs#r_4JTag=@_Ru?}x&etoMqE(t9; zDR|`0plGJZ?4?KThUNe5@KOYCgLmmI18J#pl;X3-3Tx0alWr5!2aZh_$wc( zAN3i7Cl<)XI0(Pb-IeSu$YKt&G-^&auhZ;&yTcD6#6-`({nP_gx~fMWmHe)=q!xPw zCH)S`hwX-Nkq#dtYHtkEN8;lOEdgbvPTH(@(^quPmso612DkW>x;!LCP;l4r2@+&6 zNi{$2Rm&OUGg)adHneQ5U2^QX72t=yLBwOP8Xg4e-)=nHy1`@9Kc(`jr2bKKBvVVK zKyHMD`L921FTeO_O~}F!Ahs-=f+L3BvDoFBovJ^vWd2TIaa=QDa_L} zG_-A}?t~ORyFziNq?+#gKHz0_JIpG!$LWv=Eku>{0h=DAhE>2AP|ay*p><6^uSIen z$I@a$Cx`yq{EdPAyFXhK850Si%B1ZUbIaGLnp4zE-@Cc(UgojS@llCE5w?TRuD7(o z!38ePRqO-@S8ss%8F!nFXAd)x+HSq+URNleFh?mIyt0`=oZd=$_Bp^df_L|BP_ zS=f^Zt}6<9+)o<@VRrhI9s4Zp2J%<~5BQw$Eo7IJUKgdBC!--2a0VF;mdayvfgFvZS+k>YQ&eCzck zEFW60>hbw+i{OBGPdQIxD?%lPNiy|hhzSfO^`KXna4zz%KAIs_P5DoIgR+JBL!mDh z+PtCJt-tH2SE2*Kk{XQ1g8yHQ>?_W>{LOYRpBgV7IWthad zFKUaQ7i}$d;OXNRg}@yTWN$?659MSCHlhk!rsqDC&KH`B>%P;LY(vHRaRssIqTy$K zc2o9JB=xzW`bx1jDeJK-f-kKZtwikl7y*iz+O7E=oO0JG55F*Z&A&wZg1sup8wMu?)}#xi|F zd>h3|EnzZJW!FLKdfR9UT!^a>>37F@*5m{pu{yX&&x3crdRE~OlQ)My#`feqZm(;m z9g7$dgZKnpw!RV8SM<8nEoIN#ng`W4r_9BWdzR%Sby z;1sNB6j;ap7dY>KXzszGZh6{rzfW~(ez0e#*V?0(?v(fc{+yJQpdHi4^M z+7&gf`C2{C#b=8&vj!ERkjC3f@1{FBh6Y&jUYOCgqp$oe$YLfdU)ob>E|mv+S!!O#E!H zg*N%IBD7&vi(CP%J<+i?a86J+iAdD^R(@y*7cq9c1!)x|EPeT#<_iK<+$`YYDalR+Co2&=?(T_$Rsg6`#_A- zT1cJL36FG0@tQ_5`HwPOt~ZO(f-3uMeYl&qPhm|ixrctayA^x{Dwk2T-pO@x>UDXT z7hNEwz7H4=AP%Y__oAM3trnOVc#4NRTjIt?>3N91Lf^yM(rCx|-h}(Ahe~SUW@25h zMUZjdr#~LPartm_bNi;Nz_*7cWyZH3vg9l*Esoh*+T?BgrGDryaN<{3M{CXUZEL+j z7F7kbqW+TDUjMtAAF=}K6H_}Or;R(&v%-3*z0TbKcPu2{LN4xbn$bGZ;?g<|(H(jw zjbHZH_I*G=@*+r0g&>;l0L!NIP8X4Kb=!$Moq^iI%w)H3+EJmuO*ULPd9|oi`SVX> zr2^Q3tW>Ko&5w>>NQLGNX0E+^M}oX>uCdJ@h>qaiK?Vf`SUI5({FByZ`Mxk)-{}zv zjR0wGomn$2a{;z(Y}@p>oIRmQK`Z6WCIeK>Gj*SnEZEq9C@OTsOQP%8S(Zw+XR$At z%PzNnfi{?qQl<1?E*yuS>|7ys)l>x!LzXm@_+tGG+hd5i@jST!WGU1h8}Ir zM{dzSPVu|J>WHW2rsZMrU5z)^a!TmI(EtQk`XUSPmbLpUDDAQh>u@SFGet zXUv6~@~vvsq>nL@uY_xlQ@cB)cv9Xag^85F&Mbk#ly36y)0}Aj9E@!S*MR;qR&LhE zzWe>tqWoih<86HlXXX^gR2aDA)#9J4x1keC(yD| zP$VB)Vk`8tN-d=X-}StV^lFFC8X|V=61t275tdrk6k}@8zpPbcowq9(^#CUC0KK7Q zrm20cB<|YW&0@SQ>PtB53gaj-W?r@B-8AY6v2F8Sfnj-%wZm^)ZY~9XyxtQ#^+S|j zGw#fxP^G5n&iu7$$gn8TbG%PjuL-yK*khBBp6;Ial|7`&@-B++4C_CH%7&O%bHjEE++Y1?oKv#2d`m zI~)fOMR6&HD|##Lrt;b(@XG4M#fH3FKOb(nWc(pZNL@l#pQ7X3J#7oBvhjsZda};- zSX|JStZj-?g#|VQ9|g+qC052sqZ3uJOWgKoHb-m8k$Hr-RV? z-xI&9l&#<&&))*)0j#!{F#M6Fj@Gs&6_T;G)|d)!ztfi!oFQ-QaWIq3%NpQ5|E67& z&0A?7G$W*KeXF~CO*iLn8|H*YdKnov`Y(kjw=?Xk(;0q{7^Jip@-!ZG!HxJ%xjIzP z=WFBVi!>KHW8D%OKAzfa=S~n+Lp4^K2Zbntau7k;`5)Pt@wWKaB$3nMpafUyRSXb@ z>^d%SPd?67{U$+WU2aH_2JeJ$Z_e80SzK2Hwa+k7Z88y393DZmpPm`|a{Xt`L;0y5so3 zbLtL60jL4d6N5O1P)l~D)93!+6DRBV@Zlh3%We33_40+FnL*lLMzqspr9w=nN33y*>diI**^;^H{@b1hkK}ddQgq`=3fRi z9guRc&aT4mv=wWA^_l&DUcE!}IB0E0a{Naa7|3yrM6ErJ+7w{pDJtI#;Bt2V`S3<) zwY^E1mhRWfjPF#bb&X+OZKQsb(HJGwDQ2>@%P7&wZG9$dlPN!bmF_#pVQ5;0v46{; zlcZZ*_IKXCjhpWY6er}xl*}g>xI+Gzu$l(=Aia_?6{<#Tm0M~-&D_IVM%KQi{Mh8n zB}Ch?Qo!=F$ohyF@rz=!kJ{;I`%Yy0g-~*TvgrPmtLv#1-sxNeABPw~Uol3x%e{hL zrbf+1IbctI71m2F%2Lyde2W69jhq%Fc^)nRjHS%EIP<7LVIcnwB)S&Z2YR5fj(LK@ zrO^Ize%z??W#nS+VB8iEAwHUmo5;DbuYH)6Zed}|6N&@%a(l~qtJL_a zapk!gDuj;C(Q!-DgbSq^485qf7U)ArloNODk~Alz#Op+akZQ2=4i&McGi+id%q~jZ zBF0lbp>>n58p?$_FBEFV%Ov@T*E87KBzr{L#zI^0fh%I%JrZYwBPhGww)|hkxH{P> zM)+_ALNSlhub^So?X=SJjPSX{B&G)lkmB@m=rMp-T!^CN-O^I;paktz&PBEpPXiTT zlj)}01!miJ7ZYJejZ01kA6g=+%&_)C>j#R^+Nzgn?V-nPB8 zOZ_4=&U}ovNi6XPoPJqfUXAw*WVA5DtZ2afElrDn> zaU6+0g^%tfLYj}X)z<5$=?#NJ7u>#zHSRvXQMWMz)}BO_dso(w`Vns1I0P;V-w_}% zDj`Xb!O$leZDf1W<117tsDH`mDcb7L*1t3GAgX0ku|pz_xLbE8k-|CZi^T;sS(n<_ zwVOUcuE{NbSFFhhDc$VX$z4a(0($~q_d zi!bKw#*dCF-MI6Y$(!FF2gcaryGJX{69L_RTf0LKJcf~yQ@UcAI8HVxraBv@x?wIf z?^bM`x0S4ewoy+)?&ydMDNX|N$A-fjppPC_+PTz1>QM_MYQ+v)D__uyeiFQ1M1{1* z#RNBGfw?#FL0ge8s79wXT}ic7B_(K2W{Fdwztdc@PwmOCC-hC{dgI@t(l`uAM)ao% zpKAwq{^UImD#;ecr=4Fc2Gnm}S17R)UiV@}IN^uPdO#grgEj^ne`uNVhoTdm6Lc7p zCGDQOBG)As+j1(JeQG-5rpOr-H58~-j6OK&$pU+ARcm>EPj5jVN7^Dy!UR-8$bxmp zB6ONS!)I<#VW~W4VJ<#xt~H-Dm)R77bGypnwD)Bd2IA`M#eC3@0kV=Ul-4;WZ|$f6+DG z_mbMa&i`%W_+*!^R)3ggdgjD+oLE9%e#UWRUt6iaOT`J2lC)wJCunxZ$(;dNX8`vb z$I@8kQFKo`9VZ)@=th#NnVg4$nlaCHCo^TFjLo!``>=fen&|oMEB2t)83_Jt&wt)F zEyJ`%!4GP^3A6S%6lw1fNgCo$C0N(c-S>TIORCy+H(JjP9_lZ*w<@pOvcu#6Z3UO| z>MNyFi~0iUvbgaCtsdIFDdpyaWKWQD3FD9BDIVVWJn%q^%v#DKhNsF-5Chyv^yZ=9 zMq@2&p6CT5s--%j?X-0;hweDSpKlUpT!@MxPI;HHYTO ze+Ay58J!kL4f-CJ9gu>FQq7btdvDcbIpq?7k?B}`7zZgZo#tYKT{S1vER$8Xb1L%x zUcIE|sl6UsJ6KkC5oa&_@yh4g`kg;@^ecv>Y$T7*gbHGY2@vlHfxyonvLMZB!0o$5 zX?{$$cLBgEYE&B6Sw2J`Jx@B%r`vA(4)@sQ76a^~DltKFR%Pzab_0sIcxdiX^W zw!SASJL+|h{=xs{z5Y*{e#zu?=2STer=x&aNi6h}`FWkkAr4zxV;il3(k8%eI;C29 z0tgVcy*nSIXjKe8W%))p3#0g5cz}#=FzoX1&F_4*z`Lk@v3#0MJ z3GdZW?J}NQas2H5O9VhtJYO0}qcjsMr;zK#j`_>9Rd84&Ir zTB|VL05u8szgDS_m}6trbfwDQ!{xtrL0>|;eJZHOA8_(ckHQ2QdRYmC=piuA+?47kJ+C;m#gtt?z+YW zCxfIaRObdi2r3>m)IvCMaYmu1CCfdd3>N=f~wUX|;DoZPq;otM@=(Ss- z$Hh?T7fSv8JeYNa5Y#-2s#UA@Vd%|Q(sukpGC&5>w=4l|)Am%>W>H*1l9UD(-)W6} zQtxs$!>cFRvobaNCbPsgsr?(YZ3^<{VbUwSb+LMS`dSkU5>viRS3G%mZPN9}+wjWz z@dWreyMaxR9{x8-6l21;0ACU4CTXm~B*NHW2KziBn3?queZ=T3cQD{9B-3DBI$l>u z^zXs*qQ?FnFt!Ry0PDGjQL`CYm&A>@tj__uPKC~)Qo?;qy7qr5^Yg*^3I1U*@X9(Rg}G6%kFQ|Ra68MwYyW=!)z-Fn%{$1h zp-{~;9TX%0aWk58ti3AJxaz1_T_6`?KO5f<-C%vY#r7$&RKyIvd-*3w5_sUOP=^yJOKsx1e! zjosDW<16veZcOp;=%cvixhVob?j_3g4dO4Jj2P8mlPqR0#A5A}+qigA=l)K5aZ`|GwUQDNf zW~n?c81;LgsMsuurqJ! zCPQX);aQL8Opn2_;?v|7MVL{*8IxqbeC!S@!o$V7H!T-0w zQqjy)UhOr!Z66Uu?e{v%e-0X)vjxP|&gI<=ZbK_tinDE_7n{cn@sq|E*@dXQ`=Z_< zB|l;+L_hz{-EZ;hm2a~1*RJYpNS=M5tPyu&UA^$igs2G6M_(mtep*!efDh%2c)E8T zXhJ*Yx?($o3(wAieL?}g!5vCgRsIyI5%O$?wC6VX_r$(3g#A`84HOgPcvD4xemuY za@}|dC?G<|x@-D1<#r7zxi0qvuyYVHEdSom zOFtEVZYTWwa@q^M6v|fVPxWA>zMiCbIZ-$YQ^=VSo2-ZB`u1G5-!Q`^kKqw0{#2fA|?dTeG+eX0E+BT3fBAc!#W0C4i6Gw!bP$x|w}%=CD| z@+nsY?KxRh|8y@acmI236t?bSq?|-bgLDnlQuu!POib6X2*sR&E8o^kkF z7#&C8%**xClg_dX+M)LZsH%x?gCVs`P3I;&Qsm`XAp$8f#M3+!SX!^w)+vdS66M?9 zp^@b51uK~~Q(i`c>U0&GNy!U#Pj8cTHRs?{FXMi!|1x477~nbjBFX8qCr)rXCxz`& zD_UpCeLZfdY}7pc^U9WTTotBqO%1l1xo>l!K3n?M{@m2GW%=(!q?NUc_jvY%%92cI z!|%;ixSrT%>h4a9SrDR~qbY(yR^t;`kyU$!tEPoJ=Hbkmc^DOEI{lpAW=s^Z<7Ju$ z2a1BtI~>f&4sg?tc6NV8_&#b}XT8yZi{zySp1XZVb zlALfQ=g-AoM)mLgv~i2ok z^PC$1KD_`QmZ~Y;goC}L-eYPzo)=i4D*GxoEo=?H7UNs%7sj(Krdvq& z5Z|rAo-NtmM%88W`uG1a*`#>JXuGgj>C;Zt<8HQ!!jXVIPw7AJbFjQ4`5(rHldXKS zYW&s-IykKbU0nhk4h}n7B z#kb$iUDjK%BY_dlxl1YWov>9>7;~*~6RWsq4%gJ?*VW(=`;O&^p0&F4h+o0Ad>{m z&%eGcVID`Lh^dH)u`$X>+OPhAF%Qv#O&DAklnsbL49y=jk{BC2qLoLTaz-K?*kt0)fVB=8&t@yB>=tOrs;d~&)*orJya za`)$ab>y3Smt002yoZJ8SkB;rqc;~x61f?7k_b8_HVIkh!@aaD)E7ZQk0=mOgMI!b zJlCUI;XWlJW?qdiid&I`-b)RA(!GlIWFu000T&90^6V9|3-*H3!4Zdqzs3xQGa6P@ z5uxvE2WZ&>X%DSeh#pyjfA0)`*J9cc(hKTKqwd*R57X5W0IlXAY^}vCR~Vn98n|0* zKq;D_0~BkHD<90QS4Ll1f9m6FpRgA8?ea@+{9~@Da?qGp*YEYx;CmR@6z+AU48xVK zN_@M}l)0Gfa2zGMQJbw(GGs#n9f7}kw#NY;xtz6n^#4)x9{y}D{Qv*s^E(|Btr??L z5~D__6+KboBlc=hG>NDY1i?8c5j{%vdt0@M5fZ7^tX7N|bz**0HH+9{w)eRup{1PC zR+YBD>;4Dw$RpRtb-mxO*Yl~df<#7qk*!tfC zepDYK+p{Onc{p(C4nQ-p*$EqKvKADr;KGa>m5)Qc;lnxLOD=xxL{pqs67;iF zPVw}yWerkEPmlcXRiESfiH!x}YpmNnM+PO94=F8T?4VL1C7gG?&=OrK*@1m3)Jt{^TVuUCeG-^Dj3M09Gs>@sk1*p!zH zC%9Sp)E2C5*}fR16dMU$k!S%%=RfjC)i`EF2Y9p@Q6C^-QR*x1y75Z?E!1DNQTZH~ zau+JzDBX-}l78;dVzHXo8+N*85I5Tx?s)LWPM8dK-Y^qPWJZGSBtBUT^jt^Y& z!FPtyQZGx%gb+flvM6rkYZj`z#l(s^Ir=(lrmCcm{q0n4(V^NKQn36kssvobD*#5lklB|1^rc^_qhkhn|~RD+fE8>>&R+3tZja zUbU^Phq3jTs&I9eVhTftf!SZ3@|PE3t$w9!^9l`$;@*rr`2XG)RF&9i;F-`0N2O?} zWuv^(+z8=9$aC*{?TcfCn}zy&kR-#9TK)^G*RnQ%V`2s^XyG5#I8bJRG&0I7M}o#{ zr2Ifn|2Coiy>tF7p&4^<-v-n`Fl~*`Tg&8Ff-*3CASQ@VeFhWi*WiJEY0%8a2~>l! z$?P+o@wKbxL!Zh7adz&Omf`G+F}DroFqUZfBehp)vL5XPup*C(#})9 zw$by!)#fBV!fGD^uCO(CUW%1*FG zlq!xk?xJ7AQ{;H}%`bk-%P%Xr?(gSl6Ct^?w5D2MH8rg4AJ6m=L;>#xNzgJ=T$B{H zwqPp}T`eUY2xU~Cs__Zc4^D1mt$2gTB*JJf0x`?!VV4`m!+vLqKArY0{c%XW!HlUp$g#>@W2ZG>^_Df zq8+7-yTYBC8qg7f*_Mqv0>yJ>B>*k9e*tl?A18-KKZp8*?q?-{Rl{v=M?8B@hBRtubk_q_~Yq;AL!lP84 zR3-rh0z2EW}tURsQYjOXWrl#KY)k#;Y$qC`}Te9w7Sb6Wh-+AK7tv?TQr%4s^X zh<>X`#s*V9zcJE%g*<%n<})_$&kCXywJ* zFHJ9gY2fpg(cqx_Xp$lDiP}bnmkx<&Or@ggdtar!<;Wk4=8%&-XWZzA-Vtw#g0AJwiu-z2ERxn8h>2 zr0Eu9KyCTE7YrcKirG9%O_hy6;?oY+mE7L`RPD%J26{s93+j_3U8HqI^U*LOAVSSG zBIshk)~kQ8J)e*mwXCEXOGgDlyVYjb8Pc;4Ifiv6xFXlmogmo||E7!99<27)FAA(( z+F_ek;uS|jVFdsg@~wr}8&u-w{q3U-=Hi~M8@P~K%!HL|x5L@gtN_I}KeZ6TjfR@V zSIn5SHUcH2T07C5mxlUwrR%f^-bJNJ)P1tkH= zyyTUN?HdKk7+p5{xVHboZAD^YO;3PCJYS63uOy1{k-yh-SlP51ex zFsXsY4SPt2_rvp|p-WB?0G+&9zuFcnS4HfS9W5(__DSWKIW$8Z$>`ZyKaB31O&PJe{>mpctXp8kbfZ~{Fw<=$^ zzU-zfi$~~*MW=foe@NG{uReI_X0C1>zI$GuKjMY+v z6`P)bPuJT3crUo)+OqH+SZ_S$I`ax#*6>rm{AEuaLQ7vYRThM-&)@uY^5jO5R~I}{LOMw9a%S4 z$Y8R;J8*Q(HjldWFS2G+UH-qN#&v~V;G+$j#a6%a?dLONo--x~6Yw~Nxli1>)pR{! zOyLQ@!M*UKQo5W6Q+)?9)GLf|7A#-^e6K@B;{}brCaWy{TNdSZ?Q(FS8kki1vOm95 z7fP|1g!#1UlwDTnKM@684=QlYKA#}+;D%5a<`O)llNtsCm3p1|{3C zU7^+Xc62)n#D&M`86AE7oBB?%V(*B4w}?q^Yta^Jc<6!<5b`RXEDOLVNgRiC8b| zd4!4kf!BFz5pR^x`7n1QVLwB9TN}{UKyZs|LXDJ%lHulLN0=P@IsH4*AqhUW(l$O% zCYDC1C2=E98=^agQ%@qN4&T~!6@+moVfv<)>rE{EfKsA=YfD8(Ce+yKxz%PFPS3HO zPfo4g@_&_Re3;9D99<%IFv{rV$5IfS@2qi!{=`ysxBS4w))LcOt=GUW+(?L2jtXR} z2Z_}^TlgjOevv?+oG>}d>9!b0FoDpdZwb@MZ2wNu8c~*QrQ4XjVV{>RC8tR_jOzua z1!NcEK%az95Urg2p+Kt<|5`t3TJh=2p6%#C@Z*thOB#|+e)8`a>E*-^U1J!Tb3UDG za}A7&<+^ZD;iq3^Qlr{G^m)D?8&b1coXjT^HOBOD1nU%42wWI5dp@58jWtL*mu-&I zeE+eai9Vhlwd)mSPs!mvu@R}xZJ-TR`BcjoDVgmslOuF2_xhoQZjZO&P{rN%b6xw) zEPCwO2!U&CKBT~L`+p{>*k_IfX_u0oou=MZv;)!P^^!d%DkeyoBXdeNB_lsGN>Z~X z%`X`0fZ#W+b5gy|kXX?bI_pL_L8oETDRIFk#JM0}=HuqhNz!WZ%+x`rL_^liuPL*x zi)YIJ6{)+=J}K_05W&^--%tA<^e&lOHza1KTy1;OQYkm)`t*Z=TsYaLvwv`NqMIA4 zvst>}a(k~nZE_QWn>dUz>E?G`W^f2o&PcDSbrH664J+3V)q`%RWsh4V%CJf5rq=Fd zfN5vz#S;UQft{~^QA)TRnCN@A zGm9PBA6Yb9kpzh1&^gJaT>a?iI&2QL6VNln+k^5@r#&3q-3;8=z=qdb>Se?_h@pyL zndU$Vf|qfz_BknRSI!v^HS3Jt&)@#OX&Za6QPlzAp|TsBcn;KQ+FftrhyT^NIMx;N z{B_t_bh`<_SjdhD(yY_w0qy(hAYw>Bs#JKLq{MJu@iEdsMx4_EK4X|Y?@2y3#mUc} zkjQ$>vKFkmanWfdKY?q3ZOz%WE4dzK*U$%X4vKL6kEKi`kNO)HZugr-_* zRax#AHW+MoK|LDv`l-`mx@|%3xo%c5;*?CAq`#%kPyOuaTZ=KHrKuLPyeAi8m8tLN zB-?&&)gq0WlkU@%EaBQ+d_(5U;P#RpGAEEIfJLH3cXNTq`Aw^^&loiB|7 zUSF&&9<`5K3DbP2Jtm?%WhH;Vjc`2>IH&#oqjN&&dNCr(Q;Hf1uC}y7^qpauu4Y6W z^y>FUTJ1DEeDFE*O7xF{J#{_8c`yf6p;Cl-s%KN56M<;MV%aSCcJkx9W=J=NNVA}E z8Z=_wMjphS&jOaKtU?k{PKb@HGaQZ7%Sy13q_SD{#XPplH1qXVn4)4~T5Z`Ou>R07 zcpn3uf;Z%an<#My7ThU!w+wf1tJ)J~+qs(3Ub?OgXzObAq9^79S3co5OFvKOueON2sE;2xaC_^Zeh~aqYz%F%l|h6o(wX`;612$_-@W{9 z0QgY0PRFCuz(dfh-5elDI8XUeB=rU9I_FjBJJ%|YG48;c;j*#QoG;{1F{%8p?Cn-p z(3bp{hg}IjFB3!jzWdr_)MEwbRJyKCL1epkgjhAgi=b_5S7nav z@PTT|VIkph=xsJkUs#g9Q8JX;CH}bSkSrH{Qo`|XY^VtKP3chXXQ*hz!Fpt(I-Qd9 zjPgx!c=HuM-`$id1EG23PdR92=%uM%Syl-RAD=wR0Hc1*X9PL#2IiY>=6Dctlq*fA z?)hX&;eGx2koF#!nI2C5Tlg<|W6eE7 z3C0J~0pP|Yn6p08-+EJiV?ABvVIJVo2Z_069y@p4x6KW!IV7hsk>uo`xeoh3GsIVZ zj^l;kZz_#tB_YFC^r?flEgm0RO(A^tF)s)qg`n@sE-{@HK_WPASyEDP z4$2h8|C*6MeZknW+6((I`*Hf$g>s{j&sz6KO#v3>=aFnPBfoxV%%_^H_Zf=`Cf|JtMY2zY?BAD5xnX~y4Pd?=(6*zcoXFk5916D|)!v;QMll`tKN}c*;g#tZjdS>2 zrn#`X8!Hd&$4n^1qSqyK^&v&Lv96u|ks~oA2e`1(KP?Pg@fLEbTd0dA6DhGkS9`oz(@_Z+l+mj!4>dpgPcsejqwb@moZe*SM);0U6DqgQ zs8o#Hs35jrGM|N+)gTHRYxwkWgp_fT^Mb7m4<<#^hQuHX(?V)9Pt3L45?_R9983;8 zwukQr|6JJ4GqVOrs`ZI>S+TIZDAFHw&_hGKyx*G-&okm(_*A$K^FON0mdtSXPEZ}& z!Wp1`L4+iOlq!6F8F3sKGo(Fgx`5@QsxdOAIml;@TU+~yp!}JSJHJB#%~5Qh_#aI2 z+E`>3UCx4*VY-bp^9rBgCbtGn5~WBLB50<4MS@4{?rC66M>${Mv`daUOCd#`CD~S8 z;`c(0QKXALAF_e@!Q+~pX*1KwKR*^+aei|$;PAQ{UraR3rh8oT&&c_ChPA$VvR=W? zNJx?eTX%I5+A&TU6g3r{Q%HOH*j=@HFHf4BNO*$Agy zF?P2>yi!fs4ERr<8ez|TV!1~$)U#&Ei3753@UyWL!>P(RK-#Q)94N~ICX`g7ufp{l zvw+2@PCcB!f4R8w^Acq5fA+@@e|rM&=!|tUJj_0aTHF_;#BLcZcsKW!Vgvj3&2cIpV%%t9el)X6Q{3uV zm1l!G-jkLZA-)i6@ujZP&8|O@P#O_l9`eE*dT2oKt{8dI%NS2UHG?A+zg+aX^{W2N zc0G(TE-KV;>})0ln94{8ek`QJBSCQ_h7HarYF$7F<~f(-X8rh3uC)*tHhw1l&N zya3YjOkba|h^3U^Qk-I82Du|x4dn5jp#MJf`?Ut0!=2pOjL$ljEZosx49U54XY(m~ zE3DyaQ+5l=OVFk{>rJhCaPU#-Qpb;fF0gf$>?~Iyj)wKqikd>{dj>I)D72j&r znO7We))0y--?oW!1MkczQz7# z<*7yF?ulf$n zGA7oFxk!h*iMwq6q>D9a7f}FJ_`%@G(D;g2wN(KndStQ;6)HmOGCJ3imAwdyWq5aJ z`L?f?QRu@?znUR|$xbD1a`}%JXP9FjY=E=?>oU>Q9C9wW@-PSf zaHhC|eC)gHaORz!vqeK)xFD@<93vAFvlKPjBMW{hWqdB9T34w1Dvk@AcpY~ZogAYy z@p1fseG`51^}>j}N|V9m$&fLPN=w%5+R3umqf25u#NDQ|A|a;n1puCzg27h&VuvY* z5vVz=oMVnma}&HKi*C4IoAB`GYxPVQ4Q_@sM0tR2{)|jdx!t@Xref=} z4O1?dY{tQCpjL+j)4D`r4(MCf`EIbeLHz!XfrVyBf8=`0sm}%qI$34JevgBcK5!*D zmFcx>BGjA52&JS^XTnj|$uU-`7Uz3=nx+y0g4z57n45pW_mISF4m_&x3sw6fI z-sW14rWcI+6Rn$WY9z(Cb4elXvnAb5m@PEeorXK(K=fSr}??5#!#7Eoit&Q-^L9c_tIu5U;G<51=0O3PGrm0%%<2k=3i2wJ1?3@(lxy7_!{|(B6yATx&&%0qK#@_ z7ogvGT}MKNg}OELXB(>9s?Khc%LzVC)y?n7mD~OT_bfA4^SSuZVl~0g13!~>IErs71O`6xO%(pvRGQa~xU@oy;bJN02;j;!_ zpr_t@*rFELB%C=+QNj4u(HNzmP&MaQ0>JTKyuMT2=$RvZ`aGvx__Z2 zHb`XsQ*HuGoZ(2wtFlpx`Bw~(^=3?_cN4ML>)5g&QNgfG_56|xZ_dpm;aq0+ zK2XK;CYsiZ#-u*ZRcyK|Tf?t4V*?F5!TQ_L%;u!uV+kgq!I(2CcQMziPt5n%^e3+O zx20fiv(8f#oLsV-{7cl^2mjaSgW`s+Kx2R*>-g(!lNKa#i2}*JgXm5Cp zaT^coVUAAIr1Q(pC^)1YpX6l$$6(nk2cXF*ua;)I_w}x+>P$f;8*1tWFb&NZk!)-P zIYL0TF%$1NT^}%IsXA~ zWQ;&7auVWYyf=gHz+8{{p)t_bGWOg4+{Q{NNRf7;EwkU9rPy@wHJCU_s79!|OwJ&+C#xh$w`Zb2_ZCSH%sR}$- zhHoz!`Ce1<*y_)Ny`ckl=^`q1rZWXZMSLbyNpY7r3=x7!57fdCUyDiPqtmJ2KKxs2k(^T;-ah zlP7~@{41etnB%!ijaZjD&dZ~t94~m2@pWM@n}=3TX;#{nC!{KuANrn}IbkMQJ~7A*aKxF(4$L6WaLqaXQX?l{#2Z&m+y7j+{wXF%CmuWgI;Q5- zs=l7)BYV?tqymjHyc5$m2pOXrE+tb>^DMR5Cw@_?cy~Lfi1wn z&if4`IqDO7W+^e=(sWAMli85@#jy)>yA)1|q7p|=Nan1o{K3uMBt{f4EVZKn)54sv zUfT%cg6gQ%K`%Z53di}@X3rMzSj(~lK5ehf>Mr1|zF-r0>=~POAHgXdKtcs;1Ix@d zWYN1PqaU>=EOH6!zT5BnnbYI8501V{|G(Q0zJAlZmlO(RPrx@V*t+~Qy7uDpC&ffA ztJ*RaU4F(qHKu>Zw<>nHm~Q+5cvoOofUD&YMfYdRQUxEj zRL3!1jaQ8?MJgVc1`4Rfuh8cd`4w5ciFoNDtD|4rtTGIzdTmveW1XCC39{+%)*xMf zzdrCs;!VwQX9L_M+v0k^Qe?htze!eCg}i6?5Cq2vyskkhK?N~8Wr3EmdDK4B^VDh+ z3qGf&_`*8+uPWJ#qILe;qr$xftsn38pu})&*LmSmXF_xiMhHF((Afj}5~%D&3N6(u z=*=xI`TmRPfl37dP@zqUwbDA9pMvM)t!L#Gq7n66->f}^~4{4nZ4{u)bQpt29;Xtr1aL&)e{t~ky&IF($ZDdmCadjT&1%rIZo(D1HkCdRi0Hf@e&4Fq<-*h3e)a@2X=zuKm$;Y{opHn}V&OUA$`gU^L zAsk#F%1pP~%3Y3We{?R^7!v5^c&(8NO2L8sT5QU)J-zwgw~CW>GZ!P;?7oruZ_lT_ zS$o*>>|K(n6z`?)N>+YYA?rnF&_S=XQ=UoO3%J|&COw&>Uk?M%kpg69$U$2VJyf0- zCtYl{i;*&CZVV?E+Wh#->MilZOH8@S(O*e<*>{@e{|GUJ-`0fpW&5@?gOdHU5F{Xx z!C6s=DE|E9-RnPcm?l$98ZmdtncsZ#9Ky@w6nG2S^p97d<-O@Gv76S3@#D&&Z#RET zJ`R%_naP7F2W1%L6-NgPcyPEIkgk#H-|0oX8MnoY-{h6v`ze>oE2 zJu$@_U<3$!MKoe+#jH;OeH~Pu>PIk9Fmjb(Wit9h4)Degpibr$n!Xcjec+!!c-_GK z=ciZaxG-U&*AX7rStA$AaJkNmPN92*hpyIjL8Pw?FMieBiMjrqYl;eJFNho#s4qUW ztladJ4mT3tf!}?_LDr_UL#HY#JE~Tyw^grd4gY}dOm@_cL;zelC0K=~_)@&V5_HY? zeO0%oeS1oWbieQL4an(dNu7#q{H{{X2bY4zHoPM|8-=~}%I+RlF14#6EVjywCp=PX zewj`dult3P+Cu8b6KMVbvdIlPkvo)s^lN56Qt*%g6GkZzqL;~9Bw1)rHG0o_U0ir2hOc%MqYO#cH&Z>F+AFuuDH49;v8pfE5T|o(%bBbT zdN2L26WSADUKKdMx@m$YElS4W`?#Cd5B}qjp>GR6_pZo(VdBHHZTWV%1%Pv$Mi&v) z4bj^m{-9H>F^*7l9^~cc>u<#Z9dges*o--VYk;?r0qQet4Dl#B^Kpu(wW>+C}kY)qQ&K8XEqYPgcw7>$lOFi#*eYZ{bY_uWPfH2v&CUd|@7bMsV7OP@EkIb|ob0dZm zY3?=>jH3n!dO9;gmT5-lpfC8H%Dn8nZBXxB+y(jJkg5z~!FBbf8% zdduV}L%dpi`^7Kc#QYA2uZ(vKq2CuCe6yKtazekmXBcoqKgKu}dSn~Q7@6bS7PF7s zh&NEBbhm8rdKv=_bpiy0Y7sLEt{gZbzcWy`)gD=inVVcQ{_<-9$%Dg&t2Jn9elK$o z*gk0I10au`UyX%Ep_b+J%((W-C&e0?UIKq`7g7dKCTBnnXXyBrG|cpuIhP)r9$@|`uY%4Fw9?Zc{#7sSaxq zx}8gS#M)aje@(8dJI`fBUR{`pLN${~?h(-}2_26tgTE|XV~c3pgv$(@b4ECjYohf^ z8)d^G<#^=VEvW)5q)TcX!Jg*Tum|o7fOb) z?r`5-g}SYqL@iJEfl`Bk2^6-A2L}b(?*{Ac!M-}0sPLETC3hOG?)P%kqrIC72+^>5 zY&gF=BQaL0sY3FmI#n|USojQ25GE^9Re03c@^0RlaNX5}(;xni5V8`ddl;pVf9iTq zFUGAVy#2x7>-?$oJKx{`xI$~@nzdk$@C$OS9^c)0*3U*;R^#VC{$aA0lamcE2y~ZK z2|zwAX)WDGAF$`0B`bfproU7376hn=8yxQULgA{=I?c?aEW>ik-strgq7xS%h4;P6 zZF+~_D;x#lZBf6Mi&b?%ZQV0dE{aP72JFk&BB)H)qJ2werxee^2MJW(j(qOLpURBn zF?IE_ilI?@LRnBq3q6l@7o3f`J=_l7MCiOnP9q#}V1v^KyJUBH-ae`H1nC1ibcU)K z3-$3gMy@Npe)9I@yTj2s&Svk&gOPia5Wcz4#GrmPzb?a@&}P0#Yp%x8$j4{My+QVG zjJ)NWkoGswvu!l{7DuF421qxsq&t!UI{MW)Y0Ki%--FIrUaz+REyO3k9m%LNv6ItiQu^p>lI6t59y_6d3HlF+!$0qp|eI)*r z7u0D8)@^sD0T;zw{Dqpddh_qwX0GAh)R!Dmw87!gyFVp6 zqee{UV;|EF=D{WXn5Tp@-9?i_D|N9y zZobaV0I+D~KfZg&C`s23UYTnfJlv=zIz+}g+N?|Ykqg7}mebyp?ZK@C}S7Eqn#au1_^to=T{(&QEn<}90V zhpe?);i@yXhXc^b<$inTtqA=c;vw2gS!v@zM6ix%x-;ra%Hc%m$Dsgxfx~E z0@XZjgFbkBmUrML#`4*`_aLtTwsISWk5H`)NV3T^x}QhJY8IU@SXO?-BXLk_D*kGg zII_*6-~EeR7OG{e3^bD043BwcWvAv?NJ63NpY+B+3KLqf1M{{6fIddu-v#h*6m_K~ zOsa#HRjCphR|tc5tI35wnlr&q-ly7E1LSZn>1f*sr{LnW+$H_J(V(}wXJW|iOR)R8 zjjJva0c!RZ#UKQeBKVY#t7GtBd{0wBVYFirmWuF2hKsY z443B^FXBwLil@SQSM~G9ce>po?e+RmF`BsRJN~Y_1yb9cInw}9 zaRi(X(>O;O)KPW%%z@#~>vUzkKn7fc6Qi3kxEOBzlZEqM%^0TztH5}rvDfjtL`r^! zZ&g0w>bKvM>4tL&k%I4Gv7b2iXCTVnW-%-K-hN(v8eF>+kWE){;o3Ev;F%$Ds^X;J znY^YPq6=Lhs>Kg5!YjqK+x7A`?+g&_Ua?%n?uX8_oA2+wYn*OlZS;&s&tZc5y8dbS zK0amiW+?Rs?J^l_Su`5W;23}pXWKq?Kc|)`W!24M1@wlvxT?Bvt_8wftg&EYkRKqR zr@Z*qS!~I=+1ZfHC%Ld$euNvWX3vi@L2oQ|Hv>di1Cle)I9CV+H8ch&;fjs;cx@M2SLlX#=JG@Dj3PKV>yeJPf%mBfd-n(ZFx?z?r) zija%0K=-jSj=%Zb5!mG8feaa<6mqUoGGXRO>ss?_q%HZuyX~_YRZQI5Es+C^(KSH? zA+e3WKU(tRKm4i7cZR-luX8kAi3wYg97pj=y)*y5@f51;N&G=wix)So10YVcN4dBe zCv_%0vaCx&9cchXh1^oYqArM^hKcn;BteCzS{oAs*+Ucf92TyPa-VfW?TlX37{PT; zu?>cE<{|_uzA0`M)IDm6OQjtlq>6Dr_fs{dW~4;_(A$eWU3?xY8g6#?jW&gS^Vuyp z|IN=BakhyrmW_K=TEdDJsYBaj@OiH8_+5O;`R?gDAZO0^0tc1CaB3eE{UANX$htuG`_SiWJPy6 zBU|O2S-vu@=X+;FOFbwvIrwh+S<3Y-NZD>*Ig>^>{%X&;xdEV* zD@aGM)oMJySxZyTsAl>?fn2{yC{XOpj=2N(a;ypj<0@K;0nKCmno|)SejUL4t60n{ zLmZ{}FcKALd>9I)))ynBnU1leMsU^Hm4vq5^UD;`ojiH{<%IEi`mKFa@vvW4L}f}l zXu&DV{)D9J=t?yG`ESJbbi~$Dagv!6C?DhW7jz;=g5sfu?|!}`x4sBz+(7lqQHTNU zd1OcICu1~L}BjQz!Y0{9W<8Y z?I>?N1x=r{tB`nbi+2f(vxymS>5FP-2FNoA`Z(R}*zpWSN2DQ~-C0CmO_sKH za@LJQp%o)F%O!Im8jo(V zIubpfv8pUm7->MS-B$^vaEf`7vK7iu5n*=6(<}e|Hep(?SvilxZkoXc8s!Iil@bfp z+gRtv;zjK~ARm52xYm!YDS)-U=5X9Ga$mqMIaQ#))7T4p%{d3N>QBnN3pLxy z&&Bp&WJD|z!HCoxPrJ;F@u-S`)!csJ;}lhPEUPb}6PL{VA%4)zMF2s~Y%xDH+ywM^ z=d;Jrt-r}?@}uSFk;c^lXDNa0PC7@w?kr7cFLed0?l&Fvm#$fMT_|VeIJe&dG}?`U z=AJ!}y|xPllsiZh=Q_HhN%Uw~`kmHPo~i>2n?sPQ za}c39JNM!`kZUBb^(jDlox}AJ?UwILx%YP)ocQGIYy%K}#@J{0Pj<$lqEChS#bDUd zi!{ayzW4M$4{pCRsQyw{ZriH40KEt)!yGWb_PW|%_)K$`J`XW+>9dLz(K*X48!P5@ zE&7O=sNBac2R{_E^vAnaqD=ma)RL-$T9H*`EO=6$qAmKW^DVcgwAwZUTxqJOcx~xp zt!Ie-9AwdE6aNMe5KHHM+lfEb^AsY67u`LZdsgGfckvNSigk3gtj5GE!gc;UnlH9J z#DCkp`TeL$>x8~@Wv_OGczuz^p&_mA7B3fZ%`?g{Rb@xM>JdHr49LE>`JT3a19`i} z-HTv%g2gnsd*FZvb}=({ToC|s^bjM*-3AGiMjQITkOJ(nAMO$8zWGwyC``4(H^nzc)3$e{hXJIXC0-3cqS12 z>UD+fQ^`ix;L}smXaq~LVWG+w+2-F-ZicZg?B8IU>D;GMSQLYRjB)fl)?73D$wMP> zHI$?<_sy}=K{rmo02iIA3s4?-WEd~Mip|k<+Dxb$#1qcoeUJry!_n`?t)?jW9<-ZH z4?P>WJumF9l|c%9+oPtj;#F7pZRkY@i~T?9?er@zFVaf*Tfbvc=|zMat*>7Beos82 zH&ro4FK}6piF)B7g|@8)uba4-)$H}(W;6)#Jp(bU^%Lf8hkM5dt-`nVKfjRfWx3$t zradusNnBZJr-`*ev(x;wDn#GRNArw)>nSo`wj6%FV#@e%VMfQ35R_uz-+ESGn$lZ} zJGyja<_^EYG0XLYfE0Y}+j;kw2ve{rIyW^m-llo=_zU})Uy|rflq^X`G7&Ffy#1G} z?*T?eL$Q2Km1rH}YTr&CkC9#>uY}h+qEnjl*Nl}z+C6%lLn3k*4U&@HKn_SHD+2gG z0lD{xQo~R!~6ilPfM!p?N z`~1}5D}4TD?MP}dBZ?c*j|tvgDcjb5LAYcS0BwTaQesI|>t~p2*OU<|y7|wJwu`f~ z2GsckPFQaWS0Y%8~-xTVhr0kH5Gtbn^4hi?BHwHr9x&o^a$+ z=J;#^w{*L>m`{*3lC7Z~5=eZDn{|f2nvq zq7u>qpf1JheaoG2uXq}$;!W=Z+~p9_T8P`zB~0`4|cDDs9!!{9dYOsop@~_b3J!0 zxX1UocxXA-F{jR#t1gjcyBBvZIl2c(t9Wc75$)23ZwJgY4|`2<2}x_lq7Ytpsp;jh zIO2!C_D=?xn9Zpf?0%t#jlP?pKH7fKxm_<~oOgNADwBFc@p*A)XWM=|ot%}kgKMKD zdfYw>3B`H-76ke|vii6=h44(Af*3)u3tx0I)N6Mx*5?(*|CQU3!G)!?+ zvptXxRw2mUI`sc%=}f$m-uHLE?m7)ll_O4(Bj$kSgl%j(px|6;N`P9S=E%=Z1>yY4^0T73EP8Q#zHydH8v zkhy`)ibjfiJJsbe&D&rjI7CcIc^_%Ml>FD9KYu^@mwd46<MQe9w%XADt##;hgwRGy`aiXrkJrz{9ne` z3~l9Gaof>X@4mGdR0zjFHC>eiuML&TI=@z7V4HdVzpXqsoZ!RFtF|Po5LNCs-ob6} z@={8CB~aUkjEqE-p&gnp2(jAR7?=d)s|Jqh$xikZaNZ;XlZ0O0WI@*0 z(}5o%;m@y*4#ZopzJ6cM=o2VZ_aBe|4&qU;mr-kpcQu7*Wgk}y?(;boM+vm~?$=$= zQ@xPg73PEheL(1AzG>Uyqh zZ}_gw{yo-0oIR%`2h$TBcM|<1xcfA1X{MSJRBwGnms>IqAzYe(K~8n_%VVTZma4LT z%jeN1$Q|-5!2?$bUw-zeH52qsb0V{J_05m{nPRDI%uoZB@3 zx%+GqvXAMohw7SH@spl~kRy#N$ZiA~(E`J|U9YMx1BMs;yUtIM}U*Q{_OwwjAr#!(H>Q$~Wuio(qpDYL48TL0=c7 z-6a%$S$+EB>++}X2NPSL9{%`#-ok%cfPV~{X;h3++?|bczT8Y_Iui2WlDKkIeh44K z5~|3zX!570y?UC8?`S-Z{!%Z!eUU*v_zC_dxX}^g4Zk@YJ1ynIJwE*H=Gn94blh2^ zo5zIk7P+WZv-LC1ea6W`7Gv8K`YI>HJO7UOCow&PszzSG47w=U{eRB3`v-oku$nWo zQ`u6y@NBl}QuJdKV|CN4*lRgC-T04*2l*je>;IHzb?0;#AO?H}xw=iWvW#TNsn$lh zVe|!6r<#M$=8}!GNx2RO2S%lg!!BPZ<%i95@p-npUWrfc;GMq?x*qMXS^V`~f3Nnh z|2#bvV38r(Y5L5bT&mYDvx#W-o|vHDlyJ^!mVzwZ$fn(jACo5c|u6 ztbIuZ&BhBJgSqaxuAcf*z74bA%8hC!wwN0Kc*+6Rddn= zM}yuIuu=o*p&J*l%`@nGKBN@G0KMR`D`*WXiwNvK4D786j;l1Ch!5mMdR0w7#6s@e zy7|%Re)9I|W&f&yvK8L=^5CQ09p$x8|2!%IR^>l#jz;$kOBd^cylh@fV2+^x9NTdV zWph4#KfdMYq8-`fRPTSPh|&Dcq+QgoC)5tC&N@eXvM+j(J$af3LQf9uxEbr#+^N>0 zeJje_{0$TAy=1bGeTP_REi1)U4>e?m2N^hf(zM@(P+b}D^-ayf8N6KzYC~XT!A87?Nr2h6-kKf=6b1Rd2zC(xbfth65q7*2~4&El< zXSAd-^gNW>e2_+fnQULW-;J?WEpE|t4Gy-jPQ$t;RAk0t)YFzH9zZibHt|LZ(-H*t zOdcKz2Y-Ha^^b1{H(q|*k!(NQ{V(s*fbm+a7LNZe#BU7fr3$@N+@Byup+7)iL394_ zUf}9(rB`nhI*Aa zaYY78jnq`AX|;E7RsI3O^%H-^PD&=;I>PpAsevKpk`|Plp@K}(C z@Y46Km27c$MQq)1~aH%yAuDLJL)>M=rC~1L@j$i>?r^-f9(6UyiRxSkiE_hQHZN zNp?=692(BE{d7nR*xeb?Sp^rqATfw1AyLnK zYijseyE839p!KD%v* z)UXl(9Eho!*vtcUTZHyo%hbgOQp^eu(Egp#Zp-*KJi%|y(8=w@Mr z5&W?)8m@1lMH{B$_Et0>=-d`7XIqt%jFt{WD$cv|UHsExk#u%abJ zH+cht7639?buR+JbNn^$j*E6dgJxcMWxpjpcEGrD_VjH0c2COU^_n55QNY%$k$wB# zr-z4Mk4`@Q&+V&!Z5~`$accLoU@Y_6EO?e^3g7^ zy(2tzYeGpJy(- zN(n$ATfp>*6>^i=pY=dzV2QZvR&%(pTjPc0pfK#A*4G8Ilf z3n4z*6Nb(($$^%~kw%>1@ieJ>Yv5wVOrz!k&pOX@zcX={7|<7!d+_o&45FkJdA7)& zqkOQ|Q`Otu(xnfqulPy09=>z3T{K2to9~WEWA4P+g2nhqpjA;xgGah!1if95}5aVKW!=H8rsK7!YyQ7CNAlMOSo!w zva2eM-c=^;)3XREFG%5O(=Ika=7;cXhh32_pXc#4%!L(9PW%dSgA5lh(d+ zh|mG5%miJ>()wg)o!^()8;km_+ArBBINOgEKqZ5(4t5TeCfTgaBZdB*2VY7gu0uRl z45P)2u^fdU5mKIJv)8hJ=3f=>+=DlNEcr^V6KTkM|L4k|8 z;p9C7R8&FG)=ys8$FwcAoQQHwbJC5?-%gHNEBycDH1d!z&TPDKK^d?Yt%bkQ(BTAj zk-?31mQ#~XQx)BgXfs0(E1|ahG4>8CoBg%>SU@#yJG64g+g;VdEZ9;Gmu>Yt*Do`X zvJRaQDwkA7Uwev0cde8k@D=0+fMI)Jlvh2;Y$=nHH3u}(*p+E5)B_`hnCS0bOPowg z$r1I}oz}`7*#vrq$DkLDqi}s#Vg)B-@6)5hgXJ407hc^znBT908sY!c#0!6Q!R>yC zu26lGJ?Z`{d@N71-g?n!=CVY40cm|gCl=eVXjXVyh7${}+~HO%IIHp~W~kU)v&`zX z%NO1QrpZ6+odpjb;Nzl#NgZWl44*8ZsIt<<&1R~H-mN}d>JPVj+q2?Dx?m3Vzv_@; z);|uyz#8DUnynSN=zQ7J8$Rmx9x*K;KsmrXJC4;mkzHGQceH~7x}VoK`Znq7CGPVo zxV{U-rnzKuF<2!@NbZCvXKpt-!f)2*YcHfs!mTClS*{`I13A^KO663wy_k{$qGUis;!J|{L(VAI zzsklNlFPV)PN{7JKy`Bo&`e68X0i0F9w>9+_M>nUqY`r_=A)S{i>L>GG(scey!65F zdTTr7~osy%!!{zCiqLRNF>UQ)c*#u9m^0W6 zwiZ4pE3HWCG8iSG5e3*JBzm;eo?e_z08z-h+T>ZH|`kc z&kSLwojDbcv&PJAgB?>sn(In|ssOxnsFYEFP5sv`pc7nnF84A)MaL>f*no0H9yI#W zd{4c0T+_;*J*}$(E6lhu(^TL-W@*Zs*E$?0Z>&MY6?-$ueC69hr=zsbj zk_9WhD6Z!a&@d9BNGSnO2^1>PBRgOYe$pL?ZoA;A%f++U2RWOVhx4K}!BcX)*y6n_ zF2r+JUE!em%A_&7opc7)F&eA6K@glqa>DJZ9qf+l`9-n3;kJ_gc2o5Z5pK-n%>-o; z`>}tUWN$(~!?59t|9v^WZ#i-?#gZ=`!#3W%iN<>GMwu&TP1jsE{a)VK&)iU{XM+Kr zrOj01xMj8c1&tt*wDJk&7S=M+V)9J=d$M~3m=b4L|c+ON1uOw zc`&*qp}+^5j@zMyJJ7*iQ2Rpch`YZqX-;q{8_O)yjaFHRpAN5rD)~+I@NS}dTOy#VgXAP1hPGBst?M2Y{Eru>UC#hAy-UY9ie! z_w;`p>!fAn*AP=l^g^ikjy8u7uJ5isP!{DqF^ABoJ_iOf;TP5?DrS$_;;ViVa`I+#Yv?F)f zxkPU^PQUG(4qe&)T!^c}^q$k%#KxYqD|xAzA7O>ODJP5I@W;}ukfS<}Y0an@PtSGs zT${)pBz<`oCNU>;-n5Y<$yvB-aIYpe!mCb4*C~lP7$SvRZdS3H3v(!HHCcVxrJ)^c z1gqR%XVnum-WEpw^s{e+iEfk(8tm8t6PEMk{^Mh8&OCm_Hq8-hjtwv-eTt4qlD7ZL zEnSKm&+gFXQ5i188I`t*o#62$3jE^gz#ByCDgy;2-0U76OX0L2;L4qI>`ET?a`0jQRT-gkA8fAWcBio%`X>UW*&W;-}?Dm zoDQDIHx!b_u(6$Rx3UF${O*To(&SrGa==|e$;i-9=OX*Bp(;O+O*GC^Gd7;0`4pR?nGS?_OlM~{DS`UgDcS2 zZ>6~A)q>}TbUxz~Y4zUfzp4VqVHkZR&jCL6$K4;*Yi?=8>8sW9ec!7CMy>@FM4Jk> zN6%9laQhE2uu#Xt(kBdNZG$IL9VFYg7?tHI{ex6h~SU4CNoXObONXlD7)6 z!s)5fWhqX)!q%I(B*#Z-Da6C8V(#*XCQb6M-6gEyi(pEJDyVFQ-|wGLwR%m8EI}>g z$P9!|?_;B0984>-_YTNc*H+c90^_+qzaM~K^8ffYukia8|KN?^r{5>>0-&%ok?8imC~aB$dNiMGaw3M`m0lmaiL9-*b@pe6E{mgVHitlaF?=Os`==+Q|pNewi#Fh;|PAgUNte7;zBZ+e;jJH);h{=8M_=NRyknrE9*T6N2Jiu zRHl8JO2xw&UTSuS0|xDirDYti;An3RAZGbOQh^7X^f|3!5mOg^#>iREh#>^F?X-5!TuzHW2uu3UdhI$AZ(2NBQq|`I%KI zw8P+HM=HBS_YmwisXV3F>ihheH0DICLNDN}{O?|j9hyHjEQn66I!AipKS%+0cBCbM zNMq29l<0_Pyp;sozFI?OpiAOT+Giwd4qqGtwZRDy7_o3j2tEV2CQeiLt4Q+O;sg4Y zw3f*uIcUF~;=;!wbHDq}@MfNJwf)NvtxaxuMRe+=W>)2~Zz2-MY;z!G4w#V1e)Hkb zcoPHj*ub%*-MDnYtBAIU0oLE@-K3w(v!cBM9y79npXg5Z%b{BdyccdV=%f|U`EoeG z&P*_IoC+j5ZfWj#LFHRk;L3@$?ytkD40a{8Hzy5Rc^ANe3x;XY@bh_UCt}^89e?K$=R^2 zce|2lE*P(V^SyJ`bV8!|)XWFSLaKmt*NAsIKwQ?n!iyt`-;##=r-8%tf;|qp2FQ>0 zfQR(>sgWU9K+|#NA*mUHNc|56J$_9JexeGf!^SSHJnLOulK6IgHPLJbTS)0Axj`8& z?^06@#^KIh-5|qLfQ@T(;OL@ZJJ5A;22$(hE_B70F#*UeV-u_&x?bRQ`1oy%4ab+=Y4kw#brbBdrH zoW-4IU$FSH{C?`1BYH#EDBW9b_$OWgpE8r-%Sn?))H-Sg=|xHovT;ZkDRxSgvY*#1 zgvZ2-!-bc^nr^m+U&P{x@G+^;uxkS8HzI>rFMD1X$7kNCM)gYnnS^^r7EUg(5)uN2 zP5RK?;WgEVGvT|70`Q0PrfUm5jNyR=QgYS!4#+iQXEEKHW{q%?vUX;i6nF9D0=_(A zGqP>+K|zZ6owH?wQT~aM5UX|X##^eKi7sLHY_wv`K?Majw!4XbDGrW@HC_7?n+eUi zO|}Zz&i{}u>W}@%XV}=fU^scq>L)hJ#_}iQYa|MCJ$NR7m+haJU}R6|3GA-VQDfRF zmzv<0sHP$q7i!Y0rq4J^u68%v*L-bc8_`{yknn!x-g0uaZh${vCD?rii-yOm*a^=k zXRJpjLStuu>Czf@Yg(m^In7!;y31e1ch{+wEc8Jy_6qU(th=$l=?T+1PT)Rpics}1 z9Nt4X*;Ht#JHZQl<0!bIOX{SaI|lYpSuLImVrASyI0oEfGPZ)LU+i>kXyZ!J*iyLP zuJ8e_cFBBAYc3?DDduql$8*Fj^y)x@AXfJI#&L2-^%gtmqS0_E)gSKlEZMo;Qb~cN zwFFE*GmngooC7L$28*NdNC3)og~Hv$BbXd z<5Xwu4(qU1R`4KKR%A~4#Q;i~v3d#iBybr|?C6K8V21k^NNWu80x3ewFBvDL;Dr4Z zoC;8h%z%h9Vz$3$VFeI!02px-`aYBlxlU8@%#PQ6{w66?I_5=Khj_@j-si9AV2Yxdhv@=l^I{Bw(Q{b`lw{qkJruC0QBwCecy6%tDk93Ih&Hhh z*SXvdXYheDA+YtkM!^ZL=UK)X8aieE7hSm>`Bq!V?v=an&L-6obmj2LE$%q-3}Tnn zznueJkbQ9F^{w1hnIySnB#=a!A@u{n4Z(mr(a6QhBuuJdhwA7CA|4J=bP>N^feqLt z()zpaLhPC7POtl@)!H=Gk{Q(57>F;VW&+)A%Kyt>$Yi-s+ZF>P(7Ksy(ew93B!}q1 zic>Q*US~_%dQ zxM|ZtW2S>qM}7h0K~F!{NV=GQt)~!HwZ)a~+GkvW4!)U9AVrif3%_U|mlZfgJFox7 zXg$MnkvxxSwHVbVfoy4HQTX9_>-3!T-22OZaFZl05pBw&xIi=(SIZyx3k_>aP}3g5 z%HnB6x3|_4{oy)MtKx;+o!@S?I_mNP$d(L@p2Q^@pC%Q&ZlLg++*Xl1KdReNft()z z!t|b?8{zEWL~ULla&$$}&K{3`=^$b6XqS9X1OCd}Tu(G~4QV;sao3`q<5nUOY$ zOS6s{mah!u7IT63G{oloU7J1?|K>kPiz zaTZd*k1xLLRk1bg)=r5Vf8aU^yfZQQExK-qHUH!(`Bf*0^Qp>^*^|B+J>R;|sruPGHj@68PNB=SCdg~utZJs2fh-Fwfysyk42;zjM0p)z6J1Sg5~$Rx zBqh{jLE8dL)5rO8K631eOw>O2OHn3BCkL4c1VSAA)YQN&5VauW4-)eqo4lXi1EIl}ejO1R7He=Di^44k7S1z(uLP6h?q(1>A;AflTMMQ5Hb?_! zgw)B8N1m}H6liPgOSat_3Jkan#$Okwzp91qHd4$aEiwN7;tkz+hD&o)3%h1={Z_M~ z%~t=W0bGrQ(H@#^F!h{C$%9&Hb7*SC^DD-aocg%KU2!@p1mW_qm_nXE?h>}^LDnR~sC!!c z^gQd#wQbXa?O?EkqiIV$>=9fcGP^1chstx1q#p@Yjx5R+z%xZu`XEAzRBH0B7(isJ zbu^iO$-9{wkd5ea)~WFa#%L$sG+ofa(1c;zOmj?WP$z7C8g&?#nBzPyf2n6QLXqBf zk7PMiWZw?C;J=RjF+^gxsKtP!w>3R|&ga=qK?=_$?Jn!;St+1u?|j`IN9D`}T}B{j z!I#`6GK^2?jim*DpMVj-4k59NH{QCM+^Pav8!H&(??%o9R0D-)5n*nIKJ{;}mz|hn z5n^DDo_3|Y$^-^suGX$)cV0rp}U4gIjugg%U2#d z?VR^Np>8$gAD6=n6u2n6;X@WjD^10p?Vcun83nhM#rrm0ZZR*bk&p({jdl(p`6K$^a*Ur3tO!y$m9(4#tV?~CU zn98kmUC+rhF`$>46BxY~+!OpRx)OpYGkW#38gL5)rp6SSUyJKEopP{KO=m8JpHCFq z%eND5?rSXR!#cw3?xoTXEiKj-t&k|NlU|z5Nz$n>5~xgNVaR@gI-ir-rDrbbm`)ka zqiD!#o!o*P>zNI9>#Tg*B!v~fDcOXv(}u` zo?flZw*osGb+@_SuNbESzeRFM_%lv9!Q7uO&#)#edN*@LE@}~hc^l^!q(jyUcE-ty zaGkUUpq90R&ln%`&kt9x@C8Qx{Hs!HqVw=^SyoHnsC>4Wwl*6szS4uobMlKJ>S2f< z8?+27lZ%-b>>wR^<_o%WQ(foZE|Sp2QJt?Sn?2L_-dPgV1MrTLmXf-f2GioqstI|! zf32{*qHf?0H@MtJuc;u!^Py%z@lf`!sT|f6lXQdoIea&)gY?wA$aDv}HvKUVm$e>rD}*5n=0YwA0K+kj6B2`@ybbcb5E*^)L)n1G+TfuKQyv^uUbQ< z-}k4Jkw8kJpZn3jSi|I3K3Tx^oA{WyUyz;T z0C6ee<%!YPrtY@xosA0%BLG{iiqbwVF;8tzs?5sk4*e(`!;T3qZF&dLBGgD=DK2^c zw(RBhv&;LJV{sn4ndfTkSBtX)_#pb6y~0W4!X>RnfBbdx@8h_4<@;E$`#V-abfz0m zw}L=TbMo&r=9VnY9#2cv4OQW`uy2Y(-UQN$_|?`ExB%K}PPqPEo%w%2+orK!!@Bay zD6Cvk=Wo#0fu)KuT2@mz86}U9EJlkEgW&fE`~5VGsmG@YnDAq`HuuqI$h)$D26Hm@c-P1i2TiH>hrkeGh6Ny9BUlIFG zkSX1u3HB+{UZ71xyp1pPl^{!X^B+yn*Uoz&Kg-HcGa$gr$)q@{U`Qz_9R#k{LIrIr zIX%(uaPWM=w5@1n#?{)VZ^$OU+RW@ylG(#fJ+z@`Gx2P?Mp~<_Z&2>&$@73#gLv6*DD zp*FP7*n;dvOSqqB8c>_U$NZSIexLj@#0~tG;`m?4RJS7SCSuoRDo$~)gTG*#4~AG4 z13UJg_+nE+f~U~?FS)-}VzuL3&L(Jc_C^>}TkC`N&0?so-m0ZXIc(y=c>kSb{s5mw zIG=Yax-q9|rR1F9j-Ep7((DZ0W9MF7xkXO*w?8#L#BNzfngjaJCk)1F{DyRGpq zY4WK~JH#5QQ!qOH-8nB<;HawZCza@O;QGG22Kh^8!iXkPFSj|RJiGn0a4iI!gF>D0 zsp_?!+P+E8d}k$+P!2b#)wX+Sd6AxPgP8pC92RpcW-JIA$b;vL4ip<{JhCCOe!=aHThSI zrk@HB!HaH$0wjw9_3&y}P1hRd`*$9#hvXrvw=7Anzn(1Kd~Gfp!jn>pv+ZR@B4+Oj z5qiR+jgD;q4f%r(*!9K(;QEQB{7f;kPDHty+#^(~DAJ_c*j9^JJ2T^c3A;WrOfM=? z+dc{2D>PUB!y9YWV6BFz5cK=F-p!Za)`~AofNGC4-`{RNcurx*CGg&^^j^O8FbVw{ zgXvb)+Mp4g8b!F>#=0|huPNUA^SB)0bW?JZfXV8u?{i9Gq_$Wd)KYAUWVOm-sYaCh z#L{fz&8q|B%A$uMK%?lJs})LL2GI!NQ}fp94$(`?SJoamc}Wrpz9VU`qnMjfW`c6F z9SHtbuX~2_LRu|1od+lUn>A^vMNy478!Yt+pF!TV5-wf#5E^5q5F&_-^Wx! z$h!3?Qd+uRO*m1Z*>L)?{9N7%ev>A>XC(ukYbd{+Ji!d|rAcpN%+@GLwUtGV9iTLy zPF^S3m~?`k*+p@*aw@b{<;wvVkb}A$h#>m3oupcVkhP~sU8+GCj?eiV8|BIxbi2@1 zLYKF*dSe|nny3jeH<&U(+bM*}*rNx_rC2d2;QZrBkF?%QZxl^PPu{kRqV!~3zz|b z`*fnT+$HWDCR}Svbjhd;4I_<%AVq)_2f~!3B11^dE&8Fs4x3MOXH3py} zgV=x=x*_R0`sAkhgemhO06$Gh8i_7?9;W*`JA!68)c#yu3v4)KZ>C==23S*1{ZtCjDSg(KR) zusQCr7aAetr$p}YnXjMa8teqbe6fmClI(#2S0BQY70yWP5aw?bWj0jk}Yd2-`)|Fvkgtyd9vcFShbfq9K zJ^RkOHBYMDw!Usgz_h*imPzoUXB8WPcGH z<8T3)VrXS}3>y*xB^i99Xh9M1k*ZgDP1fLcO@wj4yXfE#!9><}?lo@0z z_GIqF6+-)F+y{J9m46*! zf7BYTZl_=?YR~OMB`*dRy`{A-vw}+{;7W4AF&7RZfehxjcdAF{!9YMG^^}VwDKCx# z_j5;jW?m22U25_Rpm(jFnWflBW=?xv#j2W>OiGe`Tw49HxsF)(J1~`?k2bMtKp12% z(oIS4rF2_`Tw-KkJDHA>6&=e7DO|xPl?CC#u#+(5_j`J;o&Oj<@X_qKPU=)p${d0w zV1CLNBZwWUpiZFmMyi3+Fi&skamWP9a~l4Gp&e3^a$9nm<9Dyfe_cyYE@{crK&~9MX##FG6@2UsAnWc^>CZ1V-d{?6n*;Z*R~ z5E?2mDruthPlwl7z4a|7p-;{7*j}BPH>UMPTI%+UEt-y6Nszv;$rul>wCXxoJ!<*HL9c2^HA{mNJXX)mp1+Y zw(4jjDK6HslfGyOi|3;u72VaA1o~`a6J7A2K8$XO?1WhbyYXJk7I$=dVoqFo zDM5mPMGg5rvOj53QtSiUcOo;?CX{~!ex$1&ur3o|HjBT7uQ3}?nvBe~c2Y+$gzCB5 zd+?J7EoT;{Q*G|{ZC!rTK==hlTfQ*5xSZzVm5?UfHd5>tk;40K-Z4Kw$*w{(O*n)_AhWs zUp9L^WSy|pOhTv3l=Un=MA-!Wu_Z8^b$uF>2b4w33HnjBqs_NE`gVfy-DwW1?q4g- zO{xygCDj{k(jC_*G7Y=Xi8K=^aeO$aaQu?JNj>KF6_Yn=+MaVMu9iZhw=~N;+R>Z| z!=81I^#A6E=xVj*(jHY`CNtu5t~y$WbTw-yi$m`NuwQ+=wIc0mNJj!1HrJ?Gu|2pw zn4nTb0#mUdKc{)o*k;At<$6qG*c=3AkSXr}OrV+yS;y_%yPsRcR=(y!x$Gt0#9Y-; z(ilv(75(=0Wxvwt7|%cJ)sV^y!eB%~Fj|aSs0Y#y!0%K*?K26(N5mtv@ zjW66udn$V1eZh**1a;O6`hBb0DD%3w|ikhI-g!?!bP-JtPAZpZt6Yx`BeX~&s;Xu&{wBzGNp>f+&d zND&GA6C?jQPbaEVLG;BHbWxgy1^LOr^OURC2Nzhs<26sMevHYR#8$alM}wK+7bQ)? zJb5G(OS1`d!H?2 zGl_W!xEKF~AA&!%^b0<*6SDc+=FMF#+ocXlWG^JdLl_8kmGq0WO2gB&9(W(rJ~6h8 zZIFKqP8`IrTufr-rAxlZx1|`ZhfTt+0_p9R<%hNq#ij>fq+C{erQ1>xw6Xh=ZO5!O zan)`pe?qj+OLQ`OA6gX^ZIB~aiJ4#}T-+*T(~FxM|8?GA!@%qtyp{H+smUNa0-f0u zBtFeOp2&ir<+%T#;(e}xdcs~cldczALK|-Qkgsn<)5<8;=VI(De#?%?yPHs+HFHwE zc+n~#!NPjZaSvndJlC*lrJOZC#nXioSF~js#;6P$O^&a8QbVlLI^{Rn2)`g(T8U|k zvep_ZgyG@P!_~P~17k2m)bFL$FZ9;68wGiCf@4-~c|vmI@(E#sdgfjEkk?S2t;Eq- z3MGwDNx#mpGVm}#um!v-zH|O^J=<_XxiUP-Je+mKlw0F1d8OZZz6MV390rh8w&N}| zVsH9l$qdL$1b^h;Nd}Mi=lSOQz9w}ah3vo0(HQ>`_2z-S_2hJ^;6Bt#pZ2&DDg^4;B1W zGamFSb^O7`+(|n^6}OaA*i3yArZv={30R^YL17V{h+Q*&Rk$PEqmMWKDh4Y0Z;mlI$_Cp-0U7-QYsufz1Z=KAtS zuK7t&SpD+;GVVAr3KVIg5@2b=6(mQiKee;8%0J~f+LY%T%0O3CM={(fBJ4MUz z|4zE$l|~crbJzHhUME&LIZyN%hu$9!Rh@@y<7#aHzU4boioWGj0r z@GbAWps@?>(j0nLeWhviu3b}51C4)cHm17wlf5gr1779-U4BJoD9f5KgcLfin;L3! z817<5!g_UvK5|mZ%-Z}y+RjGGUe=S+iy*y)zGG>5=mlRDorBBXGijm+7MIX1 z=f0ShYM>W>R-KKFGW#Gs%em02_R64ToKL3`je0_y)?PEM&&|{!y<}NP z&&oCJyz*=ZGi8ei*6L}gC&fp(FR%|5D-NgoARU%#-Pvc+s#L-fV-il^yn#U1KeaKJ z0JUvRmu5S@7M>L( zH1y$Wji+H%bK)o?$UA*8AE+RNe`eLH8ltn!xx>$a|paG{5{v+gxIt4njPn;f75w1@_@DT&rUT6w)2(w zdk2Jg?unF5b0P=W5;Wh(cy<5O073%Oohh1~=K?=tNO)Z5-)I#Rp#^Ufod0ls4Bz0#t#Hu-Z%CDpnrRo%Vv~ zqPal@T88RsL%L3jwvTq+)9j-YNY|}4f@yn`lCN?0v@8sfPq9ihu-VHll?^cxj~Ndi z0jM)PI-n|W7bb)gnCy{4c@?Pa`92Jd&>{y&@j)oBQ+A?O2m5&0cJ+pL%!2zuxz+*) z(&|s$O7gL?9Jj+2yla(pq0%Xr;`)y!rQm ziu{QefNmFRq1$+Xl?^?`di>YS!qB-OurtDjQn#fw-V+J-`?Jm}0#7VMG0VA66}M4* zP*7*4B#3ka&mT+Dc6QnsZAx||*n|}Q>)D+vuLCtP6Ay}J>zm>t z;frwo9$u3kKJ0#r=?9hhW1N$baIX-A=TNkD$2x4|jQ7V$_yJSXPyZr^rZ1ghDvAqPL%A(I3WT+cTxL-MI%O_B2LIlDis zi?b&U{mmr-(*^X#*B3bc(wCDj7>CZ%h^7eJtyBgQ!3oLHns`vT;_Mj>xuZ|(reyUl zqyqlkW!>%B2yG49YA`m)zc#cJoXc);Mp^gP+2N|VhLaCS_G`aT=+D)Ibp6GzE&HO# zDQ{*2t+C}JrH1i9k%#yx;mr^&?LJ5fmAp2HNOs&Jfm#abb-A#)`h%f`)6kwwEN3iY zBW}X6??e##l3VCYvA5V=Fjf{=Y%Vzb>2q2@Ei>Bzr2vuOQa@uU+@8~-z_;AQ@Nl(-bQcB~}$N$*5|&S|;(UIW`B4_<#+ zLM5_%&b4jKMjaUS36+JKIX)wf5|xK#Cb}W^y2aEd>JEf|Fi9iF$N>RY#WW>$T%P>l z-c!;7N}I<3;Zpy3x@pxwC|%82ur=`tYG%)LUqnW&*s z&Joa#qcz_P_aziWRANd{%mvN02HQSbCW7LUN|x1UPAo2^=hB%Nr?#M?nlMeG3gx2rc%d*a$Chs=Rn*$@ADW(*$u!&$7*2Fn}9h4{K8 zwuIVkuYC8qR4(D>nAMG~iblo^XUjopHQz_=Fy@TRPcUL5D|v6ITaKH#{x3Xmt*_00 zs``Zljg!N-0+NWo{g?i+516O>Ae?;4^b1DUg(T*3Vv-6kU9ry_*x&gE9@T=LE3D>L zOkILt%+G=IhP{%Cy`sC9@gdKhJXK`q_kL=mGPv9hIMg-=rnZbUAqcbAyK6ib7Z(K$ zi(CB30M|%d!hs{1KVL(>f=~AhuGFn5et|@GD>M)J&q2%|4E8joh!x0-6$I zz2hVV&Fh8Cc-=|qR~sQKofpo#p6W`?lByra*gSmOf@zHE_GsE(fa0-8BH5eiXOBOK z7`gc(3mYyhA;Na0%k#)Qn9%EErd5WEm=R zf!+Fc6%yrXLM04(By1!3X%Pck0r&pQIs9kJ@Ec;TgXdh~C+4S55kr3qRZT;d81|S` zg(+=ORbptDlCx@7c?oI}-@NS>woiHtvNx%pRnzQVRbobiF>`J@dmQvfF?O^!$=RHrMiTPX$#A0oE!mRMB7 zXv#2wHOEXdmcHwnCeMt;4d}(D*0c0s@SJH(=Yp3j&hNjH9JF@%Ih|qtcvc-kfC2ch z`yDsBOK(Hy(_NovDr6>eengzX6Mu0Bxjg5WQfts8%L$jH2P=EX$a8?A8^rh>htx%s z%eBUnzW=$z-)=PK0VpmltX8lCGbtac))@{C(B_4EV+BZNLu1ebdds(a@%*Z zoy~vTq3(YZlVfYkh%TR*KDPh+hhD*JS@pOL#;ouJ_BbZ!n84d1P?`GP9_$4VVa99x_Gc2pN zN&4&OJ2k&#H^g-!fWfC31f`m6Kk6q8HWD;b*e4AK8M4Yt?k3t-QvVrejyRVolr0Rw zqX>bfT27Rg`Z^j}e?xNlG=so}qc)sGmh{ z+|L=N1z6kK+7l1P*K`*nG!FX%v!-D_H4|8XR>|EicfAGGF#U8-T7|({t8pi?rur{G zA+N-*LM1ZYsKVEQ4Gk}C>K6A3KpZP?t>TF9gg6?V2zePGs*dij$wHzNy*8^byb;l$ zE2^X?q{9ik+g6%1H{#Gt8?Md(ayIg=A4)!wY<&K>5z;%$3X9Vup}u6GS7plU=)q+I zZuL=1MwwHPHpipnCaNg|`MuIPwY8!DY>VUU2=(Di(2uWWa^>~FC{35Gx|Ya@?(2`e zS~LsWcu{(B4;7;fpYr#3b4iK7ycvDd8)bzY^_X`{w_p0U_iRZMwJ-ON{2#vVJL?OE zM(#AZTSH9lJsqxH2R~U@+wK=jKV*)AhTXdAK#4x8haHhKC0P&n2b6BY!?xM$&_6jL z(;JijuEP}187ra>OEUDKuID*oa^!-YdkSv^yCS);sFZQ@eRn%@FkNS>I3XU|?6i2O zM|#uQk@r8twCgP@GH=|BXbz_Pn4bIy?RJtxF5ZpvtfL{Zhf{q*Xa!ERf5cNGH*fJ*9B9!i|7}q$)HqgbJi{$q)`c^EjjJ9{>1zzyl$b-5 z1vjD7)Vt#TEDFR*U-*@y@Q)7=b3e;Ig+Y!>r2XY&QG8~1w0-jg_Ijg_@p;N^hTXP* zh2LuT$)+pQmjX?Mur4BEUO&s4EzB9PL^HorRC&)SRyoyjiKEa!X?*>(7Uzy)T%QT2 z_hh~bDY{IH^tj>_kTMaN+I05WNr$(qfqPcgU&!#cv~%+(`qz*5#>9A;_aSvu+-Q}T z$s8nZ#~j)S6J zAtpP4&qNE?V{3JZ)GJvQDu46?;fJ()dLRRiSFDO9ULK}bjoa;B&IOB(TYhXrq&kJ034LiZ3kYc%hqhw{_+?H(rmN||kA&8$C5w6u-a0H2 z(19v{8BD3>>;G8c-)j~JNF|sR7jU!Un*Ya1YTdA6hwnKlF7%dqXmbbpyqn2=!q8ye zy@OJ=0wl;LMraJ;7hSptV{Ll3vSk0U;HROAUe~4(4|{mxY9u#!)Sn(7TO8!N(~i&sOk3a zuTOgcp@~Ia>5ryMb~TGx1L}qfWm|Zbw6;fVNoXrcvQ`i^&nI!Q#=}jXcM|@}&VKUE zO4V$M-J6B@;`}_%%__%QY|X{QbM*ekNt^_SDtr6aKiP^{TGD>O+_B2n3%i7olibE@ z`{6vNo(k6C zlp?d0PvhLux3UwF5BmMDD?L;25e+ym0FcEWlz)w0g(-Dbc*s0{x@{TpRw(&uE?j$2 zfEW`UGkWok)sN=OPCJ^Xw$c?5K1cA0mLN5z5z8Ea(kWwbFdiH*7v>W%5!J%-(Lx|b z!gw>f%Ph#`%&f%EAvx>5^G^L$qowb5l|gpCnO@bys&E5Ynb;Okw|+cxztGQA_zs|% zaUT<-2bzpGdu_|fmR|V(T<&*#{q5xIg6~#S6=5dTRMZsQslU_F5?{5dv+?u{_P zwBi>16%?y0cy|=IYqjp*{fgN&HT+9=H=@sQ5)w2y?5BIjOdlA+Rqt_^GlK6lHH>`t z+A}%`YUGZ;Ebu83Z53I``&L5CC zg&l<%#nUwM76zP=6w$(5#qD38x$b8bX0ce%z-~Wyl*0TmD9hYJXi(hYAIp;tt}f|5 zc52;b$lJrMG^Yxxah!Qaq(oxQ)P#ow1dzRNR>;&GVUg~2ZQ)`bJM5@L0@enO!wB!7 zx6);M$ld%mdjTQZ068anzuI-?i3&RH(jyluO4(*8Uob2mEzQh`Sg7%2sI6H zZ5f(Intt6!ZDyPCva!?0)Ys4XH1EM>hxM!3j!VnRHiip+8EUkNazAKoILvS;Q|4!) zGX37)%}qF7++u8ZsU6q1-OyWQ`|%L9Fu0#>3OK-6)MxcJZ-mQ|eSSUl$k=9DfF7L1 z1USc6Wh+jz*~v?_iU*is;n~cbhUOA%S}5>i+dNU?wl6m2XD(blh%m;)4pT2YF^o%} zx5(_9ncr$~)8vdZcA!A}?W&>j$zZ1|Ftacf>M73uSLWL(~p8@$RO9-#h*q0YG0};O&mzLelT?m zJ!h;{Dtw~Au6Kn}US|hF<-BTZ@vJk{CPtI>xb>Jk>`sxn&t3r}1x$Y>cSF~(f%!N% z#%b8YEXbnkPG>ChUP`>Xd0|vfCiqD7TOh}?)~_riYjopr_a?dR05X{jdR`5dH0k~x4=Ie1M&vLQ#5C9 zm0A&YA~j=5?c&1K0Y=7=ptuU;(A5^MIVyIkVy@O~A}}rHvB2&9sb{5f83@xDB+tF= z`|zVN;-*zo`;e907v4xhT`AS!9@I#&g_GmToAvMJ&47-E_^_V*tI|Z(4*3@wsMzUq z@V0HrfOV56>iqu4O8`zgR^1tgZ@L#*gYjpkg=wZ-m?2}C_^A78_@ST!67$`$-mSo?r00!HuLb=bsjXcaIi(`SiJzV8oP3n1&6Rvq8qsgHv00K&7WA-!E((sh7m;U$hX;Y$zW>D%Wonwi>sPs4yG73vnlm z$oukQE7DJK9wNNz^nCWxZ&QNOcN+mBlc=OhIeN@f3y{OcqRsk|dMdBMkDg%*B1*rK z7oTR`l&xG1WVr_EA3thUjSWSsuhXJ9GvsXLbaUyz8|1ic;B%LWOCze13rQ`}5;a!x z+uCK;k&*Qu=6uKqA^t(?#(h6+rH!5fE(>T-nMnyr4%tZ8mvw7<1*s+8>1rKZ&PSaR z(L*@m1RYGjZ^6l~9VekWSP}QKhqb-jvC;#US8=}ZUL>2GurXXp4H>c#J-#sq#GI~n zWG24$aJ{8^eAY-UzE?4xx&^Y`DwyXu7RI0I_#i(Im+IE4Uo%gK+&TZjj?&DC-5XI&l`W+VUq4$&JK9)Iy`t+ z*n0th$LH5L&R+N1XyRgKfts*v7+fQ2Oqn~Li#h67P`ywf+#7OWJMU4O8VcR8W7u;k zYYVc{o8GoH7I}x$V5l${v+~=AGs>WG8++$Ug&3KhtXKH&4Mk59id)8?%NMd|L0fJz zpaa8IP7|(9+qG@rKKB$&U|;_~+btgRq2tkeg*hZq<;Cm6F5{^O z>*b^(HcVS1$^-90hpI3=9;mt{7pdHZ$6%#Ba9koZnSyBp_TS5nDPjPH*ohdn`gs}Z z&0OG}<46!F>~D`o;mn&uj%;d`6^53mnjY?qv2^StxTB{q)o!yulU-wAJgk?U%}mNf zi_TSgV#=hQ5SpVqh4F4;OUajC+8{+1HXxg1n^q%E+xiX_}d9FQD9=TZ;7>^|E<-)yHGLZ1nVKR4<=zpQ&L z03d`uK1%W1+ZAVx8$&tS)yRR%Z$nxFlL2lSd#!$O2Jp{1TeIe>-6EMs z^c2#I$>EbXDld=}*A};)RhM2Jc(z-8$pum!k#=`XKC6I(Ci*kdR(X0kuYn=^mfnJ1 zi0=4mTqaM6UGaol0LrQbsJW$tn$Q-HZg#-~_k`3lFAY|b`t}zHbW+1g#=HG7*Ob^i z$Ed>6v`7nK@3f82kYQFk-hcKb)rc}Ss@|&=>#-37(aU%NoOsC5u~rWc<@32fm@WRZ zv`caev`UdoG;-N-i}@m(kj3biK+hI|_68_@BZuZ#g^}oPckQH2t)JQUCIe^=)qAzx z|4zS8Npq=aUtG3LPAto?P)TdxKcq)^{h)d5Po62PXs;U)Y>O3@pJ2)UG0ofe$c-if zZdx$Xt=}$>pQU;v89w-Y`mJ>@UqfmhOUSt0oT~e{GdqUL6d}qD6c61Og1I}(wfKe4 z*F!kPw{=Qd(qEgHNKy}MjMSqh>#w)N-dD|_1b!@TO3RHSypcv1)B}}swuI_M&zT$R z$Zg2Tdgyt=+q9M1v;JkwN>Upv!D1iyJY2cIb}SG`X?i}pl(3wwED_S>|B@0@Y&_lT z(r`+n%9y%E$i&h5qnT+;FGZCoTJttUyM|}&9F+`;0Xl(mwt_1XTZ`sju4`wJUZE~! z>%LwDQf2nga>0*+Ya32d_G~2$KFN+>e#G!qfYz8-`P*M5#GdgRr?#X`vdIGwke7jR zS9tU4#aq_~+F%tujW#D}oo=R&%2~UjNF}+jjnR*=4p@}geY!6{&yaQmq~8?=O)xcP z`E*b1=7#?tCoSzo5a1i|4{cwG>GOr4Lx7j;s zenC+wUc4(sJe9bloa}^6(teBp$=p4N^@b->JKrlI^n;A3!TxH6@dHu74ftWUkzx1{ zyc~D@2hC6Q@`A=+p>GT)WzQT|GLEy(wp_LEZDdp{cKn(B?%3bFIaluwPSV(oA|q|a znUVBR?A90DZ>Ln!8JurFb;ftaMAXDQbd9cumz2cbB2A~)r}Wf`{k+jprAg}=nr z-~-c>r7?*iuBYglMhWvirrO#T$YH(|xRag`Z=;*(5Hp#wgn4y&I8*WMFQG;&%y=T? za)=R83(lN4Hn~JJJUJ(C>K;V2U9qujQZ$r;WtKkLtVy>%3a92!&4LH03{g|0T=OR2 z4q6=B7>k*4z8n7Z7Ho7x7ko&ZLC!^md3w$Aw;j6b-9PTtMMLgG9A-h3$fQDFme&;z z(=3t>l-Tye*(vas>z2?or(bt>(eK(YK7D+WYT9PJXNJW4K#dx0WMx-_4@WkCqve~% z;q(?1tLW5x)b#1^2Vv{O?qggiSYF;to?)qUE-~f>Zh4*ZeBDLx8~KOO#mNuzy!)+g z4uOV?lJW3laNW9EC%TjR=~QPcgDg?^CTS=aP*v$G?x^IL{^^ITGsMkj1b5}0Rcf!Y zkT?8$^RbMvz3>Y2x=>Qa%{hx&Lg61b6x{q_gk-F{KeS8j{BgDffiHLKmZ_^x_K7Vm zAC{M1<+M}w3VMhw-kzqL00V0F>vDW1wt9l3B-JL7=l&!SY`f5!vDHz>#02G#=rAK! zBH8Q32mC4aFaynoL!5k$3cuV}yY2G7kgiI~aEG>EwHPP!I8%WKKM^68>erZrhyim- zFDRUp%x+V;YZdei#diaW|IFUkoliU%$F!*Se9N0go> zCbxEM94RzBiJA`5dg*>-1S6Iaskfo@`w*j`p>Hn$8+ldvc-B3e_`~0eYWN?-e$I#b z6Iv^o2&{+^42DHro)DE(uWXsoz7o(0zvH;Og<+?54?|4U zO#Bn!R^&{3*p;W^nQ-5i&{x>^QLvQTknYCvOSqrgEpw;Y9<&F8b1^RNodhQFW{T$b z&8S660$~1R7D;0(O1W(!M$8(%W&PTEq{cIJ0N8Tdg;(pIY^5$&#r(h*S1BJm)E^TV zdu<)B_?VDUj9l!1uZNFmVJPtx6mzs~Zo2cWRB?>|6ZO`}`%uw{m$x6Nk9_o{zj3$} z9`k&b6w_vhhL{#fo3z-0nYDtevP`nxW%ZZ;Ln3R&BF%iuGg?2se-&`h9^Wz((}|w= zs&~!SfQr*x1Zq*Q7GzT9vB>GMQmPmDIrethdYJ$GYILVp&0yET__Pbr<@jbL(j~CB z-z{blsz!!B3Z*R^UmOxKKVcrwlx*7-8InH;JgVDu&ACd{75h0RNI2O6*DfsjO};pr z*>dnYdnZg|foVEHOo)y&h!R2k5rP?^Ng&L%edw>fD&+fwHcYyCb%>VxP8UH;V^u5P zY7mZ{KpfZ6UO}`LAsF0+z)nlwtU=&7b}0vKfrEt*eX<$ z->YjwE}^mL9yo;0xDB1;k5trmX8M%7Enc+&(yt{e9`j2b(e{6=E(}GBF5yLIJ+yvp zB@^Z8HoEbSy#RXQvgbMYfjU3}lM!yze~eKm@zvD`af` zeF*ynoRT)MvMjkg3<7Ug7Q)#M6(_wORv->!!!lTp9d^YH#ei+wF}-#zf})yLg##DN zKPT%c0k1M!8)Rk@X4KDwLY#x$Iqe~t2dQ~PE%tjwDZc+GGxy19K0~Fodo3;3iKv#@ zIyuCL(ElN`r6lT?b`MZFuQbh2e~A}_3C;+d7miHGZOZP~v@Yb8h2jkGwm$;2GlgkF zB%N1kt(eAGcNl)W{Pv1UyC9pn;$<;5bDfxN{|o?ZC|0i&wUa`ol@8K3)Z>QRC9HJy zV{$@SB@NxGJSSoS6D;$LHWv=>#A+`vWQHg&f#WjMZ0k-ccJ`>knX%QAedZGu(sU;a zjN=T_$Ie2MlYM`~X^dBuNt9a^pSJT=8_4=AuXWqcScBIs^`brC_Z*px#vGhgfb{!V z-gYJP$C6kl{mC_P0Q-+WO5{{^t6eZjzwb|iGt(TwmXJffTIR8XpTut)&4ey&dG&f# z(Z_)2zYhVN=gUA-c;4a7*CPOilG>u5z3x0g$Z$*zbx#GhTy)jiWU#iPH7F&-80b0i z9msJ0)#W9J%3@8C8ns~uS4~GX;eqA!7t-wiZ9hy9e&VXkv~D)4ju0w5h%le<*+-aJU~ca#RF65B#g%Ck>+PI2 zilBkho7=v$mjdF`PMe+@%n|*Qn+pZtNrXH5*8u(eN+eV@=59<3;qX!R=X#Di{>~eL zO>&_%aqlDvgES2@>8EaGp#{x0@>9Sr>0O1+#phw_Wawo~ojDl*^PG3)B-P}n?1@Vh z4{}s%aUR)bQtqVZBD7zDfLVG`VjF)a#8o%buYhJAQxu>Jd$lLc=Ejh}v|1G(k&G3U zigUit>GzlwX9nGQ#*SkC7#b7uEAV}wNK*XUxCRzl2X{u@HBe~)vG7x?=nHAo!*tQv zE)B!-8XM*68BwDE7S2rXe9KWy_EnF$Y4h|OjitNKTmVODQwgKkcyfyXfVc-r2Fr&o zC`T5~{XH^1zcHv*Zf2Qa5As!BSNp3`pSv+;ZB$0rdS<#0`g?eKOJjDRtM9)GKz3;9 zbPQJGqN7B#NT=;+_=Ge$FDNm|%ujL338h8zE4q%ZUL>NzNHBV{a;_P48rmA--^nnxfJlY2)qP%2 z0?Qj;!}^)_6!mX#5Z0ZX%f57ye07v-5nYQN9PpIqDei3oUH`o+ey$ggkEP)*x4yKC zd26LVGD->r?QgEc960UI=4pz#<8h6;aC$WJ8|dC`lPA!ID{O*%Y#p%?h@ZMH&lV=s zY64nTva8--eZkvyDLym#Gb~Ns12J>Ck-5qAi-8|0CIU7=J-9e#r8NkTZn61&=J6Zq zOUR#|l|#DiJU>%+kuMX1GLAQ114WSp4Nc{w@@i;g3E5#A6NM_f;BV+y4a0Yo(?b}1BM`q$ zQ?X#jW~J_UMGR@9=o_fkj}Eqc^QhjT5`(km&s^!mO>u+DrKH>accwfKfVBfIHigXC z13dN-UX}S_kJKzFbvm*6WZ&qWZ3=O2gz9^4e;Ump9L9 za4oY12nrY0HL*?y2L#pTd5gC5qHTss#XkS}p5IV+z&Iuc#>RltAqp2X^no!q66;*} z#=tmsB3xJh(ae(E$U09ymy6-#FEb~UctJ!7GNxmG0Z+k5w2ii4^!$GVo^ zh}drT$axae1^Q@vesrs7e=o&@TbVG^*-S5|T9?a9{T?&n z{h?8K4a)S-ojo}Qtlqo0u{7lp3?>8Mp$99qjr^>aTx3*QxwwBKA#PG?#@tz6{B};8 zrtzy4niyRIdf)o_erhD!v%a+7-2}*fp?=-&lMYu=b!2Syr8~sOcE17*ooQWWUw*>s z9-JRk9h1>r_8NCOWpgk%eBf5Y8!wKEBZkmD*&F}wob#B-ICXt^VKD#+h!N8~v~H;n zQdQ$Wg29PD*c_l@_b=Kd8nL82o`nzkXcz9NaD#a>y`|K;WyG_vhBp1Uu|z)jJ2=v| zZ_bzqs|~)Rb$cEHgPA_YM1W>Sa5w=XvY8qb&!=?^x}~$c;MMF4P~udnpB5E~V!e0u zbP6>-$tjLWu(`3aeml7*VWH}??69>uV7!sCcSX1TkMbiBwIQ6Qd&e(Ldp88(r0=XH zI?Q_Jb1*H~nqk8ucy1c(cboeO=;UdIsIdprfK0!8#1HRX{fXv-^_dqLo(x~IQh$7v z2=S$bzi{7mR0xAihv%k&ZrHnMK$T=b<(M&&kXES!8NHA04$iJYTT3*&*CHpP2P1oD zad`c-2OF~SH&$AlhLerS>9m>Ln`YiGcdwos!BQ`gBksK}K)fWicA2@GUbQZ}tAl*z zx%%H+Jhl95jcdDKX&sBdl3apifY>s?K=?d*+QKm23D3(chtG@vdl+Gm-|7$+{a41* zOIWj;1K)rAEM}&i>65ORy##N@c+4Y1h1r^l8XVAo6G=V(oHgYwQd_7d%nPn=oZ#SY z{^|aaPa&OoPVQPa3z?f7M)zCIdVE>w;w^kb+0U1LIDbG2tr~PWUM?%F^BG-c!>g^N zA6zc=jX zXFcqT0St-Ci&h~N=|0{+<3mSz)15%XK|WsjBDymJHa z-S*$mGb7%mkHR4%y_j* zalHKY9Vy>m z3Ul#Ml~M&+d1GJ64WVreCGL>k>u7Y%`+35^qKbvh+KKm&ujGdrZZ~FYQoc_@HL!4= zUJ}@5RD&3tOpA_28Q>7SeHK%)e*E&e`aOA8kah^$!cKdqc7~gSrTQ<>?(Vah-Gy@D zHWT5J_|a|!cPRCCv-j3-%{q#w?xMS^dSsCckQls3z>6jR$qP?HQWPGB*0|Ac9smI% zatWLvY3*lvlQFiSW|EN6a*#QmJri~#E#lkLDTb3TH-3(c);T8`#uK;N5BoVRE&Ax! zp;7FblK{>P3O7>TdRe1_&=0Zzk{gGr6=(Lsd?F>KJNJn;z{E`m9t!p|Aq&|xp%%w| z&NWq-F?R4huvVk^&n=r5_YZzjjzNhZ6t?W)rm?D$#6&sbP&0wZC^?AOzrw1vOLL%q~Kg@VW1X5*96l^!6w_A`5B@THAdFDd#hx+vlMPnFh5-$j+K=2qo%Ol zk}TA%l$ZVg2pj3}uy31~6epR53%yxM%?M)OthN^P@i(WLu7qmp19dnne3@c@V-qV8 zNHDp<0eH5pyL=^-KY}K8UIZX6(YZ5YHP{wm1;CXUmZp1e)w%&Ie$byl<vq04bVnwTzJD#Q}WJe0t zKXRtqGTcDjBti}&Zx(gWJ*-E2sm3}}0UNWyiqn;fs>7u$^oad-1&qZhW%y_*c*)mX zfk5VhjU*vZF*O6}GespnfX@{yqd|ek1IDVPFgDGwx7EQWIf*tWKzP&By4r`?(l^&t zS)7lAJDOu~`m^?{%&UOTItus2J9mQOJG|t%od2}2{+o`YVFU52?phQpP4<-&Pb_Lo0#h4Rz-V$t5o4(vjozbp8^8~FX+1+C7jG%r<%Tb0~ua-My;jYHSD;{)-7x~lgA$a}Ygv^@En{ZmGP@LCm&^{M;NX58(e zY8^bu=9~quq@*+E2FR=B_k&NpwGf3?2A#)hB!mg6^aae^=iF=CD)DJQ_NBIx$O-&H zQCoaglp{Z={}R+kCt}wNO670D=pRC~)fIm+y?ht@zZb6n0Q|YJy?Rs~8jR{hmHdH9 z0fFqXR|c}pSfC;t>4}vj6 zgBFr`%csns(5-)k{xDo0Gfh$30rLUuvdmK(_woOdzRVnaLC-l`9R?SSzA+*t#fZ=+ zm49@w(i)nL{CkW8O2Q^-4Ew}5UTIY_+Xipmp#LvNA3fr~oSj&FIx?=K*03!SEADBpKNqRBHpO9KT{Hi9t1q`^*gT6*$cWbs@`t!bh80BR+cY z_1>(TYeMz%dYEdhe$7{oH2v9{^;Y*0R2v1tWE_M7EZe0|Z>bl+jxKm|oY_h%_sj6C z>2(Iyku5NUW)u!mI`cIafE=-1bwe(vWmxZB;ZjeT{#pqPCqP4QgH2FZ##eGr2q0OF z-U^)b95Xx~KBxO#Qp+U>=IEZ+K{-*jGEA5>W+Q=D3}SEpXix$$mFX%L>4)3 zMNW;8T|sL9YzH~?R_3eEIIIpWkVsyhuV4;R%=FTFCXgvnd^24;Q@A+v!eu4tiGecC zzIJCW58$o-H6C@lr}Wmm`gLTcM$%19fOKRcha#U>V%Ymm4(NYpLQ!F$+>^35y|(Zh zeg~AjLPT1Z>|~PNk+Q@GH}^ggm|pPbIZ`#d7!euv(0R6#Kpe&xFnY z-e7tmIN4^YdH!^mmf%YV|9sqBPfk#UU2Qswy&>OvHjJ63Aw7Nes5jT^_)<%Bti!}` z*WxQ{h-)1O7bpK#6qH|LrEa{pNwTp>RlR?h)O=%h+MTHRZN}a8!gZL7;$HUXVICK4 zgOmwIxbnd9OqRw7`JLRcYDQD+9LJZhxdUEPIcY=f+14oQ&;!6e#vir)kCM-L|10$Y z%>f;2a4Ao$JN$OoVXuJQM{5;Lwz8rhe|+Zd{R<#+@b7cea<>|D7Ss1r`rTyRN?nZkAmnA1KC>#?~$HMz=pJ|KkBGvB|zKr&d)XXjXPhut{?szuOXa?28`5fGF^X>L!-mq8tNJ0)SF4+)H zh#2rT(vF992^;kLOSS7Qm8AFqHRYLIkDWwniLsyc2oaRxL4pkkBws?{5s1LIunqgwfXeeF9}|7(A2Q5K~^>dBtaqb ze0rU3TzNg(j_1Q}!)yfJx#nw@Y)$hcltd+f%edQtp2Wo4bqB9Y@7(i+q6z%;>Dp>a z#W7f&&B}U$yAe}A?wHhAlM``@-t>qC91i`2P|jQ7vCCogsFEZB=ccnEQNpF&#_ya1 zsf|7DM9Xn%=FW*tH%4%|D#BZLkym0GLezOqmfjX`$JNXr1(yVFf;gJrO>r`_p~lO2 zBic%|$?LiVuA`R4q{K6KNn3WoE~BJmjb7oV`(sMvMQSWrEj(JgSI~AD7l_)vOU!W)bE_#l(Ot%y$=a15MXXz6zl zG&9DEHj%C!ojOq|sd~ILO-St^*;_v=@JncB{kTQ!p5SCL`@V~GPRKON$g`Vr)z zpWBN=6vqhw8)YhVcDnkYHT5#O+^jFuDH%Mzz_e~EnFc_Pwo$iinOB+V@1}g{ir9%) zmstHATO`!d8*1Wj()tY_V9U|&fY1840wE-5z(*8!HwHmEQM}>ZGEb{mw@iSzqZxj{ zrTeQS@D0DW_iK(dr^Z0+7V(sU3?^~f9iv$^n_KIxc=Se4GQ{l9wha}UGm`>#YsB}U zq+lt@0(Cqp8d^Qut3Y7EmYV|ZxPvc@6CXPms5l)1{#RM{6RjWfChF7=2>tAsY-kq9 zx%Y2faxcE;u9X*G%NcGjcX32(9XlDezS-d?C{J>aX(m~lT$Be3sj!ll^p zWEfHu*s|wPMMR=RG&52ar4chUu;4!};>)+DFiVHY%=Bd^ecFvg9xI?QL&X+vw;JpH zTT}K6JkcFrB|){a^@LC+xT`k$7?dtxm}j6%H(qhKQ!u@y>6Ojzr3aZc*IUf&!m@O!l#KvR%^#f#RO?L2As=A-3k?S zo_9A*S^UW@v<1_vt|<^cT*l*w7)>v#gH-ve8q0|ICLm!>r~ z+G>mjbffMw>3p?0^e|eJEx6Wen}Wxu2yCzsr?s^`&5Y0?xVBnNJ^pZGmBtQQWk@RC zv&CuSuq~o{qTd+2dZumWi&Cne%_k7Pnj!#=5Z2X6L)#=XL`xeRPs)h8dkyY4A5DOP z{KglR3aerwZ7Q_KtU^haCGdm{mB2vl6RQJFqX4Sx!>j+9jhj~FNu&-gpQIW0bFq_Bt3(mY5F_XRUaPL;UKyEi2%!7h`Ynrx62}kaMywWa zfieesXv+u=h`rI{X5Ur&$HFALTVU&ZP-i!3$7i+vv)ggmlhIpP&UGl%eJx@LORuP= zv!n}{+b{K|XXAqwJ}Oi{kf%XwkTXag*E8nLw-@4bPD?$!cPj{+RWj7^TI$H&n2PZ~ zLAyEAwHr#GuFgMv#yg3!gpEkgZe}`A+i&Y`VP5tgN>6a)gsPB?h;I`kgzZ*q4N<^U z^A`_BC&cp)ny_*t5|;8M{497%jW7C>7yobUAIT3 zv-ypsNnD?Y8FU;91dA|C#8n(k#>L-aKeoj0fTRwz^hMX%$?lxpmjkr@N!+Mdy%zN!)t9Q( za#6`U{lBRKQ_4I1KHRfVKDKqAQxh5kAAWVOqaAo1DXOTE-RObn2rcGzo6t0vfc2=z znGO%6kvF@?h@XzvQ!1(Uhx-Jq434MJO+j}N(nylunep!?I0jlA zyO3>x_fv(-flbqL;Bq2y|WAL z55)7yQ58N~nY~3arK;K%p~REa{z!!7m-i z+f(?V1RmqOdGi6qd$IeMpHBTSufX|g7bD>A^s6?3CPPH@+gfE|kl|}&HKM5jxD>IH z7k`YXU-C4Ku3zx{{Y>Btrkp!+h0~jB!$CC_SFYjH^OC1kS}JQUbO7vqP;y4VmE~Ik zOJj{Bp#^X;(4pJ$CvCY2*S2WQ{kVmPGovE$L-^dNz7zT$KXV-FQwOOU)#jr-q z@n0s!n`aD}J|C;xwIoByexeK1_+QVuOA~!5q^?L0-No8J$|@S!RP8v-7hD8^f3!Q4 zaX1y`V_PyQ6hLmxgu$;XwNvB2kowhjZ6?WiQ)aw@ z?C>$xk*@;YEcHyoyY`8|UGv4p)zJioqD7P7KnPtOHskCS=2pAW7Yf6r8&i$O(b9fg z&y}6{p3?s4z&49j{HCRQxpzZKL~}!eT6jssCfj5V6raLQ)VhNJhqOKs!`t?E5ajT# znGKsCyy^zO3{HGX=t+o=tzBNQ6$Ft7GG+?_B85HVVn2i)R(X=%n-Vhr&96USL3`2V zQ#!)!DVsvUxq;khCqnKt>={jo(X6_9rGd;p6`hQ}TKy{e8E4jLmj=&C|Lo*9XBs^b z-I7w|eMoVa%V;$}g5TnOGaAPls2g*USQb?)POmPZo9<;V^gT+}uYc=Ld1nvyO>aS3 z%c6YJh8c>njBVG);5$N!j&yeP$Zd zHSdg9hZ?6L+JS`G(+d?F0lL7SNh!&*UzHgO|>x!Y=% zVGspngGbvhG642b!F%!cQ0b2VLn`3lBw(7~6UPw~@(xE@{D9>g2Wk)UvABW9pYfd6De^t`+UuC0_<%R-3964r{piQZ3C#XS z_WSGKRh%?A2^|#Qq;($$N0(Q6_0O!sq@@}X4+Nth6ns$i7-A*9yWMdB&qK;IE z84BS+0DoH~Dugp0jCe|;Z1KIv*cE51HhvxA-o&h3!D(RDb+7I!tcladh^+K&P>Ln3 znR_zv$tk9A1COLLU71BuGx(cN(d6zwA>Fq3q#mX5&7mF!!*fzHPLc>R&5O zLC~e{7W_h7&aXd%W$S%Ko)uVc%z$k{$#LdQ-VX}WiY&7yx4{Q2`r;Id&k%x$7~PYq zON+~s%ujKh`JLQkZ@}MtvJ4pA<->U_uvYf*L1+;V)wCMS+l~n(=)eAK???>z#p30w zRMPqW$A5n3TDd3Y zUZkiPB9I$4*!C601+@`eLd4udC7m0HUDND$aY<286cCjh_i;hobFl53X)Y)(sg!AR z?;Q}g^bR&Tjy2!k=l8FN!{MOd`~7-7pN|L3K{+xQGRweuHJO+N21S$IwH?7Syljy8 zw!oqdcK(`~Zhu>jWXkF*0#K;XAM z(F=<=`y1A74rN3MDj4AmgSvZ!qw_03G@6=qff~ohZ!WU3BCs;N6WJtM(#16auW||u zhq+velT*9xaIv7buk5^2ux;ByZaI)lYFQE;+~refd+5VkLI-dfpi?I{C9sQIJ(HSa2jCbJ5H(%99l+6sq#T9-m#rGO;WS_Cf=v_#&qPlUoUNx z8pY4w{UuJz+opet?T-OF=VTQNz9Hf;fL?_6{~vWo16$et-Qzuk>S#K*m= zZ3rV8RlX7-SrMuO9~DLKesG1_iW{)ng!eMDUnb3SsW^Efv((xp80($#R8>Pu#N{9S+kv*JPY$QOPL*)q|$+Lp*rt-|a4;*CMnCz!Ib6 z^_*+X&S-vn!niZ=$6~NW+5sSmoJyr-(CTsdU%67#1n|roN6^o){OWsbHv<*wQ z#!p8|Z}}GW!qoV2ieJ)?p6gDHvD_5xjoR!Z&l3v}+{Ef5z~sk>fjAw^5I;IGq6HDw z8&svps{m@CvmFj^Y)UN7Cqjc9i&87gF;B(qNAKJZzXgR!WR^f^C+^&l)Ldy^MH#hy z#iLWIN1GmgI04Y49CT{Ra()><%?|L5w+kpRlpl8Nv>-7aM=Epre9l(hCB! zLGSL8VlpDjEPM==r*(JJYzpnvR3|?AZkY%d#KMBT{3{MCV%|f*Nlr7marGrY`eX;< zRPfE2>bJP6%9cAO0G_t9fG0l$ImOk#%ykR3H_p}S6q9H!vZ16yDvnjhAU9AwZi2Sdfxe|nK}9= z!u?y=Mqb6DE)}g`yUW)^d8Zm%xWe_xd#9q9akEVqM@EJtytsyQjzsW4w02em$ti2N zvRqc14b*&ertD?WXT6>-PXidEnQeEUwN=z6Vc^zMj zu)xvw;_A5ry-jc8Ez2lQ5QVJ|-AUC`pP2A067^S zEeXxK$yWaFBGSj$6{=_?*zVe1a6pVQkHy4@AOoTk4a$NM(s?0Wu*d2}A_ zNiQ@SY=gj{EU%^ym0A0L*I`O8dJkJi=KmHbeFN7K2r5sM>|XGl$@Z!q4AW4&VG8YV z6m`h6slU>obB!TcKUusYYk~%mw>^p_?-~lQDUz2P2juF38r9W4L~<>P9wZz~EB({; zFj*&fZzo4J!&23x%|2ki7uC!cnTXNNBIyrZj^`lWK;kb6Hb}bGOP0g#R`5L_DXL{& z6sy2lXpZ-ws*QY;W*OUeZ=2Xsdl;2FwlN9{m31-_R6EKUL24fC#SvC>5#kYk%$_W= zPu5Ghpst`L=YJ!Yo;(`K`S?AWIY!h_h5~`aQGTw4(~JT*f(?BBK~gpFYxTOVt}#1@ za)HH-RYmIJ8%J-qm=8=oAJO8{Xnw2$6YLmh=|1IV8;66Zqc|gkQUs~~z3^C>9m*)< zL;Bz)9JFFAi;^R!l9nAHBHYGM9v@eny{V`m`iO zBAFlB1DEiar#CKIMZ5blzJKvZ>DeawM)`@8^w&9W82+k zF((IaV)?6%8%$fnehiCy zrJ)d-DHrkqqJLP8@r(i}+%KWjAsFC=S~!j&Y^8HbvHoSaXWM_PZh#@G$R2W#N26i0 zUfid^FNfJo4BB=8^4FP0-Va?R_SeBuSneFz_Xo0 zw<$MhFn)2{CHbTDmOI#F>S>sNd5Am!`r{64yB&4gKMp39+wk6Bxd2tPcQ-F(v{+b0 z3L|=di7)e=2ts2EC<*9&-eJ5_D(DH2D)7~)=8YF2-U5+Sy+bx0_a}Z%5FU>C7xS^W&b8NxdwL7>=zj0L8d_JS zjemV${eU3nV%YSbVPRaI0))Ck-I;bEG6_<5wM7h%y132`oraXjn|B2{>9Z5=3VZKf zQYX-s@FB)M^&!mG42kdmD@G$4+TAy1faE5?^gAz`xKYIC&8Im~r@Z#i!zY#-b!VM#=X1tY=h&0k@Ga7O zl8@gbw#st`Di%dMD9?F2zHG-9Rh(~5d+P#IENkv+tqZbqzq*#j=>u^@d2AW7U<#e7 zJ#Ny$iGgOJEYC4!r7)_u!mnGKURZDfG7{94)fONd@;TKNm*3hnmAjLPIwVO`_M{iO za~qWxCT*yRuR~VHJcpVazL07w>1J8J`LD|Kh~-$f1eMB2F&zS zmQwVH8}%edsvJJNyxpMhvM!n8yq{spHDV%8cogwufq*to% z|Id`0*r-}tmp>3Pge|O4i)NAb`Ow*`;_+NIlo57s)-BaE4Czu+4mx|*I3DlHdmo>? z>0znsi^I-@ZT^H;&Ai|I&hKMrCnD9m)M_Eq1-_-4lzQ$Zgxa3W^J;O3@87tRsZVP( z69DPwFA1D`f{a1Z<-dNe08=G0YZ(lBnXXta9yD6a`ly~GuNqw=`A=GM8o;@Cr9w3?uLxF|M7G7ZUGrThh12C^sGF7}YtetX$!T zognXuNWFeeLxXbxHg4Q~?mw^SBCf4YaH!5o%NOCx+qL)nzX}R_$H{UUF-b2Pu zAfBT(UpqRnb(dh&sc@!L>ui`TYzZ z9n4S1c5#vtpW}c1zl$AbEL)m!<>OynezdbER^M6~^e$bio>x)OI^HpG!{6|>VXdjXZK+!g z5nGGg40G@N+mo2mBgVn=yo`+4H{CN#HmH)KS3G;<|Ek;g_CedaJcji`Ts_?$xCaZSP+m#*p6f-5yHOj_T$9g-&k zOMg@Ax^uG65P|`q$Q3+GyOSx)np>9Uu9e5RG_fAu7GV)BPODy%p-maHSO7*&m|6TH z?CHokZ~B@yslZh4g!#L;%`Mg%x^y#Zgl;Bp)|?bbK92}u(z^M7@xm*vsP70EdtD*+ z)LJ(i}>?F%ik3bR0lmM3kJocQll=e?Q5j`>=$=j|5U9&izK zmp*&BE!RsG>8f~WpvTvtTifk3ooS2WTezfXcGKExO7(R~CCW`UpdDB9@V&sMsyxt& zjuKCk-`1k^Jm=}xBkBQ^9Y8T!S3bA}GZ)S-W)I4{iaJtD&k!V zHfUhp4nXERdfVFEAhT3j0XiT6-Pfpvv%-d8Nd!RrYZ*pzEm}+imb;BMhbj5Om4EiB z#C7axVZ%hKW#J0%aQZ5oB#BxEon`knRJ78kE<0u2s{!Z>Y&E`Wlj;NFTbTr>_@R=`rZZ1TbDn1gl`Uav1yh9~Z3yt3(UILTbl37Vw|8oCt=)NTD!Axc}wC zD^v^Bi2ZXpP(Zs|zWFym9pqVu(y6}K6zc+`5}^daEE?a9a^2@f)H^D+Jl`#t_K~XRO*arMe>;Pz+zenDsesTO zLW}VB>Ly(;xJnx3-%S@3P(ru=s4pLKoJ;vTfC2h$FouDMwg`6uyZzt45J2=8*y~F5 zR92noE*>)>q;`6*A-0wBHVIz=?s|48q$Lrmm)#H1w)XTuKxFv{;Q?~t8&}17ll_Q} z`e!q2Je~7Kx@&E|nHe+(9SPAc3d)CnPN6)YbBURdL&#fRq_I$3l&uDE=H-Iu1NxwH*yb=h#8gU>(V(yjBW|B-yyrwNtdV=MgXf`Kl zVgkM))~@9fIRkj*t_fhbuxJa>H)Z9Tt`YYa}vOgY&X)l$b7 zD=8Fq0kF}jkOJL49Y~AOin82*{$s)!W=Okh^CAm?XUwynn9TI^m7azE{kL(FSq8|y zsWnT$&sG-{*mpGLe0;L@)2i>@Yj+cAv|qJN{4hPH*Z%Wa_Jg0P>?C8?HcF_ALfce7 z!ZGPdBdI#|<6~vq9&j|9o_4cl{6#|RbZ=Nyq5bV0)wiMr(AWJdEut!1Z`(5H<;gxD zRsmjaqN~xL8rn(sQGR4+MAh2k<>&Qu1_H3S=|gE|uLhxSkQ-j=RiKsU2v0f7hT3m$ z-jH58RMyghBoJBT?Uz`;3+okb=2)Vf{~7{?IHeG(r)>3!^uOqD8raOR2e8FfyLb#W z+*oW>z|My`P^)X*+8tEqJdFd`R}v~PEB<2VZB1a!TuottBUUy<`e~9G4dOBOxKzFA zqb|DdOs)%|6|tXV7&^tgMvbJSXy3AxFEaWd&@fCwYIuBJ{4h&1VLy|fQCeY>CGX{+ zc*m^SY*);sT85XqY|SZ2Zb5{(wh@(I!S2z5e+&2{s#{nRxjLRZm^V}?bp0{vQf<=g z11;$##fc%MzNQcA?wS!I3^8_4-GBFj2c4Tzx1a|nU7d=gCj<80x&~dyuu*>r0IQW! zcCy2v<2U-ObgA9BPU7AHgV=lT>8+23Nl}5`k{y>~sQQk@Qvk#0sSjdNG%n}9<;+Y( zTr&hO^@V7MLU;Wz2_0foc&OAYfFcO$3(?7P*hi9JXzgv%3lCi3(a%0DNpjz9kF@J7 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%0DJDfexY5+r8Pzd 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℞|P+eXA3cTh|J4NKOI5& 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

+q%B*p> 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

yjCBNFqJOHC>#?Ba@j&CGreO;-M<82$#y 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%0D&TQmXRKZf8nTHOf3x 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 literal 0 HcmV?d00001 diff --git a/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_light_land.webp b/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_light_land.webp new file mode 100644 index 0000000000000000000000000000000000000000..5cb8502ba98f70dde1fe172ebcaeb00543f193d1 GIT binary patch literal 70976 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

1N>1lI5?Dc7PNM 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 + + + @drawable/welcome_header_dark_land + diff --git a/app-lock/presentation/src/main/res/values-land/drawable.xml b/app-lock/presentation/src/main/res/values-land/drawable.xml new file mode 100644 index 00000000..c680ff7d --- /dev/null +++ b/app-lock/presentation/src/main/res/values-land/drawable.xml @@ -0,0 +1,23 @@ + + + + @drawable/welcome_header_light_land + @drawable/welcome_header_light_land + @drawable/welcome_header_dark_land + diff --git a/app-lock/presentation/src/main/res/values-night/drawable.xml b/app-lock/presentation/src/main/res/values-night/drawable.xml new file mode 100644 index 00000000..7b219490 --- /dev/null +++ b/app-lock/presentation/src/main/res/values-night/drawable.xml @@ -0,0 +1,21 @@ + + + + @drawable/welcome_header_dark + diff --git a/app-lock/presentation/src/main/res/values/drawable.xml b/app-lock/presentation/src/main/res/values/drawable.xml new file mode 100644 index 00000000..85250111 --- /dev/null +++ b/app-lock/presentation/src/main/res/values/drawable.xml @@ -0,0 +1,21 @@ + + + + @drawable/welcome_header_light + diff --git a/app-lock/src/main/AndroidManifest.xml b/app-lock/src/main/AndroidManifest.xml new file mode 100644 index 00000000..707ffdcb --- /dev/null +++ b/app-lock/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a00aea3f..96080a54 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,7 +16,6 @@ * along with Proton Drive. If not, see . */ -import java.io.FileInputStream import java.util.Properties plugins { @@ -49,6 +48,7 @@ driveModule( serialization = true, ) { implementation(files("../../proton-libs/gopenpgp/gopenpgp.aar")) + implementation(project(":app-lock")) implementation(project(":app-ui-settings")) implementation(project(":drive")) @@ -67,13 +67,14 @@ driveModule( implementation(libs.timber) androidTestImplementation(libs.androidx.navigation.compose) + androidTestImplementation(libs.fusion) coreLibraryDesugaring(libs.desugar.jdk.libs) } val privateProperties = Properties().apply { try { - load(FileInputStream("private.properties")) + load(rootDir.resolve("private.properties").inputStream()) } catch (exception: java.io.FileNotFoundException) { // Provide empty properties to allow the app to be built without secrets logger.warn("private.properties file not found", exception) diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/1.json b/app/schemas/me.proton.android.drive.db.AppDatabase/1.json index 8d06834d..995a4108 100644 --- a/app/schemas/me.proton.android.drive.db.AppDatabase/1.json +++ b/app/schemas/me.proton.android.drive.db.AppDatabase/1.json @@ -2,1662 +2,12 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "1848c5aa24ef749bea72f819f081cfe5", + "identityHash": "be4e11f65691008e811c02bbbb906104", "entities": [ { - "tableName": "AccountEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "tableName": "AppLockEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`key`))", "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "username", - "columnName": "username", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "state", - "columnName": "state", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "sessionId", - "columnName": "sessionId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "sessionState", - "columnName": "sessionState", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "userId" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_AccountEntity_sessionId", - "unique": false, - "columnNames": [ - "sessionId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" - }, - { - "name": "index_AccountEntity_userId", - "unique": false, - "columnNames": [ - "userId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)" - } - ], - "foreignKeys": [ - { - "table": "SessionEntity", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "sessionId" - ], - "referencedColumns": [ - "sessionId" - ] - } - ] - }, - { - "tableName": "AccountMetadataEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "product", - "columnName": "product", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "primaryAtUtc", - "columnName": "primaryAtUtc", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "migrations", - "columnName": "migrations", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "userId", - "product" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_AccountMetadataEntity_userId", - "unique": false, - "columnNames": [ - "userId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" - }, - { - "name": "index_AccountMetadataEntity_product", - "unique": false, - "columnNames": [ - "product" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)" - }, - { - "name": "index_AccountMetadataEntity_primaryAtUtc", - "unique": false, - "columnNames": [ - "primaryAtUtc" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)" - } - ], - "foreignKeys": [ - { - "table": "AccountEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "userId" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "SessionEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "sessionId", - "columnName": "sessionId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "accessToken", - "columnName": "accessToken", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "refreshToken", - "columnName": "refreshToken", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "scopes", - "columnName": "scopes", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "product", - "columnName": "product", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "sessionId" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_SessionEntity_sessionId", - "unique": false, - "columnNames": [ - "sessionId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" - }, - { - "name": "index_SessionEntity_userId", - "unique": false, - "columnNames": [ - "userId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)" - } - ], - "foreignKeys": [ - { - "table": "AccountEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "userId" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "SessionDetailsEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "sessionId", - "columnName": "sessionId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "initialEventId", - "columnName": "initialEventId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "requiredAccountType", - "columnName": "requiredAccountType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "secondFactorEnabled", - "columnName": "secondFactorEnabled", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "twoPassModeEnabled", - "columnName": "twoPassModeEnabled", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "password", - "columnName": "password", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "sessionId" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_SessionDetailsEntity_sessionId", - "unique": false, - "columnNames": [ - "sessionId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" - } - ], - "foreignKeys": [ - { - "table": "SessionEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "sessionId" - ], - "referencedColumns": [ - "sessionId" - ] - } - ] - }, - { - "tableName": "UserEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "displayName", - "columnName": "displayName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "credit", - "columnName": "credit", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "usedSpace", - "columnName": "usedSpace", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "maxSpace", - "columnName": "maxSpace", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "maxUpload", - "columnName": "maxUpload", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "role", - "columnName": "role", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "private", - "columnName": "private", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subscribed", - "columnName": "subscribed", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "services", - "columnName": "services", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "delinquent", - "columnName": "delinquent", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "passphrase", - "columnName": "passphrase", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "userId" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_UserEntity_userId", - "unique": false, - "columnNames": [ - "userId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)" - } - ], - "foreignKeys": [ - { - "table": "AccountEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "userId" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "UserKeyEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "keyId", - "columnName": "keyId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "version", - "columnName": "version", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "privateKey", - "columnName": "privateKey", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isPrimary", - "columnName": "isPrimary", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isUnlockable", - "columnName": "isUnlockable", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "fingerprint", - "columnName": "fingerprint", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "activation", - "columnName": "activation", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "active", - "columnName": "active", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "keyId" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_UserKeyEntity_userId", - "unique": false, - "columnNames": [ - "userId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)" - }, - { - "name": "index_UserKeyEntity_keyId", - "unique": false, - "columnNames": [ - "keyId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" - } - ], - "foreignKeys": [ - { - "table": "UserEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "userId" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "AddressEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "addressId", - "columnName": "addressId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "displayName", - "columnName": "displayName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "signature", - "columnName": "signature", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "domainId", - "columnName": "domainId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "canSend", - "columnName": "canSend", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canReceive", - "columnName": "canReceive", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "enabled", - "columnName": "enabled", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "order", - "columnName": "order", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "signedKeyList.data", - "columnName": "signedKeyList_data", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "signedKeyList.signature", - "columnName": "signedKeyList_signature", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "addressId" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_AddressEntity_addressId", - "unique": false, - "columnNames": [ - "addressId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" - }, - { - "name": "index_AddressEntity_userId", - "unique": false, - "columnNames": [ - "userId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)" - } - ], - "foreignKeys": [ - { - "table": "UserEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "userId" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "AddressKeyEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "addressId", - "columnName": "addressId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "keyId", - "columnName": "keyId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "version", - "columnName": "version", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "privateKey", - "columnName": "privateKey", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isPrimary", - "columnName": "isPrimary", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isUnlockable", - "columnName": "isUnlockable", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "flags", - "columnName": "flags", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "passphrase", - "columnName": "passphrase", - "affinity": "BLOB", - "notNull": false - }, - { - "fieldPath": "token", - "columnName": "token", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "signature", - "columnName": "signature", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "fingerprint", - "columnName": "fingerprint", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "fingerprints", - "columnName": "fingerprints", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "activation", - "columnName": "activation", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "active", - "columnName": "active", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "keyId" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_AddressKeyEntity_addressId", - "unique": false, - "columnNames": [ - "addressId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" - }, - { - "name": "index_AddressKeyEntity_keyId", - "unique": false, - "columnNames": [ - "keyId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" - } - ], - "foreignKeys": [ - { - "table": "AddressEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "addressId" - ], - "referencedColumns": [ - "addressId" - ] - } - ] - }, - { - "tableName": "KeySaltEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "keyId", - "columnName": "keyId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "keySalt", - "columnName": "keySalt", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "userId", - "keyId" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_KeySaltEntity_userId", - "unique": false, - "columnNames": [ - "userId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)" - }, - { - "name": "index_KeySaltEntity_keyId", - "unique": false, - "columnNames": [ - "keyId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "PublicAddressEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`email`))", - "fields": [ - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "recipientType", - "columnName": "recipientType", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "mimeType", - "columnName": "mimeType", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "signedKeyListEntity.data", - "columnName": "signedKeyList_data", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "signedKeyListEntity.signature", - "columnName": "signedKeyList_signature", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "email" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_PublicAddressEntity_email", - "unique": false, - "columnNames": [ - "email" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "PublicAddressKeyEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "flags", - "columnName": "flags", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "publicKey", - "columnName": "publicKey", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isPrimary", - "columnName": "isPrimary", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "email", - "publicKey" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_PublicAddressKeyEntity_email", - "unique": false, - "columnNames": [ - "email" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)" - } - ], - "foreignKeys": [ - { - "table": "PublicAddressEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "email" - ], - "referencedColumns": [ - "email" - ] - } - ] - }, - { - "tableName": "HumanVerificationEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `captchaVerificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))", - "fields": [ - { - "fieldPath": "clientId", - "columnName": "clientId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "clientIdType", - "columnName": "clientIdType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "verificationMethods", - "columnName": "verificationMethods", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "captchaVerificationToken", - "columnName": "captchaVerificationToken", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "state", - "columnName": "state", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "humanHeaderTokenType", - "columnName": "humanHeaderTokenType", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "humanHeaderTokenCode", - "columnName": "humanHeaderTokenCode", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "clientId" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "UserSettingsEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `invoiceText` TEXT, `density` INTEGER, `theme` TEXT, `themeType` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `welcome` INTEGER, `earlyAccess` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `twoFA_u2fKeys` TEXT, `flags_welcomed` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "news", - "columnName": "news", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "logAuth", - "columnName": "logAuth", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "invoiceText", - "columnName": "invoiceText", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "density", - "columnName": "density", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "theme", - "columnName": "theme", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "themeType", - "columnName": "themeType", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "weekStart", - "columnName": "weekStart", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "dateFormat", - "columnName": "dateFormat", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "timeFormat", - "columnName": "timeFormat", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "welcome", - "columnName": "welcome", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "earlyAccess", - "columnName": "earlyAccess", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "email.value", - "columnName": "email_value", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "email.status", - "columnName": "email_status", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "email.notify", - "columnName": "email_notify", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "email.reset", - "columnName": "email_reset", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "phone.value", - "columnName": "phone_value", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "phone.status", - "columnName": "phone_status", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "phone.notify", - "columnName": "phone_notify", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "phone.reset", - "columnName": "phone_reset", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "password.mode", - "columnName": "password_mode", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "password.expirationTime", - "columnName": "password_expirationTime", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "twoFA.enabled", - "columnName": "twoFA_enabled", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "twoFA.allowed", - "columnName": "twoFA_allowed", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "twoFA.expirationTime", - "columnName": "twoFA_expirationTime", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "twoFA.u2fKeys", - "columnName": "twoFA_u2fKeys", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "flags.welcomed", - "columnName": "flags_welcomed", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "userId" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "UserEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "userId" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "OrganizationEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `vpnPlanName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "displayName", - "columnName": "displayName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "planName", - "columnName": "planName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "vpnPlanName", - "columnName": "vpnPlanName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "twoFactorGracePeriod", - "columnName": "twoFactorGracePeriod", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "theme", - "columnName": "theme", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "maxDomains", - "columnName": "maxDomains", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "maxAddresses", - "columnName": "maxAddresses", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "maxSpace", - "columnName": "maxSpace", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "maxMembers", - "columnName": "maxMembers", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "maxVPN", - "columnName": "maxVPN", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "features", - "columnName": "features", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "flags", - "columnName": "flags", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "usedDomains", - "columnName": "usedDomains", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "usedAddresses", - "columnName": "usedAddresses", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "usedSpace", - "columnName": "usedSpace", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "assignedSpace", - "columnName": "assignedSpace", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "usedMembers", - "columnName": "usedMembers", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "usedVPN", - "columnName": "usedVPN", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasKeys", - "columnName": "hasKeys", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "toMigrate", - "columnName": "toMigrate", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "userId" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "UserEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "userId" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "OrganizationKeysEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "publicKey", - "columnName": "publicKey", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "privateKey", - "columnName": "privateKey", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "userId" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "UserEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "userId" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "EventMetadataEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `response` TEXT, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "userId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "config", - "columnName": "config", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "eventId", - "columnName": "eventId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "nextEventId", - "columnName": "nextEventId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "refresh", - "columnName": "refresh", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "more", - "columnName": "more", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "response", - "columnName": "response", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "retry", - "columnName": "retry", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "state", - "columnName": "state", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "createdAt", - "columnName": "createdAt", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "updatedAt", - "columnName": "updatedAt", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "userId", - "config" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_EventMetadataEntity_userId", - "unique": false, - "columnNames": [ - "userId" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" - }, - { - "name": "index_EventMetadataEntity_config", - "unique": false, - "columnNames": [ - "config" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)" - }, - { - "name": "index_EventMetadataEntity_createdAt", - "unique": false, - "columnNames": [ - "createdAt" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)" - } - ], - "foreignKeys": [ - { - "table": "UserEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "userId" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "VolumeEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `max_space` INTEGER, `used_space` INTEGER NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "creationTime", - "columnName": "creation_time", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "maxSpace", - "columnName": "max_space", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "usedSpace", - "columnName": "used_space", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "state", - "columnName": "state", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "user_id", - "id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_VolumeEntity_user_id", - "unique": false, - "columnNames": [ - "user_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" - }, - { - "name": "index_VolumeEntity_share_id", - "unique": false, - "columnNames": [ - "share_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" - }, - { - "name": "index_VolumeEntity_id", - "unique": false, - "columnNames": [ - "id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_id` ON `${TABLE_NAME}` (`id`)" - } - ], - "foreignKeys": [ - { - "table": "AccountEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "ShareEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `link_id` TEXT NOT NULL, `block_size` INTEGER NOT NULL, `locked` INTEGER NOT NULL, `key` TEXT NOT NULL, `passphrase` TEXT NOT NULL, `passphrase_signature` TEXT NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "volumeId", - "columnName": "volume_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "flags", - "columnName": "flags", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "linkId", - "columnName": "link_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "blockSize", - "columnName": "block_size", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isLocked", - "columnName": "locked", - "affinity": "INTEGER", - "notNull": true - }, { "fieldPath": "key", "columnName": "key", @@ -1665,1569 +15,132 @@ "notNull": true }, { - "fieldPath": "passphrase", - "columnName": "passphrase", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "passphraseSignature", - "columnName": "passphrase_signature", + "fieldPath": "type", + "columnName": "type", "affinity": "TEXT", "notNull": true } ], "primaryKey": { "columnNames": [ - "user_id", - "id" + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LockEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`passphrase` TEXT NOT NULL, `key` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`passphrase`), FOREIGN KEY(`key`) REFERENCES `AppLockEntity`(`key`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "appKeyPassphrase", + "columnName": "passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appKey", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "passphrase" ], "autoGenerate": false }, "indices": [ { - "name": "index_ShareEntity_user_id", + "name": "index_LockEntity_key", "unique": false, "columnNames": [ - "user_id" + "key" ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" - }, - { - "name": "index_ShareEntity_volume_id", - "unique": false, - "columnNames": [ - "volume_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" - }, - { - "name": "index_ShareEntity_link_id", - "unique": false, - "columnNames": [ - "link_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LockEntity_key` ON `${TABLE_NAME}` (`key`)" } ], "foreignKeys": [ { - "table": "AccountEntity", + "table": "AppLockEntity", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ - "user_id" + "key" ], "referencedColumns": [ - "userId" + "key" ] } ] }, { - "tableName": "ShareUrlEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `name` TEXT, `token` TEXT NOT NULL, `creatior_email` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `creation_time` INTEGER NOT NULL, `expiration_time` INTEGER, `last_access_time` INTEGER, `max_accesses` INTEGER, `number_of_accesses` INTEGER NOT NULL, `url_password_salt` TEXT NOT NULL, `share_password_salt` TEXT NOT NULL, `srp_verifier` TEXT NOT NULL, `srp_modulus_id` TEXT NOT NULL, `password` TEXT NOT NULL, `share_passphrase_key_packet` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "tableName": "AutoLockDurationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `duration` INTEGER NOT NULL, PRIMARY KEY(`key`))", "fields": [ { - "fieldPath": "id", - "columnName": "id", + "fieldPath": "key", + "columnName": "key", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "flags", - "columnName": "flags", + "fieldPath": "durationInSeconds", + "columnName": "duration", "affinity": "INTEGER", "notNull": true - }, + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "EnableAppLockEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `last_access_time` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "token", - "columnName": "token", + "fieldPath": "key", + "columnName": "key", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "creatorEmail", - "columnName": "creatior_email", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "permissions", - "columnName": "permissions", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "creationTime", - "columnName": "creation_time", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "expirationTime", - "columnName": "expiration_time", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "lastAccessTime", + "fieldPath": "timestamp", "columnName": "last_access_time", "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "maxAccesses", - "columnName": "max_accesses", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "numberOfAccesses", - "columnName": "number_of_accesses", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "urlPasswordSalt", - "columnName": "url_password_salt", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "sharePasswordSalt", - "columnName": "share_password_salt", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "srpVerifier", - "columnName": "srp_verifier", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "srpModulusId", - "columnName": "srp_modulus_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "password", - "columnName": "password", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "sharePassphraseKeyPacket", - "columnName": "share_passphrase_key_packet", - "affinity": "TEXT", "notNull": true } ], "primaryKey": { "columnNames": [ - "user_id", - "share_id", - "id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_ShareUrlEntity_user_id_share_id", - "unique": false, - "columnNames": [ - "user_id", - "share_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)" - } - ], - "foreignKeys": [ - { - "table": "ShareEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id", - "share_id" - ], - "referencedColumns": [ - "user_id", - "id" - ] - } - ] - }, - { - "tableName": "LinkEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `parent_id` TEXT, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_signature_email` TEXT, `hash` TEXT NOT NULL, `state` INTEGER NOT NULL, `expiration_time` INTEGER, `size` INTEGER NOT NULL, `mime_type` TEXT NOT NULL, `attributes` INTEGER NOT NULL, `permissions` INTEGER NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `signature_address` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, `trashed_time` INTEGER, `is_shared` INTEGER NOT NULL, `number_of_accesses` INTEGER NOT NULL, `share_url_expiration_time` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `parent_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "parentId", - "columnName": "parent_id", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "nameSignatureEmail", - "columnName": "name_signature_email", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "hash", - "columnName": "hash", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "state", - "columnName": "state", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "expirationTime", - "columnName": "expiration_time", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "size", - "columnName": "size", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "mimeType", - "columnName": "mime_type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "attributes", - "columnName": "attributes", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "permissions", - "columnName": "permissions", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "nodeKey", - "columnName": "node_key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "nodePassphrase", - "columnName": "node_passphrase", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "nodePassphraseSignature", - "columnName": "node_passphrase_signature", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "signatureAddress", - "columnName": "signature_address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "creationTime", - "columnName": "creation_time", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lastModified", - "columnName": "last_modified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "trashedTime", - "columnName": "trashed_time", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "shared", - "columnName": "is_shared", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "numberOfAccesses", - "columnName": "number_of_accesses", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "shareUrlExpirationTime", - "columnName": "share_url_expiration_time", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "user_id", - "share_id", - "id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_LinkEntity_user_id_share_id", - "unique": false, - "columnNames": [ - "user_id", - "share_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)" - }, - { - "name": "index_LinkEntity_user_id_id", - "unique": false, - "columnNames": [ - "user_id", - "id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_id` ON `${TABLE_NAME}` (`user_id`, `id`)" - }, - { - "name": "index_LinkEntity_user_id_share_id_parent_id", - "unique": false, - "columnNames": [ - "user_id", - "share_id", - "parent_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id_parent_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `parent_id`)" - } - ], - "foreignKeys": [ - { - "table": "AccountEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id" - ], - "referencedColumns": [ - "userId" - ] - }, - { - "table": "ShareEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id", - "share_id" - ], - "referencedColumns": [ - "user_id", - "id" - ] - }, - { - "table": "LinkEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id", - "share_id", - "parent_id" - ], - "referencedColumns": [ - "user_id", - "share_id", - "id" - ] - } - ] - }, - { - "tableName": "LinkFilePropertiesEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file_user_id` TEXT NOT NULL, `file_share_id` TEXT NOT NULL, `file_link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `has_thumbnail` INTEGER NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT, `file_signature_address` TEXT, PRIMARY KEY(`file_user_id`, `file_share_id`, `file_link_id`), FOREIGN KEY(`file_user_id`, `file_share_id`, `file_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "file_user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "file_share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "linkId", - "columnName": "file_link_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "activeRevisionId", - "columnName": "revision_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "hasThumbnail", - "columnName": "has_thumbnail", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "contentKeyPacket", - "columnName": "content_key_packet", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "contentKeyPacketSignature", - "columnName": "content_key_packet_signature", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "activeRevisionSignatureAddress", - "columnName": "file_signature_address", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "file_user_id", - "file_share_id", - "file_link_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id", - "unique": false, - "columnNames": [ - "file_user_id", - "file_share_id", - "file_link_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id` ON `${TABLE_NAME}` (`file_user_id`, `file_share_id`, `file_link_id`)" - }, - { - "name": "index_LinkFilePropertiesEntity_revision_id", - "unique": false, - "columnNames": [ - "revision_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" - } - ], - "foreignKeys": [ - { - "table": "LinkEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "file_user_id", - "file_share_id", - "file_link_id" - ], - "referencedColumns": [ - "user_id", - "share_id", - "id" - ] - } - ] - }, - { - "tableName": "LinkFolderPropertiesEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`folder_user_id` TEXT NOT NULL, `folder_share_id` TEXT NOT NULL, `folder_link_id` TEXT NOT NULL, `node_hash_key` TEXT NOT NULL, PRIMARY KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`), FOREIGN KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "folder_user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "folder_share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "linkId", - "columnName": "folder_link_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "nodeHashKey", - "columnName": "node_hash_key", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "folder_user_id", - "folder_share_id", - "folder_link_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id", - "unique": false, - "columnNames": [ - "folder_user_id", - "folder_share_id", - "folder_link_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id` ON `${TABLE_NAME}` (`folder_user_id`, `folder_share_id`, `folder_link_id`)" - } - ], - "foreignKeys": [ - { - "table": "LinkEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "folder_user_id", - "folder_share_id", - "folder_link_id" - ], - "referencedColumns": [ - "user_id", - "share_id", - "id" - ] - } - ] - }, - { - "tableName": "LinkOfflineEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "linkId", - "columnName": "link_id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "user_id", - "share_id", - "link_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_LinkOfflineEntity_user_id_share_id_link_id", - "unique": true, - "columnNames": [ - "user_id", - "share_id", - "link_id" - ], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" - } - ], - "foreignKeys": [ - { - "table": "LinkEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id", - "share_id", - "link_id" - ], - "referencedColumns": [ - "user_id", - "share_id", - "id" - ] - } - ] - }, - { - "tableName": "LinkDownloadStateEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "linkId", - "columnName": "link_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "revisionId", - "columnName": "revision_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "state", - "columnName": "state", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "user_id", - "share_id", - "link_id", - "revision_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_LinkDownloadStateEntity_user_id_share_id_link_id", - "unique": false, - "columnNames": [ - "user_id", - "share_id", - "link_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" - } - ], - "foreignKeys": [ - { - "table": "LinkEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id", - "share_id", - "link_id" - ], - "referencedColumns": [ - "user_id", - "share_id", - "id" - ] - } - ] - }, - { - "tableName": "DownloadBlockEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `index` INTEGER NOT NULL, `uri` TEXT NOT NULL, `encrypted_signature` TEXT, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`, `index`), FOREIGN KEY(`user_id`, `share_id`, `link_id`, `revision_id`) REFERENCES `LinkDownloadStateEntity`(`user_id`, `share_id`, `link_id`, `revision_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "linkId", - "columnName": "link_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "revisionId", - "columnName": "revision_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "index", - "columnName": "index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "uri", - "columnName": "uri", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "encryptedSignature", - "columnName": "encrypted_signature", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "user_id", - "share_id", - "link_id", - "revision_id", - "index" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_DownloadBlockEntity_user_id_share_id_link_id_revision_id", - "unique": false, - "columnNames": [ - "user_id", - "share_id", - "link_id", - "revision_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id_share_id_link_id_revision_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`, `revision_id`)" - } - ], - "foreignKeys": [ - { - "table": "LinkDownloadStateEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id", - "share_id", - "link_id", - "revision_id" - ], - "referencedColumns": [ - "user_id", - "share_id", - "link_id", - "revision_id" - ] - } - ] - }, - { - "tableName": "LinkTrashStateEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "linkId", - "columnName": "link_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "state", - "columnName": "state", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "user_id", - "share_id", - "link_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_LinkTrashStateEntity_user_id_share_id_link_id", - "unique": false, - "columnNames": [ - "user_id", - "share_id", - "link_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" - } - ], - "foreignKeys": [ - { - "table": "LinkEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id", - "share_id", - "link_id" - ], - "referencedColumns": [ - "user_id", - "share_id", - "id" - ] - } - ] - }, - { - "tableName": "TrashWorkEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `work_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "linkId", - "columnName": "link_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "workId", - "columnName": "work_id", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "user_id", - "share_id", - "link_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_TrashWorkEntity_user_id_share_id_link_id", - "unique": false, - "columnNames": [ - "user_id", - "share_id", - "link_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" - }, - { - "name": "index_TrashWorkEntity_work_id", - "unique": false, - "columnNames": [ - "work_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_work_id` ON `${TABLE_NAME}` (`work_id`)" - } - ], - "foreignKeys": [ - { - "table": "LinkEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id", - "share_id", - "link_id" - ], - "referencedColumns": [ - "user_id", - "share_id", - "id" - ] - } - ] - }, - { - "tableName": "MessageEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `content` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_MessageEntity_user_id", - "unique": false, - "columnNames": [ - "user_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_MessageEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" - } - ], - "foreignKeys": [ - { - "table": "AccountEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "UiSettingsEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `layout_type` TEXT NOT NULL, `theme_style` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "layoutType", - "columnName": "layout_type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "themeStyle", - "columnName": "theme_style", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "user_id" + "key" ], "autoGenerate": false }, "indices": [], - "foreignKeys": [ - { - "table": "AccountEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "DriveLinkRemoteKeyEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `previous_key` INTEGER, `next_key` INTEGER, PRIMARY KEY(`key`, `user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "linkId", - "columnName": "link_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "prevKey", - "columnName": "previous_key", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "nextKey", - "columnName": "next_key", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "key", - "user_id", - "share_id", - "link_id" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id", - "unique": true, - "columnNames": [ - "user_id", - "share_id", - "link_id" - ], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" - }, - { - "name": "index_DriveLinkRemoteKeyEntity_user_id", - "unique": false, - "columnNames": [ - "user_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" - } - ], - "foreignKeys": [ - { - "table": "AccountEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id" - ], - "referencedColumns": [ - "userId" - ] - }, - { - "table": "LinkEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id", - "share_id", - "link_id" - ], - "referencedColumns": [ - "user_id", - "share_id", - "id" - ] - } - ] - }, - { - "tableName": "SortingEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `sorting_by` TEXT NOT NULL, `sorting_direction` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "sortingBy", - "columnName": "sorting_by", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "sortingDirection", - "columnName": "sorting_direction", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "user_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "AccountEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "LinkUploadEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `name` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT NOT NULL, `manifest_signature` TEXT NOT NULL, `state` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "volumeId", - "columnName": "volume_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "parentId", - "columnName": "parent_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "linkId", - "columnName": "link_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "revisionId", - "columnName": "revision_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "mimeType", - "columnName": "mime_type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "nodeKey", - "columnName": "node_key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "nodePassphrase", - "columnName": "node_passphrase", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "nodePassphraseSignature", - "columnName": "node_passphrase_signature", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "contentKeyPacket", - "columnName": "content_key_packet", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "contentKeyPacketSignature", - "columnName": "content_key_packet_signature", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "manifestSignature", - "columnName": "manifest_signature", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "state", - "columnName": "state", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [ - { - "name": "index_LinkUploadEntity_user_id", - "unique": false, - "columnNames": [ - "user_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" - }, - { - "name": "index_LinkUploadEntity_volume_id", - "unique": false, - "columnNames": [ - "volume_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" - }, - { - "name": "index_LinkUploadEntity_share_id", - "unique": false, - "columnNames": [ - "share_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" - }, - { - "name": "index_LinkUploadEntity_link_id", - "unique": false, - "columnNames": [ - "link_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" - }, - { - "name": "index_LinkUploadEntity_revision_id", - "unique": false, - "columnNames": [ - "revision_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" - }, - { - "name": "index_LinkUploadEntity_parent_id", - "unique": false, - "columnNames": [ - "parent_id" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)" - } - ], - "foreignKeys": [ - { - "table": "AccountEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id" - ], - "referencedColumns": [ - "userId" - ] - } - ] - }, - { - "tableName": "UploadBlockEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_link_id` INTEGER NOT NULL, `index` INTEGER NOT NULL, `size` INTEGER NOT NULL, `encrypted_signature` TEXT NOT NULL, `hash` TEXT NOT NULL, `token` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`upload_link_id`, `index`), FOREIGN KEY(`upload_link_id`) REFERENCES `LinkUploadEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "uploadLinkId", - "columnName": "upload_link_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "index", - "columnName": "index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "size", - "columnName": "size", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "encryptedSignature", - "columnName": "encrypted_signature", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "hash", - "columnName": "hash", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "uploadToken", - "columnName": "token", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "upload_link_id", - "index" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "LinkUploadEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "upload_link_id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "FolderMetadataEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `last_fetch_children_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "linkId", - "columnName": "link_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastFetchChildrenTimestamp", - "columnName": "last_fetch_children_timestamp", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "user_id", - "share_id", - "link_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "LinkEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id", - "share_id", - "link_id" - ], - "referencedColumns": [ - "user_id", - "share_id", - "id" - ] - } - ] - }, - { - "tableName": "TrashMetadataEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `last_fetch_trash_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "userId", - "columnName": "user_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "shareId", - "columnName": "share_id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastFetchTrashTimestamp", - "columnName": "last_fetch_trash_timestamp", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "user_id", - "share_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "ShareEntity", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "user_id", - "share_id" - ], - "referencedColumns": [ - "user_id", - "id" - ] - } - ] + "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1848c5aa24ef749bea72f819f081cfe5')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be4e11f65691008e811c02bbbb906104')" ] } } \ No newline at end of file diff --git a/app/src/dynamic/AndroidManifest.xml b/app/src/dynamic/AndroidManifest.xml new file mode 100644 index 00000000..15a9aa08 --- /dev/null +++ b/app/src/dynamic/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e449c412..ee360a1d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -126,6 +126,17 @@ android:authorities="${applicationId}.androidx-startup" android:exported="false" tools:node="merge"> + + + @@ -136,9 +147,18 @@ android:name="androidx.work.WorkManagerInitializer" android:value="androidx.startup" tools:node="remove" /> + + android:value="androidx.startup" + tools:node="remove" /> + @@ -147,7 +167,16 @@ android:value="androidx.startup" /> + android:value="androidx.startup" + tools:node="remove" /> + + diff --git a/app/src/main/kotlin/me/proton/android/drive/App.kt b/app/src/main/kotlin/me/proton/android/drive/App.kt index 02fa8345..b5485e41 100644 --- a/app/src/main/kotlin/me/proton/android/drive/App.kt +++ b/app/src/main/kotlin/me/proton/android/drive/App.kt @@ -20,6 +20,12 @@ package me.proton.android.drive import android.app.Application import dagger.hilt.android.HiltAndroidApp +import me.proton.android.drive.initializer.MainInitializer @HiltAndroidApp -class App : Application() +class App : Application() { + override fun onCreate() { + super.onCreate() + MainInitializer.init(this) + } +} diff --git a/app/src/main/kotlin/me/proton/android/drive/db/AppDatabase.kt b/app/src/main/kotlin/me/proton/android/drive/db/AppDatabase.kt index 35e5880c..a697010d 100644 --- a/app/src/main/kotlin/me/proton/android/drive/db/AppDatabase.kt +++ b/app/src/main/kotlin/me/proton/android/drive/db/AppDatabase.kt @@ -19,247 +19,33 @@ package me.proton.android.drive.db import android.content.Context -import androidx.room.AutoMigration import androidx.room.Database -import androidx.room.TypeConverters -import me.proton.core.account.data.db.AccountConverters -import me.proton.core.account.data.db.AccountDatabase -import me.proton.core.account.data.entity.AccountEntity -import me.proton.core.account.data.entity.AccountMetadataEntity -import me.proton.core.account.data.entity.SessionDetailsEntity -import me.proton.core.account.data.entity.SessionEntity -import me.proton.core.challenge.data.db.ChallengeConverters -import me.proton.core.challenge.data.db.ChallengeDatabase -import me.proton.core.challenge.data.entity.ChallengeFrameEntity -import me.proton.core.crypto.android.keystore.CryptoConverters +import androidx.room.migration.Migration +import me.proton.android.drive.lock.data.db.AppLockDatabase +import me.proton.android.drive.lock.data.db.entity.AppLockEntity +import me.proton.android.drive.lock.data.db.entity.AutoLockDurationEntity +import me.proton.android.drive.lock.data.db.entity.EnableAppLockEntity +import me.proton.android.drive.lock.data.db.entity.LockEntity import me.proton.core.data.room.db.BaseDatabase -import me.proton.core.data.room.db.CommonConverters -import me.proton.core.drive.drivelink.data.db.DriveLinkDatabase -import me.proton.core.drive.drivelink.download.data.db.DriveLinkDownloadDatabase -import me.proton.core.drive.drivelink.offline.data.db.DriveLinkOfflineDatabase -import me.proton.core.drive.drivelink.paged.data.db.DriveLinkPagedDatabase -import me.proton.core.drive.drivelink.paged.data.db.entity.DriveLinkRemoteKeyEntity -import me.proton.core.drive.drivelink.selection.data.db.DriveLinkSelectionDatabase -import me.proton.core.drive.drivelink.shared.data.db.DriveLinkSharedDatabase -import me.proton.core.drive.drivelink.trash.data.db.DriveLinkTrashDatabase -import me.proton.core.drive.folder.data.db.FolderDatabase -import me.proton.core.drive.folder.data.db.FolderMetadataEntity -import me.proton.core.drive.link.data.db.LinkDatabase -import me.proton.core.drive.link.data.db.entity.LinkEntity -import me.proton.core.drive.link.data.db.entity.LinkFilePropertiesEntity -import me.proton.core.drive.link.data.db.entity.LinkFolderPropertiesEntity -import me.proton.core.drive.link.selection.data.db.LinkSelectionConverters -import me.proton.core.drive.link.selection.data.db.LinkSelectionDatabase -import me.proton.core.drive.link.selection.data.db.entity.LinkSelectionEntity -import me.proton.core.drive.linkdownload.data.db.LinkDownloadDatabase -import me.proton.core.drive.linkdownload.data.db.entity.DownloadBlockEntity -import me.proton.core.drive.linkdownload.data.db.entity.LinkDownloadStateEntity -import me.proton.core.drive.linknode.data.db.LinkAncestorDatabase -import me.proton.core.drive.linkoffline.data.db.LinkOfflineDatabase -import me.proton.core.drive.linkoffline.data.db.LinkOfflineEntity -import me.proton.core.drive.linktrash.data.db.LinkTrashDatabase -import me.proton.core.drive.linktrash.data.db.entity.LinkTrashStateEntity -import me.proton.core.drive.linktrash.data.db.entity.TrashMetadataEntity -import me.proton.core.drive.linktrash.data.db.entity.TrashWorkEntity -import me.proton.core.drive.linkupload.data.db.LinkUploadDatabase -import me.proton.core.drive.linkupload.data.db.entity.LinkUploadEntity -import me.proton.core.drive.linkupload.data.db.entity.UploadBlockEntity -import me.proton.core.drive.linkupload.data.db.entity.UploadBulkEntity -import me.proton.core.drive.linkupload.data.db.entity.UploadBulkUriStringEntity -import me.proton.core.drive.messagequeue.data.storage.db.MessageQueueDatabase -import me.proton.core.drive.messagequeue.data.storage.db.entity.MessageEntity -import me.proton.core.drive.notification.data.db.NotificationConverters -import me.proton.core.drive.notification.data.db.NotificationDatabase -import me.proton.core.drive.notification.data.db.entity.NotificationChannelEntity -import me.proton.core.drive.notification.data.db.entity.NotificationEventEntity -import me.proton.core.drive.share.data.db.ShareDatabase -import me.proton.core.drive.share.data.db.ShareEntity -import me.proton.core.drive.shareurl.base.data.db.ShareUrlDatabase -import me.proton.core.drive.shareurl.base.data.db.entity.ShareUrlEntity -import me.proton.core.drive.sorting.data.db.SortingDatabase -import me.proton.core.drive.sorting.data.db.entity.SortingEntity -import me.proton.core.drive.volume.data.db.VolumeDatabase -import me.proton.core.drive.volume.data.db.VolumeEntity -import me.proton.core.eventmanager.data.db.EventManagerConverters -import me.proton.core.eventmanager.data.db.EventMetadataDatabase -import me.proton.core.eventmanager.data.entity.EventMetadataEntity -import me.proton.core.featureflag.data.db.FeatureFlagDatabase -import me.proton.core.featureflag.data.entity.FeatureFlagEntity -import me.proton.core.humanverification.data.db.HumanVerificationConverters -import me.proton.core.humanverification.data.db.HumanVerificationDatabase -import me.proton.core.humanverification.data.entity.HumanVerificationEntity -import me.proton.core.key.data.db.KeySaltDatabase -import me.proton.core.key.data.db.PublicAddressDatabase -import me.proton.core.key.data.entity.KeySaltEntity -import me.proton.core.key.data.entity.PublicAddressEntity -import me.proton.core.key.data.entity.PublicAddressKeyEntity -import me.proton.core.payment.data.local.db.PaymentDatabase -import me.proton.core.payment.data.local.entity.GooglePurchaseEntity -import me.proton.core.user.data.db.AddressDatabase -import me.proton.core.user.data.db.UserConverters -import me.proton.core.user.data.db.UserDatabase -import me.proton.core.user.data.entity.AddressEntity -import me.proton.core.user.data.entity.AddressKeyEntity -import me.proton.core.user.data.entity.UserEntity -import me.proton.core.user.data.entity.UserKeyEntity -import me.proton.core.usersettings.data.db.OrganizationDatabase -import me.proton.core.usersettings.data.db.UserSettingsConverters -import me.proton.core.usersettings.data.db.UserSettingsDatabase -import me.proton.core.usersettings.data.entity.OrganizationEntity -import me.proton.core.usersettings.data.entity.OrganizationKeysEntity -import me.proton.core.usersettings.data.entity.UserSettingsEntity -import me.proton.drive.android.settings.data.db.AppUiSettingsDatabase -import me.proton.drive.android.settings.data.db.entity.UiSettingsEntity @Database( entities = [ - // Core - AccountEntity::class, - AccountMetadataEntity::class, - SessionEntity::class, - SessionDetailsEntity::class, - UserEntity::class, - UserKeyEntity::class, - AddressEntity::class, - AddressKeyEntity::class, - KeySaltEntity::class, - PublicAddressEntity::class, - PublicAddressKeyEntity::class, - HumanVerificationEntity::class, - UserSettingsEntity::class, - OrganizationEntity::class, - OrganizationKeysEntity::class, - EventMetadataEntity::class, - FeatureFlagEntity::class, - ChallengeFrameEntity::class, - GooglePurchaseEntity::class, - // Drive - VolumeEntity::class, - ShareEntity::class, - ShareUrlEntity::class, - LinkEntity::class, - LinkFilePropertiesEntity::class, - LinkFolderPropertiesEntity::class, - LinkOfflineEntity::class, - LinkDownloadStateEntity::class, - DownloadBlockEntity::class, - LinkTrashStateEntity::class, - // Trash - TrashWorkEntity::class, - // MessageQueue - MessageEntity::class, - // AppUiSettings - UiSettingsEntity::class, - // DriveLinkPaged - DriveLinkRemoteKeyEntity::class, - // Sorting - SortingEntity::class, - // Upload - LinkUploadEntity::class, - UploadBlockEntity::class, - UploadBulkEntity::class, - UploadBulkUriStringEntity::class, - FolderMetadataEntity::class, - TrashMetadataEntity::class, - // Notification - NotificationChannelEntity::class, - NotificationEventEntity::class, - // Selection - LinkSelectionEntity::class, + // AppLock + AppLockEntity::class, + LockEntity::class, + AutoLockDurationEntity::class, + EnableAppLockEntity::class, ], version = AppDatabase.VERSION, - autoMigrations = [ - AutoMigration(from = 4, to = 5), - AutoMigration(from = 5, to = 6), - AutoMigration(from = 7, to = 8), - AutoMigration(from = 9, to = 10), - AutoMigration(from = 13, to = 14), - AutoMigration(from = 15, to = 16), - AutoMigration(from = 16, to = 17), - AutoMigration(from = 17, to = 18, spec = ShareDatabase.DeleteBlockSizeFromShareEntity::class), - AutoMigration(from = 18, to = 19), - ], - exportSchema = true, ) -@TypeConverters( - // Core - CommonConverters::class, - AccountConverters::class, - UserConverters::class, - CryptoConverters::class, - HumanVerificationConverters::class, - UserSettingsConverters::class, - EventManagerConverters::class, - ChallengeConverters::class, - // Drive - NotificationConverters::class, - LinkSelectionConverters::class, -) -abstract class AppDatabase : - BaseDatabase(), - AccountDatabase, - UserDatabase, - AddressDatabase, - KeySaltDatabase, - HumanVerificationDatabase, - PublicAddressDatabase, - UserSettingsDatabase, - OrganizationDatabase, - FeatureFlagDatabase, - VolumeDatabase, - ShareDatabase, - ShareUrlDatabase, - LinkDatabase, - FolderDatabase, - LinkAncestorDatabase, - LinkOfflineDatabase, - LinkDownloadDatabase, - LinkTrashDatabase, - LinkSelectionDatabase, - MessageQueueDatabase, - AppUiSettingsDatabase, - EventMetadataDatabase, - ChallengeDatabase, - SortingDatabase, - LinkUploadDatabase, - DriveLinkDatabase, - DriveLinkPagedDatabase, - DriveLinkTrashDatabase, - DriveLinkOfflineDatabase, - DriveLinkDownloadDatabase, - DriveLinkSharedDatabase, - DriveLinkSelectionDatabase, - NotificationDatabase, - PaymentDatabase { +abstract class AppDatabase : BaseDatabase(), AppLockDatabase { companion object { - const val VERSION = 21 - - private val migrations = listOf( - AppDatabaseMigrations.MIGRATION_1_2, - AppDatabaseMigrations.MIGRATION_2_3, - AppDatabaseMigrations.MIGRATION_3_4, - //AutoMigration(from = 4, to = 5) - //AutoMigration(from = 5, to = 6) - AppDatabaseMigrations.MIGRATION_6_7, - //AutoMigration(from = 7, to = 8) - AppDatabaseMigrations.MIGRATION_8_9, - //AutoMigration(from = 9, to = 10) - AppDatabaseMigrations.MIGRATION_10_11, - AppDatabaseMigrations.MIGRATION_11_12, - AppDatabaseMigrations.MIGRATION_12_13, - //AutoMigration(from = 13, to = 14) - AppDatabaseMigrations.MIGRATION_14_15, - //AutoMigration(from = 15, to = 16) - //AutoMigration(from = 16, to = 17) - //AutoMigration(from = 17, to = 18) - //AutoMigration(from = 18, to = 19) - AppDatabaseMigrations.MIGRATION_19_20, - AppDatabaseMigrations.MIGRATION_20_21, - ) + const val VERSION = 1 + private val migrations = listOf() fun buildDatabase(context: Context): AppDatabase = - databaseBuilder(context, "db-drive") + databaseBuilder(context, "db-app") .apply { migrations.forEach { addMigrations(it) } } .build() } diff --git a/app/src/main/kotlin/me/proton/android/drive/di/AppDatabaseModule.kt b/app/src/main/kotlin/me/proton/android/drive/di/AppDatabaseModule.kt index bd1d5fa6..c4371a3e 100644 --- a/app/src/main/kotlin/me/proton/android/drive/di/AppDatabaseModule.kt +++ b/app/src/main/kotlin/me/proton/android/drive/di/AppDatabaseModule.kt @@ -26,154 +26,23 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import me.proton.android.drive.db.AppDatabase -import me.proton.core.account.data.db.AccountDatabase -import me.proton.core.challenge.data.db.ChallengeDatabase -import me.proton.core.drive.drivelink.data.db.DriveLinkDatabase -import me.proton.core.drive.drivelink.download.data.db.DriveLinkDownloadDatabase -import me.proton.core.drive.drivelink.offline.data.db.DriveLinkOfflineDatabase -import me.proton.core.drive.drivelink.paged.data.db.DriveLinkPagedDatabase -import me.proton.core.drive.drivelink.selection.data.db.DriveLinkSelectionDatabase -import me.proton.core.drive.drivelink.shared.data.db.DriveLinkSharedDatabase -import me.proton.core.drive.drivelink.trash.data.db.DriveLinkTrashDatabase -import me.proton.core.drive.folder.data.db.FolderDatabase -import me.proton.core.drive.link.data.db.LinkDatabase -import me.proton.core.drive.link.selection.data.db.LinkSelectionDatabase -import me.proton.core.drive.linkdownload.data.db.LinkDownloadDatabase -import me.proton.core.drive.linknode.data.db.LinkAncestorDatabase -import me.proton.core.drive.linkoffline.data.db.LinkOfflineDatabase -import me.proton.core.drive.linktrash.data.db.LinkTrashDatabase -import me.proton.core.drive.linkupload.data.db.LinkUploadDatabase -import me.proton.core.drive.messagequeue.data.storage.db.MessageQueueDatabase -import me.proton.core.drive.notification.data.db.NotificationDatabase -import me.proton.core.drive.share.data.db.ShareDatabase -import me.proton.core.drive.shareurl.base.data.db.ShareUrlDatabase -import me.proton.core.drive.sorting.data.db.SortingDatabase -import me.proton.core.drive.volume.data.db.VolumeDatabase -import me.proton.core.eventmanager.data.db.EventMetadataDatabase -import me.proton.core.featureflag.data.db.FeatureFlagDatabase -import me.proton.core.humanverification.data.db.HumanVerificationDatabase -import me.proton.core.key.data.db.KeySaltDatabase -import me.proton.core.key.data.db.PublicAddressDatabase -import me.proton.core.payment.data.local.db.PaymentDatabase -import me.proton.core.user.data.db.AddressDatabase -import me.proton.core.user.data.db.UserDatabase -import me.proton.core.usersettings.data.db.OrganizationDatabase -import me.proton.core.usersettings.data.db.UserSettingsDatabase -import me.proton.drive.android.settings.data.db.AppUiSettingsDatabase +import me.proton.android.drive.lock.data.db.AppLockDatabase import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object AppDatabaseModule { + @Provides @Singleton fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase = AppDatabase.buildDatabase(context) -} +} @Module @InstallIn(SingletonComponent::class) abstract class AppDatabaseBindsModule { @Binds - abstract fun provideVolumeDatabase(db: AppDatabase): VolumeDatabase - - @Binds - abstract fun provideShareDatabase(db: AppDatabase): ShareDatabase - - @Binds - abstract fun provideShareUrlDatabase(db: AppDatabase): ShareUrlDatabase - - @Binds - abstract fun provideLinkDatabase(db: AppDatabase): LinkDatabase - - @Binds - abstract fun provideFolderDatabase(db: AppDatabase): FolderDatabase - - @Binds - abstract fun provideLinkAncestorDatabase(db: AppDatabase): LinkAncestorDatabase - - @Binds - abstract fun provideLinkOfflineDatabase(db: AppDatabase): LinkOfflineDatabase - - @Binds - abstract fun provideLinkDownloadDatabase(db: AppDatabase): LinkDownloadDatabase - - @Binds - abstract fun provideLinkTrashDatabase(db: AppDatabase): LinkTrashDatabase - - @Binds - abstract fun provideLinkSelectionDatabase(db: AppDatabase): LinkSelectionDatabase - - @Binds - abstract fun provideMessageQueueDatabase(db: AppDatabase): MessageQueueDatabase - - @Binds - abstract fun provideSortingDatabase(db: AppDatabase): SortingDatabase - - @Binds - abstract fun provideLinkUploadDatabase(db: AppDatabase): LinkUploadDatabase - - @Binds - abstract fun provideAccountDatabase(db: AppDatabase): AccountDatabase - - @Binds - abstract fun provideUserDatabase(db: AppDatabase): UserDatabase - - @Binds - abstract fun provideAddressDatabase(db: AppDatabase): AddressDatabase - - @Binds - abstract fun provideFeatureFlagDatabase(db: AppDatabase): FeatureFlagDatabase - - @Binds - abstract fun provideKeySaltDatabase(db: AppDatabase): KeySaltDatabase - - @Binds - abstract fun providePublicAddressDatabase(db: AppDatabase): PublicAddressDatabase - - @Binds - abstract fun provideHumanVerificationDatabase(db: AppDatabase): HumanVerificationDatabase - - @Binds - abstract fun provideUserSettingsDatabase(db: AppDatabase): UserSettingsDatabase - - @Binds - abstract fun provideOrganizationDatabase(db: AppDatabase): OrganizationDatabase - - @Binds - abstract fun provideAppUiSettingsDatabase(db: AppDatabase): AppUiSettingsDatabase - - @Binds - abstract fun provideEventMetadataDatabase(db: AppDatabase): EventMetadataDatabase - - @Binds - abstract fun provideChallengeDatabase(appDatabase: AppDatabase): ChallengeDatabase - - @Binds - abstract fun provideDriveLinkDatabase(db: AppDatabase): DriveLinkDatabase - - @Binds - abstract fun provideDriveLinkPagedDatabase(db: AppDatabase): DriveLinkPagedDatabase - - @Binds - abstract fun provideDriveLinkTrashDatabase(db: AppDatabase): DriveLinkTrashDatabase - - @Binds - abstract fun provideDriveLinkOfflineDatabase(db: AppDatabase): DriveLinkOfflineDatabase - - @Binds - abstract fun provideDriveLinkDownloadDatabase(db: AppDatabase): DriveLinkDownloadDatabase - - @Binds - abstract fun provideDriveLinkSharedDatabase(db: AppDatabase): DriveLinkSharedDatabase - - @Binds - abstract fun provideDriveLinkSelectionDatabase(db: AppDatabase): DriveLinkSelectionDatabase - - @Binds - abstract fun provideNotificationDatabase(db: AppDatabase): NotificationDatabase - - @Binds - abstract fun providePaymentDatabase(db: AppDatabase): PaymentDatabase + abstract fun provideAppLockDatabase(db: AppDatabase): AppLockDatabase } diff --git a/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt b/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt index 3f2b2674..a21aa4dd 100644 --- a/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt +++ b/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt @@ -18,6 +18,7 @@ package me.proton.android.drive.di +import android.app.ActivityManager import android.content.ClipboardManager import android.content.Context import androidx.core.app.NotificationManagerCompat @@ -29,15 +30,19 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import me.proton.android.drive.BuildConfig +import me.proton.android.drive.lock.data.usecase.BuildAppKeyImpl +import me.proton.android.drive.lock.domain.usecase.BuildAppKey import me.proton.android.drive.log.DriveLogger import me.proton.android.drive.notification.AppNotificationBuilderProvider import me.proton.android.drive.notification.AppNotificationEventHandler import me.proton.android.drive.provider.BuildConfigurationProvider import me.proton.android.drive.settings.DebugSettings +import me.proton.android.drive.usecase.GetDocumentsProviderRootsImpl import me.proton.core.account.domain.entity.AccountType import me.proton.core.domain.entity.AppStore import me.proton.core.domain.entity.Product import me.proton.core.drive.base.domain.provider.ConfigurationProvider +import me.proton.core.drive.documentsprovider.domain.usecase.GetDocumentsProviderRoots import me.proton.core.drive.notification.data.provider.NotificationBuilderProvider import me.proton.core.drive.notification.domain.handler.NotificationEventHandler import me.proton.drive.android.settings.data.datastore.AppUiSettingsDataStore @@ -79,7 +84,7 @@ object ApplicationModule { @Provides @Singleton - fun provideDriveLogger(): DriveLogger = DriveLogger() + fun provideDriveLogger(@ApplicationContext context: Context): DriveLogger = DriveLogger(context) @Provides @Singleton @@ -105,6 +110,11 @@ object ApplicationModule { @Singleton fun provideClipboardManager(@ApplicationContext context: Context): ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + + @Provides + @Singleton + fun provideActivityManager(@ApplicationContext context: Context): ActivityManager = + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager } @Module @@ -119,4 +129,8 @@ abstract class ApplicationBindsModule { abstract fun bindsNotificationBuilderProvider( impl: AppNotificationBuilderProvider ): NotificationBuilderProvider + + @Binds + @Singleton + abstract fun bindsGetDocumentsProviderRootsImpl(impl: GetDocumentsProviderRootsImpl): GetDocumentsProviderRoots } diff --git a/app/src/main/kotlin/me/proton/android/drive/di/DriveDatabaseModule.kt b/app/src/main/kotlin/me/proton/android/drive/di/DriveDatabaseModule.kt new file mode 100644 index 00000000..c306c64d --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/di/DriveDatabaseModule.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.di + +import android.content.Context +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import me.proton.android.drive.db.DriveDatabase +import me.proton.core.account.data.db.AccountDatabase +import me.proton.core.challenge.data.db.ChallengeDatabase +import me.proton.core.drive.drivelink.data.db.DriveLinkDatabase +import me.proton.core.drive.drivelink.download.data.db.DriveLinkDownloadDatabase +import me.proton.core.drive.drivelink.offline.data.db.DriveLinkOfflineDatabase +import me.proton.core.drive.drivelink.paged.data.db.DriveLinkPagedDatabase +import me.proton.core.drive.drivelink.selection.data.db.DriveLinkSelectionDatabase +import me.proton.core.drive.drivelink.shared.data.db.DriveLinkSharedDatabase +import me.proton.core.drive.drivelink.trash.data.db.DriveLinkTrashDatabase +import me.proton.core.drive.folder.data.db.FolderDatabase +import me.proton.core.drive.link.data.db.LinkDatabase +import me.proton.core.drive.link.selection.data.db.LinkSelectionDatabase +import me.proton.core.drive.linkdownload.data.db.LinkDownloadDatabase +import me.proton.core.drive.linknode.data.db.LinkAncestorDatabase +import me.proton.core.drive.linkoffline.data.db.LinkOfflineDatabase +import me.proton.core.drive.linktrash.data.db.LinkTrashDatabase +import me.proton.core.drive.linkupload.data.db.LinkUploadDatabase +import me.proton.core.drive.messagequeue.data.storage.db.MessageQueueDatabase +import me.proton.core.drive.notification.data.db.NotificationDatabase +import me.proton.core.drive.share.data.db.ShareDatabase +import me.proton.core.drive.shareurl.base.data.db.ShareUrlDatabase +import me.proton.core.drive.sorting.data.db.SortingDatabase +import me.proton.core.drive.volume.data.db.VolumeDatabase +import me.proton.core.eventmanager.data.db.EventMetadataDatabase +import me.proton.core.featureflag.data.db.FeatureFlagDatabase +import me.proton.core.humanverification.data.db.HumanVerificationDatabase +import me.proton.core.key.data.db.KeySaltDatabase +import me.proton.core.key.data.db.PublicAddressDatabase +import me.proton.core.observability.data.db.ObservabilityDatabase +import me.proton.core.payment.data.local.db.PaymentDatabase +import me.proton.core.user.data.db.AddressDatabase +import me.proton.core.user.data.db.UserDatabase +import me.proton.core.usersettings.data.db.OrganizationDatabase +import me.proton.core.usersettings.data.db.UserSettingsDatabase +import me.proton.drive.android.settings.data.db.AppUiSettingsDatabase +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DriveDatabaseModule { + @Provides + @Singleton + fun provideDriveDatabase(@ApplicationContext context: Context): DriveDatabase = + DriveDatabase.buildDatabase(context) +} + +@Module +@InstallIn(SingletonComponent::class) +abstract class DriveDatabaseBindsModule { + + @Binds + abstract fun provideVolumeDatabase(db: DriveDatabase): VolumeDatabase + + @Binds + abstract fun provideShareDatabase(db: DriveDatabase): ShareDatabase + + @Binds + abstract fun provideShareUrlDatabase(db: DriveDatabase): ShareUrlDatabase + + @Binds + abstract fun provideLinkDatabase(db: DriveDatabase): LinkDatabase + + @Binds + abstract fun provideFolderDatabase(db: DriveDatabase): FolderDatabase + + @Binds + abstract fun provideLinkAncestorDatabase(db: DriveDatabase): LinkAncestorDatabase + + @Binds + abstract fun provideLinkOfflineDatabase(db: DriveDatabase): LinkOfflineDatabase + + @Binds + abstract fun provideLinkDownloadDatabase(db: DriveDatabase): LinkDownloadDatabase + + @Binds + abstract fun provideLinkTrashDatabase(db: DriveDatabase): LinkTrashDatabase + + @Binds + abstract fun provideLinkSelectionDatabase(db: DriveDatabase): LinkSelectionDatabase + + @Binds + abstract fun provideMessageQueueDatabase(db: DriveDatabase): MessageQueueDatabase + + @Binds + abstract fun provideSortingDatabase(db: DriveDatabase): SortingDatabase + + @Binds + abstract fun provideLinkUploadDatabase(db: DriveDatabase): LinkUploadDatabase + + @Binds + abstract fun provideAccountDatabase(db: DriveDatabase): AccountDatabase + + @Binds + abstract fun provideUserDatabase(db: DriveDatabase): UserDatabase + + @Binds + abstract fun provideAddressDatabase(db: DriveDatabase): AddressDatabase + + @Binds + abstract fun provideFeatureFlagDatabase(db: DriveDatabase): FeatureFlagDatabase + + @Binds + abstract fun provideKeySaltDatabase(db: DriveDatabase): KeySaltDatabase + + @Binds + abstract fun providePublicAddressDatabase(db: DriveDatabase): PublicAddressDatabase + + @Binds + abstract fun provideHumanVerificationDatabase(db: DriveDatabase): HumanVerificationDatabase + + @Binds + abstract fun provideUserSettingsDatabase(db: DriveDatabase): UserSettingsDatabase + + @Binds + abstract fun provideOrganizationDatabase(db: DriveDatabase): OrganizationDatabase + + @Binds + abstract fun provideAppUiSettingsDatabase(db: DriveDatabase): AppUiSettingsDatabase + + @Binds + abstract fun provideEventMetadataDatabase(db: DriveDatabase): EventMetadataDatabase + + @Binds + abstract fun provideChallengeDatabase(driveDatabase: DriveDatabase): ChallengeDatabase + + @Binds + abstract fun provideDriveLinkDatabase(db: DriveDatabase): DriveLinkDatabase + + @Binds + abstract fun provideDriveLinkPagedDatabase(db: DriveDatabase): DriveLinkPagedDatabase + + @Binds + abstract fun provideDriveLinkTrashDatabase(db: DriveDatabase): DriveLinkTrashDatabase + + @Binds + abstract fun provideDriveLinkOfflineDatabase(db: DriveDatabase): DriveLinkOfflineDatabase + + @Binds + abstract fun provideDriveLinkDownloadDatabase(db: DriveDatabase): DriveLinkDownloadDatabase + + @Binds + abstract fun provideDriveLinkSharedDatabase(db: DriveDatabase): DriveLinkSharedDatabase + + @Binds + abstract fun provideDriveLinkSelectionDatabase(db: DriveDatabase): DriveLinkSelectionDatabase + + @Binds + abstract fun provideNotificationDatabase(db: DriveDatabase): NotificationDatabase + + @Binds + abstract fun providePaymentDatabase(db: DriveDatabase): PaymentDatabase + + @Binds + abstract fun provideObservabilityDatabase(db: DriveDatabase): ObservabilityDatabase +} diff --git a/app/src/main/kotlin/me/proton/android/drive/extension/DriveException.kt b/app/src/main/kotlin/me/proton/android/drive/extension/DriveException.kt index df5b91a4..7ade3ddb 100644 --- a/app/src/main/kotlin/me/proton/android/drive/extension/DriveException.kt +++ b/app/src/main/kotlin/me/proton/android/drive/extension/DriveException.kt @@ -19,18 +19,19 @@ package me.proton.android.drive.extension import android.content.Context +import me.proton.android.drive.lock.domain.exception.LockException import me.proton.core.drive.base.domain.exception.DriveException import me.proton.core.drive.base.presentation.R as BasePresentation import me.proton.core.drive.share.domain.exception.ShareException import me.proton.core.util.kotlin.CoreLogger +import me.proton.android.drive.lock.presentation.extension.getDefaultMessage as lockGetDefaultMessage -fun DriveException.getDefaultMessage(context: Context): String = context.getString( - when (this) { - is ShareException.MainShareLocked -> BasePresentation.string.error_main_share_locked - is ShareException.MainShareNotFound -> BasePresentation.string.error_main_share_not_found - else -> throw IllegalStateException("Default message for exception is missing") - } -) +fun DriveException.getDefaultMessage(context: Context): String = when (this) { + is ShareException.MainShareLocked -> context.getString(BasePresentation.string.error_main_share_locked) + is ShareException.MainShareNotFound -> context.getString(BasePresentation.string.error_main_share_not_found) + is LockException -> lockGetDefaultMessage(context) + else -> throw IllegalStateException("Default message for exception is missing") +} fun DriveException.log(tag: String, message: String = this.message.orEmpty()): DriveException = also { CoreLogger.d(tag, this, message) diff --git a/app/src/main/kotlin/me/proton/android/drive/initializer/AutoLockInitializer.kt b/app/src/main/kotlin/me/proton/android/drive/initializer/AutoLockInitializer.kt new file mode 100644 index 00000000..1645acbe --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/initializer/AutoLockInitializer.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ +package me.proton.android.drive.initializer + +import android.content.Context +import androidx.lifecycle.coroutineScope +import androidx.startup.Initializer +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import me.proton.android.drive.lock.domain.manager.AppLockManager +import me.proton.android.drive.lock.domain.manager.AutoLockManager +import me.proton.core.presentation.app.AppLifecycleProvider + +class AutoLockInitializer : Initializer { + + override fun create(context: Context) { + with ( + EntryPointAccessors.fromApplication( + context.applicationContext, + AutoLockInitializerEntryPoint::class.java + ) + ) { + appLifecycleProvider.state + .onEach { state -> + if (appLockManager.isEnabled()) { + when (state) { + AppLifecycleProvider.State.Background -> autoLockManager.autoLock() + AppLifecycleProvider.State.Foreground -> autoLockManager.cancelAutoLock() + } + } + } + .launchIn(appLifecycleProvider.lifecycle.coroutineScope) + } + } + + override fun dependencies(): List>> = listOf( + LoggerInitializer::class.java, + WorkManagerInitializer::class.java, + ) + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface AutoLockInitializerEntryPoint { + val appLifecycleProvider: AppLifecycleProvider + val appLockManager: AppLockManager + val autoLockManager: AutoLockManager + } +} diff --git a/app/src/main/kotlin/me/proton/android/drive/initializer/DocumentsProviderInitializer.kt b/app/src/main/kotlin/me/proton/android/drive/initializer/DocumentsProviderInitializer.kt new file mode 100644 index 00000000..a3ed2cd5 --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/initializer/DocumentsProviderInitializer.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.initializer + +import android.content.Context +import androidx.lifecycle.coroutineScope +import androidx.startup.Initializer +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import me.proton.android.drive.lock.domain.manager.AppLockManager +import me.proton.core.drive.documentsprovider.data.DriveDocumentsProvider +import me.proton.core.presentation.app.AppLifecycleProvider + +class DocumentsProviderInitializer : Initializer { + + override fun create(context: Context) { + with ( + EntryPointAccessors.fromApplication( + context.applicationContext, + AutoLockInitializerEntryPoint::class.java + ) + ) { + appLockManager.enabled + .onEach { + DriveDocumentsProvider.notifyRootsHaveChanged(context) + } + .launchIn(appLifecycleProvider.lifecycle.coroutineScope) + } + } + + override fun dependencies(): List>> = listOf( + LoggerInitializer::class.java, + ) + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface AutoLockInitializerEntryPoint { + val appLifecycleProvider: AppLifecycleProvider + val appLockManager: AppLockManager + } +} diff --git a/app/src/main/kotlin/me/proton/android/drive/initializer/MainInitializer.kt b/app/src/main/kotlin/me/proton/android/drive/initializer/MainInitializer.kt new file mode 100644 index 00000000..5b1581d3 --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/initializer/MainInitializer.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.initializer + +import android.content.Context +import androidx.startup.AppInitializer +import androidx.startup.Initializer +import me.proton.core.auth.presentation.MissingScopeInitializer +import me.proton.core.humanverification.presentation.HumanVerificationInitializer +import me.proton.core.network.presentation.init.UnAuthSessionFetcherInitializer +import me.proton.core.plan.presentation.UnredeemedPurchaseInitializer + +class MainInitializer : Initializer { + + override fun create(context: Context) { + // No-op needed + } + + override fun dependencies() = listOf( + EventManagerInitializer::class.java, + HumanVerificationInitializer::class.java, + UnredeemedPurchaseInitializer::class.java, + MissingScopeInitializer::class.java, + UnAuthSessionFetcherInitializer::class.java, + AutoLockInitializer::class.java, + ) + + companion object { + + fun init(appContext: Context) { + with(AppInitializer.getInstance(appContext)) { + // WorkManager need to be initialized before any other dependant initializer. + initializeComponent(WorkManagerInitializer::class.java) + initializeComponent(MainInitializer::class.java) + } + } + } +} diff --git a/app/src/main/kotlin/me/proton/android/drive/log/DriveLogger.kt b/app/src/main/kotlin/me/proton/android/drive/log/DriveLogger.kt index a07bec08..b5483c59 100644 --- a/app/src/main/kotlin/me/proton/android/drive/log/DriveLogger.kt +++ b/app/src/main/kotlin/me/proton/android/drive/log/DriveLogger.kt @@ -20,21 +20,28 @@ package me.proton.android.drive.log +import android.content.Context import android.os.Build import android.os.LocaleList +import dagger.hilt.android.qualifiers.ApplicationContext import io.sentry.Breadcrumb import io.sentry.Sentry import io.sentry.SentryEvent import io.sentry.SentryLevel import io.sentry.protocol.Message +import me.proton.core.drive.base.presentation.extension.getDefaultMessage import me.proton.core.util.kotlin.Logger import me.proton.core.util.kotlin.LoggerLogTag import timber.log.Timber import java.util.Locale +import javax.inject.Inject import javax.inject.Singleton +import me.proton.core.drive.base.presentation.R as BasePresentation @Singleton -class DriveLogger : Logger { +class DriveLogger @Inject constructor( + @ApplicationContext private val appContext: Context, +) : Logger { override fun v(tag: String, message: String) { DriveSentry.addBreadcrumb(tag, message) @@ -61,11 +68,11 @@ class DriveLogger : Logger { Timber.tag(tag).i(e, message) } override fun e(tag: String, e: Throwable) { - DriveSentry.captureException(tag, e) + DriveSentry.captureException(appContext, tag, e) Timber.tag(tag).e(e) } override fun e(tag: String, e: Throwable, message: String) { - DriveSentry.captureException(tag, e, message) + DriveSentry.captureException(appContext, tag, e, message) Timber.tag(tag).e(e, message) } override fun log(tag: LoggerLogTag, message: String) = i(tag.name, message) @@ -85,12 +92,24 @@ class DriveLogger : Logger { private object DriveSentry { - fun captureException(tag: String, e: Throwable) { + fun captureException( + context: Context, + tag: String, + e: Throwable, + ) { + setInternalErrorTag(context, e) Sentry.setTag("CoreLogger", tag) Sentry.captureException(e) } - fun captureException(tag: String, e: Throwable, message: String, level: SentryLevel = SentryLevel.ERROR) { + fun captureException( + context: Context, + tag: String, + e: Throwable, + message: String, + level: SentryLevel = SentryLevel.ERROR, + ) { + setInternalErrorTag(context, e) Sentry.setTag("CoreLogger", tag) Sentry.captureEvent( SentryEvent(e).apply { @@ -122,6 +141,15 @@ class DriveLogger : Logger { } ) } + + private fun setInternalErrorTag(context: Context, e: Throwable) { + val internalErrorMessage = context.getString(BasePresentation.string.common_error_internal) + val errorMessage = e.getDefaultMessage( + context = context, + useExceptionMessage = false, + ) + Sentry.setTag("InternalError", (errorMessage == internalErrorMessage).toString()) + } } } diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/MainActivity.kt b/app/src/main/kotlin/me/proton/android/drive/ui/MainActivity.kt index 0567f7b4..c61ca54c 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/MainActivity.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/MainActivity.kt @@ -69,6 +69,8 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import me.proton.android.drive.extension.deepLinkBaseUrl +import me.proton.android.drive.lock.data.provider.BiometricPromptProvider +import me.proton.android.drive.lock.domain.manager.AppLockManager import me.proton.android.drive.log.DriveLogTag import me.proton.android.drive.ui.navigation.AppNavGraph import me.proton.android.drive.ui.provider.LocalSnackbarPadding @@ -100,6 +102,8 @@ class MainActivity : FragmentActivity() { @Inject lateinit var actionProvider: ActionProvider @Inject lateinit var getThemeStyle: GetThemeStyle @Inject lateinit var processIntent: ProcessIntent + @Inject lateinit var biometricPromptProvider: BiometricPromptProvider + @Inject lateinit var appLockManager: AppLockManager lateinit var configurationProvider: ConfigurationProvider private val accountViewModel: AccountViewModel by viewModels() private val bugReportViewModel: BugReportViewModel by viewModels() @@ -121,6 +125,7 @@ class MainActivity : FragmentActivity() { applySecureFlag() setTheme(CorePresentation.style.ProtonTheme_Drive) super.onCreate(savedInstanceState) + biometricPromptProvider.bindToActivity(this) setupAccountsViewModel() bugReportViewModel.initialize(this) WindowCompat.setDecorFitsSystemWindows(window, false) @@ -145,12 +150,13 @@ class MainActivity : FragmentActivity() { deepLinkBaseUrl = this@MainActivity.deepLinkBaseUrl, clearBackstackTrigger = clearBackstackTrigger, deepLinkIntent = deepLinkIntent, + locked = appLockManager.locked, + primaryAccount = accountViewModel.primaryAccount, exitApp = { finish() }, sendBugReport = bugReportViewModel::sendBugReport, ) { isOpen -> isDrawerOpen = isOpen } - HeadlessSnackBar(snackbarHostState) } } diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/dialog/AutoLockDurations.kt b/app/src/main/kotlin/me/proton/android/drive/ui/dialog/AutoLockDurations.kt new file mode 100644 index 00000000..6331daf8 --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/ui/dialog/AutoLockDurations.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.ui.dialog + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material.RadioButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import me.proton.android.drive.ui.viewevent.AutoLockDurationsViewEvent +import me.proton.android.drive.ui.viewmodel.AutoLockDurationsViewModel +import me.proton.android.drive.ui.viewstate.AutoLockDurationsViewState +import me.proton.core.compose.component.bottomsheet.BottomSheetContent +import me.proton.core.compose.component.bottomsheet.RunAction +import me.proton.core.compose.flow.rememberFlowWithLifecycle +import me.proton.core.compose.theme.ProtonDimens.DefaultSpacing +import me.proton.core.drive.settings.presentation.extension.toString + +@Composable +fun AutoLockDurations( + runAction: RunAction, + dismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val viewModel = hiltViewModel() + val viewState by rememberFlowWithLifecycle(flow = viewModel.viewState) + .collectAsState(initial = viewModel.initialViewState) + AutoLockDurations( + viewState = viewState, + viewEvent = viewModel.viewEvent(runAction, dismiss), + modifier = modifier.navigationBarsPadding(), + ) +} + +@Composable +fun AutoLockDurations( + viewState: AutoLockDurationsViewState, + viewEvent: AutoLockDurationsViewEvent, + modifier: Modifier = Modifier, +) { + BottomSheetContent( + modifier = modifier, + header = { + Text(text = viewState.title) + }, + content = { + viewState.durations.forEach { duration -> + Duration( + title = duration.toString(LocalContext.current), + isSelected = duration == viewState.selected, + ) { + viewEvent.onDuration(duration) + } + } + } + ) +} + +@Composable +private fun Duration( + title: String, + isSelected: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { onClick() }, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = title, + modifier = Modifier + .padding(start = DefaultSpacing) + .weight(1f), + ) + RadioButton( + selected = isSelected, + onClick = { onClick() }, + ) + } +} + +@Preview +@Composable +private fun PreviewSelectedDuration() { + Duration(title = "Immediately", isSelected = true) {} +} + +@Preview +@Composable +private fun PreviewUnselectedDuration() { + Duration(title = "15 minutes", isSelected = false) {} +} diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/dialog/SystemAccessDialog.kt b/app/src/main/kotlin/me/proton/android/drive/ui/dialog/SystemAccessDialog.kt new file mode 100644 index 00000000..71049641 --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/ui/dialog/SystemAccessDialog.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.ui.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import me.proton.android.drive.ui.viewmodel.SystemAccessDialogViewModel +import me.proton.core.compose.component.ProtonAlertDialog +import me.proton.core.compose.component.ProtonAlertDialogButton +import me.proton.core.compose.component.ProtonAlertDialogText +import me.proton.core.drive.base.presentation.R + +@Composable +fun SystemAccessDialog( + modifier: Modifier = Modifier, + onDismiss: () -> Unit, +) { + val viewModel = hiltViewModel() + SystemAccessDialog( + modifier = modifier, + onDismiss = onDismiss, + onSettings = viewModel.viewEvent(LocalContext.current, onDismiss).onSettings, + ) +} + +@Composable +fun SystemAccessDialog( + modifier: Modifier = Modifier, + onDismiss: () -> Unit, + onSettings: () -> Unit, +) { + ProtonAlertDialog( + modifier = modifier, + titleResId = R.string.app_lock_system_dialog_title, + text = { + Column { + ProtonAlertDialogText(textResId = R.string.app_lock_system_dialog_description) + } + }, + onDismissRequest = onDismiss, + dismissButton = { + ProtonAlertDialogButton( + titleResId = R.string.common_cancel_action, + onClick = onDismiss, + ) + }, + confirmButton = { + ProtonAlertDialogButton( + titleResId = R.string.app_lock_system_dialog_settings_button, + onClick = onSettings, + ) + } + ) +} diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/navigation/AppNavGraph.kt b/app/src/main/kotlin/me/proton/android/drive/ui/navigation/AppNavGraph.kt index 682dc91b..ea300a15 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/navigation/AppNavGraph.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/navigation/AppNavGraph.kt @@ -43,6 +43,7 @@ import androidx.navigation.navArgument import androidx.navigation.navDeepLink import com.google.accompanist.navigation.animation.composable import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.collectLatest import me.proton.android.drive.extension.get @@ -50,7 +51,9 @@ import me.proton.android.drive.extension.popAllBackStack import me.proton.android.drive.extension.require import me.proton.android.drive.extension.requireArguments import me.proton.android.drive.extension.runFromRoute +import me.proton.android.drive.lock.presentation.component.AppLock import me.proton.android.drive.log.DriveLogTag +import me.proton.android.drive.ui.dialog.AutoLockDurations import me.proton.android.drive.ui.dialog.ConfirmDeletionDialog import me.proton.android.drive.ui.dialog.ConfirmEmptyTrashDialog import me.proton.android.drive.ui.dialog.ConfirmStopSharingDialog @@ -59,6 +62,7 @@ import me.proton.android.drive.ui.dialog.MultipleFileOrFolderOptions import me.proton.android.drive.ui.dialog.ParentFolderOptions import me.proton.android.drive.ui.dialog.SendFileDialog import me.proton.android.drive.ui.dialog.SortingList +import me.proton.android.drive.ui.dialog.SystemAccessDialog import me.proton.android.drive.ui.navigation.animation.defaultEnterSlideTransition import me.proton.android.drive.ui.navigation.animation.defaultPopExitSlideTransition import me.proton.android.drive.ui.navigation.animation.slideComposable @@ -67,6 +71,7 @@ import me.proton.android.drive.ui.navigation.internal.MutableNavControllerSaver import me.proton.android.drive.ui.navigation.internal.createNavController import me.proton.android.drive.ui.navigation.internal.modalBottomSheet import me.proton.android.drive.ui.navigation.internal.rememberAnimatedNavController +import me.proton.android.drive.ui.screen.AppAccessScreen import me.proton.android.drive.ui.screen.FileInfoScreen import me.proton.android.drive.ui.screen.HomeScreen import me.proton.android.drive.ui.screen.LauncherScreen @@ -78,6 +83,7 @@ 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.WelcomeScreen +import me.proton.core.account.domain.entity.Account import me.proton.core.compose.component.bottomsheet.ModalBottomSheetViewState import me.proton.core.crypto.common.keystore.KeyStoreCrypto import me.proton.core.domain.entity.UserId @@ -101,6 +107,8 @@ fun AppNavGraph( deepLinkBaseUrl: String, clearBackstackTrigger: SharedFlow, deepLinkIntent: SharedFlow, + locked: Flow, + primaryAccount: Flow, exitApp: () -> Unit, sendBugReport: () -> Unit, onDrawerStateChanged: (Boolean) -> Unit, @@ -136,14 +144,16 @@ fun AppNavGraph( homeNavController = createNavController(localContext) } } - AppNavGraph( - navController = navController, - homeNavController = homeNavController, - deepLinkBaseUrl = deepLinkBaseUrl, - exitApp = exitApp, - sendBugReport = sendBugReport, - onDrawerStateChanged = onDrawerStateChanged, - ) + AppLock(locked = locked, primaryAccount = primaryAccount) { + AppNavGraph( + navController = navController, + homeNavController = homeNavController, + deepLinkBaseUrl = deepLinkBaseUrl, + exitApp = exitApp, + sendBugReport = sendBugReport, + onDrawerStateChanged = onDrawerStateChanged, + ) + } } @Composable @@ -188,6 +198,9 @@ fun AppNavGraph( addShareViaLink(navController) addDiscardShareViaLinkChanges(navController) addUploadTo(navController, deepLinkBaseUrl, exitApp) + addAppAccess(navController) + addSystemAccessDialog(navController) + addAutoLockDurations(navController) } } @@ -673,9 +686,16 @@ fun NavGraphBuilder.addSettings(navController: NavHostController) = composable( arguments = listOf( navArgument(Screen.PagerPreview.USER_ID) { type = NavType.StringType }, ), -) { +) { navBackStackEntry -> + val userId = UserId(navBackStackEntry.require(Screen.Files.USER_ID)) SettingsScreen( navigateBack = { navController.popBackStack() }, + navigateToAppAccess = { + navController.navigate(Screen.Settings.AppAccess(userId)) + }, + navigateToAutoLockDurations = { + navController.navigate(Screen.Settings.AutoLockDurations(userId)) + }, ) } @@ -872,3 +892,47 @@ fun NavGraphBuilder.addUploadTo( exitApp = exitApp, ) } + +@ExperimentalAnimationApi +fun NavGraphBuilder.addAppAccess(navController: NavHostController) = composable( + route = Screen.Settings.AppAccess.route, + enterTransition = defaultEnterSlideTransition { true }, + exitTransition = { ExitTransition.None }, + popEnterTransition = { EnterTransition.None }, + popExitTransition = defaultPopExitSlideTransition { true }, + arguments = listOf( + navArgument(Screen.Settings.USER_ID) { type = NavType.StringType }, + ), +) { navBackStackEntry -> + val userId = UserId(navBackStackEntry.require(Screen.Settings.USER_ID)) + AppAccessScreen( + navigateToSystemAccess = { + navController.navigate(Screen.Settings.AppAccess.Dialogs.SystemAccess(userId)) + }, + navigateBack = { + navController.popBackStack() + }, + ) +} + +@ExperimentalCoroutinesApi +fun NavGraphBuilder.addSystemAccessDialog(navController: NavHostController) = dialog( + route = Screen.Settings.AppAccess.Dialogs.SystemAccess.route, + arguments = listOf( + navArgument(Screen.Settings.USER_ID) { type = NavType.StringType }, + ), +) { + SystemAccessDialog(onDismiss = { navController.popBackStack() }) +} + +fun NavGraphBuilder.addAutoLockDurations( + navController: NavHostController, +) = modalBottomSheet( + route = Screen.Settings.AutoLockDurations.route, + viewState = ModalBottomSheetViewState(dismissOnAction = false), +) { _, runAction -> + AutoLockDurations( + runAction = runAction, + dismiss = { navController.popBackStack() } + ) +} diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/navigation/Screen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/navigation/Screen.kt index f6cf989c..a948b678 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/navigation/Screen.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/navigation/Screen.kt @@ -271,6 +271,22 @@ sealed class Screen(val route: String) { operator fun invoke(userId: UserId) = "settings/${userId.id}" const val USER_ID = Screen.USER_ID + + object AppAccess : Screen("settings/{userId}/appAccess") { + + operator fun invoke(userId: UserId) = "settings/${userId.id}/appAccess" + + object Dialogs { + + object SystemAccess : Screen("settings/{userId}/appAccess/systemAccess") { + operator fun invoke(userId: UserId) = "settings/${userId.id}/appAccess/systemAccess" + } + } + } + + object AutoLockDurations : Screen("settings/{userId}/autoLockDurations") { + operator fun invoke(userId: UserId) = "settings/${userId.id}/autoLockDurations" + } } object SendFile : Screen("send/{userId}/shares/{shareId}/files/{fileId}") { diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/AppAccessScreen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/AppAccessScreen.kt new file mode 100644 index 00000000..5f47a6b1 --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/AppAccessScreen.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.ui.screen + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.RadioButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import me.proton.android.drive.ui.viewevent.AppAccessViewEvent +import me.proton.android.drive.ui.viewmodel.AppAccessViewModel +import me.proton.android.drive.ui.viewstate.AccessOption +import me.proton.android.drive.ui.viewstate.AppAccessViewState +import me.proton.core.compose.flow.rememberFlowWithLifecycle +import me.proton.core.compose.theme.ProtonDimens.DefaultSpacing +import me.proton.core.compose.theme.ProtonTheme +import me.proton.core.drive.base.presentation.component.ProtonListItem +import me.proton.core.drive.base.presentation.component.TopAppBar +import me.proton.core.drive.base.presentation.R as BasePresentation +import me.proton.core.presentation.R as CorePresentation + +@Composable +fun AppAccessScreen( + modifier: Modifier = Modifier, + navigateToSystemAccess: () -> Unit, + navigateBack: () -> Unit, +) { + val viewModel = hiltViewModel() + val viewState by rememberFlowWithLifecycle(flow = viewModel.viewState) + .collectAsState(initial = viewModel.initialViewState) + AppAccess( + viewState = viewState, + viewEvent = viewModel.viewEvent(navigateToSystemAccess, navigateBack), + modifier = modifier.fillMaxSize(), + navigateBack = navigateBack, + ) +} + +@Composable +fun AppAccess( + viewState: AppAccessViewState, + viewEvent: AppAccessViewEvent, + modifier: Modifier = Modifier, + navigateBack: () -> Unit, +) { + Column(modifier = modifier) { + TopAppBar( + navigationIcon = painterResource(id = CorePresentation.drawable.ic_arrow_back), + onNavigationIcon = navigateBack, + title = viewState.title, + modifier = Modifier.statusBarsPadding() + ) + AppAccessOptions(viewState.enabledOption, viewEvent) + } +} + +@Composable +fun AppAccessOptions( + enabledOption: AccessOption, + viewEvent: AppAccessViewEvent, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.verticalScroll(rememberScrollState()) + ) { + AppAccessOption( + iconResId = BasePresentation.drawable.ic_proton_lock_open, + titleResId = BasePresentation.string.app_lock_option_none, + isSelected = enabledOption == AccessOption.NONE, + ) { + viewEvent.onDisable() + } + AppAccessOption( + iconResId = CorePresentation.drawable.ic_proton_fingerprint, + titleResId = BasePresentation.string.app_lock_option_system, + isSelected = enabledOption == AccessOption.SYSTEM, + ) { + viewEvent.onSystem() + } + } +} + +@Composable +fun AppAccessOption( + iconResId: Int, + titleResId: Int, + isSelected: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { + onClick() + }, + verticalAlignment = Alignment.CenterVertically, + ) { + ProtonListItem( + icon = painterResource(id = iconResId), + title = stringResource(id = titleResId), + modifier = modifier + .weight(1f) + .padding(start = DefaultSpacing), + ) + RadioButton( + selected = isSelected, + onClick = { onClick() }, + ) + } +} + +@Preview +@Composable +private fun AppAccessPreview() { + ProtonTheme { + AppAccess( + viewState = AppAccessViewState( + title = "Title", + enabledOption = AccessOption.NONE, + ), + viewEvent = object : AppAccessViewEvent { + override val onDisable = {} + override val onSystem = {} + }, + navigateBack = {} + ) + } +} + diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/FilesScreen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/FilesScreen.kt index 8c9ec600..16659ddc 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/screen/FilesScreen.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/FilesScreen.kt @@ -66,15 +66,18 @@ fun FilesScreen( val selected by rememberFlowWithLifecycle(flow = viewState.selected) .collectAsState(initial = null) val inMultiselect = remember(selected) { selected?.isNotEmpty() ?: false } - val viewEvent = viewModel.viewEvent( - navigateToFiles, - navigateToPreview, - navigateToSortingDialog, - navigateToFileOrFolderOptions, - navigateToMultipleFileOrFolderOptions, - navigateToParentFolderOptions, - navigateBack, - ) + + val viewEvent = remember { + viewModel.viewEvent( + navigateToFiles, + navigateToPreview, + navigateToSortingDialog, + navigateToFileOrFolderOptions, + navigateToMultipleFileOrFolderOptions, + navigateToParentFolderOptions, + navigateBack, + ) + } BackHandler(enabled = inMultiselect) { viewEvent.onBack() } LaunchedEffect(viewState) { homeScaffoldState.topAppBar.value = { diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/MoveToFolder.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/MoveToFolder.kt index fe46c2bb..f0a3fac0 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/screen/MoveToFolder.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/MoveToFolder.kt @@ -98,6 +98,7 @@ fun MoveToFolder( modifier .systemBarsPadding() .padding(vertical = DefaultSpacing) + .testTag(MoveToFolderScreenTestTag.screen) ) { Column { Title( @@ -198,5 +199,5 @@ fun TitleText( object MoveToFolderScreenTestTag { const val screen = "move to folder screen" - const val plusFolderButton = "plus folder button" + const val plusFolderButton = "move to folder plus folder button" } diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/OfflineScreen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/OfflineScreen.kt index d249006c..f707476e 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/screen/OfflineScreen.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/OfflineScreen.kt @@ -29,12 +29,12 @@ import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import me.proton.android.drive.ui.navigation.PagerType import me.proton.android.drive.ui.viewmodel.OfflineViewModel +import me.proton.core.compose.flow.rememberFlowWithLifecycle import me.proton.core.drive.files.presentation.component.DriveLinksFlow import me.proton.core.drive.files.presentation.component.Files 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 -import me.proton.core.compose.flow.rememberFlowWithLifecycle import me.proton.core.drive.sorting.domain.entity.Sorting @Composable diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/PreviewScreen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/PreviewScreen.kt index ae6ef7bc..2def56e4 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/screen/PreviewScreen.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/PreviewScreen.kt @@ -107,7 +107,6 @@ fun PreviewScreen( navigateBack = navigateBack, navigateToFileOrFolderOptions = navigateToFileOrFolderOptions, ), - zoomEffect = viewModel.zoomEffect, modifier = modifier, ) { page -> viewModel.onPageChanged(page) diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/SettingsScreen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/SettingsScreen.kt index 30bf8932..c86e595c 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/screen/SettingsScreen.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/SettingsScreen.kt @@ -41,6 +41,8 @@ import me.proton.core.drive.settings.presentation.Settings @Composable fun SettingsScreen( navigateBack: () -> Unit, + navigateToAppAccess: () -> Unit, + navigateToAutoLockDurations: () -> Unit, modifier: Modifier = Modifier, ) { val viewModel = hiltViewModel() @@ -54,15 +56,19 @@ fun SettingsScreen( }.launchIn(this) } - settingsViewState?.let { viewState -> - Box( - modifier = modifier - .fillMaxSize() - .systemBarsPadding() - ) { + Box( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + ) { + settingsViewState?.let { viewState -> Settings( viewState = viewState, - viewEvent = viewModel.viewEvent(navigateBack), + viewEvent = viewModel.viewEvent( + navigateBack = navigateBack, + navigateToAppAccess = navigateToAppAccess, + navigateToAutoLockDurations = navigateToAutoLockDurations, + ), ) ProtonSnackbarHost( diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AppAccessViewEvent.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AppAccessViewEvent.kt new file mode 100644 index 00000000..fab81f21 --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AppAccessViewEvent.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.ui.viewevent + +interface AppAccessViewEvent { + val onDisable: () -> Unit + val onSystem: () -> Unit +} diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AutoLockDurationsViewEvent.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AutoLockDurationsViewEvent.kt new file mode 100644 index 00000000..0acb58cf --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AutoLockDurationsViewEvent.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.ui.viewevent + +import kotlin.time.Duration + +interface AutoLockDurationsViewEvent { + val onDuration: (Duration) -> Unit +} diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/SystemAccessDialogViewEvent.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/SystemAccessDialogViewEvent.kt new file mode 100644 index 00000000..70641340 --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/SystemAccessDialogViewEvent.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.ui.viewevent + +interface SystemAccessDialogViewEvent { + val onSettings: () -> Unit +} diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AppAccessViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AppAccessViewModel.kt new file mode 100644 index 00000000..e2333ee6 --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AppAccessViewModel.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.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.map +import kotlinx.coroutines.launch +import me.proton.android.drive.extension.getDefaultMessage +import me.proton.android.drive.lock.data.extension.onNotAvailable +import me.proton.android.drive.lock.data.extension.onReady +import me.proton.android.drive.lock.data.extension.onSetupRequired +import me.proton.android.drive.lock.domain.entity.AppLockType +import me.proton.android.drive.lock.domain.manager.AppLockManager +import me.proton.android.drive.lock.domain.usecase.DisableAppLock +import me.proton.android.drive.lock.domain.usecase.EnableAppLock +import me.proton.android.drive.lock.domain.usecase.GetLockState +import me.proton.android.drive.ui.viewevent.AppAccessViewEvent +import me.proton.android.drive.ui.viewstate.AccessOption +import me.proton.android.drive.ui.viewstate.AppAccessViewState +import me.proton.core.drive.base.domain.usecase.BroadcastMessages +import me.proton.core.drive.base.presentation.viewmodel.UserViewModel +import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage +import javax.inject.Inject +import me.proton.core.drive.base.presentation.R as BasePresentation + +@Suppress("StaticFieldLeak") +@HiltViewModel +class AppAccessViewModel @Inject constructor( + @ApplicationContext private val appContext: Context, + savedStateHandle: SavedStateHandle, + private val disableAppLock: DisableAppLock, + private val enableAppLock: EnableAppLock, + private val getLockState: GetLockState, + private val broadcastMessages: BroadcastMessages, + private val appLockManager: AppLockManager, +) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) { + + val initialViewState: AppAccessViewState = AppAccessViewState( + title = appContext.getString(BasePresentation.string.app_lock_access_title), + enabledOption = AccessOption.NONE, + ) + val viewState: Flow = appLockManager.enabled.map { enabled -> + initialViewState.copy( + enabledOption = if (!enabled) AccessOption.NONE else AccessOption.SYSTEM + ) + } + + fun viewEvent( + navigateToSystemAccess: () -> Unit, + navigateBack: () -> Unit, + ): AppAccessViewEvent = object : AppAccessViewEvent { + override val onDisable: () -> Unit = { doDisableAppLock(navigateBack) } + override val onSystem: () -> Unit = { + getLockState() + .onNotAvailable { + broadcastMessages( + userId = userId, + message = appContext.getString(BasePresentation.string.app_lock_system_not_available), + type = BroadcastMessage.Type.WARNING, + ) + } + .onSetupRequired { + navigateToSystemAccess() + } + .onReady { + doEnableSystemLock(navigateBack) + } + } + } + + private fun doDisableAppLock(navigateBack: () -> Unit) = viewModelScope.launch { + if (appLockManager.isEnabled().not()) return@launch + disableAppLock() + .onFailure { error -> + broadcastMessages( + userId = userId, + message = error.getDefaultMessage(appContext, true), + type = BroadcastMessage.Type.ERROR, + ) + } + .onSuccess { + broadcastMessages( + userId = userId, + message = appContext.getString(BasePresentation.string.app_lock_disabled), + type = BroadcastMessage.Type.INFO, + ) + navigateBack() + } + } + + private fun doEnableSystemLock(navigateBack: () -> Unit) = viewModelScope.launch { + if (appLockManager.isEnabled()) return@launch + enableAppLock(lockType = AppLockType.SYSTEM) + .onFailure { error -> + broadcastMessages( + userId = userId, + message = error.getDefaultMessage(appContext, true), + type = BroadcastMessage.Type.ERROR, + ) + } + .onSuccess { + broadcastMessages( + userId = userId, + message = appContext.getString(BasePresentation.string.app_lock_enabled), + type = BroadcastMessage.Type.INFO, + ) + } + navigateBack() + } +} diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AutoLockDurationsViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AutoLockDurationsViewModel.kt new file mode 100644 index 00000000..3fc4ed6f --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AutoLockDurationsViewModel.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.ui.viewmodel + +import android.content.Context +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.map +import kotlinx.coroutines.launch +import me.proton.android.drive.lock.domain.usecase.GetAutoLockDuration +import me.proton.android.drive.lock.domain.usecase.UpdateAutoLockDuration +import me.proton.android.drive.ui.viewevent.AutoLockDurationsViewEvent +import me.proton.android.drive.ui.viewstate.AutoLockDurationsViewState +import me.proton.core.compose.component.bottomsheet.RunAction +import me.proton.core.drive.base.domain.provider.ConfigurationProvider +import javax.inject.Inject +import kotlin.time.Duration +import me.proton.core.drive.settings.R as SettingsPresentation + +@HiltViewModel +class AutoLockDurationsViewModel @Inject constructor( + @ApplicationContext appContext: Context, + configurationProvider: ConfigurationProvider, + getAutoLockDuration: GetAutoLockDuration, + private val updateAutoLockDuration: UpdateAutoLockDuration, +) : ViewModel() { + val initialViewState: AutoLockDurationsViewState = AutoLockDurationsViewState( + title = appContext.getString(SettingsPresentation.string.settings_auto_lock), + durations = configurationProvider.autoLockDurations, + selected = configurationProvider.autoLockDurations.first(), + ) + val viewState: Flow = getAutoLockDuration().map { duration -> + initialViewState.copy( + selected = duration + ) + } + + fun viewEvent( + runAction: RunAction, + dismiss: () -> Unit, + ): AutoLockDurationsViewEvent = object : AutoLockDurationsViewEvent { + override val onDuration: (Duration) -> Unit = { duration -> + runAction { + viewModelScope.launch { + updateAutoLockDuration(duration) + dismiss() + } + } + } + } +} diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/BugReportViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/BugReportViewModel.kt index 9767d5e4..01939388 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/BugReportViewModel.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/BugReportViewModel.kt @@ -23,10 +23,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import me.proton.core.accountmanager.domain.AccountManager import me.proton.core.accountmanager.domain.getPrimaryAccount import me.proton.core.drive.base.domain.usecase.BroadcastMessages +import me.proton.core.drive.base.domain.usecase.GetUserEmail import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage import me.proton.core.report.presentation.ReportOrchestrator import me.proton.core.report.presentation.entity.BugReportInput @@ -38,6 +42,7 @@ class BugReportViewModel @Inject constructor( accountManager: AccountManager, private val reportOrchestrator: ReportOrchestrator, private val broadcastMessages: BroadcastMessages, + private val getUserEmail: GetUserEmail, ) : ViewModel() { private val primaryAccount = accountManager @@ -57,11 +62,12 @@ class BugReportViewModel @Inject constructor( } fun sendBugReport() { - primaryAccount.value?.let { account -> + viewModelScope.launch { + val account = primaryAccount.filterNotNull().first() reportOrchestrator.startBugReport( input = BugReportInput( - email = account.email.orEmpty(), - username = account.username + email = getUserEmail(account.userId), + username = account.username, ), ) } diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/FilesViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/FilesViewModel.kt index 1d3139a9..b449f6a7 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/FilesViewModel.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/FilesViewModel.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first @@ -44,6 +43,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch import me.proton.android.drive.R @@ -66,9 +66,9 @@ import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLin import me.proton.core.drive.drivelink.domain.entity.DriveLink import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted import me.proton.core.drive.drivelink.download.domain.usecase.GetDownloadProgress -import me.proton.core.drive.drivelink.list.domain.usecase.GetDriveLinks import me.proton.core.drive.drivelink.list.domain.usecase.GetPagedDriveLinksList 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.files.presentation.event.FilesViewEvent import me.proton.core.drive.files.presentation.state.FilesViewState import me.proton.core.drive.files.presentation.state.ListContentAppendingState @@ -110,14 +110,15 @@ class FilesViewModel @Inject constructor( private val getUploadFileLinks: GetUploadFileLinks, private val getUploadProgress: GetUploadProgress, private val onFilesDriveLinkError: OnFilesDriveLinkError, - private val getDriveLinks: GetDriveLinks, private val selectLinks: SelectLinks, + private val selectAll: SelectAll, private val deselectLinks: DeselectLinks, private val getSelectedDriveLinks: GetSelectedDriveLinks, private val savedStateHandle: SavedStateHandle, getSorting: GetSorting, private val configurationProvider: ConfigurationProvider, ) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle), HomeTabViewModel { + private val shareId = savedStateHandle.get(Screen.Files.SHARE_ID) private val folderId = savedStateHandle.get(Screen.Files.FOLDER_ID)?.let { folderId -> shareId?.let { FolderId(ShareId(userId, shareId), folderId) } @@ -141,9 +142,6 @@ class FilesViewModel @Inject constructor( } ) }.stateIn(viewModelScope, SharingStarted.Eagerly, null) - private val encryptedDriveLinks: Flow> = - driveLink.filterNotNull() - .flatMapLatest { folder -> getDriveLinks(folder.id) } val driveLinks: Flow> = driveLink.filterNotNull() @@ -175,9 +173,9 @@ class FilesViewModel @Inject constructor( contentDescriptionResId = BasePresentation.string.content_description_select_all, onAction = { viewModelScope.launch { - addSelected( - encryptedDriveLinks.first().map { driveLink -> driveLink.id } - ) + driveLink.value?.let { parent -> + selectAll(parent.id, selectionId.value) + } } } ) @@ -187,23 +185,20 @@ class FilesViewModel @Inject constructor( onAction = { viewEvent?.onSelectedOptions?.invoke() } ) private val topBarActions: MutableStateFlow> = MutableStateFlow(setOf(addFilesAction)) - private val selected: StateFlow> = combine( - selectionId.filterNotNull().flatMapLatest { selectionId -> getSelectedDriveLinks(selectionId) }, - encryptedDriveLinks, - ) { selectedDriveLinks, availableDriveLinks -> - val selectedDriveLinkIds = selectedDriveLinks.map { driveLink -> driveLink.id }.toSet() - val availableDriveLinkIds = availableDriveLinks.map { driveLink -> driveLink.id }.toSet() - val removedDriveLinkIds = selectedDriveLinkIds.subtract(availableDriveLinkIds) - if (removedDriveLinkIds.isNotEmpty()) { - removeSelected(removedDriveLinkIds.toList()) - } - if (selectedDriveLinkIds.isEmpty()) { - topBarActions.value = setOf(addFilesAction) - } else { - topBarActions.value = setOf(selectAllAction, selectedOptionsAction) - } - selectedDriveLinkIds - }.stateIn(viewModelScope, SharingStarted.Eagerly, setOf()) + private val selected: StateFlow> = selectionId + .filterNotNull() + .transformLatest { id -> + emitAll( + getSelectedDriveLinks(id).map { driveLinks -> + if (driveLinks.isEmpty()) { + topBarActions.value = setOf(addFilesAction) + } else { + topBarActions.value = setOf(selectAllAction, selectedOptionsAction) + } + driveLinks.map { driveLink -> driveLink.id }.toSet() + } + ) + }.stateIn(viewModelScope, SharingStarted.Eagerly, emptySet()) val isBottomNavigationEnabled: Flow = selected.map { set -> set.isEmpty() } val initialViewState = FilesViewState( title = savedStateHandle[Screen.Files.FOLDER_NAME], @@ -269,7 +264,6 @@ class FilesViewModel @Inject constructor( actionResId = BasePresentation.string.action_empty_files_add_files, ) private var viewEvent: FilesViewEvent? = null - fun viewEvent( navigateToFiles: (folderId: FolderId, folderName: String?) -> Unit, navigateToPreview: (fileId: FileId) -> Unit, @@ -279,6 +273,15 @@ class FilesViewModel @Inject constructor( navigateToParentFolderOptions: (folderId: FolderId) -> Unit, navigateBack: () -> Unit, ): FilesViewEvent = object : FilesViewEvent { + + private val driveLinkShareFlow = MutableSharedFlow(extraBufferCapacity = 1).also { flow -> + viewModelScope.launch { + flow.take(1).collect { driveLink -> + driveLink.onClick(navigateToFiles, navigateToPreview) + } + } + } + override val onTopAppBarNavigation = { if (selected.value.isNotEmpty()) { selectionId.value?.let { viewModelScope.launch { deselectLinks(it) } } @@ -301,7 +304,8 @@ class FilesViewModel @Inject constructor( addSelected(listOf(driveLink.id)) } } else { - driveLink.onClick(navigateToFiles, navigateToPreview) + driveLinkShareFlow.tryEmit(driveLink) + Unit } } override val onLoadState = onLoadState( diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/MoveToFolderViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/MoveToFolderViewModel.kt index b7e84837..44632cd9 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/MoveToFolderViewModel.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/MoveToFolderViewModel.kt @@ -141,13 +141,13 @@ class MoveToFolderViewModel @Inject constructor( override val onAppendErrorAction: () -> Unit = this@MoveToFolderViewModel.onRetry } - fun confirmMove() { + suspend fun confirmMove() { parentLink.value?.let { folder -> if (folder.id != parentId) { viewModelScope.launch { moveFile(userId, driveLinksToMove.value.map { driveLink -> driveLink.id }, folder.id) selectionId?.let{ deselectLinks(selectionId) } - } + }.join() } } } diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/OfflineViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/OfflineViewModel.kt index e3590fb0..d5ed07ea 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/OfflineViewModel.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/OfflineViewModel.kt @@ -25,6 +25,7 @@ import androidx.paging.CombinedLoadStates import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -34,16 +35,23 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch +import me.proton.android.drive.ui.common.onClick import me.proton.android.drive.ui.navigation.PagerType import me.proton.android.drive.ui.navigation.Screen import me.proton.android.drive.ui.viewevent.OfflineViewEvent import me.proton.android.drive.ui.viewstate.OfflineViewState import me.proton.core.domain.arch.onSuccess import me.proton.core.drive.base.domain.entity.Percentage +import me.proton.core.drive.base.domain.entity.onProcessing +import me.proton.core.drive.base.domain.extension.onFailure import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL +import me.proton.core.drive.base.presentation.extension.log +import me.proton.core.drive.base.presentation.viewmodel.UserViewModel import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink import me.proton.core.drive.drivelink.domain.entity.DriveLink +import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted import me.proton.core.drive.drivelink.download.domain.usecase.GetDownloadProgress import me.proton.core.drive.drivelink.list.domain.usecase.GetSortedDecryptedDriveLinks import me.proton.core.drive.drivelink.offline.domain.usecase.GetDecryptedOfflineDriveLinks @@ -53,12 +61,6 @@ import me.proton.core.drive.files.presentation.state.ListContentState 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 -import me.proton.android.drive.ui.common.onClick -import me.proton.core.drive.base.presentation.viewmodel.UserViewModel -import me.proton.core.drive.base.domain.entity.onProcessing -import me.proton.core.drive.base.domain.extension.onFailure -import me.proton.core.drive.base.presentation.extension.log -import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted import me.proton.core.drive.share.domain.entity.ShareId import me.proton.core.drive.sorting.domain.entity.Sorting import me.proton.core.drive.sorting.domain.usecase.GetSorting @@ -165,20 +167,30 @@ class OfflineViewModel @Inject constructor( navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit, navigateBack: () -> Unit, ): OfflineViewEvent = object : OfflineViewEvent { + + private val driveLinkShareFlow = MutableSharedFlow(extraBufferCapacity = 1).also { flow -> + viewModelScope.launch { + flow.take(1).collect { driveLink -> + driveLink.onClick( + navigateToFolder = navigateToFiles, + navigateToPreview = { fileId -> + navigateToPreview( + if (folderId == null) PagerType.OFFLINE else PagerType.FOLDER, + fileId + ) + } + ) + } + } + } + override val onTopAppBarNavigation = { navigateBack() } override val onSorting = navigateToSortingDialog override val onDriveLink = { driveLink: DriveLink -> - driveLink.onClick( - navigateToFolder = navigateToFiles, - navigateToPreview = { fileId -> - navigateToPreview( - if (folderId == null) PagerType.OFFLINE else PagerType.FOLDER, - fileId - ) - } - ) + driveLinkShareFlow.tryEmit(driveLink) + Unit } override val onLoadState = { _: CombinedLoadStates, _: Int -> } override val onMoreOptions = { driveLink: DriveLink -> navigateToFileOrFolderOptions(driveLink.id) } diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/PreviewViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/PreviewViewModel.kt index 2de880eb..e9f0d95e 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/PreviewViewModel.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/PreviewViewModel.kt @@ -18,11 +18,14 @@ package me.proton.android.drive.ui.viewmodel +import android.annotation.SuppressLint +import android.content.Context import android.net.Uri 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.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -32,6 +35,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -41,6 +45,7 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch +import me.proton.android.drive.extension.getDefaultMessage import me.proton.android.drive.ui.effect.PreviewEffect import me.proton.android.drive.ui.navigation.PagerType import me.proton.android.drive.ui.navigation.Screen @@ -51,11 +56,12 @@ import me.proton.core.domain.arch.transformSuccess import me.proton.core.domain.entity.UserId import me.proton.core.drive.base.domain.extension.asSuccess import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL +import me.proton.core.drive.base.domain.provider.ConfigurationProvider import me.proton.core.drive.base.presentation.entity.toFileTypeCategory import me.proton.core.drive.base.presentation.extension.require import me.proton.core.drive.base.presentation.viewmodel.UserViewModel -import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink import me.proton.core.drive.documentsprovider.domain.usecase.GetDocumentUri +import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink import me.proton.core.drive.drivelink.domain.entity.DriveLink import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted import me.proton.core.drive.drivelink.download.domain.usecase.GetFile @@ -66,7 +72,6 @@ import me.proton.core.drive.files.preview.presentation.component.event.PreviewVi import me.proton.core.drive.files.preview.presentation.component.state.ContentState import me.proton.core.drive.files.preview.presentation.component.state.PreviewContentState import me.proton.core.drive.files.preview.presentation.component.state.PreviewViewState -import me.proton.core.drive.files.preview.presentation.component.state.ZoomEffect import me.proton.core.drive.files.preview.presentation.component.toComposable import me.proton.core.drive.link.domain.entity.FileId import me.proton.core.drive.link.domain.entity.LinkId @@ -77,8 +82,11 @@ import me.proton.core.drive.base.presentation.R as BasePresentation import me.proton.core.presentation.R as CorePresentation @HiltViewModel +@SuppressLint("StaticFieldLeak") @OptIn(ExperimentalCoroutinesApi::class) class PreviewViewModel @Inject constructor( + @ApplicationContext private val appContext: Context, + private val configurationProvider: ConfigurationProvider, getDriveLink: GetDecryptedDriveLink, private val getFile: GetFile, private val getDocumentUri: GetDocumentUri, @@ -110,11 +118,12 @@ class PreviewViewModel @Inject constructor( PagerType.OFFLINE -> OfflineContentProvider(userId, getOfflineDriveNodes) } + private val contentStatesCache = mutableMapOf>() + private val driveLinks: StateFlow?> = provider.getDriveLinks() .stateIn(viewModelScope, SharingStarted.Eagerly, null) private val _previewEffect = MutableSharedFlow() - private val _zoomEffect = MutableSharedFlow() private val isFullscreen = MutableStateFlow(false) private val renderFailed = MutableStateFlow(null) val initialViewState = PreviewViewState( @@ -151,7 +160,6 @@ class PreviewViewModel @Inject constructor( val previewEffect: Flow = _previewEffect.asSharedFlow() .onStart { emit(PreviewEffect.Fullscreen(isFullscreen.value)) } - val zoomEffect: Flow = _zoomEffect.asSharedFlow() fun viewEvent( navigateBack: () -> Unit, @@ -160,7 +168,6 @@ class PreviewViewModel @Inject constructor( override val onTopAppBarNavigation = { navigateBack() } override val onMoreOptions = { navigateToFileOrFolderOptions(fileId) } override val onSingleTap = { toggleFullscreen() } - override val onDoubleTap = { resetZoom() } override val onRenderFailed = { throwable: Throwable -> renderFailed.value = throwable } override val mediaControllerVisibility = { visible: Boolean -> if ((visible && isFullscreen.value) || (!visible && !isFullscreen.value)) { @@ -197,48 +204,37 @@ class PreviewViewModel @Inject constructor( } } - private fun resetZoom() { - viewModelScope.launch { - _zoomEffect.emit(ZoomEffect.Reset) - } - } - private fun getContentState( getFileState: GetFile.State, renderFailed: Throwable? = null, ): ContentState { return renderFailed?.let { throwable -> - ContentState.Error.NonRetryable(throwable.message, 0) + ContentState.Error.NonRetryable( + message = throwable.getDefaultMessage( + context = appContext, + useExceptionMessage = configurationProvider.useExceptionMessage, + ), + messageResId = 0, + ) } ?: getFileState.toContentState(this) } fun getUri(fileId: FileId) = getDocumentUri(userId, fileId) - - private fun DriveLink.File.getContentStateFlow(): Flow { - if (mimeType.toFileTypeCategory().toComposable() == PreviewComposable.Unknown) { - return NO_PREVIEW_SUPPORTED - } - var savedFlow: Flow? = null - return trigger.flatMapLatest { trigger -> - val availableFlow = savedFlow - when { - availableFlow != null -> availableFlow - trigger.fileId == id -> { + private fun DriveLink.File.getContentStateFlow(): Flow = + contentStatesCache.getOrPut(id) { + if (mimeType.toFileTypeCategory().toComposable() == PreviewComposable.Unknown) { + NO_PREVIEW_SUPPORTED + } else { + trigger.filter { trigger -> trigger.fileId == id }.flatMapLatest { trigger -> combine( getFile(this, trigger.verifySignature), renderFailed, ) { fileState, renderFailed -> - getContentState(fileState, renderFailed).also { state -> - if (state is ContentState.Available) { - savedFlow = flowOf(state) - } - } + getContentState(fileState, renderFailed) } } - else -> DEFAULT_STATE } } - } private data class Trigger( val fileId: FileId, @@ -247,7 +243,6 @@ class PreviewViewModel @Inject constructor( companion object { private val NO_PREVIEW_SUPPORTED = flowOf(ContentState.Available(Uri.EMPTY)) - private val DEFAULT_STATE = flowOf(ContentState.Downloading(null)) } } diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SettingsViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SettingsViewModel.kt index 86a7f35d..8535b3a2 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SettingsViewModel.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SettingsViewModel.kt @@ -23,6 +23,7 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Build import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -32,12 +33,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import me.proton.android.drive.BuildConfig import me.proton.android.drive.R -import me.proton.core.presentation.R as CorePresentation +import me.proton.android.drive.lock.domain.manager.AppLockManager +import me.proton.android.drive.lock.domain.usecase.GetAutoLockDuration +import me.proton.android.drive.lock.domain.usecase.HasEnableAppLockTimestamp import me.proton.android.drive.settings.DebugSettings import me.proton.core.drive.base.presentation.viewmodel.UserViewModel import me.proton.core.drive.settings.presentation.component.DebugSettingsStateAndEvent @@ -46,10 +48,13 @@ import me.proton.core.drive.settings.presentation.event.SettingsViewEvent import me.proton.core.drive.settings.presentation.state.DebugSettingsViewState import me.proton.core.drive.settings.presentation.state.LegalLink import me.proton.core.drive.settings.presentation.state.SettingsViewState +import me.proton.drive.android.settings.domain.entity.ThemeStyle import me.proton.drive.android.settings.domain.usecase.GetThemeStyle import me.proton.drive.android.settings.domain.usecase.UpdateThemeStyle -import me.proton.drive.android.settings.domain.entity.ThemeStyle import javax.inject.Inject +import me.proton.core.drive.base.domain.extension.combine as baseCombine +import me.proton.core.drive.base.presentation.R as BasePresentation +import me.proton.core.presentation.R as CorePresentation @HiltViewModel @SuppressLint("StaticFieldLeak") @@ -59,18 +64,23 @@ class SettingsViewModel @Inject constructor( getThemeStyle: GetThemeStyle, private val updateThemeStyle: UpdateThemeStyle, savedStateHandle: SavedStateHandle, + appLockManager: AppLockManager, + getAutoLockDuration: GetAutoLockDuration, + private val hasEnableAppLockTimestamp: HasEnableAppLockTimestamp, ) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) { private val _errorMessage = MutableSharedFlow() val errorMessage: SharedFlow = _errorMessage - val viewState: Flow = combine( + val viewState: Flow = baseCombine( debugSettings.baseUrlFlow, debugSettings.hostFlow, debugSettings.appVersionHeaderFlow, debugSettings.useExceptionMessageFlow, getThemeStyle(userId), - ) { baseUrl, host, appVersionHeader, useExceptionMessage, themeStyle -> + appLockManager.enabled, + getAutoLockDuration(), + ) { baseUrl, host, appVersionHeader, useExceptionMessage, themeStyle, enabled, autoLockDuration -> SettingsViewState( navigationIcon = CorePresentation.drawable.ic_arrow_back, appNameResId = R.string.app_name, @@ -92,11 +102,18 @@ class SettingsViewModel @Inject constructor( baseUrl = baseUrl, appVersionHeader = appVersionHeader, useExceptionMessage = useExceptionMessage, - ) + ), + appAccessSubtitleResId = getAppAccessSubtitleResId(enabled), + isAutoLockDurationsVisible = enabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O, + autoLockDuration = autoLockDuration, ) }.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) - fun viewEvent(navigateBack: () -> Unit) = SettingsViewEvent( + fun viewEvent( + navigateBack: () -> Unit, + navigateToAppAccess: () -> Unit, + navigateToAutoLockDurations: () -> Unit, + ) = SettingsViewEvent( navigateBack = navigateBack, onLinkClicked = { link -> when (link) { @@ -107,9 +124,21 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { updateThemeStyle(userId, enumValues().first { style -> style.resId == newStyle }) } + }, + onAppAccess = { + navigateToAppAccess() + }, + onAutoLockDurations = { + navigateToAutoLockDurations() } ) + private suspend fun getAppAccessSubtitleResId(isAppLockEnabled: Boolean): Int = when { + isAppLockEnabled -> BasePresentation.string.app_lock_option_system + hasEnableAppLockTimestamp().not() -> BasePresentation.string.app_lock_option_never_set + else -> BasePresentation.string.app_lock_option_none + } + private fun onExternalLinkClicked(link: LegalLink.External) { try { context.startActivity( diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SharedViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SharedViewModel.kt index 72b59a0a..a6e6fbbf 100644 --- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SharedViewModel.kt +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SharedViewModel.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch import me.proton.android.drive.ui.common.onClick @@ -114,7 +115,7 @@ class SharedViewModel @Inject constructor( }.stateIn(viewModelScope, SharingStarted.Eagerly, initialValue = DataResult.Processing(ResponseSource.Local)) val driveLinksFlow = driveLinks.mapSuccessValueOrNull() - .map { driveLinks -> driveLinks.orEmpty() } + .filterNotNull() .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) val initialViewState = SharedViewState( @@ -146,7 +147,11 @@ class SharedViewModel @Inject constructor( }.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) private fun DataResult>.toListContentState(): ListContentState = when (val cause = this) { - is DataResult.Processing -> ListContentState.Loading + is DataResult.Processing -> if (driveLinksFlow.replayCache.isNotEmpty()) { + ListContentState.Content(true) + } else { + ListContentState.Loading + } is DataResult.Success -> if (cause.value.isNotEmpty()) { ListContentState.Content(false) } else { @@ -171,18 +176,22 @@ class SharedViewModel @Inject constructor( navigateToSortingDialog: (Sorting) -> Unit, navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit, ): SharedViewEvent = object : SharedViewEvent { + + private val driveLinkShareFlow = MutableSharedFlow(extraBufferCapacity = 1).also { flow -> + viewModelScope.launch { + flow.take(1).collect { driveLink -> + driveLink.onClick(navigateToFiles, navigateToPreview) + } + } + } override val onTopAppBarNavigation = { viewModelScope.launch { _effects.emit(HomeEffect.OpenDrawer) } Unit } override val onSorting = navigateToSortingDialog - override val onDriveLink = { driveNode: DriveLink -> - driveNode.onClick( - navigateToFolder = navigateToFiles, - navigateToPreview = { fileId -> - navigateToPreview(fileId) - } - ) + override val onDriveLink = { driveLink: DriveLink -> + driveLinkShareFlow.tryEmit(driveLink) + Unit } override val onLoadState = { _: CombinedLoadStates, _: Int -> } override val onMoreOptions = { driveLink: DriveLink -> navigateToFileOrFolderOptions(driveLink.id) } diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SystemAccessDialogViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SystemAccessDialogViewModel.kt new file mode 100644 index 00000000..58085b9c --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SystemAccessDialogViewModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.ui.viewmodel + +import android.annotation.TargetApi +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.content.Intent +import android.os.Build +import android.provider.Settings +import androidx.lifecycle.ViewModel +import me.proton.android.drive.ui.viewevent.SystemAccessDialogViewEvent +import javax.inject.Inject + +class SystemAccessDialogViewModel @Inject constructor( + +) : ViewModel() { + fun viewEvent( + context: Context, + dismiss: () -> Unit, + ) = object : SystemAccessDialogViewEvent { + override val onSettings = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.showBiometricSettings() + } else { + context.showSetNewPasswordSettings() + } + dismiss() + } + } + + private fun Context.showSetNewPasswordSettings() { + startActivity(Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD)) + } + + @TargetApi(Build.VERSION_CODES.P) + private fun Context.showBiometricSettings() { + val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Settings.ACTION_BIOMETRIC_ENROLL + } else { + @Suppress("DEPRECATION") + Settings.ACTION_FINGERPRINT_ENROLL + } + startActivity(Intent(action)) + } +} diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AppAccessViewState.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AppAccessViewState.kt new file mode 100644 index 00000000..9b78a7aa --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AppAccessViewState.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.ui.viewstate + +import androidx.compose.runtime.Immutable + +@Immutable +data class AppAccessViewState( + val title: String, + val enabledOption: AccessOption, +) + +enum class AccessOption { + NONE, + SYSTEM, +} diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AutoLockDurationsViewState.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AutoLockDurationsViewState.kt new file mode 100644 index 00000000..27ea18b9 --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AutoLockDurationsViewState.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.ui.viewstate + +import kotlin.time.Duration + +data class AutoLockDurationsViewState( + val title: String, + val durations: Set, + val selected: Duration, +) diff --git a/app/src/main/kotlin/me/proton/android/drive/usecase/CleanUpAccount.kt b/app/src/main/kotlin/me/proton/android/drive/usecase/CleanUpAccount.kt index d7ed4580..f457a5bc 100644 --- a/app/src/main/kotlin/me/proton/android/drive/usecase/CleanUpAccount.kt +++ b/app/src/main/kotlin/me/proton/android/drive/usecase/CleanUpAccount.kt @@ -18,6 +18,7 @@ package me.proton.android.drive.usecase +import me.proton.android.drive.lock.domain.usecase.DisableAppLock import me.proton.core.domain.entity.UserId import me.proton.core.drive.base.domain.usecase.DeleteAllFolders import me.proton.core.drive.drivelink.crypto.domain.usecase.RemoveAllDecryptedText @@ -28,11 +29,13 @@ class CleanUpAccount @Inject constructor( private val deleteAllFolders: DeleteAllFolders, private val removeAllKeys: RemoveAllKeys, private val removeAllDecryptedText: RemoveAllDecryptedText, + private val disableAppLock: DisableAppLock, ) { suspend operator fun invoke(userId: UserId) { deleteAllFolders(userId) removeAllKeys(userId) removeAllDecryptedText(userId) + disableAppLock(userAuthenticationRequired = false) } } diff --git a/app/src/main/kotlin/me/proton/android/drive/usecase/GetDocumentsProviderRootsImpl.kt b/app/src/main/kotlin/me/proton/android/drive/usecase/GetDocumentsProviderRootsImpl.kt new file mode 100644 index 00000000..d7606952 --- /dev/null +++ b/app/src/main/kotlin/me/proton/android/drive/usecase/GetDocumentsProviderRootsImpl.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Proton AG. + * This file is part of Proton Drive. + * + * Proton Drive is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Drive is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Drive. If not, see . + */ + +package me.proton.android.drive.usecase + +import kotlinx.coroutines.flow.first +import me.proton.android.drive.lock.domain.manager.AppLockManager +import me.proton.core.account.domain.entity.Account +import me.proton.core.accountmanager.domain.AccountManager +import me.proton.core.drive.documentsprovider.domain.usecase.GetDocumentsProviderRoots +import javax.inject.Inject + +class GetDocumentsProviderRootsImpl @Inject constructor( + private val appLockManager: AppLockManager, + private val accountManager: AccountManager, +) : GetDocumentsProviderRoots { + + override suspend fun invoke(): List = takeUnless { appLockManager.isEnabled() } + ?.let { accountManager.getAccounts().first() }.orEmpty() +} diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/CreateFolderRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/CreateFolderRobot.kt index 669870b8..f8d4da7d 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/CreateFolderRobot.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/CreateFolderRobot.kt @@ -18,34 +18,22 @@ package me.proton.android.drive.ui.robot -import androidx.compose.ui.test.assertCountEquals -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.filter -import androidx.compose.ui.test.hasAnyAncestor -import androidx.compose.ui.test.hasSetTextAction -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.hasText -import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag import me.proton.core.drive.folder.create.presentation.CreateFolderComponentTestTag +import me.proton.test.fusion.Fusion.node import me.proton.core.drive.base.presentation.R as BasePresentation object CreateFolderRobot : Robot { - private val createFolderScreen get() = node(hasTestTag(CreateFolderComponentTestTag.screen)) - private val cancelButton get() = node(hasTextResource(BasePresentation.string.common_cancel_action)) - private val createButton get() = node(hasTextResource(BasePresentation.string.common_create_action)) - private val folderNameField - get() = node( - hasSetTextAction(), - hasAnyAncestor(hasTestTag(CreateFolderComponentTestTag.folderNameTextField)) - ) + private val createFolderScreen get() = node.withTag(CreateFolderComponentTestTag.screen) + private val cancelButton get() = node.withText(BasePresentation.string.common_cancel_action) + private val createButton get() = node.withText(BasePresentation.string.common_create_action) + private val folderNameField get() = node.isSetText().hasAncestor( + node.withTag(CreateFolderComponentTestTag.folderNameTextField) + ) - fun clickCancel() = cancelButton.tryToClickAndGoTo(FilesTabRobot) - fun typeFolderName(text: String) = folderNameField.tryToTypeText(text, CreateFolderRobot) - fun clearName() = folderNameField.clearText(CreateFolderRobot) - - fun clickCreate() { - createButton.tryToClickAndGoTo(this) - } + fun clickCancel() = cancelButton.clickTo(FilesTabRobot) + fun typeFolderName(text: String) = apply { folderNameField.typeText(text) } + fun clearName() = apply { folderNameField.clearText() } + fun clickCreate() = Unit.also { createButton.clickTo(this) } override fun robotDisplayed() { createFolderScreen.assertIsDisplayed() diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FileFolderOptionsRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FileFolderOptionsRobot.kt index e844d495..4e45a904 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FileFolderOptionsRobot.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FileFolderOptionsRobot.kt @@ -18,22 +18,22 @@ package me.proton.android.drive.ui.robot -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasTestTag +import me.proton.test.fusion.Fusion.node import me.proton.android.drive.ui.dialog.FileFolderOptionsDialogTestTag import me.proton.core.drive.base.presentation.R -object FileFolderOptionsRobot : Robot { - private val fileFolderOptionsScreen get() = node(hasTestTag(FileFolderOptionsDialogTestTag.fileOrFolderOptions)) - private val moveButton get() = node(hasTextResource(R.string.files_move_file_action)) - private val moveToTrash get() = node(hasTextResource(R.string.files_send_to_trash_action)) - private val makeAvailableOfflineButton get() = node(hasTextResource(R.string.title_make_available_offline)) - private val getLinkButton get() = node(hasTextResource(R.string.title_get_link)) - private val renameButton get() = node(hasTextResource(R.string.files_rename_file_action)) - private val folderDetailsButton get() = node(hasTextResource(R.string.files_display_folder_info_action)) - fun clickMove() = moveButton.tryToClickAndGoTo(MoveToFolderRobot) - fun clickRename() = renameButton.tryToClickAndGoTo(RenameRobot) +object FileFolderOptionsRobot : Robot { + private val fileFolderOptionsScreen get() = node.withTag(FileFolderOptionsDialogTestTag.fileOrFolderOptions) + private val moveButton get() = node.withText(R.string.files_move_file_action) + private val moveToTrash get() = node.withText(R.string.files_send_to_trash_action) + private val makeAvailableOfflineButton get() = node.withText(R.string.title_make_available_offline) + private val getLinkButton get() = node.withText(R.string.title_get_link) + private val renameButton get() = node.withText(R.string.files_rename_file_action) + private val folderDetailsButton get() = node.withText(R.string.files_display_folder_info_action) + + fun clickMove() = moveButton.clickTo(MoveToFolderRobot) + fun clickRename() = renameButton.clickTo(RenameRobot) override fun robotDisplayed() { fileFolderOptionsScreen.assertIsDisplayed() diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FilesTabRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FilesTabRobot.kt index d82d62a9..3db5ce04 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FilesTabRobot.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FilesTabRobot.kt @@ -18,76 +18,59 @@ package me.proton.android.drive.ui.robot -import androidx.compose.ui.test.assertCountEquals -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsSelected -import androidx.compose.ui.test.filter -import androidx.compose.ui.test.hasAnyChild -import androidx.compose.ui.test.hasAnySibling -import androidx.compose.ui.test.hasContentDescription -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.hasTextExactly -import androidx.compose.ui.test.performTouchInput -import androidx.compose.ui.test.swipeUp + import me.proton.core.drive.base.presentation.R import me.proton.core.drive.files.presentation.component.FilesTestTag -import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag +import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag.threeDotsButton +import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag.item import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag.ItemType -import me.proton.core.test.android.instrumented.utils.StringUtils +import me.proton.test.fusion.Fusion.allNodes +import me.proton.test.fusion.Fusion.node +import me.proton.test.fusion.ui.common.enums.SwipeDirection object FilesTabRobot : HomeRobot { - private val uploadFileDesc = - StringUtils.stringFromResource(R.string.files_upload_content_description_upload_file) - private val plusButton get() = node(hasContentDescription(uploadFileDesc)) - private val filesListItems get() = nodes(hasTestTag(FilesListItemComponentTestTag.item)) - + private val plusButton get() = node.withContentDescription(R.string.files_upload_content_description_upload_file) + private fun itemWithName(name: String) = node.withTag(item).withText(name) + private val fileList get() = node.withTag(FilesTestTag.content) private fun moreButton(itemName: String, itemType: ItemType) = - node( - hasTestTag(FilesListItemComponentTestTag.threeDotsButton(itemType)), - hasAnySibling(hasTextExactly(itemName)) - ) - - private val filesContent get() = node(hasTestTag(FilesTestTag.content)) + node + .withTag(threeDotsButton(itemType)) + .hasSibling(node.withText(itemName)) fun itemWithTextDisplayed(text: String) { - filesListItems.filter(hasText(text)).assertCountEquals(1) + itemWithName(text).await { assertIsDisplayed() } } - fun swipeUpToItemWithName(itemName: String): FilesTabRobot = waitFor(this) { - try { - node(hasText(itemName)).assertIsDisplayed() - } catch (error: AssertionError) { - filesContent.performTouchInput { swipeUp(durationMillis = 1000L) } - throw error - } + fun scrollToItemWithName(itemName: String): FilesTabRobot = apply { + allNodes.withTag(item).assertAny(node.isEnabled()) + fileList.scrollTo(node.withText(itemName)) } - fun clickPlusButton() = plusButton.tryToClickAndGoTo(ParentFolderOptionsRobot) + fun clickPlusButton() = plusButton.clickTo(ParentFolderOptionsRobot) fun clickMoreOnItem(title: String) = - node( - hasTestTag(FilesListItemComponentTestTag.threeDotsButton(ItemType.File)) or hasTestTag(FilesListItemComponentTestTag.threeDotsButton(ItemType.Folder)), - hasAnySibling(hasTextExactly(title)) - ).tryToClickAndGoTo(FileFolderOptionsRobot) + node + .withAnyTag(threeDotsButton(ItemType.File), threeDotsButton(ItemType.Folder)) + .hasSibling(node.withText(title)) + .clickTo(FileFolderOptionsRobot) fun clickMoreOnFolder(title: String) = - moreButton(title, ItemType.Folder).tryToClickAndGoTo(FileFolderOptionsRobot) + moreButton(title, ItemType.Folder).clickTo(FileFolderOptionsRobot) fun clickMoreOnFile(title: String) = - moreButton(title, ItemType.File).tryToClickAndGoTo(FileFolderOptionsRobot) + moreButton(title, ItemType.File).clickTo(FileFolderOptionsRobot) fun clickOnFile(name: String) = - node( - hasTestTag(FilesListItemComponentTestTag.threeDotsButton(ItemType.File)), - hasAnyChild(hasText(name)) - ).tryToClickAndGoTo(PreviewRobot) + node + .withTag(threeDotsButton(ItemType.File)) + .hasChild(node.withText(name)) + .clickTo(this) fun clickOnFolder(name: String) = - node( - hasAnySibling(hasTestTag(FilesListItemComponentTestTag.threeDotsButton(ItemType.Folder))), - hasText(name) - ).tryToClickAndGoTo(FilesTabRobot) + node + .withText(name) + .hasSibling(node.withTag(threeDotsButton(ItemType.Folder))) + .clickTo(this) override fun robotDisplayed() { homeScreenDisplayed() diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/HomeRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/HomeRobot.kt index d047fd85..3b17e553 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/HomeRobot.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/HomeRobot.kt @@ -18,28 +18,26 @@ package me.proton.android.drive.ui.robot +import me.proton.test.fusion.Fusion.node import androidx.annotation.StringRes -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasAnyChild -import androidx.compose.ui.test.hasTestTag import me.proton.android.drive.ui.screen.HomeScreenTestTag import me.proton.core.drive.base.presentation.R import me.proton.core.drive.base.presentation.component.BottomNavigationComponentTestTag interface HomeRobot : Robot { - private val homeScreen get() = node(hasTestTag(HomeScreenTestTag.screen)) + private val homeScreen get() = node.withTag(HomeScreenTestTag.screen) val filesTab get() = tabWithText(R.string.title_files) val sharedTab get() = tabWithText(R.string.title_shared) - fun clickFilesTab() = filesTab.tryToClickAndGoTo(FilesTabRobot) - fun clickSharedTab() = sharedTab.tryToClickAndGoTo(SharedTabRobot) + fun clickFilesTab() = filesTab.clickTo(FilesTabRobot) + fun clickSharedTab() = sharedTab.clickTo(SharedTabRobot) fun homeScreenDisplayed() { homeScreen.assertIsDisplayed() } - private fun tabWithText(@StringRes textRes: Int) = node( - hasTestTag(BottomNavigationComponentTestTag.tab), - hasAnyChild(hasTextResource(textRes)), - ) + private fun tabWithText(@StringRes textRes: Int) = + node + .withTag(BottomNavigationComponentTestTag.tab) + .hasChild(node.withText(textRes)) } diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/MoveToFolderRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/MoveToFolderRobot.kt index 12ccdde2..40994157 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/MoveToFolderRobot.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/MoveToFolderRobot.kt @@ -18,38 +18,33 @@ package me.proton.android.drive.ui.robot -import androidx.compose.ui.test.assertCountEquals -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.filter -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.performTouchInput -import androidx.compose.ui.test.swipeDown +import me.proton.test.fusion.Fusion.node import me.proton.android.drive.R import me.proton.android.drive.ui.screen.MoveToFolderScreenTestTag import me.proton.core.drive.files.presentation.component.FilesTestTag import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag -import me.proton.core.test.android.instrumented.utils.StringUtils object MoveToFolderRobot : Robot { - private val moveToFolderScreen get() = node(hasTestTag(MoveToFolderScreenTestTag.screen)) - private val addFolderButton get() = node(hasTestTag(MoveToFolderScreenTestTag.plusFolderButton)) - private val cancelButton get() = node(hasText(StringUtils.stringFromResource(R.string.move_file_dismiss_action))) - private val moveButton get() = node(hasText(StringUtils.stringFromResource(R.string.move_file_confirm_action))) - private val filesListItems get() = nodes(hasTestTag(FilesListItemComponentTestTag.item)) - private val filesContent get() = node(hasTestTag(FilesTestTag.content)) + private val moveToFolderScreen get() = node.withTag(MoveToFolderScreenTestTag.screen) + private val addFolderButton get() = node.withTag(MoveToFolderScreenTestTag.plusFolderButton) + private val cancelButton get() = node.withText(R.string.move_file_dismiss_action) + private val moveButton get() = node.withText(R.string.move_file_confirm_action) + private val fileList get() = node.withTag(FilesTestTag.content) + private fun itemWithName(name: String) = + node.withTag(FilesListItemComponentTestTag.item).withText(name) - fun clickAddFolder() = addFolderButton.tryToClickAndGoTo(CreateFolderRobot) - fun clickCancel() = cancelButton.tryToClickAndGoTo(CreateFolderRobot) - fun clickMove() = moveButton.tryToClickAndGoTo(CreateFolderRobot) + fun clickAddFolder() = addFolderButton.clickTo(CreateFolderRobot) + fun clickCancel() = cancelButton.clickTo(CreateFolderRobot) + fun clickMove() = moveButton.clickTo(CreateFolderRobot) - fun swipeDown() = filesContent.tryPerformTouchInputAndGoTo(this) { swipeDown()} - - fun itemListWithTextDisplayed(text: String, count: Int = 1) { - filesListItems.filter(hasText(text)).assertCountEquals(count) + fun itemWithTextDisplayed(text: String) { + fileList.scrollTo(node.withText(text)) } override fun robotDisplayed() { moveToFolderScreen.assertIsDisplayed() + addFolderButton.assertIsDisplayed() + cancelButton.assertIsDisplayed() + moveButton.assertIsDisplayed() } } diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/ParentFolderOptionsRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/ParentFolderOptionsRobot.kt index 7fe79107..44ed4874 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/ParentFolderOptionsRobot.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/ParentFolderOptionsRobot.kt @@ -18,18 +18,19 @@ package me.proton.android.drive.ui.robot +import me.proton.test.fusion.Fusion.node import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasTestTag import me.proton.android.drive.R import me.proton.android.drive.ui.dialog.ParentFolderOptionsDialogTestTag object ParentFolderOptionsRobot : Robot { - private val contextMenu get() = node(hasTestTag(ParentFolderOptionsDialogTestTag.contextMenu)) - private val createFolderButton get() = node(hasTextResource(R.string.folder_option_create_folder)) - private val uploadAFileButton get() = node(hasTextResource(R.string.folder_option_import_file)) - private val takePhotoButton get() = node(hasTextResource(R.string.folder_option_take_a_photo)) + private val contextMenu get() = node.withTag(ParentFolderOptionsDialogTestTag.contextMenu) + private val createFolderButton get() = node.withText(R.string.folder_option_create_folder) + private val uploadAFileButton get() = node.withText(R.string.folder_option_import_file) + private val takePhotoButton get() = node.withText(R.string.folder_option_take_a_photo) - fun clickCreateFolder() = createFolderButton.tryToClickAndGoTo(CreateFolderRobot) + fun clickCreateFolder() = createFolderButton.clickTo(CreateFolderRobot) override fun robotDisplayed() { contextMenu.assertIsDisplayed() diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/PreviewRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/PreviewRobot.kt index 1d44b90d..76d32c9e 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/PreviewRobot.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/PreviewRobot.kt @@ -18,6 +18,7 @@ package me.proton.android.drive.ui.robot +import me.proton.test.fusion.Fusion.node import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasTestTag @@ -25,18 +26,16 @@ import androidx.compose.ui.test.hasText import me.proton.core.drive.base.presentation.R import me.proton.core.drive.base.presentation.component.TopAppBarComponentTestTag import me.proton.core.drive.files.preview.presentation.component.PreviewComponentTestTag -import me.proton.core.test.android.instrumented.utils.StringUtils object PreviewRobot : Robot { - private val contextualButtonDesc = StringUtils.stringFromResource(R.string.content_description_more_options) - private val previewScreen get() = node(hasTestTag(PreviewComponentTestTag.screen)) - private val contextualButton get() = node(hasContentDescription(contextualButtonDesc)) + private val previewScreen get() = node.withTag(PreviewComponentTestTag.screen) + private val contextualButton get() = node.withContentDescription(R.string.content_description_more_options) - fun clickOnContextualButton() = contextualButton.tryToClickAndGoTo(FileFolderOptionsRobot) + fun clickOnContextualButton() = contextualButton.clickTo(FileFolderOptionsRobot) fun topBarWithTextDisplayed(itemName: String) { - node(hasTestTag(TopAppBarComponentTestTag.appBar) and hasText(text = itemName, substring = true)).assertExists() + node.withTag(TopAppBarComponentTestTag.appBar).withTextSubstring(itemName).assertExists() } override fun robotDisplayed() { diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/RenameRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/RenameRobot.kt index c7a3e2c1..1517cb5b 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/RenameRobot.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/RenameRobot.kt @@ -18,28 +18,20 @@ package me.proton.android.drive.ui.robot -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasAnyAncestor -import androidx.compose.ui.test.hasSetTextAction -import androidx.compose.ui.test.hasTestTag +import me.proton.test.fusion.Fusion.node import me.proton.core.drive.drivelink.rename.presentation.R import me.proton.core.drive.drivelink.rename.presentation.RenameScreenTestTag object RenameRobot : Robot { + private val renameScreen get() = node.withTag(RenameScreenTestTag.screen) + private val cancelRenameButton get() = node.withText(R.string.link_rename_dismiss_button) + private val confirmRenameButton get() = node.withText(R.string.link_rename_button) + private val renameTextField get() = node.isSetText().hasAncestor(node.withTag(RenameScreenTestTag.textField)) - private val renameScreen get() = node(hasTestTag(RenameScreenTestTag.screen)) - private val cancelRenameButton get() = node(hasTextResource(R.string.link_rename_dismiss_button)) - private val confirmRenameButton get() = node(hasTextResource(R.string.link_rename_button)) - private val renameTextField - get() = node( - hasSetTextAction(), - hasAnyAncestor(hasTestTag(RenameScreenTestTag.textField)) - ) - - fun clickCancel() = cancelRenameButton.tryToClickAndGoTo(FilesTabRobot) - fun clickRename() = confirmRenameButton.tryToClickAndGoTo(FilesTabRobot) - fun typeName(text: String) = renameTextField.tryToTypeText(text, RenameRobot) - fun clearName() = renameTextField.clearText(RenameRobot) + fun clickCancel() = cancelRenameButton.clickTo(FilesTabRobot) + fun clickRename() = confirmRenameButton.clickTo(FilesTabRobot) + fun typeName(text: String) = apply { renameTextField.typeText(text) } + fun clearName() = apply { renameTextField.clearText() } override fun robotDisplayed() { renameScreen.assertIsDisplayed() diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/Robot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/Robot.kt index 23c9bc0d..8d9034a4 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/Robot.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/Robot.kt @@ -18,183 +18,23 @@ package me.proton.android.drive.ui.robot -import android.app.Instrumentation -import android.graphics.Bitmap -import androidx.annotation.StringRes -import androidx.compose.ui.graphics.asAndroidBitmap -import androidx.compose.ui.test.ComposeTimeoutException -import androidx.compose.ui.test.SemanticsMatcher -import androidx.compose.ui.test.SemanticsNodeInteraction -import androidx.compose.ui.test.SemanticsNodeInteractionCollection -import androidx.compose.ui.test.TouchInjectionScope -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.captureToImage -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.onFirst -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextClearance -import androidx.compose.ui.test.performTextInput -import androidx.compose.ui.test.performTouchInput -import androidx.test.platform.app.InstrumentationRegistry -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import me.proton.android.drive.ui.test.BaseTest.Companion.composeTestRule -import me.proton.android.drive.ui.test.BaseTest.Companion.screenshotLocation -import me.proton.android.drive.ui.test.BaseTest.Companion.testName +import me.proton.test.fusion.Fusion.node import me.proton.core.drive.folder.create.presentation.R -import me.proton.core.test.android.instrumented.ProtonTest.Companion.testTag import me.proton.core.test.android.instrumented.utils.StringUtils -import me.proton.core.util.kotlin.CoreLogger -import me.proton.core.util.kotlin.EMPTY_STRING -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.util.concurrent.TimeoutException -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds +import me.proton.test.fusion.ui.compose.builders.OnNode interface Robot { - val instrumentation: Instrumentation get() = InstrumentationRegistry.getInstrumentation() - val waitForScreenDuration: Duration get() = 10.seconds - val waitForItemToAppearInList: Duration get() = 30.seconds - val watchInterval: Duration get() = 100.milliseconds - val shouldUseUnmergedTree: Boolean get() = true - fun robotDisplayed() - fun node( - vararg semanticsMatchers: SemanticsMatcher, - useUnmergedTree: Boolean = shouldUseUnmergedTree, - ): SemanticsNodeInteraction = - composeTestRule.onNode(getFinalSemanticMatcher(semanticsMatchers), useUnmergedTree) - - fun nodes( - vararg semanticsMatchers: SemanticsMatcher, - useUnmergedTree: Boolean = shouldUseUnmergedTree, - ): SemanticsNodeInteractionCollection = - composeTestRule.onAllNodes(getFinalSemanticMatcher(semanticsMatchers), useUnmergedTree) - - private fun getFinalSemanticMatcher( - semanticsMatchers: Array - ): SemanticsMatcher { - var finalSemanticsMatcher = semanticsMatchers.first() - semanticsMatchers.drop(1).forEach { - finalSemanticsMatcher = finalSemanticsMatcher.and(it) - } - return finalSemanticsMatcher - } - - /** - * Common semantics matchers - */ - fun hasTextResource( - @StringRes resourceId: Int, - substring: Boolean = false, - ignoreCase: Boolean = false, - formatString: String = EMPTY_STRING - ): SemanticsMatcher = hasText( - StringUtils.stringFromResource(resourceId, substring, ignoreCase).format(formatString) - ) - - private fun successGrowler(itemName: String) = node( - hasText( - StringUtils.stringFromResource - (R.string.folder_create_successful, itemName)) - ) - - /** - * Common user actions - */ - fun SemanticsNodeInteraction.tryToClickAndGoTo(goesTo: T): T = - waitFor(goesTo) { performClick() } - - fun SemanticsNodeInteraction.tryToTypeText(text: String, goesTo: T): T = - waitFor(goesTo) { performTextInput(text) } - - fun SemanticsNodeInteraction.clearText(goesTo: T): T = - waitFor(goesTo) { performTextClearance() } - - fun SemanticsNodeInteraction.tryPerformTouchInputAndGoTo( - goesTo: T, - touchInput: TouchInjectionScope.() -> Unit - ): T = waitFor(goesTo) { performTouchInput { touchInput() } } + fun OnNode.clickTo(goesTo: T): T = goesTo.apply { click() } + /** Common actions **/ fun dismissSuccessGrowler(itemName: String, goesTo: T) = - successGrowler(itemName).tryToClickAndGoTo(goesTo) + node + .withText(StringUtils.stringFromResource(R.string.folder_create_successful, itemName)) + .clickTo(goesTo) - - /** - * Other extensions and Helpers - */ - fun waitFor( - goesTo: T, - timeout: Duration = waitForScreenDuration, - interval: Duration = watchInterval, - block: () -> Any, - ): T { - var error: Throwable = TimeoutException("Condition not met in ${timeout}ms") - try { - composeTestRule.waitForIdle() - composeTestRule.waitUntil(timeout.inWholeMilliseconds) { - try { - block() - true - } catch (e: AssertionError) { - // Thrown on Compose failed actions and assertions - error = handleTestError(e, 1) - runBlocking { delay(interval) } - false - } catch (e: IllegalStateException) { - // Thrown when Compose view is not ready - error = handleTestError(e, 0) - runBlocking { delay(interval) } - false - } - } - } catch (e: ComposeTimeoutException) { - CoreLogger.e(testTag, e) - composeTestRule - .onAllNodes(SemanticsMatcher("isRoot") { it.isRoot }) - .onFirst() - .screenshot("${screenshotLocation}/${testName.methodName}.png") - throw error - } - return goesTo - } - - private fun handleTestError(throwable: Throwable, messageLine: Int): Throwable { - val lines = throwable.message?.lines() - if (!lines.isNullOrEmpty() && messageLine < lines.size) { - CoreLogger.i(testTag, "Condition not yet met: ${lines[messageLine]}") - return throwable - } - val message = "Could not extract message line at position $messageLine. Printing full message" - CoreLogger.e(testTag, throwable, message) - return throwable - } - - private fun SemanticsNodeInteraction.screenshot(file: String) { - try { - val bitmap = captureToImage().asAndroidBitmap() - FileOutputStream(file).use { destination -> - bitmap.compress(Bitmap.CompressFormat.PNG, 100, destination) - } - } catch (throwable: Throwable) { - when (throwable) { - is FileNotFoundException -> "File not found" - is OutOfMemoryError -> "Out of memory" - else -> "Unknown error" - }.let { - CoreLogger.e(testTag, throwable, "Could not take screenshot: $it") - } - } - } - - fun nodeWithTextDisplayed(@StringRes stringRes: String) { - node(hasText(stringRes)).assertIsDisplayed() - } - - fun nodeWithTextResourceDisplayed(@StringRes stringRes: Int, formatAttr: Any = EMPTY_STRING) { - node(hasTextResource(stringRes, formatString = formatAttr.toString())).assertIsDisplayed() - } + /** Common assertions **/ + fun nodeWithTextDisplayed(text: String) = + node.withText(text).await { assertIsDisplayed() } } diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/SharedTabRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/SharedTabRobot.kt index a7089737..52af5630 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/SharedTabRobot.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/SharedTabRobot.kt @@ -18,15 +18,13 @@ package me.proton.android.drive.ui.robot -import androidx.compose.ui.test.assertCountEquals +import me.proton.test.fusion.Fusion.allNodes +import me.proton.test.fusion.Fusion.node import androidx.compose.ui.test.assertIsSelected -import androidx.compose.ui.test.filter -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.hasText import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag object SharedTabRobot : HomeRobot { - private val filesListItems get() = nodes(hasTestTag(FilesListItemComponentTestTag.item)) + private val filesListItems get() = allNodes.withTag(FilesListItemComponentTestTag.item) override fun robotDisplayed() { homeScreenDisplayed() @@ -34,6 +32,6 @@ object SharedTabRobot : HomeRobot { } fun itemWithTextDisplayed(text: String) { - filesListItems.filter(hasText(text)).assertCountEquals(1) + filesListItems.filter(node.withText(text)).assertCountEquals(1) } } diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/WelcomeRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/WelcomeRobot.kt index 3b5470ff..60c75b11 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/WelcomeRobot.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/WelcomeRobot.kt @@ -18,33 +18,31 @@ package me.proton.android.drive.ui.robot -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.onFirst -import androidx.compose.ui.test.swipeLeft -import androidx.compose.ui.test.swipeRight +import me.proton.test.fusion.Fusion.node +import me.proton.test.fusion.Fusion.allNodes import me.proton.android.drive.ui.screen.WelcomeScreenTestTag +import me.proton.test.fusion.ui.common.enums.SwipeDirection import me.proton.core.drive.base.presentation.R as BasePresentation import me.proton.core.presentation.R as CorePresentation object WelcomeRobot : Robot { - private val skipButton get() = nodes(hasTextResource(CorePresentation.string.presentation_skip)).onFirst() - private val nextButton get() = nodes(hasTextResource(BasePresentation.string.common_next_action)).onFirst() - private val getStartedButton get() = node(hasTextResource(BasePresentation.string.welcome_get_started_action)) - private val welcomeLabel get() = node(hasTextResource(BasePresentation.string.welcome_to_description)) - private val filesTitle get() = node(hasTextResource(BasePresentation.string.title_welcome_files)) - private val titleSharing get() = node(hasTextResource(BasePresentation.string.title_welcome_sharing)) - private val filesDescription get() = node(hasTextResource(BasePresentation.string.welcome_files_description)) - private val sharingDescription get() = node(hasTextResource(BasePresentation.string.welcome_sharing_description)) - private val welcomeScreen get() = node(hasTestTag(WelcomeScreenTestTag.screen)) + private val skipButton get() = allNodes.withText(CorePresentation.string.presentation_skip).onFirst() + private val nextButton get() = allNodes.withText(BasePresentation.string.common_next_action).onFirst() + private val getStartedButton get() = node.withText(BasePresentation.string.welcome_get_started_action) + private val welcomeLabel get() = node.withText(BasePresentation.string.welcome_to_description) + private val filesTitle get() = node.withText(BasePresentation.string.title_welcome_files) + private val titleSharing get() = node.withText(BasePresentation.string.title_welcome_sharing) + private val filesDescription get() = node.withText(BasePresentation.string.welcome_files_description) + private val sharingDescription get() = node.withText(BasePresentation.string.welcome_sharing_description) + private val welcomeScreen get() = node.withTag(WelcomeScreenTestTag.screen) - fun clickNext() = nextButton.tryToClickAndGoTo(this) - fun clickSkip() = skipButton.tryToClickAndGoTo(FilesTabRobot) - fun clickGetStarted() = getStartedButton.tryToClickAndGoTo(FilesTabRobot) - fun swipeLeft() = welcomeScreen.tryPerformTouchInputAndGoTo(this) { this.swipeLeft() } - fun swipeRight() = welcomeScreen.tryPerformTouchInputAndGoTo(this) { this.swipeRight() } + fun clickNext() = nextButton.clickTo(this) + fun clickSkip() = skipButton.clickTo(FilesTabRobot) + fun clickGetStarted() = getStartedButton.clickTo(FilesTabRobot) + fun swipeLeft() = apply { welcomeScreen.swipe(SwipeDirection.Left) } + fun swipeRight() = apply { welcomeScreen.swipe(SwipeDirection.Right) } override fun robotDisplayed() { - welcomeScreen.assertIsDisplayed() + welcomeScreen.await { assertIsDisplayed() } } } diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/rules/LogoutAllRule.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/rules/LogoutAllRule.kt index 3e1488f3..abd27516 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/rules/LogoutAllRule.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/rules/LogoutAllRule.kt @@ -20,13 +20,17 @@ package me.proton.android.drive.ui.rules import kotlinx.coroutines.runBlocking import me.proton.android.drive.ui.test.BaseTest.Companion.loginTestHelper +import me.proton.android.drive.ui.test.BaseTest.Companion.quark import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement class LogoutAllRule : TestRule { override fun apply(base: Statement, description: Description): Statement { - runBlocking { loginTestHelper.logoutAll() } + runBlocking { + quark.jailUnban() + loginTestHelper.logoutAll() + } return base } } diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/BaseTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/BaseTest.kt index 824d85d2..d5f50904 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/BaseTest.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/BaseTest.kt @@ -19,26 +19,25 @@ package me.proton.android.drive.ui.test import android.app.Application -import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.platform.app.InstrumentationRegistry import dagger.hilt.android.EntryPointAccessors import me.proton.android.drive.test.BuildConfig import me.proton.android.drive.ui.MainActivity import me.proton.android.drive.ui.robot.Robot import me.proton.android.drive.ui.rules.LogoutAllRule +import me.proton.android.drive.ui.toolkits.screenshot import me.proton.core.auth.presentation.testing.ProtonTestEntryPoint import me.proton.core.test.quark.Quark import me.proton.core.test.quark.data.User import me.proton.core.util.kotlin.deserialize import me.proton.core.util.kotlin.deserializeList +import me.proton.test.fusion.FusionConfig import org.junit.Rule import org.junit.rules.RuleChain import org.junit.rules.TestName - -typealias AndroidComposeRule = AndroidComposeTestRule, MainActivity> +import java.util.concurrent.atomic.AtomicInteger open class BaseTest { @Rule @@ -46,17 +45,19 @@ open class BaseTest { val ruleChain: RuleChain = RuleChain.outerRule(testName) @get:Rule - val composeTestRule: AndroidComposeRule = createAndroidComposeRule() + val composeTestRule = createAndroidComposeRule() @get:Rule(order = 0) val logoutAllRule = LogoutAllRule() init { - setGlobalComposeRule(composeTestRule) + FusionConfig.Compose.testRule.set(composeTestRule) + FusionConfig.Compose.useUnmergedTree.set(true) + FusionConfig.Compose.onFailure = { screenshot() } + screenshotCounter.set(0) } - inline fun T. verify(crossinline block: T.() -> Any): T = - apply { waitFor(this) { block() } } + inline fun T.verify(crossinline block: T.() -> Any): T = apply { block() } companion object { private val protonTestEntryPoint by lazy { @@ -73,17 +74,11 @@ open class BaseTest { ) } - private var globalComposeRule: AndroidComposeRule? = null - val loginTestHelper by lazy { protonTestEntryPoint.loginTestHelper } val uiTestHelper by lazy { uiTestEntryPoint.uiTestHelper } val testName = TestName() - val screenshotLocation: String get() = "/sdcard/Pictures" - val composeTestRule: AndroidComposeRule - get() = globalComposeRule - ?: throw AssertionError( - "Compose test rule was not set. Make sure to call setGlobalComposeRule(rule) first" - ) + val screenshotLocation get() = "/sdcard/Pictures/Screenshots/${testName.methodName}/" + val screenshotCounter = AtomicInteger(0) // TODO: before publishing to github, this information should be moved from assets into gitlab vars val users = User.Users(InstrumentationRegistry.getInstrumentation().context @@ -102,9 +97,5 @@ open class BaseTest { .bufferedReader() .use { it.readText() } .deserialize()) - - fun setGlobalComposeRule(rule: AndroidComposeRule) { - globalComposeRule = rule - } } } diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowErrorTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowErrorTest.kt index 7aec11a1..db787b96 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowErrorTest.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowErrorTest.kt @@ -35,7 +35,8 @@ import org.junit.runners.Parameterized class CreatingFolderFlowErrorTest( private val folderName: String, private val errorMessage: String, -): BaseTest() { + private val friendlyName: String +) : BaseTest() { private val user get() = User( @@ -64,11 +65,15 @@ class CreatingFolderFlowErrorTest( } companion object { - @get:Parameterized.Parameters(name = "folderName={0}, errorMessage={2}") + @get:Parameterized.Parameters(name = "{2}") @get:JvmStatic val data = listOf( - arrayOf("folder1", "A file or folder with that name already exists"), - arrayOf(getRandomString(256), StringUtils.stringFromResource(R.string.common_error_name_too_long, 255)), + arrayOf("folder1", "A file or folder with that name already exists", "alreadyExists"), + arrayOf( + getRandomString(256), + StringUtils.stringFromResource(R.string.common_error_name_too_long, 255), + "tooLongFilename" + ), ) } } diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowSuccessTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowSuccessTest.kt index 8145ddc6..bfdb1a52 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowSuccessTest.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowSuccessTest.kt @@ -53,9 +53,12 @@ class CreatingFolderFlowSuccessTest : BaseTest() { val newFolderName = getRandomString() FilesTabRobot - .swipeUpToItemWithName(subFolderName) + .scrollToItemWithName(subFolderName) .clickMoreOnFolder(subFolderName) .clickMove() + .verify { + robotDisplayed() + } .clickAddFolder() .typeFolderName(newFolderName) .clickCreate() @@ -63,10 +66,7 @@ class CreatingFolderFlowSuccessTest : BaseTest() { CreateFolderRobot .dismissSuccessGrowler(newFolderName, MoveToFolderRobot) .verify { - // Workaround (possibly a bug?) - // For some reason does not auto swipe to top on emulators only - swipeDown() - itemListWithTextDisplayed(newFolderName) + itemWithTextDisplayed(newFolderName) } } diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFileSuccessFlowTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFileSuccessFlowTest.kt index 4ff26b3c..476c2a5f 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFileSuccessFlowTest.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFileSuccessFlowTest.kt @@ -54,7 +54,7 @@ class RenamingFileSuccessFlowTest: BaseTest() { val newName = "picture.jpg" FilesTabRobot - .swipeUpToItemWithName(oldName) + .scrollToItemWithName(oldName) .clickOnFile(oldName) .verify { robotDisplayed() diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFlowSuccessTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFlowSuccessTest.kt index d7179faa..c086ee2b 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFlowSuccessTest.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFlowSuccessTest.kt @@ -50,7 +50,7 @@ class RenamingFlowSuccessTest( @Test fun renameSuccess() { FilesTabRobot - .swipeUpToItemWithName(itemToBeRenamed) + .scrollToItemWithName(itemToBeRenamed) .clickMoreOnItem(itemToBeRenamed) .clickRename() .clearName() @@ -62,7 +62,7 @@ class RenamingFlowSuccessTest( } companion object { - @get:Parameterized.Parameters(name = "folderToBeRenamed={0}, newFolderName={1}") + @get:Parameterized.Parameters(name = "folderToBeRenamed={0}_newFolderName={1}") @get:JvmStatic val data = listOf( arrayOf("folder1", "...folder1"), diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFolderFlowErrorTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFolderFlowErrorTest.kt index cea09cec..cc30ba72 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFolderFlowErrorTest.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFolderFlowErrorTest.kt @@ -36,7 +36,8 @@ class RenamingFolderFlowErrorTest( private val itemToBeRenamed: String, private val newItemName: String, private val errorMessage: String, -): BaseTest() { + private val friendlyName: String +) : BaseTest() { private val user get() = User( @@ -53,7 +54,7 @@ class RenamingFolderFlowErrorTest( @Test fun renameError() { FilesTabRobot - .swipeUpToItemWithName(itemToBeRenamed) + .scrollToItemWithName(itemToBeRenamed) .clickMoreOnFolder(itemToBeRenamed) .clickRename() .clearName() @@ -65,13 +66,33 @@ class RenamingFolderFlowErrorTest( } companion object { - @get:Parameterized.Parameters(name = "folderToBeRenamed={0}, newFolderName={1}, errorMessage={2}") + @get:Parameterized.Parameters(name = "{3}") @get:JvmStatic val data = listOf( - arrayOf("folder1", "folder2", "An item with that name already exists in current folder"), - arrayOf("folder1", "", StringUtils.stringFromResource(R.string.common_error_name_is_blank)), - arrayOf("folder1", getRandomString(256), StringUtils.stringFromResource(R.string.common_error_name_too_long, 255)), - arrayOf("folder2", "folder1", "An item with that name already exists in current folder"), + arrayOf( + "folder1", + "folder2", + "An item with that name already exists in current folder", + "Existing folder" + ), + arrayOf( + "folder1", + "", + StringUtils.stringFromResource(R.string.common_error_name_is_blank), + "Empty folder name" + ), + arrayOf( + "folder1", + getRandomString(256), + StringUtils.stringFromResource(R.string.common_error_name_too_long, 255), + "Very long name" + ), + arrayOf( + "folder2", + "folder1", + "An item with that name already exists in current folder", + "Existing folder 2" + ) ) } } diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/toolkits/Helpers.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/toolkits/Helpers.kt index 27e4d781..35219739 100644 --- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/toolkits/Helpers.kt +++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/toolkits/Helpers.kt @@ -18,9 +18,56 @@ package me.proton.android.drive.ui.toolkits +import android.graphics.Bitmap +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.captureToImage +import androidx.compose.ui.test.onFirst +import androidx.test.platform.app.InstrumentationRegistry +import me.proton.android.drive.ui.test.BaseTest +import me.proton.core.test.android.instrumented.ProtonTest +import me.proton.core.util.kotlin.CoreLogger +import me.proton.test.fusion.FusionConfig +import java.io.FileNotFoundException +import java.io.FileOutputStream + fun getRandomString(length: Int = 10): String { val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9').shuffled() return (1..length) .map { allowedChars.random() } .joinToString("") } + +fun BaseTest.Companion.screenshot() { + val screenshotNumber = screenshotCounter.getAndIncrement() + val fileName = "${screenshotLocation}/${screenshotNumber}_${testName.methodName}.png" + + if (screenshotNumber == 0) { + InstrumentationRegistry + .getInstrumentation() + .uiAutomation + .executeShellCommand("mkdir -p $screenshotLocation") + } + + try { + FusionConfig.Compose.testRule.get() + .onAllNodes(SemanticsMatcher("isRoot") { it.isRoot }) + .onFirst() + .captureToImage() + .asAndroidBitmap() + .let { bitmap -> + FileOutputStream(fileName).use { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) + } + } + + } catch (throwable: Throwable) { + when (throwable) { + is FileNotFoundException -> "File not found" + is OutOfMemoryError -> "Out of memory" + else -> "Unknown error" + }.let { + CoreLogger.e(ProtonTest.testTag, throwable, "Could not take screenshot: $it") + } + } +} diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index b8c1bb04..343de68a 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -22,7 +22,7 @@ object Config { const val minSdk = 23 const val targetSdk = 33 const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - const val versionName = "1.0.4" + const val versionName = "1.1.0" const val archivesBaseName = "ProtonDrive-$versionName" val resourceConfigurations = listOf("en") } diff --git a/buildSrc/src/main/kotlin/DeleteTestPlugin.kt b/buildSrc/src/main/kotlin/DeleteTestPlugin.kt index 6f4dd797..3abbdd76 100644 --- a/buildSrc/src/main/kotlin/DeleteTestPlugin.kt +++ b/buildSrc/src/main/kotlin/DeleteTestPlugin.kt @@ -1,7 +1,3 @@ -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.tasks.Delete - /* * Copyright (c) 2023 Proton AG. * This file is part of Proton Drive. @@ -20,6 +16,11 @@ import org.gradle.api.tasks.Delete * along with Proton Drive. If not, see . */ +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.tasks.Delete + + class DeleteTestPlugin : Plugin { override fun apply(project: Project) { @@ -33,4 +34,4 @@ class DeleteTestPlugin : Plugin { ) } } -} \ No newline at end of file +} diff --git a/buildSrc/src/main/kotlin/DriveModule.kt b/buildSrc/src/main/kotlin/DriveModule.kt index 23723e02..df0f8851 100644 --- a/buildSrc/src/main/kotlin/DriveModule.kt +++ b/buildSrc/src/main/kotlin/DriveModule.kt @@ -100,6 +100,15 @@ fun Project.driveModule( extensions.findByType()?.apply { compileSdk = Config.compileSdk + + defaultConfig { + javaCompileOptions { + annotationProcessorOptions { + arguments["room.schemaLocation"] = "$projectDir/schemas" + } + } + } + buildTypes { debug { enableUnitTestCoverage = true @@ -189,7 +198,7 @@ fun Project.driveModule( } if (includeSubmodules) { val fullName = project.fullName - projectDir.findModules().forEach { module -> + projectDir.findModules().filterNot { it.endsWith("-test") }.forEach { module -> add("api", project(":$fullName$module")) } } @@ -202,7 +211,7 @@ fun Project.driveModule( } // first alpha and fourth beta were not tagged in git so we add them to list of all git tags -val Project.tags get() = "1.0.0-alpha01\n1.0.0_cancelled(16)\n1.0.0_cancelled(18)\n1.0.0_cancelled(20)\n1.0.0-beta04\n" + "git tag".runCommand(workingDir = rootDir) +val Project.tags get() = "1.0.0-alpha01\n1.0.0_cancelled(16)\n1.0.0_cancelled(18)\n1.0.0_cancelled(20)\n1.0.0-beta04\n1.0.3_iap(26)\n" + "git tag".runCommand(workingDir = rootDir) val Project.versionCodeFromTags: Int get() = tags.countSubstrings("\n") + 2 // last new line + next tag diff --git a/drive/base/data-test/build.gradle.kts b/drive/base/data-test/build.gradle.kts new file mode 100644 index 00000000..ab13ea8f --- /dev/null +++ b/drive/base/data-test/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 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 . + */ +plugins { + id("com.android.library") +} + +driveModule( + hilt = true, +) { + api(libs.core.domain) +} + +configureJacoco() \ No newline at end of file diff --git a/drive/base/data-test/src/main/AndroidManifest.xml b/drive/base/data-test/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d88b8a21 --- /dev/null +++ b/drive/base/data-test/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + diff --git a/drive/base/data-test/src/main/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManager.kt b/drive/base/data-test/src/main/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManager.kt new file mode 100644 index 00000000..8628766d --- /dev/null +++ b/drive/base/data-test/src/main/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManager.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.base.data.test.manager + +import kotlinx.coroutines.flow.MutableStateFlow +import me.proton.core.domain.arch.DataResult +import me.proton.core.domain.arch.ResponseSource +import me.proton.core.domain.arch.onSuccess +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class StubbedWorkManager @Inject constructor() { + + var behavior: () -> DataResult = BEHAVIOR_SUCCESS + + data class Work( + val name: String, + val data: List + ) + + val works = MutableStateFlow(listOf()) + + fun add(name: String, vararg data: Any): DataResult = add(Work(name, listOf(*data))) + + private fun add(work: Work) = behavior().onSuccess { + works.value = works.value + work + } + + fun execute() { + works.value = emptyList() + } + + companion object { + val BEHAVIOR_SUCCESS = { DataResult.Success(ResponseSource.Local, "") } + val BEHAVIOR_ERROR = { DataResult.Error.Local("behavior_error", null) } + } +} + +fun StubbedWorkManager.assertHasWorks(name: String) { + if (works.value.none { it.name == name }) { + throw AssertionError("Does not contains work: $name in ${works.value.map { it.name }}") + } +} + +fun StubbedWorkManager.assertHasWork(name: String, vararg data: Any) { + val namedWorks = works.value.filter { it.name == name } + if (namedWorks.isEmpty()) { + throw AssertionError("Does not contains work: $name in ${works.value.map { it.name }}") + } + val dataAsList = listOf(*data) + val dataWorks = namedWorks.filter { it.data == dataAsList } + if (dataWorks.isEmpty()) { + throw AssertionError("Does not contains work: $name with the same data, expected:$dataAsList but was: ${namedWorks.map { it.data }}") + } else if (dataWorks.size > 1) { + throw AssertionError("Does contains more then one work: $name with data: $dataAsList") + } +} diff --git a/drive/base/data-test/src/test/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManagerTest.kt b/drive/base/data-test/src/test/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManagerTest.kt new file mode 100644 index 00000000..052f4690 --- /dev/null +++ b/drive/base/data-test/src/test/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManagerTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.base.data.test.manager + +import me.proton.core.domain.arch.DataResult +import me.proton.core.domain.arch.ResponseSource +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test + +class StubbedWorkManagerTest { + private val manager = StubbedWorkManager() + + @Test + fun `Given success behavior When add work without data Then returns success and succeed asserting`() { + manager.behavior = StubbedWorkManager.BEHAVIOR_SUCCESS + + val result = manager.add("name") + + assertEquals(DataResult.Success(ResponseSource.Local, ""), result) + manager.assertHasWorks("name") + } + + @Test + fun `Given success behavior When add work with data Then returns success and succeed asserting`() { + manager.behavior = StubbedWorkManager.BEHAVIOR_SUCCESS + + val result = manager.add("name", "data") + + assertEquals(DataResult.Success(ResponseSource.Local, ""), result) + manager.assertHasWork("name", "data") + } + + @Test + fun `Given error behavior When add work Then returns error and fail asserting`() { + manager.behavior = StubbedWorkManager.BEHAVIOR_ERROR + + val result = manager.add("name") + + assertEquals(DataResult.Error.Local("behavior_error", null), result) + assertThrows(AssertionError::class.java) { + manager.assertHasWorks("name") + } + } + + @Test + fun `Given a work When execute Then have no work`() { + manager.add("name") + + manager.execute() + + assertEquals(emptyList(), manager.works.value) + } + + @Test + fun `Given no work When asserting for works Then fail`() { + val exception = assertThrows(AssertionError::class.java) { + manager.assertHasWorks("name") + } + assertEquals( + "Does not contains work: name in []", + exception.message + ) + } + + @Test + fun `Given work with different name When asserting for work Then fail`() { + manager.add("different-name") + + val exception = assertThrows(AssertionError::class.java) { + manager.assertHasWork("name") + } + + assertEquals( + "Does not contains work: name in [different-name]", + exception.message + ) + } + + @Test + fun `Given two same works When asserting for one work Then fail`() { + manager.add("name", "data") + manager.add("name", "data") + + val exception = assertThrows(AssertionError::class.java) { + manager.assertHasWork("name", "data") + } + + assertEquals( + "Does contains more then one work: name with data: [data]", + exception.message + ) + } + + @Test + fun `Given two same works with different data When asserting for one work Then fail`() { + manager.add("name", "data1") + manager.add("name", "data2") + + manager.assertHasWork("name", "data1") + } + + @Test + fun `Given work with different data When asserting for work Then fail`() { + manager.add("name", "different-data") + + val exception = assertThrows(AssertionError::class.java) { + manager.assertHasWork("name", "data") + } + + assertEquals( + "Does not contains work: name with the same data, expected:[data] but was: [[different-data]]", + exception.message + ) + } +} \ No newline at end of file diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/RunCatchingApiException.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/RunCatchingApiException.kt index 816c9f18..f351b120 100644 --- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/RunCatchingApiException.kt +++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/RunCatchingApiException.kt @@ -17,15 +17,15 @@ */ package me.proton.core.drive.base.data.api -import me.proton.core.data.arch.toDataResult import me.proton.core.domain.arch.DataResult import me.proton.core.domain.arch.ResponseSource +import me.proton.core.drive.base.domain.extension.toDataResult import me.proton.core.network.domain.ApiException inline fun runCatchingApiException(block: () -> T): DataResult { return try { DataResult.Success(ResponseSource.Remote, block()) } catch (e: ApiException) { - e.error.toDataResult() + e.toDataResult() } } diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt index 017d73d1..6e07fbfe 100644 --- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt +++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt @@ -27,6 +27,8 @@ object Column { const val CONTENT_KEY_PACKET_SIGNATURE = "content_key_packet_signature" const val CREATION_TIME = "creation_time" const val CREATOR_EMAIL = "creatior_email" + const val DIGESTS = "digests" + const val DURATION = "duration" const val ENCRYPTED = "encrypted" const val ENCRYPTED_NAME = "encrypted_name" const val ENCRYPTED_SIGNATURE = "encrypted_signature" diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/paging/Flow.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/paging/Flow.kt index 1331403f..12fa8181 100644 --- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/paging/Flow.kt +++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/paging/Flow.kt @@ -22,19 +22,24 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.flow.transformLatest import me.proton.core.drive.base.domain.extension.mapWithPrevious import me.proton.core.drive.base.domain.log.LogTag import me.proton.core.util.kotlin.CoreLogger import kotlin.coroutines.CoroutineContext +import kotlin.math.ceil /** * When using Room's [PagingSource] implementation, if the tables listened on are being updated @@ -74,7 +79,7 @@ fun Flow>>.asPagingSource( val pageList = list .onFailure { throwable -> val error = throwable.cause ?: throwable - CoreLogger.d(LogTag.PAGING, "load (key=$pageKey) from flow failed with $error") + CoreLogger.d(LogTag.PAGING, throwable, "load (key=$pageKey) from flow failed with $error") return LoadResult.Error(error) } .getOrThrow() @@ -93,7 +98,7 @@ fun Flow>>.asPagingSource( nextKey = nextKey, ) } catch (e: Throwable) { - CoreLogger.d(LogTag.PAGING, "load (key=$pageKey) from flow failed with ${e.cause ?: e}") + CoreLogger.d(LogTag.PAGING, e, "load (key=$pageKey) from flow failed with ${e.cause ?: e}") LoadResult.Invalid() } } @@ -103,3 +108,117 @@ object PagingSourceScope : CoroutineScope { override val coroutineContext: CoroutineContext get() = Dispatchers.IO + Job() } + +@OptIn(ExperimentalCoroutinesApi::class) +fun ((fromIndex: Int, count: Int) -> Flow>>).asPagingSource( + sourceSize: Flow, + observablePageSize: Int, + stopOnFailure: Boolean = true, + processPage: (suspend (List) -> List)? = null, +): PagingSource = + object : PagingSource() { + + private val itemsCount: StateFlow = sourceSize + .takeWhile { invalid.not() } + .distinctUntilChanged() + .mapWithPrevious { previous, current -> + if (previous != null) invalidate() + current + } + .stateIn(PagingSourceScope, SharingStarted.Eagerly, null) + + private val fromIndex = MutableStateFlow(null) + + private val listFlow: StateFlow>?> = fromIndex + .takeWhile { invalid.not() } + .filterNotNull() + .transformLatest { fromIndex -> + emitAll( + this@asPagingSource(fromIndex, observablePageSize) + .takeWhile { invalid.not() } + .distinctUntilChanged() + .mapWithPrevious { previous, current -> + if (previous != null && !(previous.isFailure && stopOnFailure)) { + invalidate() + } + current + } + ) + } + .stateIn(PagingSourceScope, SharingStarted.Eagerly, null) + + override fun getRefreshKey(state: PagingState): Int? = + state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + + override suspend fun load(params: LoadParams): LoadResult { + require(observablePageSize >= params.loadSize * 4) { + "Observable page size ($observablePageSize) must be at least 4 times as big as load page size (${params.loadSize})" + } + val pageKey = params.key ?: 0 + val items = itemsCount.filterNotNull().first() + val pageRange = rangeFromPage(pageKey, params.loadSize, items) + val currentIndex = fromIndex.value ?: findIndexForRange(pageRange, observablePageSize, items) + fromIndex.value = currentIndex + val list = listFlow.filterNotNull().first() + return try { + val pageList = list + .onFailure { throwable -> + val error = throwable.cause ?: throwable + CoreLogger.d(LogTag.PAGING, throwable, "load (key=$pageKey) from flow failed with $error") + return LoadResult.Error(error) + } + .getOrThrow() + val page = pageList.subList(pageRange.first - currentIndex, pageRange.last - currentIndex + 1) + val prevKey = (pageKey - 1).takeIf { key -> key >= 0 } + val nextKey = (pageKey + 1).takeIf { key -> key < ceil(items / params.loadSize.toDouble()).toInt() } + CoreLogger.d( + tag = LogTag.PAGING, + message = "load (key=$pageKey, items=${page.size}) from flow (items=${pageList.size}) nextKey=$nextKey prevKey=$prevKey" + ) + LoadResult.Page( + data = processPage?.invoke(page) ?: page, + prevKey = prevKey, + nextKey = nextKey, + ).also { + val rangeDirection = rangeDirection(items, currentIndex, observablePageSize, pageRange) + if (rangeDirection != RangeDirection.CURRENT) { + invalidate() + } + } + } catch (e: Throwable) { + CoreLogger.d(LogTag.PAGING, e, "load (key=$pageKey) from flow failed with ${e.cause ?: e}") + LoadResult.Invalid() + } + } + + private fun rangeFromPage(pageIndex: Int, pageSize: Int, itemsCount: Int): IntRange = + IntRange(pageIndex * pageSize, minOf(((pageIndex + 1) * pageSize) - 1, itemsCount - 1)) + + private fun rangeDirection(itemsCount: Int, fromIndex: Int, observablePageSize: Int, pageRange: IntRange): RangeDirection { + val hasPrevious = fromIndex > 0 + val hasNext = fromIndex + observablePageSize < itemsCount + val pageSize = pageRange.last - pageRange.first + 1 + return when { + !hasPrevious && !hasNext -> RangeDirection.CURRENT + hasPrevious && pageRange.first - pageSize <= fromIndex -> RangeDirection.PREVIOUS + hasNext && pageRange.last + pageSize >= fromIndex + observablePageSize -> RangeDirection.NEXT + else -> RangeDirection.CURRENT + } + } + + private fun findIndexForRange(pageRange: IntRange, observablePageSize: Int, itemsCount: Int): Int = when { + itemsCount <= observablePageSize -> 0 + pageRange.first <= observablePageSize / 2 -> 0 + itemsCount - pageRange.last <= observablePageSize / 2 -> itemsCount - observablePageSize + else -> pageRange.first - observablePageSize / 2 + } + } + +private enum class RangeDirection { + CURRENT, + PREVIOUS, + NEXT, +} diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/di/BaseBindModule.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/di/BaseBindModule.kt index d44a22e5..5ce3bda0 100644 --- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/di/BaseBindModule.kt +++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/di/BaseBindModule.kt @@ -21,17 +21,13 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import dagger.multibindings.IntoSet import me.proton.core.drive.base.data.formatter.DateTimeFormatterImpl -import me.proton.core.drive.base.data.provider.ExifImageResolutionProvider -import me.proton.core.drive.base.data.provider.MetadataRetrieverVideoResolutionProvider -import me.proton.core.drive.base.data.provider.MimeTypeProviderImpl import me.proton.core.drive.base.data.usecase.CopyToClipboardImpl +import me.proton.core.drive.base.data.usecase.GetMemoryInfoImpl import me.proton.core.drive.base.data.usecase.Sha256Impl import me.proton.core.drive.base.domain.formatter.DateTimeFormatter -import me.proton.core.drive.base.domain.provider.MediaResolutionProvider -import me.proton.core.drive.base.domain.provider.MimeTypeProvider import me.proton.core.drive.base.domain.usecase.CopyToClipboard +import me.proton.core.drive.base.domain.usecase.GetMemoryInfo import me.proton.core.drive.base.domain.usecase.Sha256 import javax.inject.Singleton @@ -50,4 +46,8 @@ interface BaseBindModule { @Binds @Singleton fun bindsSha256Impl(impl: Sha256Impl): Sha256 + + @Binds + @Singleton + fun bindsGetMemoryInfoImpl(impl: GetMemoryInfoImpl): GetMemoryInfo } diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/usecase/GetMemoryInfoImpl.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/usecase/GetMemoryInfoImpl.kt new file mode 100644 index 00000000..b0707893 --- /dev/null +++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/usecase/GetMemoryInfoImpl.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.base.data.usecase + +import android.app.ActivityManager +import me.proton.core.drive.base.domain.entity.MemoryInfo +import me.proton.core.drive.base.domain.extension.bytes +import me.proton.core.drive.base.domain.usecase.GetMemoryInfo +import javax.inject.Inject + +class GetMemoryInfoImpl @Inject constructor( + private val activityManager: ActivityManager, +) : GetMemoryInfo { + override operator fun invoke(): Result = runCatching { + with( + ActivityManager.MemoryInfo().also { memoryInfo -> + activityManager.getMemoryInfo(memoryInfo) + } + ) { + MemoryInfo( + isLowOnMemory = lowMemory, + memoryClass = (activityManager.memoryClass * 1024 * 2024).bytes + ) + } + } +} diff --git a/drive/base/data/src/test/kotlin/me/proton/core/drive/base/data/db/paging/FlowTest.kt b/drive/base/data/src/test/kotlin/me/proton/core/drive/base/data/db/paging/FlowTest.kt new file mode 100644 index 00000000..cf6a0b15 --- /dev/null +++ b/drive/base/data/src/test/kotlin/me/proton/core/drive/base/data/db/paging/FlowTest.kt @@ -0,0 +1,252 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.base.data.db.paging + +import androidx.paging.PagingSource +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.test.runTest +import me.proton.core.test.kotlin.assertEquals +import org.junit.Assert +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class PagingSourceTest { + private val backingData = listOf( + "first", + "second", + "third", + "fourth", + "fifth", + "sixth", + "seventh", + "eighth", + "ninth", + "tenth", + "eleventh", + "twelfth", + "thirteenth", + "fourteenth", + "fifteenth", + ) + private val source = MutableStateFlow(backingData) + private fun flowFromBackingData(fromIndex: Int, count: Int): Flow>> = flow { + val source = this@PagingSourceTest.source.value + emit( + when { + fromIndex < 0 || fromIndex >= source.count() -> Result.failure(IllegalArgumentException()) + else -> Result.success(source.subList(fromIndex, (fromIndex + count).coerceIn(0, source.size))) + } + ) + } + private fun pagingSource(observablePageSize: Int = DEFAULT_OBSERVABLE_PAGE_SIZE_S) = { fromIndex: Int, count: Int -> + flowFromBackingData(fromIndex, count) + }.asPagingSource( + sourceSize = source.map { backingData -> backingData.size }, + observablePageSize = observablePageSize, + ) + + @Test + fun `refresh paging list when observable page size is bigger then underlying data`() = runTest { + val pagingSource = pagingSource(maxOf(DEFAULT_OBSERVABLE_PAGE_SIZE_L, backingData.size)) + assertEquals( + expected = PagingSource.LoadResult.Page( + data = listOf("first", "second", "third"), + prevKey = null, + nextKey = 1, + ), + actual = pagingSource.load( + PagingSource.LoadParams.Refresh( + key = null, + loadSize = LOAD_SIZE_M, + placeholdersEnabled = false, + ) + ), + message = { "" }, + ) + assertEquals( + expected = PagingSource.LoadResult.Page( + data = listOf("thirteenth", "fourteenth", "fifteenth"), + prevKey = 1, + nextKey = null, + ), + actual = pagingSource.load( + PagingSource.LoadParams.Refresh( + key = 2, + loadSize = LOAD_SIZE_L, + placeholdersEnabled = false, + ) + ), + message = { "" }, + ) + } + + @Test + fun `refresh paging list`() = runTest { + val pagingSource = pagingSource(DEFAULT_OBSERVABLE_PAGE_SIZE_M) + assertEquals( + expected = PagingSource.LoadResult.Page( + data = listOf("first", "second"), + prevKey = null, + nextKey = 1, + ), + actual = pagingSource.load( + PagingSource.LoadParams.Refresh( + key = null, + loadSize = LOAD_SIZE_S, + placeholdersEnabled = false, + ) + ), + message = { "" }, + ) + assertEquals( + expected = PagingSource.LoadResult.Page( + data = listOf("first", "second", "third"), + prevKey = null, + nextKey = 1, + ), + actual = pagingSource.load( + PagingSource.LoadParams.Refresh( + key = null, + loadSize = LOAD_SIZE_M, + placeholdersEnabled = false, + ) + ), + message = { "" }, + ) + } + + @Test + fun `append list with second page`() = runTest { + assertEquals( + expected = PagingSource.LoadResult.Page( + data = listOf("third", "fourth"), + prevKey = 0, + nextKey = 2, + ), + actual = pagingSource().load( + PagingSource.LoadParams.Append( + key = 1, + loadSize = LOAD_SIZE_S, + placeholdersEnabled = false, + ) + ), + message = { "" }, + ) + } + + @Test + fun `append middle pages`() = runTest { + val pagingSource = pagingSource(DEFAULT_OBSERVABLE_PAGE_SIZE_M) + assertEquals( + expected = PagingSource.LoadResult.Page( + data = listOf("seventh", "eighth", "ninth"), + prevKey = 1, + nextKey = 3, + ), + actual = pagingSource.load( + PagingSource.LoadParams.Append( + key = 2, + loadSize = LOAD_SIZE_M, + placeholdersEnabled = false, + ) + ), + message = { "" }, + ) + assertEquals( + expected = PagingSource.LoadResult.Page( + data = listOf("tenth", "eleventh", "twelfth"), + prevKey = 2, + nextKey = 4, + ), + actual = pagingSource.load( + PagingSource.LoadParams.Append( + key = 3, + loadSize = LOAD_SIZE_M, + placeholdersEnabled = false, + ) + ), + message = { "" }, + ) + } + + @Test + fun `append list with last page`() = runTest { + assertEquals( + expected = PagingSource.LoadResult.Page( + data = listOf("fifteenth"), + prevKey = 6, + nextKey = null, + ), + actual = pagingSource().load( + PagingSource.LoadParams.Append( + key = 7, + loadSize = LOAD_SIZE_S, + placeholdersEnabled = false, + ) + ), + message = { "" }, + ) + } + + @Test + fun `prepend list with third page`() = runTest { + assertEquals( + expected = PagingSource.LoadResult.Page( + data = listOf("fifth", "sixth"), + prevKey = 1, + nextKey = 3, + ), + actual = pagingSource().load( + PagingSource.LoadParams.Prepend( + key = 2, + loadSize = LOAD_SIZE_S, + placeholdersEnabled = false, + ) + ), + message = { "" }, + ) + } + + @Test + fun `load page size must be less then observablePageSize`() { + Assert.assertThrows(IllegalArgumentException::class.java) { + runTest { + pagingSource(DEFAULT_OBSERVABLE_PAGE_SIZE_S).load( + PagingSource.LoadParams.Refresh( + key = null, + loadSize = DEFAULT_OBSERVABLE_PAGE_SIZE_S, + placeholdersEnabled = false, + ) + ) + } + } + } + + companion object { + const val LOAD_SIZE_S = 2 + const val LOAD_SIZE_M = 3 + const val LOAD_SIZE_L = 6 + const val DEFAULT_OBSERVABLE_PAGE_SIZE_S = LOAD_SIZE_S * 4 + const val DEFAULT_OBSERVABLE_PAGE_SIZE_M = LOAD_SIZE_M * 4 + const val DEFAULT_OBSERVABLE_PAGE_SIZE_L = LOAD_SIZE_L * 4 + } +} diff --git a/drive/base/domain/build.gradle.kts b/drive/base/domain/build.gradle.kts index 92251e83..59f67c2c 100644 --- a/drive/base/domain/build.gradle.kts +++ b/drive/base/domain/build.gradle.kts @@ -26,12 +26,12 @@ driveModule( api(project(":drive:message-queue:domain")) api(libs.core.cryptoCommon) api(libs.core.domain) - api(libs.core.user) + api(libs.core.user.domain) - implementation(libs.core.accountManager) + implementation(libs.core.accountManager.domain) implementation(libs.core.data) - implementation(libs.core.key) - implementation(libs.core.network) + implementation(libs.core.key.domain) + implementation(libs.core.network.domain) } configureJacoco() diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/entity/MemoryInfo.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/entity/MemoryInfo.kt new file mode 100644 index 00000000..ac0a877f --- /dev/null +++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/entity/MemoryInfo.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.base.domain.entity + +data class MemoryInfo( + val isLowOnMemory: Boolean, + val memoryClass: Bytes, +) diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/ApiException.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/ApiException.kt new file mode 100644 index 00000000..a5932e33 --- /dev/null +++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/ApiException.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.base.domain.extension + +import me.proton.core.domain.arch.DataResult +import me.proton.core.network.domain.ApiException +import me.proton.core.network.domain.ApiResult +import me.proton.core.util.kotlin.exhaustive + +fun ApiException.toDataResult() : DataResult.Error = when (val e = error) { + is ApiResult.Error.Http -> { + DataResult.Error.Remote( + message = e.proton?.error ?: message, + cause = this, + protonCode = e.proton?.code ?: 0, + httpCode = e.httpCode + ) + } + is ApiResult.Error.Parse -> DataResult.Error.Remote(cause?.message, this) + is ApiResult.Error.Connection -> DataResult.Error.Remote(cause?.message, this) +}.exhaustive diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/Bytes.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/Bytes.kt index 98d36e6b..ef3631b7 100644 --- a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/Bytes.kt +++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/Bytes.kt @@ -20,6 +20,7 @@ package me.proton.core.drive.base.domain.extension import me.proton.core.drive.base.domain.entity.Bytes import java.io.File +inline val Int.GiB: Bytes get() = (this * 1_073_741_824L).bytes inline val Int.MiB: Bytes get() = (this * 1_048_576L).bytes inline val Int.KiB: Bytes get() = (this * 1_024L).bytes diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/provider/ConfigurationProvider.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/provider/ConfigurationProvider.kt index 4eed3f71..6011ecc9 100644 --- a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/provider/ConfigurationProvider.kt +++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/provider/ConfigurationProvider.kt @@ -23,6 +23,8 @@ import me.proton.core.drive.base.domain.extension.MiB import me.proton.core.drive.base.domain.extension.bytes import kotlin.time.Duration import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds interface ConfigurationProvider { val host: String @@ -30,6 +32,7 @@ interface ConfigurationProvider { val appVersionHeader: String val uiPageSize: Int get() = 50 val apiPageSize: Int get() = 150 + val dbPageSize: Int get() = 500 val cacheMaxEntries: Int get() = 10_000 val linkMaxNameLength: Int get() = 255 val blockMaxSize: Bytes get() = 4.MiB @@ -49,4 +52,8 @@ interface ConfigurationProvider { val validateUploadLimit: Boolean get() = true val uploadLimitThreshold: Int get() = 250 val useExceptionMessage: Boolean get() = false + val digestAlgorithms: List get() = listOf("SHA1") + val autoLockDurations: Set get() = setOf( + 0.seconds, 60.seconds, 2.minutes, 5.minutes, 15.minutes, 30.minutes + ) } diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/repository/Fetcher.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/repository/Fetcher.kt index 649e5668..af28a865 100644 --- a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/repository/Fetcher.kt +++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/repository/Fetcher.kt @@ -19,9 +19,9 @@ package me.proton.core.drive.base.domain.repository import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.FlowCollector -import me.proton.core.data.arch.toDataResult import me.proton.core.domain.arch.DataResult import me.proton.core.domain.arch.ResponseSource +import me.proton.core.drive.base.domain.extension.toDataResult import me.proton.core.network.domain.ApiException suspend inline fun FlowCollector>.fetcher(fetchAction: () -> Unit) { @@ -31,7 +31,7 @@ suspend inline fun FlowCollector>.fetcher(fetchAction: () -> U } catch (e: CancellationException) { throw e } catch (e: ApiException) { - emit(e.error.toDataResult()) + emit(e.toDataResult()) } catch (e: RuntimeException) { emit(DataResult.Error.Local(e.message, e)) } @@ -44,7 +44,7 @@ suspend inline fun FlowCollector>>.listFetcherEmitOnEmpty } catch (e: CancellationException) { throw e } catch (e: ApiException) { - emit(e.error.toDataResult()) + emit(e.toDataResult()) } catch (e: RuntimeException) { emit(DataResult.Error.Local(e.message, e)) } diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/usecase/GetMemoryInfo.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/usecase/GetMemoryInfo.kt new file mode 100644 index 00000000..ad73d71a --- /dev/null +++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/usecase/GetMemoryInfo.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.base.domain.usecase + +import me.proton.core.drive.base.domain.entity.MemoryInfo + +interface GetMemoryInfo { + operator fun invoke(): Result +} diff --git a/drive/base/domain/src/test/kotlin/me/proton/core/drive/base/domain/repository/FetcherTest.kt b/drive/base/domain/src/test/kotlin/me/proton/core/drive/base/domain/repository/FetcherTest.kt new file mode 100644 index 00000000..61cb98d9 --- /dev/null +++ b/drive/base/domain/src/test/kotlin/me/proton/core/drive/base/domain/repository/FetcherTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.base.domain.repository + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import me.proton.core.domain.arch.DataResult +import me.proton.core.domain.arch.ResponseSource +import me.proton.core.network.domain.ApiException +import me.proton.core.network.domain.ApiResult +import org.junit.Assert.* +import org.junit.Test +import java.net.UnknownHostException + +@OptIn(ExperimentalCoroutinesApi::class) +class FetcherTest { + + @Test + fun `Given success When fetch Then emit value`() = runTest { + val values = flow> { + fetcher { + emit(DataResult.Success(ResponseSource.Local, Unit)) + } + }.toList() + assertEquals(DataResult.Processing(ResponseSource.Remote), values[0]) + assertEquals(DataResult.Success(ResponseSource.Local, Unit), values[1]) + } + + @Test + fun `Given api exception When fetch Then emit error with api exception`() = runTest { + val errorMessage = "Unable to resolve host" + val value = flow> { + fetcher { + throw ApiException( + ApiResult.Error.Connection( + cause = UnknownHostException(errorMessage) + ) + ) + } + }.last() + + val error = value as DataResult.Error + assertEquals(errorMessage, error.message) + assertEquals(ResponseSource.Remote, error.source) + assertEquals(ApiException::class.java, error.cause?.javaClass) + } + + @Test + fun `Given runtime exception When fetch Then emit error with runtime exception`() = runTest { + val errorMessage = "error" + val value = flow> { + fetcher { + throw RuntimeException(errorMessage) + } + }.last() + + val error = value as DataResult.Error + assertEquals(errorMessage, error.message) + assertEquals(ResponseSource.Local, error.source) + assertEquals(RuntimeException::class.java, error.cause?.javaClass) + } + + @Test + fun `Given cancellation exception When fetch Then emit error with runtime exception`() { + val flow = flow> { + fetcher { + throw CancellationException() + } + } + assertThrows(CancellationException::class.java) { + runTest { flow.last() } + } + } +} \ No newline at end of file diff --git a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/OutlinedTextFieldWithError.kt b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/OutlinedTextFieldWithError.kt index 1c4cae51..d789012a 100644 --- a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/OutlinedTextFieldWithError.kt +++ b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/OutlinedTextFieldWithError.kt @@ -22,79 +22,16 @@ import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import me.proton.core.compose.theme.ProtonTheme import me.proton.core.compose.theme.caption import me.proton.core.compose.theme.default -@Composable -fun OutlinedTextFieldWithError( - text: String, - modifier: Modifier = Modifier, - selection: IntRange = IntRange(text.length, text.length), - errorText: String? = null, - focusRequester: FocusRequester = remember { FocusRequester() }, - maxLines: Int = MaxLines, - onValueChanged: (String) -> Unit, -) { - // This code is based on BasicTextField:122 - // Holds the latest internal TextFieldValue state. We need to keep it to have the correct value - // of the composition. - var textFieldValueState by remember { - mutableStateOf( - TextFieldValue( - text = text, - selection = TextRange(selection.first, selection.last) - ) - ) - } - // Holds the latest TextFieldValue that OutlinedTextFieldWithError was recomposed with. - // We couldn't simply pass `TextFieldValue(text = text, selection=[..])` to the CoreTextField - // because we need to preserve the composition. - val textFieldValue = textFieldValueState.copy( - text = text, - selection = TextRange(selection.first, selection.last), - ) - // Last String value that either text field was recomposed with or updated in the onValueChange - // callback. We keep track of it to prevent calling onValueChange(String) for same String when - // CoreTextField's onValueChange is called multiple times without recomposition in between. - var lastTextValue by remember(text) { mutableStateOf(text) } - - SideEffect { - if (textFieldValue.selection != textFieldValueState.selection || - textFieldValue.composition != textFieldValueState.composition - ) { - textFieldValueState = textFieldValue - } - } - OutlinedTextFieldWithError( - textFieldValue = textFieldValue, - modifier = modifier, - errorText = errorText, - focusRequester = focusRequester, - maxLines = maxLines, - ) { newTextFieldValueState -> - textFieldValueState = newTextFieldValueState - - val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text - lastTextValue = newTextFieldValueState.text - - if (stringChangedSinceLastInvocation) { - onValueChanged(newTextFieldValueState.text) - } - } -} - @Composable fun OutlinedTextFieldWithError( textFieldValue: TextFieldValue, diff --git a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/ProtonListItem.kt b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/ProtonListItem.kt index 11c6bed2..c21a433e 100644 --- a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/ProtonListItem.kt +++ b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/ProtonListItem.kt @@ -50,7 +50,8 @@ fun ProtonListItem( @StringRes title: Int, modifier: Modifier = Modifier, iconTitlePadding: Dp = ListItemTextStartPadding, -) = ProtonListItem(painterResource(icon), stringResource(title), modifier, iconTitlePadding) + iconTintColor: Color = ProtonTheme.colors.iconNorm, +) = ProtonListItem(painterResource(icon), stringResource(title), modifier, iconTitlePadding, iconTintColor) @Composable fun ProtonListItem( @@ -58,6 +59,7 @@ fun ProtonListItem( title: String, modifier: Modifier = Modifier, iconTitlePadding: Dp = ListItemTextStartPadding, + iconTintColor: Color = ProtonTheme.colors.iconNorm, ) { Row( modifier = modifier @@ -72,7 +74,7 @@ fun ProtonListItem( modifier = Modifier .size(DefaultIconSize), painter = icon, - tint = ProtonTheme.colors.iconNorm, + tint = iconTintColor, contentDescription = null, ) Text( diff --git a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/checkbox/CheckboxDefaults.kt b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Composable.kt similarity index 53% rename from drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/checkbox/CheckboxDefaults.kt rename to drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Composable.kt index 7ca0a108..902c5296 100644 --- a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/checkbox/CheckboxDefaults.kt +++ b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Composable.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023 Proton AG. + * Copyright (c) 2023 Proton AG. * This file is part of Proton Core. * * Proton Core is free software: you can redistribute it and/or modify @@ -15,20 +15,15 @@ * You should have received a copy of the GNU General Public License * along with Proton Core. If not, see . */ -package me.proton.core.drive.base.presentation.component -import androidx.compose.material.CheckboxColors -import androidx.compose.material.CheckboxDefaults +package me.proton.core.drive.base.presentation.extension + +import android.content.res.Configuration import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import me.proton.core.compose.theme.ProtonTheme +import androidx.compose.ui.platform.LocalConfiguration -@Composable -fun CheckboxDefaults.protonColors(): CheckboxColors = - colors( - checkedColor = ProtonTheme.colors.interactionNorm, - uncheckedColor = ProtonTheme.colors.shade60, - checkmarkColor = Color.White, - disabledColor = ProtonTheme.colors.shade40, - disabledIndeterminateColor = ProtonTheme.colors.shade40, - ) +val isPortrait: Boolean @Composable get() = + LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT + +val isLandscape: Boolean @Composable get() = + LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE diff --git a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Modifier.kt b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Modifier.kt index 7dd8e081..519b2856 100644 --- a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Modifier.kt +++ b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Modifier.kt @@ -18,6 +18,13 @@ package me.proton.core.drive.base.presentation.extension import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp fun Modifier.conditional(condition: Boolean, block: Modifier.() -> Modifier) = if (condition) { @@ -25,3 +32,36 @@ fun Modifier.conditional(condition: Boolean, block: Modifier.() -> Modifier) = } else { this } + +fun Modifier.shadow( + color: Color = Color.Black, + alpha: Float = 1f, + cornersRadius: Dp = 0.dp, + blurRadius: Dp = 0.dp, + offsetX: Dp = 0.dp, + offsetY: Dp = 0.dp, +) = drawBehind { + val shadow = color.copy(alpha = alpha).toArgb() + val transparent = color.copy(alpha = 0f).toArgb() + + drawIntoCanvas { canvas -> + val paint = Paint() + val frameworkPaint = paint.asFrameworkPaint() + frameworkPaint.color = transparent + frameworkPaint.setShadowLayer( + blurRadius.toPx(), + offsetX.toPx(), + offsetY.toPx(), + shadow, + ) + canvas.drawRoundRect( + left = 0f, + top = 0f, + right = size.width, + bottom = size.height, + radiusX = cornersRadius.toPx(), + radiusY = cornersRadius.toPx(), + paint = paint, + ) + } +} diff --git a/drive/base/presentation/src/main/res/drawable/ic_checkmark_circle_filled.xml b/drive/base/presentation/src/main/res/drawable/ic_checkmark_circle_filled.xml new file mode 100644 index 00000000..c10a83f0 --- /dev/null +++ b/drive/base/presentation/src/main/res/drawable/ic_checkmark_circle_filled.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/drive/base/presentation/src/main/res/drawable/ic_proton_lock_open.xml b/drive/base/presentation/src/main/res/drawable/ic_proton_lock_open.xml new file mode 100644 index 00000000..a63cff7f --- /dev/null +++ b/drive/base/presentation/src/main/res/drawable/ic_proton_lock_open.xml @@ -0,0 +1,13 @@ + + + + diff --git a/drive/base/presentation/src/main/res/values/strings.xml b/drive/base/presentation/src/main/res/values/strings.xml index 9c95a93f..c972c990 100644 --- a/drive/base/presentation/src/main/res/values/strings.xml +++ b/drive/base/presentation/src/main/res/values/strings.xml @@ -424,4 +424,24 @@ %1$d item %1$d items + + + App locked + Unlock using your device lock + Confirmation + To enable unlock, confirm with your device lock + To disable unlock, confirm with your device lock + Enable biometrics or device lock + Keep unlocked + Biometrics or device lock + Turn on biometrics access + Set up biometrics or device lock access on this device. + Settings + Biometrics or device lock is not available on your device + Biometrics authentication failed + Biometrics or device lock access activated + App lock has been deactivated + Choose how to lock the app + Unlock the app + diff --git a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/repository/BlockRepositoryImpl.kt b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/repository/BlockRepositoryImpl.kt index a2af1c7f..62a9b3e9 100644 --- a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/repository/BlockRepositoryImpl.kt +++ b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/repository/BlockRepositoryImpl.kt @@ -30,7 +30,7 @@ import javax.inject.Inject class BlockRepositoryImpl @Inject constructor( private val api: BlockApiDataSource, -): BlockRepository { +) : BlockRepository { override suspend fun getUploadBlocksUrl( userId: UserId, addressId: AddressId, @@ -39,13 +39,17 @@ class BlockRepositoryImpl @Inject constructor( uploadBlocks: List, uploadThumbnail: UploadBlock?, ): Result = coRunCatching { - api.uploadBlock( - userId = userId, - addressId = addressId, - fileId = fileId, - revisionId = revisionId, - uploadBlocks = uploadBlocks, - uploadThumbnail = uploadThumbnail, - ).toUploadBlocksUrl() + if (uploadThumbnail != null || uploadBlocks.isNotEmpty()) { + api.uploadBlock( + userId = userId, + addressId = addressId, + fileId = fileId, + revisionId = revisionId, + uploadBlocks = uploadBlocks, + uploadThumbnail = uploadThumbnail, + ).toUploadBlocksUrl() + } else { + UploadBlocksUrl(emptyList(), null) + } } } diff --git a/drive/crypto-base/domain/build.gradle.kts b/drive/crypto-base/domain/build.gradle.kts index 38028a20..6be398db 100644 --- a/drive/crypto-base/domain/build.gradle.kts +++ b/drive/crypto-base/domain/build.gradle.kts @@ -21,7 +21,7 @@ plugins { driveModule(hilt = true) { api(project(":drive:base:domain")) - api(libs.core.auth) - api(libs.core.crypto) - api(libs.core.key) + api(libs.core.auth.domain) + api(libs.core.cryptoCommon) + api(libs.core.key.domain) } diff --git a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/extension/UserAddressRepository.kt b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/extension/UserAddressRepository.kt index d6de0429..d426746b 100644 --- a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/extension/UserAddressRepository.kt +++ b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/extension/UserAddressRepository.kt @@ -24,16 +24,16 @@ import me.proton.core.user.domain.entity.AddressId import me.proton.core.user.domain.repository.UserAddressRepository suspend fun UserAddressRepository.getAddressKeys(userId: UserId, email: String): KeyHolder = - getAddresses(userId) - .first { userAddress -> userAddress.email == email } - .keys - .keyHolder() + with (getAddresses(userId)) { + firstOrNull { userAddress -> userAddress.email == email }?.keys?.keyHolder() + ?: flatMap { userAddress -> userAddress.keys }.keyHolder() + } suspend fun UserAddressRepository.getAddressKeys(userId: UserId, addressId: AddressId): KeyHolder = - getAddresses(userId) - .first { userAddress -> userAddress.addressId == addressId } - .keys - .keyHolder() + with (getAddresses(userId)) { + firstOrNull { userAddress -> userAddress.addressId == addressId }?.keys?.keyHolder() + ?: flatMap { userAddress -> userAddress.keys }.keyHolder() + } fun List.keyHolder() = object : KeyHolder { override val keys: List = this@keyHolder diff --git a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/DecryptNestedPrivateKey.kt b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/DecryptNestedPrivateKey.kt index a0686e76..bcc32799 100644 --- a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/DecryptNestedPrivateKey.kt +++ b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/DecryptNestedPrivateKey.kt @@ -17,6 +17,7 @@ */ package me.proton.core.drive.cryptobase.domain.usecase +import kotlinx.coroutines.withContext import me.proton.core.crypto.common.context.CryptoContext import me.proton.core.domain.entity.UserId import me.proton.core.drive.base.domain.util.coRunCatching @@ -70,12 +71,14 @@ class DecryptNestedPrivateKey @Inject constructor( signatureAddress: String, allowCompromisedVerificationKeys: Boolean = false, coroutineContext: CoroutineContext = CryptoScope.EncryptAndDecrypt.coroutineContext, - ): Result = + ): Result = withContext(coroutineContext) { invoke( decryptKey = decryptKey, key = key, - verifyKeyRing = userAddressRepository.getAddressKeys(userId, signatureAddress).publicKeyRing(cryptoContext), + verifyKeyRing = userAddressRepository.getAddressKeys(userId, signatureAddress) + .publicKeyRing(cryptoContext), allowCompromisedVerificationKeys = allowCompromisedVerificationKeys, coroutineContext = coroutineContext, ) + } } diff --git a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/GenerateSrpForShareUrl.kt b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/GenerateSrpForShareUrl.kt index 6aedea52..068904f1 100644 --- a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/GenerateSrpForShareUrl.kt +++ b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/GenerateSrpForShareUrl.kt @@ -19,16 +19,20 @@ package me.proton.core.drive.cryptobase.domain.usecase import me.proton.core.auth.domain.repository.AuthRepository import me.proton.core.crypto.common.context.CryptoContext +import me.proton.core.domain.entity.UserId import me.proton.core.drive.base.domain.util.coRunCatching import me.proton.core.drive.cryptobase.domain.entity.SrpForShareUrl +import me.proton.core.network.domain.session.SessionProvider import javax.inject.Inject class GenerateSrpForShareUrl @Inject constructor( private val cryptoContext: CryptoContext, private val authRepository: AuthRepository, + private val sessionProvider: SessionProvider, ) { - suspend operator fun invoke(urlPassword: ByteArray) = coRunCatching { - val modulus = authRepository.randomModulus() + suspend operator fun invoke(userId: UserId, urlPassword: ByteArray) = coRunCatching { + val sessionId = sessionProvider.getSessionId(userId) + val modulus = authRepository.randomModulus(sessionId) val auth = cryptoContext.srpCrypto.calculatePasswordVerifier( username = "", password = urlPassword, diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlCustomPasswordInfo.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlCustomPasswordInfo.kt index 97eb9920..9059203c 100644 --- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlCustomPasswordInfo.kt +++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlCustomPasswordInfo.kt @@ -53,7 +53,7 @@ class CreateShareUrlCustomPasswordInfo @Inject constructor( .getOrThrow() .take(RANDOM_URL_PASSWORD_SIZE) val urlPassword = "$randomPassword$customPassword" - val srpForShareUrl = generateSrpForShareUrl(urlPassword.toByteArray()).getOrThrow() + val srpForShareUrl = generateSrpForShareUrl(userId, urlPassword.toByteArray()).getOrThrow() val addressKeys = getAddressKeys(userId, addressId) val reencryptedSharePassphrase = reencryptSharePassphraseWithUrlPassword( decryptKey = addressKeys.keyHolder, diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlInfo.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlInfo.kt index d842d9bc..8d289183 100644 --- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlInfo.kt +++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlInfo.kt @@ -46,7 +46,7 @@ class CreateShareUrlInfo @Inject constructor( "Random URL password size (${randomUrlPassword.length}) does not match requirement (${RANDOM_URL_PASSWORD_SIZE})" } val addressKeys = getAddressKeys(userId, addressId) - val srpForShareUrl = generateSrpForShareUrl(randomUrlPassword.toByteArray()).getOrThrow() + val srpForShareUrl = generateSrpForShareUrl(userId, randomUrlPassword.toByteArray()).getOrThrow() val reencryptedSharePassphrase = reencryptSharePassphraseWithUrlPassword( decryptKey = addressKeys.keyHolder, urlPassword = randomUrlPassword.toByteArray(), diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/upload/EncryptUploadBlocks.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/upload/EncryptUploadBlocks.kt index 47afed0c..28f47152 100644 --- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/upload/EncryptUploadBlocks.kt +++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/upload/EncryptUploadBlocks.kt @@ -54,36 +54,22 @@ class EncryptUploadBlocks @Inject constructor( coroutineContext = coroutineContext, ) { sessionKey -> input.mapIndexed { index, input -> - if (input.length() == 0L) { - block( - index, - input, - output[index].apply { createNewFile() }, - encryptedSignature( - unlockedEncryptKey = unlockedSignatureEncryptionKey, - unlockedSignKey = unlockedFileSignKey, - input = ByteArray(size = 0), - coroutineContext = coroutineContext, - ).getOrThrow() - ) - } else { - block( - index, - input, - encryptFile( - encryptKey = sessionKey, - source = input, - destination = output[index], - coroutineContext = coroutineContext, - ).getOrThrow(), - encryptedSignature( - unlockedEncryptKey = unlockedSignatureEncryptionKey, - unlockedSignKey = unlockedFileSignKey, - file = input, - coroutineContext = coroutineContext, - ).getOrThrow() - ) - } + block( + index, + input, + encryptFile( + encryptKey = sessionKey, + source = input, + destination = output[index], + coroutineContext = coroutineContext, + ).getOrThrow(), + encryptedSignature( + unlockedEncryptKey = unlockedSignatureEncryptionKey, + unlockedSignKey = unlockedFileSignKey, + file = input, + coroutineContext = coroutineContext, + ).getOrThrow() + ) } }.getOrThrow() }.getOrThrow() diff --git a/drive/db/build.gradle.kts b/drive/db/build.gradle.kts new file mode 100644 index 00000000..d820e529 --- /dev/null +++ b/drive/db/build.gradle.kts @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2021-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 . + */ +plugins { + id("com.android.library") +} + +driveModule( + hilt = true, + room = true, +) { + api(libs.core.account.data) + api(libs.core.challenge.data) + api(libs.core.crypto.android) + api(libs.core.eventManager.data) + api(libs.core.featureFlag.data) + api(libs.core.humanVerification.data) + api(libs.core.key.data) + api(libs.core.observability.data) + api(libs.core.payment.data) + api(libs.core.user.data) + api(libs.core.userSettings.data) + // TODO: Extract from drive db + api(project(":app-ui-settings")) + api(project(":drive:drivelink:data")) + api(project(":drive:drivelink-download:data")) + api(project(":drive:drivelink-offline:data")) + api(project(":drive:drivelink-paged:data")) + api(project(":drive:drivelink-selection:data")) + api(project(":drive:drivelink-shared:data")) + api(project(":drive:drivelink-trash:data")) + api(project(":drive:folder:data")) + api(project(":drive:link:data")) + api(project(":drive:link-download:data")) + api(project(":drive:link-node:data")) + api(project(":drive:link-offline:data")) + api(project(":drive:link-selection:data")) + api(project(":drive:link-trash:data")) + api(project(":drive:link-upload:data")) + api(project(":drive:message-queue:data")) + api(project(":drive:notification:data")) + api(project(":drive:share:data")) + api(project(":drive:share-url:base:data")) + api(project(":drive:sorting:data")) + api(project(":drive:volume:data")) +} diff --git a/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/1.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/1.json new file mode 100644 index 00000000..8d06834d --- /dev/null +++ b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/1.json @@ -0,0 +1,3233 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "1848c5aa24ef749bea72f819f081cfe5", + "entities": [ + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionState", + "columnName": "sessionState", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AccountEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_AccountEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "AccountMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primaryAtUtc", + "columnName": "primaryAtUtc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "migrations", + "columnName": "migrations", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "product" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AccountMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_AccountMetadataEntity_product", + "unique": false, + "columnNames": [ + "product" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)" + }, + { + "name": "index_AccountMetadataEntity_primaryAtUtc", + "unique": false, + "columnNames": [ + "primaryAtUtc" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refreshToken", + "columnName": "refreshToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scopes", + "columnName": "scopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sessionId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_SessionEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_SessionEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "initialEventId", + "columnName": "initialEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiredAccountType", + "columnName": "requiredAccountType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondFactorEnabled", + "columnName": "secondFactorEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "twoPassModeEnabled", + "columnName": "twoPassModeEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "sessionId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_SessionDetailsEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "credit", + "columnName": "credit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxUpload", + "columnName": "maxUpload", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "private", + "columnName": "private", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "services", + "columnName": "services", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delinquent", + "columnName": "delinquent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_UserEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UserKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_UserKeyEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_UserKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "domainId", + "columnName": "domainId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canSend", + "columnName": "canSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canReceive", + "columnName": "canReceive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "signedKeyList.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyList.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "addressId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AddressEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprints", + "columnName": "fingerprints", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AddressKeyEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "AddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "addressId" + ], + "referencedColumns": [ + "addressId" + ] + } + ] + }, + { + "tableName": "KeySaltEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keySalt", + "columnName": "keySalt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_KeySaltEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_KeySaltEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`email`))", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipientType", + "columnName": "recipientType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "email" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_PublicAddressEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "email", + "publicKey" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_PublicAddressKeyEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [ + { + "table": "PublicAddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "email" + ], + "referencedColumns": [ + "email" + ] + } + ] + }, + { + "tableName": "HumanVerificationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `captchaVerificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))", + "fields": [ + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientIdType", + "columnName": "clientIdType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verificationMethods", + "columnName": "verificationMethods", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "captchaVerificationToken", + "columnName": "captchaVerificationToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "humanHeaderTokenType", + "columnName": "humanHeaderTokenType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "humanHeaderTokenCode", + "columnName": "humanHeaderTokenCode", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "clientId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `invoiceText` TEXT, `density` INTEGER, `theme` TEXT, `themeType` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `welcome` INTEGER, `earlyAccess` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `twoFA_u2fKeys` TEXT, `flags_welcomed` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "news", + "columnName": "news", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "logAuth", + "columnName": "logAuth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "invoiceText", + "columnName": "invoiceText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "density", + "columnName": "density", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeType", + "columnName": "themeType", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "weekStart", + "columnName": "weekStart", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateFormat", + "columnName": "dateFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timeFormat", + "columnName": "timeFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "welcome", + "columnName": "welcome", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "earlyAccess", + "columnName": "earlyAccess", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.value", + "columnName": "email_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email.status", + "columnName": "email_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.notify", + "columnName": "email_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.reset", + "columnName": "email_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.value", + "columnName": "phone_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone.status", + "columnName": "phone_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.notify", + "columnName": "phone_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.reset", + "columnName": "phone_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.mode", + "columnName": "password_mode", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.expirationTime", + "columnName": "password_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.enabled", + "columnName": "twoFA_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.allowed", + "columnName": "twoFA_allowed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.expirationTime", + "columnName": "twoFA_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.u2fKeys", + "columnName": "twoFA_u2fKeys", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "flags.welcomed", + "columnName": "flags_welcomed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `vpnPlanName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "planName", + "columnName": "planName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "vpnPlanName", + "columnName": "vpnPlanName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "twoFactorGracePeriod", + "columnName": "twoFactorGracePeriod", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxDomains", + "columnName": "maxDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxAddresses", + "columnName": "maxAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMembers", + "columnName": "maxMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxVPN", + "columnName": "maxVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedDomains", + "columnName": "usedDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedAddresses", + "columnName": "usedAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignedSpace", + "columnName": "assignedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedMembers", + "columnName": "usedMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedVPN", + "columnName": "usedVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasKeys", + "columnName": "hasKeys", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "toMigrate", + "columnName": "toMigrate", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationKeysEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "EventMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `response` TEXT, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "nextEventId", + "columnName": "nextEventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "refresh", + "columnName": "refresh", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "more", + "columnName": "more", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "response", + "columnName": "response", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "retry", + "columnName": "retry", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "config" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_EventMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_EventMetadataEntity_config", + "unique": false, + "columnNames": [ + "config" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)" + }, + { + "name": "index_EventMetadataEntity_createdAt", + "unique": false, + "columnNames": [ + "createdAt" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "VolumeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `max_space` INTEGER, `used_space` INTEGER NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxSpace", + "columnName": "max_space", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedSpace", + "columnName": "used_space", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_VolumeEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_VolumeEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_VolumeEntity_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ShareEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `link_id` TEXT NOT NULL, `block_size` INTEGER NOT NULL, `locked` INTEGER NOT NULL, `key` TEXT NOT NULL, `passphrase` TEXT NOT NULL, `passphrase_signature` TEXT NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volumeId", + "columnName": "volume_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockSize", + "columnName": "block_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "passphraseSignature", + "columnName": "passphrase_signature", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_ShareEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_ShareEntity_volume_id", + "unique": false, + "columnNames": [ + "volume_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" + }, + { + "name": "index_ShareEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ShareUrlEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `name` TEXT, `token` TEXT NOT NULL, `creatior_email` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `creation_time` INTEGER NOT NULL, `expiration_time` INTEGER, `last_access_time` INTEGER, `max_accesses` INTEGER, `number_of_accesses` INTEGER NOT NULL, `url_password_salt` TEXT NOT NULL, `share_password_salt` TEXT NOT NULL, `srp_verifier` TEXT NOT NULL, `srp_modulus_id` TEXT NOT NULL, `password` TEXT NOT NULL, `share_passphrase_key_packet` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creatorEmail", + "columnName": "creatior_email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTime", + "columnName": "expiration_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastAccessTime", + "columnName": "last_access_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxAccesses", + "columnName": "max_accesses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "numberOfAccesses", + "columnName": "number_of_accesses", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "urlPasswordSalt", + "columnName": "url_password_salt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sharePasswordSalt", + "columnName": "share_password_salt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "srpVerifier", + "columnName": "srp_verifier", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "srpModulusId", + "columnName": "srp_modulus_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sharePassphraseKeyPacket", + "columnName": "share_passphrase_key_packet", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_ShareUrlEntity_user_id_share_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)" + } + ], + "foreignKeys": [ + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `parent_id` TEXT, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_signature_email` TEXT, `hash` TEXT NOT NULL, `state` INTEGER NOT NULL, `expiration_time` INTEGER, `size` INTEGER NOT NULL, `mime_type` TEXT NOT NULL, `attributes` INTEGER NOT NULL, `permissions` INTEGER NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `signature_address` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, `trashed_time` INTEGER, `is_shared` INTEGER NOT NULL, `number_of_accesses` INTEGER NOT NULL, `share_url_expiration_time` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `parent_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nameSignatureEmail", + "columnName": "name_signature_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTime", + "columnName": "expiration_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeKey", + "columnName": "node_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphrase", + "columnName": "node_passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphraseSignature", + "columnName": "node_passphrase_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "signatureAddress", + "columnName": "signature_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastModified", + "columnName": "last_modified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trashedTime", + "columnName": "trashed_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shared", + "columnName": "is_shared", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "numberOfAccesses", + "columnName": "number_of_accesses", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shareUrlExpirationTime", + "columnName": "share_url_expiration_time", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkEntity_user_id_share_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)" + }, + { + "name": "index_LinkEntity_user_id_id", + "unique": false, + "columnNames": [ + "user_id", + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_id` ON `${TABLE_NAME}` (`user_id`, `id`)" + }, + { + "name": "index_LinkEntity_user_id_share_id_parent_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "parent_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id_parent_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + }, + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + }, + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "parent_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkFilePropertiesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file_user_id` TEXT NOT NULL, `file_share_id` TEXT NOT NULL, `file_link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `has_thumbnail` INTEGER NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT, `file_signature_address` TEXT, PRIMARY KEY(`file_user_id`, `file_share_id`, `file_link_id`), FOREIGN KEY(`file_user_id`, `file_share_id`, `file_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "file_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "file_share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "file_link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeRevisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasThumbnail", + "columnName": "has_thumbnail", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentKeyPacket", + "columnName": "content_key_packet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacketSignature", + "columnName": "content_key_packet_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activeRevisionSignatureAddress", + "columnName": "file_signature_address", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id", + "unique": false, + "columnNames": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id` ON `${TABLE_NAME}` (`file_user_id`, `file_share_id`, `file_link_id`)" + }, + { + "name": "index_LinkFilePropertiesEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkFolderPropertiesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`folder_user_id` TEXT NOT NULL, `folder_share_id` TEXT NOT NULL, `folder_link_id` TEXT NOT NULL, `node_hash_key` TEXT NOT NULL, PRIMARY KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`), FOREIGN KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "folder_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "folder_share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "folder_link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeHashKey", + "columnName": "node_hash_key", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id", + "unique": false, + "columnNames": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id` ON `${TABLE_NAME}` (`folder_user_id`, `folder_share_id`, `folder_link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkOfflineEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkOfflineEntity_user_id_share_id_link_id", + "unique": true, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkDownloadStateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkDownloadStateEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "DownloadBlockEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `index` INTEGER NOT NULL, `uri` TEXT NOT NULL, `encrypted_signature` TEXT, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`, `index`), FOREIGN KEY(`user_id`, `share_id`, `link_id`, `revision_id`) REFERENCES `LinkDownloadStateEntity`(`user_id`, `share_id`, `link_id`, `revision_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedSignature", + "columnName": "encrypted_signature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id", + "index" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_DownloadBlockEntity_user_id_share_id_link_id_revision_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id_share_id_link_id_revision_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`, `revision_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkDownloadStateEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ] + } + ] + }, + { + "tableName": "LinkTrashStateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkTrashStateEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "TrashWorkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `work_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "workId", + "columnName": "work_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TrashWorkEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + }, + { + "name": "index_TrashWorkEntity_work_id", + "unique": false, + "columnNames": [ + "work_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_work_id` ON `${TABLE_NAME}` (`work_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "MessageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `content` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_MessageEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MessageEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UiSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `layout_type` TEXT NOT NULL, `theme_style` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "layoutType", + "columnName": "layout_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "themeStyle", + "columnName": "theme_style", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "DriveLinkRemoteKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `previous_key` INTEGER, `next_key` INTEGER, PRIMARY KEY(`key`, `user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prevKey", + "columnName": "previous_key", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nextKey", + "columnName": "next_key", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "key", + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id", + "unique": true, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + }, + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "SortingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `sorting_by` TEXT NOT NULL, `sorting_direction` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortingBy", + "columnName": "sorting_by", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortingDirection", + "columnName": "sorting_direction", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "LinkUploadEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `name` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT NOT NULL, `manifest_signature` TEXT NOT NULL, `state` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volumeId", + "columnName": "volume_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeKey", + "columnName": "node_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphrase", + "columnName": "node_passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphraseSignature", + "columnName": "node_passphrase_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacket", + "columnName": "content_key_packet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacketSignature", + "columnName": "content_key_packet_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "manifestSignature", + "columnName": "manifest_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_LinkUploadEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkUploadEntity_volume_id", + "unique": false, + "columnNames": [ + "volume_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" + }, + { + "name": "index_LinkUploadEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkUploadEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkUploadEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_LinkUploadEntity_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UploadBlockEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_link_id` INTEGER NOT NULL, `index` INTEGER NOT NULL, `size` INTEGER NOT NULL, `encrypted_signature` TEXT NOT NULL, `hash` TEXT NOT NULL, `token` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`upload_link_id`, `index`), FOREIGN KEY(`upload_link_id`) REFERENCES `LinkUploadEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uploadLinkId", + "columnName": "upload_link_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encryptedSignature", + "columnName": "encrypted_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploadToken", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "upload_link_id", + "index" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "LinkUploadEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "upload_link_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FolderMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `last_fetch_children_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchChildrenTimestamp", + "columnName": "last_fetch_children_timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "TrashMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `last_fetch_trash_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchTrashTimestamp", + "columnName": "last_fetch_trash_timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1848c5aa24ef749bea72f819f081cfe5')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/10.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/10.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/10.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/10.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/11.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/11.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/11.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/11.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/12.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/12.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/12.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/12.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/13.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/13.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/13.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/13.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/14.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/14.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/14.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/14.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/15.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/15.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/15.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/15.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/16.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/16.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/16.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/16.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/17.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/17.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/17.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/17.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/18.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/18.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/18.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/18.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/19.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/19.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/19.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/19.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/2.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/2.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/2.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/2.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/20.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/20.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/20.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/20.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/21.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/21.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/21.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/21.json diff --git a/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/22.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/22.json new file mode 100644 index 00000000..4520cac4 --- /dev/null +++ b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/22.json @@ -0,0 +1,4237 @@ +{ + "formatVersion": 1, + "database": { + "version": 22, + "identityHash": "294985598d7bdcd4fa1e0709f83e5ead", + "entities": [ + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionState", + "columnName": "sessionState", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AccountEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_AccountEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "AccountMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primaryAtUtc", + "columnName": "primaryAtUtc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "migrations", + "columnName": "migrations", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "product" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AccountMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_AccountMetadataEntity_product", + "unique": false, + "columnNames": [ + "product" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)" + }, + { + "name": "index_AccountMetadataEntity_primaryAtUtc", + "unique": false, + "columnNames": [ + "primaryAtUtc" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refreshToken", + "columnName": "refreshToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scopes", + "columnName": "scopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sessionId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_SessionEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_SessionEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "initialEventId", + "columnName": "initialEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiredAccountType", + "columnName": "requiredAccountType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondFactorEnabled", + "columnName": "secondFactorEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "twoPassModeEnabled", + "columnName": "twoPassModeEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "sessionId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_SessionDetailsEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "credit", + "columnName": "credit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxUpload", + "columnName": "maxUpload", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isPrivate", + "columnName": "private", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "services", + "columnName": "services", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delinquent", + "columnName": "delinquent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_UserEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UserKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_UserKeyEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_UserKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "domainId", + "columnName": "domainId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canSend", + "columnName": "canSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canReceive", + "columnName": "canReceive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "signedKeyList.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyList.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "addressId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AddressEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprints", + "columnName": "fingerprints", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AddressKeyEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "AddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "addressId" + ], + "referencedColumns": [ + "addressId" + ] + } + ] + }, + { + "tableName": "KeySaltEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keySalt", + "columnName": "keySalt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_KeySaltEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_KeySaltEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`email`))", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipientType", + "columnName": "recipientType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "email" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_PublicAddressEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "email", + "publicKey" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_PublicAddressKeyEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [ + { + "table": "PublicAddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "email" + ], + "referencedColumns": [ + "email" + ] + } + ] + }, + { + "tableName": "HumanVerificationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `verificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))", + "fields": [ + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientIdType", + "columnName": "clientIdType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verificationMethods", + "columnName": "verificationMethods", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verificationToken", + "columnName": "verificationToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "humanHeaderTokenType", + "columnName": "humanHeaderTokenType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "humanHeaderTokenCode", + "columnName": "humanHeaderTokenCode", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "clientId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `invoiceText` TEXT, `density` INTEGER, `theme` TEXT, `themeType` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `welcome` INTEGER, `earlyAccess` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `flags_welcomed` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "news", + "columnName": "news", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "logAuth", + "columnName": "logAuth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "invoiceText", + "columnName": "invoiceText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "density", + "columnName": "density", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeType", + "columnName": "themeType", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "weekStart", + "columnName": "weekStart", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateFormat", + "columnName": "dateFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timeFormat", + "columnName": "timeFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "welcome", + "columnName": "welcome", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "earlyAccess", + "columnName": "earlyAccess", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.value", + "columnName": "email_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email.status", + "columnName": "email_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.notify", + "columnName": "email_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.reset", + "columnName": "email_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.value", + "columnName": "phone_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone.status", + "columnName": "phone_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.notify", + "columnName": "phone_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.reset", + "columnName": "phone_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.mode", + "columnName": "password_mode", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.expirationTime", + "columnName": "password_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.enabled", + "columnName": "twoFA_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.allowed", + "columnName": "twoFA_allowed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.expirationTime", + "columnName": "twoFA_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "flags.welcomed", + "columnName": "flags_welcomed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `vpnPlanName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `maxCalendars` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `usedCalendars` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "planName", + "columnName": "planName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "vpnPlanName", + "columnName": "vpnPlanName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "twoFactorGracePeriod", + "columnName": "twoFactorGracePeriod", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxDomains", + "columnName": "maxDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxAddresses", + "columnName": "maxAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMembers", + "columnName": "maxMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxVPN", + "columnName": "maxVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxCalendars", + "columnName": "maxCalendars", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedDomains", + "columnName": "usedDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedAddresses", + "columnName": "usedAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignedSpace", + "columnName": "assignedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedMembers", + "columnName": "usedMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedVPN", + "columnName": "usedVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedCalendars", + "columnName": "usedCalendars", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasKeys", + "columnName": "hasKeys", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "toMigrate", + "columnName": "toMigrate", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationKeysEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "EventMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `response` TEXT, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "nextEventId", + "columnName": "nextEventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "refresh", + "columnName": "refresh", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "more", + "columnName": "more", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "response", + "columnName": "response", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "retry", + "columnName": "retry", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "config" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_EventMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_EventMetadataEntity_config", + "unique": false, + "columnNames": [ + "config" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)" + }, + { + "name": "index_EventMetadataEntity_createdAt", + "unique": false, + "columnNames": [ + "createdAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "FeatureFlagEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `featureId` TEXT NOT NULL, `scope` TEXT NOT NULL, `defaultValue` INTEGER NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`userId`, `featureId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "featureId", + "columnName": "featureId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultValue", + "columnName": "defaultValue", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "featureId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_FeatureFlagEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_FeatureFlagEntity_featureId", + "unique": false, + "columnNames": [ + "featureId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_featureId` ON `${TABLE_NAME}` (`featureId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ChallengeFrameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`challengeFrame` TEXT NOT NULL, `flow` TEXT NOT NULL, `focusTime` TEXT NOT NULL, `clicks` INTEGER NOT NULL, `copy` TEXT NOT NULL, `paste` TEXT NOT NULL, `keys` TEXT NOT NULL, PRIMARY KEY(`challengeFrame`))", + "fields": [ + { + "fieldPath": "challengeFrame", + "columnName": "challengeFrame", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flow", + "columnName": "flow", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "focusTime", + "columnName": "focusTime", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clicks", + "columnName": "clicks", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "copy", + "columnName": "copy", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paste", + "columnName": "paste", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keys", + "columnName": "keys", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "challengeFrame" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GooglePurchaseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`googlePurchaseToken` TEXT NOT NULL, `paymentToken` TEXT NOT NULL, PRIMARY KEY(`googlePurchaseToken`))", + "fields": [ + { + "fieldPath": "googlePurchaseToken", + "columnName": "googlePurchaseToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentToken", + "columnName": "paymentToken", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "googlePurchaseToken" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_GooglePurchaseEntity_paymentToken", + "unique": true, + "columnNames": [ + "paymentToken" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_GooglePurchaseEntity_paymentToken` ON `${TABLE_NAME}` (`paymentToken`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ObservabilityEventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "VolumeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `max_space` INTEGER, `used_space` INTEGER NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxSpace", + "columnName": "max_space", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedSpace", + "columnName": "used_space", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_VolumeEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_VolumeEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_VolumeEntity_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ShareEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `address_id` TEXT, `flags` INTEGER NOT NULL, `link_id` TEXT NOT NULL, `locked` INTEGER NOT NULL, `key` TEXT NOT NULL, `passphrase` TEXT NOT NULL, `passphrase_signature` TEXT NOT NULL, `creation_time` INTEGER, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volumeId", + "columnName": "volume_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "address_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "passphraseSignature", + "columnName": "passphrase_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_ShareEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_ShareEntity_volume_id", + "unique": false, + "columnNames": [ + "volume_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" + }, + { + "name": "index_ShareEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_ShareEntity_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ShareEntity_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ShareUrlEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `name` TEXT, `token` TEXT NOT NULL, `creatior_email` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `creation_time` INTEGER NOT NULL, `expiration_time` INTEGER, `last_access_time` INTEGER, `max_accesses` INTEGER, `number_of_accesses` INTEGER NOT NULL, `url_password_salt` TEXT NOT NULL, `share_password_salt` TEXT NOT NULL, `srp_verifier` TEXT NOT NULL, `srp_modulus_id` TEXT NOT NULL, `password` TEXT NOT NULL, `share_passphrase_key_packet` TEXT NOT NULL, `public_url` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creatorEmail", + "columnName": "creatior_email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTime", + "columnName": "expiration_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastAccessTime", + "columnName": "last_access_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxAccesses", + "columnName": "max_accesses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "numberOfAccesses", + "columnName": "number_of_accesses", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "urlPasswordSalt", + "columnName": "url_password_salt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sharePasswordSalt", + "columnName": "share_password_salt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "srpVerifier", + "columnName": "srp_verifier", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "srpModulusId", + "columnName": "srp_modulus_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedUrlPassword", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sharePassphraseKeyPacket", + "columnName": "share_passphrase_key_packet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicUrl", + "columnName": "public_url", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_ShareUrlEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_ShareUrlEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_ShareUrlEntity_user_id_share_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)" + } + ], + "foreignKeys": [ + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `parent_id` TEXT, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_signature_email` TEXT, `hash` TEXT NOT NULL, `state` INTEGER NOT NULL, `expiration_time` INTEGER, `size` INTEGER NOT NULL, `mime_type` TEXT NOT NULL, `attributes` INTEGER NOT NULL, `permissions` INTEGER NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `signature_address` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, `trashed_time` INTEGER, `is_shared` INTEGER NOT NULL, `number_of_accesses` INTEGER NOT NULL, `share_url_expiration_time` INTEGER, `x_attr` TEXT, `share_url_share_id` TEXT DEFAULT NULL, `share_url_id` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `parent_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nameSignatureEmail", + "columnName": "name_signature_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTime", + "columnName": "expiration_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeKey", + "columnName": "node_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphrase", + "columnName": "node_passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphraseSignature", + "columnName": "node_passphrase_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "signatureAddress", + "columnName": "signature_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastModified", + "columnName": "last_modified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trashedTime", + "columnName": "trashed_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shared", + "columnName": "is_shared", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "numberOfAccesses", + "columnName": "number_of_accesses", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shareUrlExpirationTime", + "columnName": "share_url_expiration_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "xAttr", + "columnName": "x_attr", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareUrlShareId", + "columnName": "share_url_share_id", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "shareUrlId", + "columnName": "share_url_id", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkEntity_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + }, + { + "name": "index_LinkEntity_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_LinkEntity_user_id_share_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)" + }, + { + "name": "index_LinkEntity_user_id_id", + "unique": false, + "columnNames": [ + "user_id", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_id` ON `${TABLE_NAME}` (`user_id`, `id`)" + }, + { + "name": "index_LinkEntity_user_id_share_id_parent_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id_parent_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + }, + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + }, + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "parent_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkFilePropertiesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file_user_id` TEXT NOT NULL, `file_share_id` TEXT NOT NULL, `file_link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `has_thumbnail` INTEGER NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT, `file_signature_address` TEXT, PRIMARY KEY(`file_user_id`, `file_share_id`, `file_link_id`), FOREIGN KEY(`file_user_id`, `file_share_id`, `file_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "file_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "file_share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "file_link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeRevisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasThumbnail", + "columnName": "has_thumbnail", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentKeyPacket", + "columnName": "content_key_packet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacketSignature", + "columnName": "content_key_packet_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activeRevisionSignatureAddress", + "columnName": "file_signature_address", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkFilePropertiesEntity_file_share_id", + "unique": false, + "columnNames": [ + "file_share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_share_id` ON `${TABLE_NAME}` (`file_share_id`)" + }, + { + "name": "index_LinkFilePropertiesEntity_file_link_id", + "unique": false, + "columnNames": [ + "file_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_link_id` ON `${TABLE_NAME}` (`file_link_id`)" + }, + { + "name": "index_LinkFilePropertiesEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id", + "unique": false, + "columnNames": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id` ON `${TABLE_NAME}` (`file_user_id`, `file_share_id`, `file_link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkFolderPropertiesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`folder_user_id` TEXT NOT NULL, `folder_share_id` TEXT NOT NULL, `folder_link_id` TEXT NOT NULL, `node_hash_key` TEXT NOT NULL, PRIMARY KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`), FOREIGN KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "folder_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "folder_share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "folder_link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeHashKey", + "columnName": "node_hash_key", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkFolderPropertiesEntity_folder_share_id", + "unique": false, + "columnNames": [ + "folder_share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_share_id` ON `${TABLE_NAME}` (`folder_share_id`)" + }, + { + "name": "index_LinkFolderPropertiesEntity_folder_link_id", + "unique": false, + "columnNames": [ + "folder_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_link_id` ON `${TABLE_NAME}` (`folder_link_id`)" + }, + { + "name": "index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id", + "unique": false, + "columnNames": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id` ON `${TABLE_NAME}` (`folder_user_id`, `folder_share_id`, `folder_link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkOfflineEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkOfflineEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkOfflineEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkOfflineEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkOfflineEntity_user_id_share_id_link_id", + "unique": true, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkDownloadStateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `state` TEXT NOT NULL, `manifest_signature` TEXT DEFAULT NULL, `signature_address` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "manifestSignature", + "columnName": "manifest_signature", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "signatureAddress", + "columnName": "signature_address", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkDownloadStateEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_state", + "unique": false, + "columnNames": [ + "state" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_state` ON `${TABLE_NAME}` (`state`)" + }, + { + "name": "index_LinkDownloadStateEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "DownloadBlockEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `index` INTEGER NOT NULL, `uri` TEXT NOT NULL, `encrypted_signature` TEXT, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`, `index`), FOREIGN KEY(`user_id`, `share_id`, `link_id`, `revision_id`) REFERENCES `LinkDownloadStateEntity`(`user_id`, `share_id`, `link_id`, `revision_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedSignature", + "columnName": "encrypted_signature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id", + "index" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_DownloadBlockEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_DownloadBlockEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_DownloadBlockEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_DownloadBlockEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_DownloadBlockEntity_user_id_share_id_link_id_revision_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id_share_id_link_id_revision_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`, `revision_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkDownloadStateEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ] + } + ] + }, + { + "tableName": "LinkTrashStateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkTrashStateEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkTrashStateEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkTrashStateEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkTrashStateEntity_state", + "unique": false, + "columnNames": [ + "state" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_state` ON `${TABLE_NAME}` (`state`)" + }, + { + "name": "index_LinkTrashStateEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "TrashWorkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `work_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "workId", + "columnName": "work_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TrashWorkEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + }, + { + "name": "index_TrashWorkEntity_work_id", + "unique": false, + "columnNames": [ + "work_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_work_id` ON `${TABLE_NAME}` (`work_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "MessageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `content` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_MessageEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MessageEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UiSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `layout_type` TEXT NOT NULL, `theme_style` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "layoutType", + "columnName": "layout_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "themeStyle", + "columnName": "theme_style", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "DriveLinkRemoteKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `previous_key` INTEGER, `next_key` INTEGER, PRIMARY KEY(`key`, `user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prevKey", + "columnName": "previous_key", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nextKey", + "columnName": "next_key", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "key", + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id", + "unique": true, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_key", + "unique": false, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + }, + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "SortingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `sorting_by` TEXT NOT NULL, `sorting_direction` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortingBy", + "columnName": "sorting_by", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortingDirection", + "columnName": "sorting_direction", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "LinkUploadEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `name` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT NOT NULL, `manifest_signature` TEXT NOT NULL, `state` TEXT NOT NULL, `size` INTEGER DEFAULT NULL, `last_modified` INTEGER, `uri` TEXT DEFAULT NULL, `should_delete_source_uri` INTEGER NOT NULL DEFAULT false, `media_resolution_width` INTEGER DEFAULT NULL, `media_resolution_height` INTEGER DEFAULT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volumeId", + "columnName": "volume_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeKey", + "columnName": "node_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphrase", + "columnName": "node_passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphraseSignature", + "columnName": "node_passphrase_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacket", + "columnName": "content_key_packet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacketSignature", + "columnName": "content_key_packet_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "manifestSignature", + "columnName": "manifest_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "lastModified", + "columnName": "last_modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "shouldDeleteSourceUri", + "columnName": "should_delete_source_uri", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "mediaResolutionWidth", + "columnName": "media_resolution_width", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "mediaResolutionHeight", + "columnName": "media_resolution_height", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_LinkUploadEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkUploadEntity_volume_id", + "unique": false, + "columnNames": [ + "volume_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" + }, + { + "name": "index_LinkUploadEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkUploadEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkUploadEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_LinkUploadEntity_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UploadBlockEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_link_id` INTEGER NOT NULL, `index` INTEGER NOT NULL, `size` INTEGER NOT NULL, `encrypted_signature` TEXT NOT NULL, `hash` TEXT NOT NULL, `token` TEXT NOT NULL, `url` TEXT NOT NULL, `raw_size` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`upload_link_id`, `index`), FOREIGN KEY(`upload_link_id`) REFERENCES `LinkUploadEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uploadLinkId", + "columnName": "upload_link_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encryptedSignature", + "columnName": "encrypted_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploadToken", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rawSize", + "columnName": "raw_size", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "upload_link_id", + "index" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "LinkUploadEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "upload_link_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "UploadBulkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `should_delete_source_uri` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volumeId", + "columnName": "volume_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shouldDeleteSourceUri", + "columnName": "should_delete_source_uri", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_UploadBulkEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_UploadBulkEntity_volume_id", + "unique": false, + "columnNames": [ + "volume_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" + }, + { + "name": "index_UploadBulkEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_UploadBulkEntity_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UploadBulkUriStringEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_bulk_id` INTEGER NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`upload_bulk_id`, `uri`), FOREIGN KEY(`upload_bulk_id`) REFERENCES `UploadBulkEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "upload_bulk_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "upload_bulk_id", + "uri" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_UploadBulkUriStringEntity_upload_bulk_id", + "unique": false, + "columnNames": [ + "upload_bulk_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkUriStringEntity_upload_bulk_id` ON `${TABLE_NAME}` (`upload_bulk_id`)" + } + ], + "foreignKeys": [ + { + "table": "UploadBulkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "upload_bulk_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FolderMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `last_fetch_children_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchChildrenTimestamp", + "columnName": "last_fetch_children_timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "TrashMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `last_fetch_trash_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchTrashTimestamp", + "columnName": "last_fetch_trash_timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + } + ] + }, + { + "tableName": "NotificationChannelEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`user_id`, `type`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "type" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_NotificationChannelEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationChannelEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "NotificationEventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `channel_type` TEXT NOT NULL, `notification_tag` TEXT NOT NULL, `notification_id` INTEGER NOT NULL, `notification_event_id` TEXT NOT NULL, `notification_event` TEXT NOT NULL, PRIMARY KEY(`user_id`, `channel_type`, `notification_tag`, `notification_id`, `notification_event_id`), FOREIGN KEY(`user_id`, `channel_type`) REFERENCES `NotificationChannelEntity`(`user_id`, `type`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "channelType", + "columnName": "channel_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationTag", + "columnName": "notification_tag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notification_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationEventId", + "columnName": "notification_event_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationEvent", + "columnName": "notification_event", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "channel_type", + "notification_tag", + "notification_id", + "notification_event_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id", + "unique": false, + "columnNames": [ + "user_id", + "channel_type", + "notification_tag", + "notification_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id` ON `${TABLE_NAME}` (`user_id`, `channel_type`, `notification_tag`, `notification_id`)" + } + ], + "foreignKeys": [ + { + "table": "NotificationChannelEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "channel_type" + ], + "referencedColumns": [ + "user_id", + "type" + ] + } + ] + }, + { + "tableName": "LinkSelectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `selection_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `selection_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "selectionId", + "columnName": "selection_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id", + "selection_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkSelectionEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + }, + { + "name": "index_LinkSelectionEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkSelectionEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkSelectionEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkSelectionEntity_selection_id", + "unique": false, + "columnNames": [ + "selection_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_selection_id` ON `${TABLE_NAME}` (`selection_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '294985598d7bdcd4fa1e0709f83e5ead')" + ] + } +} \ No newline at end of file diff --git a/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/23.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/23.json new file mode 100644 index 00000000..efaae2b9 --- /dev/null +++ b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/23.json @@ -0,0 +1,4244 @@ +{ + "formatVersion": 1, + "database": { + "version": 23, + "identityHash": "59591b661e49ae97591acd9c77f82223", + "entities": [ + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionState", + "columnName": "sessionState", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AccountEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_AccountEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "AccountMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primaryAtUtc", + "columnName": "primaryAtUtc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "migrations", + "columnName": "migrations", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "product" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AccountMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_AccountMetadataEntity_product", + "unique": false, + "columnNames": [ + "product" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)" + }, + { + "name": "index_AccountMetadataEntity_primaryAtUtc", + "unique": false, + "columnNames": [ + "primaryAtUtc" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refreshToken", + "columnName": "refreshToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scopes", + "columnName": "scopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "product", + "columnName": "product", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sessionId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_SessionEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + }, + { + "name": "index_SessionEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "SessionDetailsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "sessionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "initialEventId", + "columnName": "initialEventId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requiredAccountType", + "columnName": "requiredAccountType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondFactorEnabled", + "columnName": "secondFactorEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "twoPassModeEnabled", + "columnName": "twoPassModeEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "sessionId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_SessionDetailsEntity_sessionId", + "unique": false, + "columnNames": [ + "sessionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)" + } + ], + "foreignKeys": [ + { + "table": "SessionEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sessionId" + ], + "referencedColumns": [ + "sessionId" + ] + } + ] + }, + { + "tableName": "UserEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "credit", + "columnName": "credit", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxUpload", + "columnName": "maxUpload", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isPrivate", + "columnName": "private", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "services", + "columnName": "services", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delinquent", + "columnName": "delinquent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_UserEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UserKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_UserKeyEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_UserKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "domainId", + "columnName": "domainId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canSend", + "columnName": "canSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canReceive", + "columnName": "canReceive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "signedKeyList.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyList.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "addressId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AddressEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "AddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "addressId", + "columnName": "addressId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUnlockable", + "columnName": "isUnlockable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprints", + "columnName": "fingerprints", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activation", + "columnName": "activation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_AddressKeyEntity_addressId", + "unique": false, + "columnNames": [ + "addressId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)" + }, + { + "name": "index_AddressKeyEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [ + { + "table": "AddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "addressId" + ], + "referencedColumns": [ + "addressId" + ] + } + ] + }, + { + "tableName": "KeySaltEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keyId", + "columnName": "keyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keySalt", + "columnName": "keySalt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "keyId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_KeySaltEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_KeySaltEntity_keyId", + "unique": false, + "columnNames": [ + "keyId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`email`))", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipientType", + "columnName": "recipientType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.data", + "columnName": "signedKeyList_data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signedKeyListEntity.signature", + "columnName": "signedKeyList_signature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "email" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_PublicAddressEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PublicAddressKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "email", + "publicKey" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_PublicAddressKeyEntity_email", + "unique": false, + "columnNames": [ + "email" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [ + { + "table": "PublicAddressEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "email" + ], + "referencedColumns": [ + "email" + ] + } + ] + }, + { + "tableName": "HumanVerificationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `verificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))", + "fields": [ + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientIdType", + "columnName": "clientIdType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verificationMethods", + "columnName": "verificationMethods", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verificationToken", + "columnName": "verificationToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "humanHeaderTokenType", + "columnName": "humanHeaderTokenType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "humanHeaderTokenCode", + "columnName": "humanHeaderTokenCode", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "clientId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UserSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `invoiceText` TEXT, `density` INTEGER, `theme` TEXT, `themeType` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `welcome` INTEGER, `earlyAccess` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `flags_welcomed` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "news", + "columnName": "news", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "logAuth", + "columnName": "logAuth", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "invoiceText", + "columnName": "invoiceText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "density", + "columnName": "density", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themeType", + "columnName": "themeType", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "weekStart", + "columnName": "weekStart", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateFormat", + "columnName": "dateFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timeFormat", + "columnName": "timeFormat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "welcome", + "columnName": "welcome", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "earlyAccess", + "columnName": "earlyAccess", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.value", + "columnName": "email_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email.status", + "columnName": "email_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.notify", + "columnName": "email_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email.reset", + "columnName": "email_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.value", + "columnName": "phone_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone.status", + "columnName": "phone_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.notify", + "columnName": "phone_notify", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phone.reset", + "columnName": "phone_reset", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.mode", + "columnName": "password_mode", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "password.expirationTime", + "columnName": "password_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.enabled", + "columnName": "twoFA_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.allowed", + "columnName": "twoFA_allowed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "twoFA.expirationTime", + "columnName": "twoFA_expirationTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "flags.welcomed", + "columnName": "flags_welcomed", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `vpnPlanName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `maxCalendars` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `usedCalendars` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "planName", + "columnName": "planName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "vpnPlanName", + "columnName": "vpnPlanName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "twoFactorGracePeriod", + "columnName": "twoFactorGracePeriod", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "theme", + "columnName": "theme", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxDomains", + "columnName": "maxDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxAddresses", + "columnName": "maxAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxSpace", + "columnName": "maxSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMembers", + "columnName": "maxMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxVPN", + "columnName": "maxVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxCalendars", + "columnName": "maxCalendars", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "features", + "columnName": "features", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedDomains", + "columnName": "usedDomains", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedAddresses", + "columnName": "usedAddresses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedSpace", + "columnName": "usedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assignedSpace", + "columnName": "assignedSpace", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedMembers", + "columnName": "usedMembers", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedVPN", + "columnName": "usedVPN", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedCalendars", + "columnName": "usedCalendars", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasKeys", + "columnName": "hasKeys", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "toMigrate", + "columnName": "toMigrate", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "OrganizationKeysEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "userId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "EventMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `response` TEXT, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eventId", + "columnName": "eventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "nextEventId", + "columnName": "nextEventId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "refresh", + "columnName": "refresh", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "more", + "columnName": "more", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "response", + "columnName": "response", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "retry", + "columnName": "retry", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "config" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_EventMetadataEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_EventMetadataEntity_config", + "unique": false, + "columnNames": [ + "config" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)" + }, + { + "name": "index_EventMetadataEntity_createdAt", + "unique": false, + "columnNames": [ + "createdAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)" + } + ], + "foreignKeys": [ + { + "table": "UserEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "FeatureFlagEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `featureId` TEXT NOT NULL, `scope` TEXT NOT NULL, `defaultValue` INTEGER NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`userId`, `featureId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "featureId", + "columnName": "featureId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultValue", + "columnName": "defaultValue", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "userId", + "featureId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_FeatureFlagEntity_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_userId` ON `${TABLE_NAME}` (`userId`)" + }, + { + "name": "index_FeatureFlagEntity_featureId", + "unique": false, + "columnNames": [ + "featureId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_featureId` ON `${TABLE_NAME}` (`featureId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ChallengeFrameEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`challengeFrame` TEXT NOT NULL, `flow` TEXT NOT NULL, `focusTime` TEXT NOT NULL, `clicks` INTEGER NOT NULL, `copy` TEXT NOT NULL, `paste` TEXT NOT NULL, `keys` TEXT NOT NULL, PRIMARY KEY(`challengeFrame`))", + "fields": [ + { + "fieldPath": "challengeFrame", + "columnName": "challengeFrame", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flow", + "columnName": "flow", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "focusTime", + "columnName": "focusTime", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clicks", + "columnName": "clicks", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "copy", + "columnName": "copy", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paste", + "columnName": "paste", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "keys", + "columnName": "keys", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "challengeFrame" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GooglePurchaseEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`googlePurchaseToken` TEXT NOT NULL, `paymentToken` TEXT NOT NULL, PRIMARY KEY(`googlePurchaseToken`))", + "fields": [ + { + "fieldPath": "googlePurchaseToken", + "columnName": "googlePurchaseToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "paymentToken", + "columnName": "paymentToken", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "googlePurchaseToken" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_GooglePurchaseEntity_paymentToken", + "unique": true, + "columnNames": [ + "paymentToken" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_GooglePurchaseEntity_paymentToken` ON `${TABLE_NAME}` (`paymentToken`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ObservabilityEventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "VolumeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `max_space` INTEGER, `used_space` INTEGER NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxSpace", + "columnName": "max_space", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "usedSpace", + "columnName": "used_space", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_VolumeEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_VolumeEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_VolumeEntity_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ShareEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `address_id` TEXT, `flags` INTEGER NOT NULL, `link_id` TEXT NOT NULL, `locked` INTEGER NOT NULL, `key` TEXT NOT NULL, `passphrase` TEXT NOT NULL, `passphrase_signature` TEXT NOT NULL, `creation_time` INTEGER, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volumeId", + "columnName": "volume_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addressId", + "columnName": "address_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isLocked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "passphrase", + "columnName": "passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "passphraseSignature", + "columnName": "passphrase_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_ShareEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_ShareEntity_volume_id", + "unique": false, + "columnNames": [ + "volume_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" + }, + { + "name": "index_ShareEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_ShareEntity_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ShareEntity_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "ShareUrlEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `name` TEXT, `token` TEXT NOT NULL, `creatior_email` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `creation_time` INTEGER NOT NULL, `expiration_time` INTEGER, `last_access_time` INTEGER, `max_accesses` INTEGER, `number_of_accesses` INTEGER NOT NULL, `url_password_salt` TEXT NOT NULL, `share_password_salt` TEXT NOT NULL, `srp_verifier` TEXT NOT NULL, `srp_modulus_id` TEXT NOT NULL, `password` TEXT NOT NULL, `share_passphrase_key_packet` TEXT NOT NULL, `public_url` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creatorEmail", + "columnName": "creatior_email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTime", + "columnName": "expiration_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastAccessTime", + "columnName": "last_access_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxAccesses", + "columnName": "max_accesses", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "numberOfAccesses", + "columnName": "number_of_accesses", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "urlPasswordSalt", + "columnName": "url_password_salt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sharePasswordSalt", + "columnName": "share_password_salt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "srpVerifier", + "columnName": "srp_verifier", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "srpModulusId", + "columnName": "srp_modulus_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedUrlPassword", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sharePassphraseKeyPacket", + "columnName": "share_passphrase_key_packet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicUrl", + "columnName": "public_url", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_ShareUrlEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_ShareUrlEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_ShareUrlEntity_user_id_share_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)" + } + ], + "foreignKeys": [ + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `parent_id` TEXT, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_signature_email` TEXT, `hash` TEXT NOT NULL, `state` INTEGER NOT NULL, `expiration_time` INTEGER, `size` INTEGER NOT NULL, `mime_type` TEXT NOT NULL, `attributes` INTEGER NOT NULL, `permissions` INTEGER NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `signature_address` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, `trashed_time` INTEGER, `is_shared` INTEGER NOT NULL, `number_of_accesses` INTEGER NOT NULL, `share_url_expiration_time` INTEGER, `x_attr` TEXT, `share_url_share_id` TEXT DEFAULT NULL, `share_url_id` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `parent_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nameSignatureEmail", + "columnName": "name_signature_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expirationTime", + "columnName": "expiration_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nodeKey", + "columnName": "node_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphrase", + "columnName": "node_passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphraseSignature", + "columnName": "node_passphrase_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "signatureAddress", + "columnName": "signature_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "creationTime", + "columnName": "creation_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastModified", + "columnName": "last_modified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trashedTime", + "columnName": "trashed_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shared", + "columnName": "is_shared", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "numberOfAccesses", + "columnName": "number_of_accesses", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shareUrlExpirationTime", + "columnName": "share_url_expiration_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "xAttr", + "columnName": "x_attr", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareUrlShareId", + "columnName": "share_url_share_id", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "shareUrlId", + "columnName": "share_url_id", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkEntity_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + }, + { + "name": "index_LinkEntity_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_LinkEntity_user_id_share_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)" + }, + { + "name": "index_LinkEntity_user_id_id", + "unique": false, + "columnNames": [ + "user_id", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_id` ON `${TABLE_NAME}` (`user_id`, `id`)" + }, + { + "name": "index_LinkEntity_user_id_share_id_parent_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id_parent_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + }, + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + }, + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "parent_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkFilePropertiesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file_user_id` TEXT NOT NULL, `file_share_id` TEXT NOT NULL, `file_link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `has_thumbnail` INTEGER NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT, `file_signature_address` TEXT, PRIMARY KEY(`file_user_id`, `file_share_id`, `file_link_id`), FOREIGN KEY(`file_user_id`, `file_share_id`, `file_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "file_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "file_share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "file_link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeRevisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasThumbnail", + "columnName": "has_thumbnail", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentKeyPacket", + "columnName": "content_key_packet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacketSignature", + "columnName": "content_key_packet_signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activeRevisionSignatureAddress", + "columnName": "file_signature_address", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkFilePropertiesEntity_file_share_id", + "unique": false, + "columnNames": [ + "file_share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_share_id` ON `${TABLE_NAME}` (`file_share_id`)" + }, + { + "name": "index_LinkFilePropertiesEntity_file_link_id", + "unique": false, + "columnNames": [ + "file_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_link_id` ON `${TABLE_NAME}` (`file_link_id`)" + }, + { + "name": "index_LinkFilePropertiesEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id", + "unique": false, + "columnNames": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id` ON `${TABLE_NAME}` (`file_user_id`, `file_share_id`, `file_link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "file_user_id", + "file_share_id", + "file_link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkFolderPropertiesEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`folder_user_id` TEXT NOT NULL, `folder_share_id` TEXT NOT NULL, `folder_link_id` TEXT NOT NULL, `node_hash_key` TEXT NOT NULL, PRIMARY KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`), FOREIGN KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "folder_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "folder_share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "folder_link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeHashKey", + "columnName": "node_hash_key", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkFolderPropertiesEntity_folder_share_id", + "unique": false, + "columnNames": [ + "folder_share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_share_id` ON `${TABLE_NAME}` (`folder_share_id`)" + }, + { + "name": "index_LinkFolderPropertiesEntity_folder_link_id", + "unique": false, + "columnNames": [ + "folder_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_link_id` ON `${TABLE_NAME}` (`folder_link_id`)" + }, + { + "name": "index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id", + "unique": false, + "columnNames": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id` ON `${TABLE_NAME}` (`folder_user_id`, `folder_share_id`, `folder_link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder_user_id", + "folder_share_id", + "folder_link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkOfflineEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkOfflineEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkOfflineEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkOfflineEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkOfflineEntity_user_id_share_id_link_id", + "unique": true, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "LinkDownloadStateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `state` TEXT NOT NULL, `manifest_signature` TEXT DEFAULT NULL, `signature_address` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "manifestSignature", + "columnName": "manifest_signature", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "signatureAddress", + "columnName": "signature_address", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkDownloadStateEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_LinkDownloadStateEntity_state", + "unique": false, + "columnNames": [ + "state" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_state` ON `${TABLE_NAME}` (`state`)" + }, + { + "name": "index_LinkDownloadStateEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "DownloadBlockEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `index` INTEGER NOT NULL, `uri` TEXT NOT NULL, `encrypted_signature` TEXT, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`, `index`), FOREIGN KEY(`user_id`, `share_id`, `link_id`, `revision_id`) REFERENCES `LinkDownloadStateEntity`(`user_id`, `share_id`, `link_id`, `revision_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encryptedSignature", + "columnName": "encrypted_signature", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id", + "index" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_DownloadBlockEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_DownloadBlockEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_DownloadBlockEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_DownloadBlockEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_DownloadBlockEntity_user_id_share_id_link_id_revision_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id_share_id_link_id_revision_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`, `revision_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkDownloadStateEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "link_id", + "revision_id" + ] + } + ] + }, + { + "tableName": "LinkTrashStateEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkTrashStateEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkTrashStateEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkTrashStateEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkTrashStateEntity_state", + "unique": false, + "columnNames": [ + "state" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_state` ON `${TABLE_NAME}` (`state`)" + }, + { + "name": "index_LinkTrashStateEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "TrashWorkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `work_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "workId", + "columnName": "work_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TrashWorkEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + }, + { + "name": "index_TrashWorkEntity_work_id", + "unique": false, + "columnNames": [ + "work_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_work_id` ON `${TABLE_NAME}` (`work_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "MessageEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `content` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_MessageEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_MessageEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UiSettingsEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `layout_type` TEXT NOT NULL, `theme_style` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "layoutType", + "columnName": "layout_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "themeStyle", + "columnName": "theme_style", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "DriveLinkRemoteKeyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `previous_key` INTEGER, `next_key` INTEGER, PRIMARY KEY(`key`, `user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prevKey", + "columnName": "previous_key", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nextKey", + "columnName": "next_key", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "key", + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id", + "unique": true, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_DriveLinkRemoteKeyEntity_key", + "unique": false, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + }, + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "SortingEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `sorting_by` TEXT NOT NULL, `sorting_direction` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortingBy", + "columnName": "sorting_by", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortingDirection", + "columnName": "sorting_direction", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "LinkUploadEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `name` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT NOT NULL, `manifest_signature` TEXT NOT NULL, `state` TEXT NOT NULL, `size` INTEGER DEFAULT NULL, `last_modified` INTEGER, `uri` TEXT DEFAULT NULL, `should_delete_source_uri` INTEGER NOT NULL DEFAULT false, `media_resolution_width` INTEGER DEFAULT NULL, `media_resolution_height` INTEGER DEFAULT NULL, `digests` TEXT DEFAULT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volumeId", + "columnName": "volume_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "revisionId", + "columnName": "revision_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodeKey", + "columnName": "node_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphrase", + "columnName": "node_passphrase", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nodePassphraseSignature", + "columnName": "node_passphrase_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacket", + "columnName": "content_key_packet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentKeyPacketSignature", + "columnName": "content_key_packet_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "manifestSignature", + "columnName": "manifest_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "lastModified", + "columnName": "last_modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "shouldDeleteSourceUri", + "columnName": "should_delete_source_uri", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "mediaResolutionWidth", + "columnName": "media_resolution_width", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "mediaResolutionHeight", + "columnName": "media_resolution_height", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "digests", + "columnName": "digests", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_LinkUploadEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkUploadEntity_volume_id", + "unique": false, + "columnNames": [ + "volume_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" + }, + { + "name": "index_LinkUploadEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkUploadEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkUploadEntity_revision_id", + "unique": false, + "columnNames": [ + "revision_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)" + }, + { + "name": "index_LinkUploadEntity_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UploadBlockEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_link_id` INTEGER NOT NULL, `index` INTEGER NOT NULL, `size` INTEGER NOT NULL, `encrypted_signature` TEXT NOT NULL, `hash` TEXT NOT NULL, `token` TEXT NOT NULL, `url` TEXT NOT NULL, `raw_size` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`upload_link_id`, `index`), FOREIGN KEY(`upload_link_id`) REFERENCES `LinkUploadEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "uploadLinkId", + "columnName": "upload_link_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encryptedSignature", + "columnName": "encrypted_signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploadToken", + "columnName": "token", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rawSize", + "columnName": "raw_size", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "upload_link_id", + "index" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "LinkUploadEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "upload_link_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "UploadBulkEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `should_delete_source_uri` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volumeId", + "columnName": "volume_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parent_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shouldDeleteSourceUri", + "columnName": "should_delete_source_uri", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_UploadBulkEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_UploadBulkEntity_volume_id", + "unique": false, + "columnNames": [ + "volume_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)" + }, + { + "name": "index_UploadBulkEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_UploadBulkEntity_parent_id", + "unique": false, + "columnNames": [ + "parent_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "UploadBulkUriStringEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_bulk_id` INTEGER NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`upload_bulk_id`, `uri`), FOREIGN KEY(`upload_bulk_id`) REFERENCES `UploadBulkEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "upload_bulk_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "upload_bulk_id", + "uri" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_UploadBulkUriStringEntity_upload_bulk_id", + "unique": false, + "columnNames": [ + "upload_bulk_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkUriStringEntity_upload_bulk_id` ON `${TABLE_NAME}` (`upload_bulk_id`)" + } + ], + "foreignKeys": [ + { + "table": "UploadBulkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "upload_bulk_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "FolderMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `last_fetch_children_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchChildrenTimestamp", + "columnName": "last_fetch_children_timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + }, + { + "tableName": "TrashMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `last_fetch_trash_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastFetchTrashTimestamp", + "columnName": "last_fetch_trash_timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "ShareEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id" + ], + "referencedColumns": [ + "user_id", + "id" + ] + } + ] + }, + { + "tableName": "NotificationChannelEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`user_id`, `type`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "type" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_NotificationChannelEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationChannelEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [ + { + "table": "AccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "userId" + ] + } + ] + }, + { + "tableName": "NotificationEventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `channel_type` TEXT NOT NULL, `notification_tag` TEXT NOT NULL, `notification_id` INTEGER NOT NULL, `notification_event_id` TEXT NOT NULL, `notification_event` TEXT NOT NULL, PRIMARY KEY(`user_id`, `channel_type`, `notification_tag`, `notification_id`, `notification_event_id`), FOREIGN KEY(`user_id`, `channel_type`) REFERENCES `NotificationChannelEntity`(`user_id`, `type`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "channelType", + "columnName": "channel_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationTag", + "columnName": "notification_tag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notification_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationEventId", + "columnName": "notification_event_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationEvent", + "columnName": "notification_event", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "channel_type", + "notification_tag", + "notification_id", + "notification_event_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id", + "unique": false, + "columnNames": [ + "user_id", + "channel_type", + "notification_tag", + "notification_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id` ON `${TABLE_NAME}` (`user_id`, `channel_type`, `notification_tag`, `notification_id`)" + } + ], + "foreignKeys": [ + { + "table": "NotificationChannelEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "channel_type" + ], + "referencedColumns": [ + "user_id", + "type" + ] + } + ] + }, + { + "tableName": "LinkSelectionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `selection_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `selection_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shareId", + "columnName": "share_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "linkId", + "columnName": "link_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "selectionId", + "columnName": "selection_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id", + "share_id", + "link_id", + "selection_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_LinkSelectionEntity_user_id_share_id_link_id", + "unique": false, + "columnNames": [ + "user_id", + "share_id", + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)" + }, + { + "name": "index_LinkSelectionEntity_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id` ON `${TABLE_NAME}` (`user_id`)" + }, + { + "name": "index_LinkSelectionEntity_share_id", + "unique": false, + "columnNames": [ + "share_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_share_id` ON `${TABLE_NAME}` (`share_id`)" + }, + { + "name": "index_LinkSelectionEntity_link_id", + "unique": false, + "columnNames": [ + "link_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_link_id` ON `${TABLE_NAME}` (`link_id`)" + }, + { + "name": "index_LinkSelectionEntity_selection_id", + "unique": false, + "columnNames": [ + "selection_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_selection_id` ON `${TABLE_NAME}` (`selection_id`)" + } + ], + "foreignKeys": [ + { + "table": "LinkEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "user_id", + "share_id", + "link_id" + ], + "referencedColumns": [ + "user_id", + "share_id", + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '59591b661e49ae97591acd9c77f82223')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/3.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/3.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/3.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/3.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/4.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/4.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/4.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/4.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/5.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/5.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/5.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/5.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/6.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/6.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/6.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/6.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/7.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/7.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/7.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/7.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/8.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/8.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/8.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/8.json diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/9.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/9.json similarity index 100% rename from app/schemas/me.proton.android.drive.db.AppDatabase/9.json rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/9.json diff --git a/drive/db/src/main/AndroidManifest.xml b/drive/db/src/main/AndroidManifest.xml new file mode 100644 index 00000000..35ad1b9a --- /dev/null +++ b/drive/db/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt new file mode 100644 index 00000000..836b8db6 --- /dev/null +++ b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt @@ -0,0 +1,273 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.android.drive.db + +import android.content.Context +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.TypeConverters +import me.proton.core.account.data.db.AccountConverters +import me.proton.core.account.data.db.AccountDatabase +import me.proton.core.account.data.entity.AccountEntity +import me.proton.core.account.data.entity.AccountMetadataEntity +import me.proton.core.account.data.entity.SessionDetailsEntity +import me.proton.core.account.data.entity.SessionEntity +import me.proton.core.challenge.data.db.ChallengeConverters +import me.proton.core.challenge.data.db.ChallengeDatabase +import me.proton.core.challenge.data.entity.ChallengeFrameEntity +import me.proton.core.crypto.android.keystore.CryptoConverters +import me.proton.core.data.room.db.BaseDatabase +import me.proton.core.data.room.db.CommonConverters +import me.proton.core.drive.drivelink.data.db.DriveLinkDatabase +import me.proton.core.drive.drivelink.download.data.db.DriveLinkDownloadDatabase +import me.proton.core.drive.drivelink.offline.data.db.DriveLinkOfflineDatabase +import me.proton.core.drive.drivelink.paged.data.db.DriveLinkPagedDatabase +import me.proton.core.drive.drivelink.paged.data.db.entity.DriveLinkRemoteKeyEntity +import me.proton.core.drive.drivelink.selection.data.db.DriveLinkSelectionDatabase +import me.proton.core.drive.drivelink.shared.data.db.DriveLinkSharedDatabase +import me.proton.core.drive.drivelink.trash.data.db.DriveLinkTrashDatabase +import me.proton.core.drive.folder.data.db.FolderDatabase +import me.proton.core.drive.folder.data.db.FolderMetadataEntity +import me.proton.core.drive.link.data.db.LinkDatabase +import me.proton.core.drive.link.data.db.entity.LinkEntity +import me.proton.core.drive.link.data.db.entity.LinkFilePropertiesEntity +import me.proton.core.drive.link.data.db.entity.LinkFolderPropertiesEntity +import me.proton.core.drive.link.selection.data.db.LinkSelectionConverters +import me.proton.core.drive.link.selection.data.db.LinkSelectionDatabase +import me.proton.core.drive.link.selection.data.db.entity.LinkSelectionEntity +import me.proton.core.drive.linkdownload.data.db.LinkDownloadDatabase +import me.proton.core.drive.linkdownload.data.db.entity.DownloadBlockEntity +import me.proton.core.drive.linkdownload.data.db.entity.LinkDownloadStateEntity +import me.proton.core.drive.linknode.data.db.LinkAncestorDatabase +import me.proton.core.drive.linkoffline.data.db.LinkOfflineDatabase +import me.proton.core.drive.linkoffline.data.db.LinkOfflineEntity +import me.proton.core.drive.linktrash.data.db.LinkTrashDatabase +import me.proton.core.drive.linktrash.data.db.entity.LinkTrashStateEntity +import me.proton.core.drive.linktrash.data.db.entity.TrashMetadataEntity +import me.proton.core.drive.linktrash.data.db.entity.TrashWorkEntity +import me.proton.core.drive.linkupload.data.db.LinkUploadDatabase +import me.proton.core.drive.linkupload.data.db.entity.LinkUploadEntity +import me.proton.core.drive.linkupload.data.db.entity.UploadBlockEntity +import me.proton.core.drive.linkupload.data.db.entity.UploadBulkEntity +import me.proton.core.drive.linkupload.data.db.entity.UploadBulkUriStringEntity +import me.proton.core.drive.messagequeue.data.storage.db.MessageQueueDatabase +import me.proton.core.drive.messagequeue.data.storage.db.entity.MessageEntity +import me.proton.core.drive.notification.data.db.NotificationConverters +import me.proton.core.drive.notification.data.db.NotificationDatabase +import me.proton.core.drive.notification.data.db.entity.NotificationChannelEntity +import me.proton.core.drive.notification.data.db.entity.NotificationEventEntity +import me.proton.core.drive.share.data.db.ShareDatabase +import me.proton.core.drive.share.data.db.ShareEntity +import me.proton.core.drive.shareurl.base.data.db.ShareUrlDatabase +import me.proton.core.drive.shareurl.base.data.db.entity.ShareUrlEntity +import me.proton.core.drive.sorting.data.db.SortingDatabase +import me.proton.core.drive.sorting.data.db.entity.SortingEntity +import me.proton.core.drive.volume.data.db.VolumeDatabase +import me.proton.core.drive.volume.data.db.VolumeEntity +import me.proton.core.eventmanager.data.db.EventManagerConverters +import me.proton.core.eventmanager.data.db.EventMetadataDatabase +import me.proton.core.eventmanager.data.entity.EventMetadataEntity +import me.proton.core.featureflag.data.db.FeatureFlagDatabase +import me.proton.core.featureflag.data.entity.FeatureFlagEntity +import me.proton.core.humanverification.data.db.HumanVerificationConverters +import me.proton.core.humanverification.data.db.HumanVerificationDatabase +import me.proton.core.humanverification.data.entity.HumanVerificationEntity +import me.proton.core.key.data.db.KeySaltDatabase +import me.proton.core.key.data.db.PublicAddressDatabase +import me.proton.core.key.data.entity.KeySaltEntity +import me.proton.core.key.data.entity.PublicAddressEntity +import me.proton.core.key.data.entity.PublicAddressKeyEntity +import me.proton.core.observability.data.db.ObservabilityDatabase +import me.proton.core.observability.data.entity.ObservabilityEventEntity +import me.proton.core.payment.data.local.db.PaymentDatabase +import me.proton.core.payment.data.local.entity.GooglePurchaseEntity +import me.proton.core.user.data.db.AddressDatabase +import me.proton.core.user.data.db.UserConverters +import me.proton.core.user.data.db.UserDatabase +import me.proton.core.user.data.entity.AddressEntity +import me.proton.core.user.data.entity.AddressKeyEntity +import me.proton.core.user.data.entity.UserEntity +import me.proton.core.user.data.entity.UserKeyEntity +import me.proton.core.usersettings.data.db.OrganizationDatabase +import me.proton.core.usersettings.data.db.UserSettingsConverters +import me.proton.core.usersettings.data.db.UserSettingsDatabase +import me.proton.core.usersettings.data.entity.OrganizationEntity +import me.proton.core.usersettings.data.entity.OrganizationKeysEntity +import me.proton.core.usersettings.data.entity.UserSettingsEntity +import me.proton.drive.android.settings.data.db.AppUiSettingsDatabase +import me.proton.drive.android.settings.data.db.entity.UiSettingsEntity + +@Database( + entities = [ + // Core + AccountEntity::class, + AccountMetadataEntity::class, + SessionEntity::class, + SessionDetailsEntity::class, + UserEntity::class, + UserKeyEntity::class, + AddressEntity::class, + AddressKeyEntity::class, + KeySaltEntity::class, + PublicAddressEntity::class, + PublicAddressKeyEntity::class, + HumanVerificationEntity::class, + UserSettingsEntity::class, + OrganizationEntity::class, + OrganizationKeysEntity::class, + EventMetadataEntity::class, + FeatureFlagEntity::class, + ChallengeFrameEntity::class, + GooglePurchaseEntity::class, + ObservabilityEventEntity::class, + // Drive + VolumeEntity::class, + ShareEntity::class, + ShareUrlEntity::class, + LinkEntity::class, + LinkFilePropertiesEntity::class, + LinkFolderPropertiesEntity::class, + LinkOfflineEntity::class, + LinkDownloadStateEntity::class, + DownloadBlockEntity::class, + LinkTrashStateEntity::class, + // Trash + TrashWorkEntity::class, + // MessageQueue + MessageEntity::class, + // AppUiSettings + UiSettingsEntity::class, + // DriveLinkPaged + DriveLinkRemoteKeyEntity::class, + // Sorting + SortingEntity::class, + // Upload + LinkUploadEntity::class, + UploadBlockEntity::class, + UploadBulkEntity::class, + UploadBulkUriStringEntity::class, + FolderMetadataEntity::class, + TrashMetadataEntity::class, + // Notification + NotificationChannelEntity::class, + NotificationEventEntity::class, + // Selection + LinkSelectionEntity::class, + ], + version = DriveDatabase.VERSION, + autoMigrations = [ + AutoMigration(from = 4, to = 5), + AutoMigration(from = 5, to = 6), + AutoMigration(from = 7, to = 8), + AutoMigration(from = 9, to = 10), + AutoMigration(from = 13, to = 14), + AutoMigration(from = 15, to = 16), + AutoMigration(from = 16, to = 17), + AutoMigration(from = 17, to = 18, spec = ShareDatabase.DeleteBlockSizeFromShareEntity::class), + AutoMigration(from = 18, to = 19), + AutoMigration(from = 22, to = 23), + ], + exportSchema = true, +) +@TypeConverters( + // Core + CommonConverters::class, + AccountConverters::class, + UserConverters::class, + CryptoConverters::class, + HumanVerificationConverters::class, + UserSettingsConverters::class, + EventManagerConverters::class, + ChallengeConverters::class, + // Drive + NotificationConverters::class, + LinkSelectionConverters::class, +) +abstract class DriveDatabase : + BaseDatabase(), + AccountDatabase, + UserDatabase, + AddressDatabase, + KeySaltDatabase, + HumanVerificationDatabase, + PublicAddressDatabase, + UserSettingsDatabase, + OrganizationDatabase, + FeatureFlagDatabase, + VolumeDatabase, + ShareDatabase, + ShareUrlDatabase, + LinkDatabase, + FolderDatabase, + LinkAncestorDatabase, + LinkOfflineDatabase, + LinkDownloadDatabase, + LinkTrashDatabase, + LinkSelectionDatabase, + MessageQueueDatabase, + AppUiSettingsDatabase, + EventMetadataDatabase, + ChallengeDatabase, + SortingDatabase, + LinkUploadDatabase, + DriveLinkDatabase, + DriveLinkPagedDatabase, + DriveLinkTrashDatabase, + DriveLinkOfflineDatabase, + DriveLinkDownloadDatabase, + DriveLinkSharedDatabase, + DriveLinkSelectionDatabase, + NotificationDatabase, + PaymentDatabase, + ObservabilityDatabase { + + companion object { + const val VERSION = 23 + + private val migrations = listOf( + DriveDatabaseMigrations.MIGRATION_1_2, + DriveDatabaseMigrations.MIGRATION_2_3, + DriveDatabaseMigrations.MIGRATION_3_4, + //AutoMigration(from = 4, to = 5) + //AutoMigration(from = 5, to = 6) + DriveDatabaseMigrations.MIGRATION_6_7, + //AutoMigration(from = 7, to = 8) + DriveDatabaseMigrations.MIGRATION_8_9, + //AutoMigration(from = 9, to = 10) + DriveDatabaseMigrations.MIGRATION_10_11, + DriveDatabaseMigrations.MIGRATION_11_12, + DriveDatabaseMigrations.MIGRATION_12_13, + //AutoMigration(from = 13, to = 14) + DriveDatabaseMigrations.MIGRATION_14_15, + //AutoMigration(from = 15, to = 16) + //AutoMigration(from = 16, to = 17) + //AutoMigration(from = 17, to = 18) + //AutoMigration(from = 18, to = 19) + DriveDatabaseMigrations.MIGRATION_19_20, + DriveDatabaseMigrations.MIGRATION_20_21, + DriveDatabaseMigrations.MIGRATION_21_22, + //AutoMigration(from = 22, to = 23) + ) + + fun buildDatabase(context: Context): DriveDatabase = + databaseBuilder(context, "db-drive") + .apply { migrations.forEach { addMigrations(it) } } + .build() + } +} diff --git a/app/src/main/kotlin/me/proton/android/drive/db/AppDatabaseMigrations.kt b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabaseMigrations.kt similarity index 87% rename from app/src/main/kotlin/me/proton/android/drive/db/AppDatabaseMigrations.kt rename to drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabaseMigrations.kt index d71b5a8e..7e271812 100644 --- a/app/src/main/kotlin/me/proton/android/drive/db/AppDatabaseMigrations.kt +++ b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabaseMigrations.kt @@ -1,19 +1,19 @@ /* * Copyright (c) 2023 Proton AG. - * This file is part of Proton Drive. + * This file is part of Proton Core. * - * Proton Drive is free software: you can redistribute it and/or modify + * 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 Drive is distributed in the hope that it will be useful, + * 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 Drive. If not, see . + * along with Proton Core. If not, see . */ package me.proton.android.drive.db @@ -28,11 +28,12 @@ import me.proton.core.drive.notification.data.db.NotificationDatabase import me.proton.core.drive.share.data.db.ShareDatabase import me.proton.core.featureflag.data.db.FeatureFlagDatabase import me.proton.core.humanverification.data.db.HumanVerificationDatabase +import me.proton.core.observability.data.db.ObservabilityDatabase import me.proton.core.payment.data.local.db.PaymentDatabase import me.proton.core.usersettings.data.db.OrganizationDatabase import me.proton.core.usersettings.data.db.UserSettingsDatabase -object AppDatabaseMigrations { +object DriveDatabaseMigrations { val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { NotificationDatabase.MIGRATION_0.migrate(database) @@ -104,4 +105,9 @@ object AppDatabaseMigrations { } } + val MIGRATION_21_22 = object : Migration(21, 22) { + override fun migrate(database: SupportSQLiteDatabase) { + ObservabilityDatabase.MIGRATION_0.migrate(database) + } + } } diff --git a/drive/documentsprovider/domain/src/main/kotlin/me/proton/core/drive/documentsprovider/domain/usecase/GetDocumentsProviderRoots.kt b/drive/documentsprovider/domain/src/main/kotlin/me/proton/core/drive/documentsprovider/domain/usecase/GetDocumentsProviderRoots.kt index 98587e97..62117be0 100644 --- a/drive/documentsprovider/domain/src/main/kotlin/me/proton/core/drive/documentsprovider/domain/usecase/GetDocumentsProviderRoots.kt +++ b/drive/documentsprovider/domain/src/main/kotlin/me/proton/core/drive/documentsprovider/domain/usecase/GetDocumentsProviderRoots.kt @@ -18,14 +18,8 @@ package me.proton.core.drive.documentsprovider.domain.usecase -import kotlinx.coroutines.flow.first -import me.proton.core.accountmanager.domain.AccountManager -import javax.inject.Inject +import me.proton.core.account.domain.entity.Account -class GetDocumentsProviderRoots @Inject constructor( - private val accountManager: AccountManager, -) { - - suspend operator fun invoke() = - accountManager.getAccounts().first() +interface GetDocumentsProviderRoots { + suspend operator fun invoke(): List } diff --git a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDecryptedDriveLinks.kt b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDecryptedDriveLinks.kt index a425cba3..4447f00d 100644 --- a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDecryptedDriveLinks.kt +++ b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDecryptedDriveLinks.kt @@ -34,4 +34,10 @@ class GetDecryptedDriveLinks @Inject constructor( .mapCatching { driveLinks -> decryptDriveLinks(driveLinks) } + + operator fun invoke(parentId: FolderId, fromIndex: Int, count: Int,): Flow>> = + getFolderChildrenDriveLinks(parentId, fromIndex, count) + .mapCatching { driveLinks -> + decryptDriveLinks(driveLinks) + } } diff --git a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDriveLinks.kt b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDriveLinks.kt index c756c8e6..731b3182 100644 --- a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDriveLinks.kt +++ b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDriveLinks.kt @@ -33,4 +33,8 @@ class GetDriveLinks @Inject constructor( operator fun invoke(parentId: FolderId): Flow> = repository.getDriveLinks(parentId) .map { driveLinks -> updateIsAnyAncestorMarkedAsOffline(driveLinks) } + + operator fun invoke(parentId: FolderId, fromIndex: Int, count: Int): Flow> = + repository.getDriveLinks(parentId, fromIndex, count) + .map { driveLinks -> updateIsAnyAncestorMarkedAsOffline(driveLinks) } } diff --git a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetFolderChildrenDriveLinks.kt b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetFolderChildrenDriveLinks.kt index a8ba0a2f..2a77a89d 100644 --- a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetFolderChildrenDriveLinks.kt +++ b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetFolderChildrenDriveLinks.kt @@ -59,4 +59,25 @@ class GetFolderChildrenDriveLinks @Inject constructor( } emitAll(getDriveLinks(folderId).map { driveLinks -> driveLinks.asSuccess }) } + + operator fun invoke( + folderId: FolderId, + fromIndex: Int, + count: Int, + refresh: Flow = flowOf { folderRepository.shouldInitiallyFetchFolderChildren(folderId) } + ): Flow>> = + refresh.transform { shouldRefresh -> + if (shouldRefresh) { + fetcher> { + val (_, saveAction) = folderRepository.fetchFolderChildren( + folderId = folderId, + pageIndex = 0, + pageSize = configurationProvider.uiPageSize, + sorting = getSorting(folderId.userId).first().toFolderSorting() + ).getOrThrow() + saveAction() + } + } + emitAll(getDriveLinks(folderId, fromIndex, count).map { driveLinks -> driveLinks.asSuccess }) + } } diff --git a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetPagedDriveLinksList.kt b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetPagedDriveLinksList.kt index bc0f9282..778f222c 100644 --- a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetPagedDriveLinksList.kt +++ b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetPagedDriveLinksList.kt @@ -27,6 +27,7 @@ import me.proton.core.domain.arch.DataResult import me.proton.core.domain.entity.UserId import me.proton.core.drive.base.domain.extension.mapCatching import me.proton.core.drive.drivelink.crypto.domain.usecase.DecryptDriveLinks +import me.proton.core.drive.drivelink.domain.usecase.GetDriveLinksCount import me.proton.core.drive.drivelink.paged.domain.usecase.GetPagedDriveLinks import me.proton.core.drive.drivelink.sorting.domain.usecase.SortDriveLinks import me.proton.core.drive.link.domain.entity.FolderId @@ -42,6 +43,7 @@ class GetPagedDriveLinksList @Inject constructor( private val getMainShare: GetMainShare, private val getPagedDriveLinks: GetPagedDriveLinks, private val getDecryptedDriveLinks: GetDecryptedDriveLinks, + private val getDriveLinksCount: GetDriveLinksCount, private val getFolderChildrenDriveLinks: GetFolderChildrenDriveLinks, private val fetchDriveLinksListPage: FetchDriveLinksListPage, private val getSorting: GetSorting, @@ -74,22 +76,23 @@ class GetPagedDriveLinksList @Inject constructor( pageSize ) }, - localDriveLinks = { + localPagedDriveLinks = { fromIndex, count -> if (sorting.by == By.NAME || sorting.by == By.LAST_MODIFIED) { - getDecryptedDriveLinks(folderId) + getDecryptedDriveLinks(folderId, fromIndex, count) .mapCatching { driveLinks -> sortDriveLinks(sorting, driveLinks) } } else { - getFolderChildrenDriveLinks(folderId) + getFolderChildrenDriveLinks(folderId, fromIndex, count) .mapCatching { driveLinks -> sortDriveLinks(sorting, driveLinks) } } }, + localDriveLinksCount = { getDriveLinksCount(parentId = folderId) }, processPage = takeIf { sorting.by != By.NAME && sorting.by != By.LAST_MODIFIED }?.let { { page -> decryptDriveLinks(page) } - } + }, ) } } diff --git a/drive/drivelink-paged/domain/build.gradle.kts b/drive/drivelink-paged/domain/build.gradle.kts index 4040eab7..887f35d3 100644 --- a/drive/drivelink-paged/domain/build.gradle.kts +++ b/drive/drivelink-paged/domain/build.gradle.kts @@ -22,5 +22,5 @@ plugins { driveModule(hilt = true) { api(project(":drive:drivelink:domain")) api(libs.androidx.paging.common) - implementation(project(":drive:base:data")) + implementation(project(":drive:base:data")) // use for asPagingSource } diff --git a/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetObservablePageSize.kt b/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetObservablePageSize.kt new file mode 100644 index 00000000..1783fe40 --- /dev/null +++ b/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetObservablePageSize.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.drivelink.paged.domain.usecase + +import me.proton.core.drive.base.domain.extension.MiB +import me.proton.core.drive.base.domain.provider.ConfigurationProvider +import me.proton.core.drive.base.domain.usecase.GetMemoryInfo +import javax.inject.Inject + +class GetObservablePageSize @Inject constructor( + private val getMemoryInfo: GetMemoryInfo, + private val configurationProvider: ConfigurationProvider, +) { + operator fun invoke(): Int { + val memoryInfo = getMemoryInfo().getOrNull() + return if (memoryInfo == null || memoryInfo.isLowOnMemory) { + configurationProvider.dbPageSize + } else { + val multiplier = when { + memoryInfo.memoryClass > 512.MiB -> 4 + memoryInfo.memoryClass > 256.MiB -> 3 + memoryInfo.memoryClass > 128.MiB -> 2 + else -> 1 + } + configurationProvider.dbPageSize * multiplier + } + } +} diff --git a/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetPagedDriveLinks.kt b/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetPagedDriveLinks.kt index fb43446d..9b6b7b8b 100644 --- a/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetPagedDriveLinks.kt +++ b/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetPagedDriveLinks.kt @@ -34,6 +34,7 @@ import javax.inject.Inject class GetPagedDriveLinks @Inject constructor( private val factory: DriveLinkRemoteMediatorFactory, private val configurationProvider: ConfigurationProvider, + private val getObservablePageSize: GetObservablePageSize, ) { @OptIn(ExperimentalPagingApi::class) @@ -55,4 +56,33 @@ class GetPagedDriveLinks @Inject constructor( pagingSourceFactory = { localDriveLinks().asPagingSource(processPage = processPage) } ).flow } + + @OptIn(ExperimentalPagingApi::class) + operator fun invoke( + userId: UserId, + pagedListKey: String, + remoteDriveLinks: suspend (page: Int, pageSize: Int) -> Result, + localPagedDriveLinks: (Int, Int) -> Flow>>, + localDriveLinksCount: () -> Flow, + pageSize: Int = configurationProvider.uiPageSize, + processPage: (suspend (List) -> List)? = null, + ): Flow> { + return Pager( + PagingConfig( + pageSize = pageSize, + initialLoadSize = pageSize, + enablePlaceholders = false, + ), + remoteMediator = factory.create(userId, pagedListKey, remoteDriveLinks), + pagingSourceFactory = { + { fromIndex: Int, count: Int -> + localPagedDriveLinks(fromIndex, count) + }.asPagingSource( + sourceSize = localDriveLinksCount(), + observablePageSize = getObservablePageSize(), + processPage = processPage, + ) + } + ).flow + } } diff --git a/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/db/dao/DriveLinkSelectionDao.kt b/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/db/dao/DriveLinkSelectionDao.kt index 6d2ccce0..2ae9616f 100644 --- a/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/db/dao/DriveLinkSelectionDao.kt +++ b/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/db/dao/DriveLinkSelectionDao.kt @@ -25,6 +25,7 @@ import me.proton.core.drive.drivelink.data.db.dao.DriveLinkDao import me.proton.core.drive.drivelink.data.db.entity.DriveLinkEntityWithBlock import me.proton.core.drive.drivelink.data.db.entity.DriveLinkEntityWithBlock.Companion.SELECTION_PREFIX import me.proton.core.drive.link.selection.domain.entity.SelectionId +import me.proton.core.drive.linktrash.data.db.dao.LinkTrashDao @Dao interface DriveLinkSelectionDao : DriveLinkDao { @@ -32,7 +33,7 @@ interface DriveLinkSelectionDao : DriveLinkDao { @Query( """ SELECT ${DriveLinkDao.DRIVE_LINK_SELECT} FROM ${DriveLinkDao.DRIVE_LINK_ENTITY} - WHERE ${SELECTION_PREFIX}_${Column.SELECTION_ID} = :selectionId + WHERE ${SELECTION_PREFIX}_${Column.SELECTION_ID} = :selectionId AND ${LinkTrashDao.NOT_TRASHED_CONDITION} """ ) fun getSelectedLinks(selectionId: SelectionId): Flow> diff --git a/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/repository/DriveLinkSelectionRepositoryImpl.kt b/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/repository/DriveLinkSelectionRepositoryImpl.kt index 2ea47367..bc23f7d4 100644 --- a/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/repository/DriveLinkSelectionRepositoryImpl.kt +++ b/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/repository/DriveLinkSelectionRepositoryImpl.kt @@ -18,20 +18,54 @@ package me.proton.core.drive.drivelink.selection.data.repository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import me.proton.core.drive.base.domain.provider.ConfigurationProvider import me.proton.core.drive.drivelink.data.extension.toDriveLinks import me.proton.core.drive.drivelink.domain.entity.DriveLink import me.proton.core.drive.drivelink.selection.data.db.DriveLinkSelectionDatabase import javax.inject.Inject import me.proton.core.drive.drivelink.selection.domain.repository.DriveLinkSelectionRepository +import me.proton.core.drive.link.domain.entity.FolderId +import me.proton.core.drive.link.domain.entity.LinkId import me.proton.core.drive.link.selection.domain.entity.SelectionId class DriveLinkSelectionRepositoryImpl @Inject constructor( private val db: DriveLinkSelectionDatabase, + private val configurationProvider: ConfigurationProvider, ) : DriveLinkSelectionRepository { override fun getSelectedDriveLinks(selectionId: SelectionId): Flow> = db.driveLinkSelectionDao.getSelectedLinks(selectionId).map { entities -> entities.toDriveLinks() } + + override suspend fun selectAll( + parentId: FolderId, + selectionId: SelectionId?, + getDriveLinks: (fromIndex: Int, count: Int) -> Flow>, + selectLinks: suspend (SelectionId?, List) -> Result, + ) { + val pageSize = configurationProvider.dbPageSize + var id = selectionId + db.inTransaction { + var pageIndex = 0 + while (true) { + val driveLinks = getDriveLinks( + pageIndex++ * pageSize, + pageSize, + ) + .map { driveLinks -> driveLinks.map { driveLink -> driveLink.id } } + .first() + if (driveLinks.isNotEmpty()) { + id = selectLinks( + id, + driveLinks, + ).getOrNull() + } else { + break + } + } + } + } } diff --git a/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/repository/DriveLinkSelectionRepository.kt b/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/repository/DriveLinkSelectionRepository.kt index 9b9208a4..12cf3eb9 100644 --- a/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/repository/DriveLinkSelectionRepository.kt +++ b/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/repository/DriveLinkSelectionRepository.kt @@ -19,9 +19,18 @@ package me.proton.core.drive.drivelink.selection.domain.repository import kotlinx.coroutines.flow.Flow import me.proton.core.drive.drivelink.domain.entity.DriveLink +import me.proton.core.drive.link.domain.entity.FolderId +import me.proton.core.drive.link.domain.entity.LinkId import me.proton.core.drive.link.selection.domain.entity.SelectionId interface DriveLinkSelectionRepository { fun getSelectedDriveLinks(selectionId: SelectionId): Flow> + + suspend fun selectAll( + parentId: FolderId, + selectionId: SelectionId?, + getDriveLinks: (fromIndex: Int, count: Int) -> Flow>, + selectLinks: suspend (SelectionId?, List) -> Result, + ) } diff --git a/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/usecase/SelectAll.kt b/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/usecase/SelectAll.kt new file mode 100644 index 00000000..3ee1719d --- /dev/null +++ b/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/usecase/SelectAll.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.drivelink.selection.domain.usecase + +import me.proton.core.drive.drivelink.domain.repository.DriveLinkRepository +import me.proton.core.drive.drivelink.selection.domain.repository.DriveLinkSelectionRepository +import me.proton.core.drive.link.domain.entity.FolderId +import me.proton.core.drive.link.selection.domain.entity.SelectionId +import me.proton.core.drive.link.selection.domain.usecase.SelectLinks +import javax.inject.Inject + +class SelectAll @Inject constructor( + private val selectLinks: SelectLinks, + private val driveLinkSelectionRepository: DriveLinkSelectionRepository, + private val driveLinkRepository: DriveLinkRepository, +) { + + suspend operator fun invoke(parentId: FolderId, selectionId: SelectionId?) = driveLinkSelectionRepository.selectAll( + parentId = parentId, + selectionId = selectionId, + getDriveLinks = { fromIndex, count -> + driveLinkRepository.getDriveLinks(parentId, fromIndex, count) + }, + selectLinks = { id, linkIds -> + id?.let { + selectLinks(id, linkIds) + Result.success(id) + } ?: selectLinks(linkIds) + } + ) +} diff --git a/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorter.kt b/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorter.kt new file mode 100644 index 00000000..b4789f36 --- /dev/null +++ b/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorter.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.drivelink.sorting.domain.sorter + +import android.os.Build +import me.proton.core.drive.drivelink.domain.entity.DriveLink +import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted +import me.proton.core.drive.sorting.domain.entity.Direction +import java.text.Collator +import android.icu.text.Collator as IcuCollator +import android.icu.text.RuleBasedCollator as IcuRuleBasedCollator + +object LocaleNameSorter : Sorter() { + + private val comparator: Comparator = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + (IcuCollator.getInstance() as IcuRuleBasedCollator).apply { + numericCollation = true + } + } else { + Collator.getInstance() + } + + override fun sort(driveLinks: List, direction: Direction): List = + driveLinks.sortedWith( + compareBy { driveLink -> if (driveLink is DriveLink.Folder) 0 else 1 } + .thenBy { driveLink -> if (driveLink.isNameEncrypted) 0 else 1 } + .thenComparator { a, b -> + when (direction) { + Direction.ASCENDING -> comparator.compare(a.name, b.name) + Direction.DESCENDING -> comparator.compare(b.name, a.name) + } + } + ) +} diff --git a/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Sorter.kt b/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Sorter.kt index 2c32920f..2dab3f6d 100644 --- a/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Sorter.kt +++ b/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Sorter.kt @@ -28,7 +28,7 @@ sealed class Sorter { companion object Factory { operator fun get(by: By): Sorter = when (by) { - By.NAME -> NameSorter + By.NAME -> LocaleNameSorter By.LAST_MODIFIED -> LastModifiedSorter By.SIZE -> SizeSorter By.TYPE -> TypeSorter diff --git a/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Files.kt b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Files.kt new file mode 100644 index 00000000..2e9572dd --- /dev/null +++ b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Files.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.drivelink.sorting.domain.sorter + +import io.mockk.every +import io.mockk.mockk +import me.proton.core.crypto.common.pgp.VerificationStatus +import me.proton.core.drive.base.domain.entity.Bytes +import me.proton.core.drive.base.domain.entity.CryptoProperty +import me.proton.core.drive.base.domain.entity.TimestampS +import me.proton.core.drive.drivelink.domain.entity.DriveLink + +fun file(name: String, type: String = "", lastModified: Long = 0L, size: Long = 0L) = + mockk() + .apply(name, type, lastModified, size) + +fun cryptedFile(name: String, type: String, lastModified: Long, size: Long) = + mockk() + .apply(name, type, lastModified, size) + .apply { every { cryptoName } returns CryptoProperty.Encrypted(name) } + +fun folder(name: String, lastModified: Long, size: Long) = mockk() + .apply(name, "Folder", lastModified, size) + +fun cryptedFolder(name: String, lastModified: Long, size: Long) = + mockk() + .apply(name, "Folder", lastModified, size) + .apply { every { cryptoName } returns CryptoProperty.Encrypted(name) } + +fun T.apply( + name: String, + type: String, + lastModifiedS: Long, + sizeB: Long +) = apply { + every { cryptoName } returns CryptoProperty.Decrypted(name, VerificationStatus.Success) + every { this@apply.name } returns name + every { mimeType } returns type + every { lastModified } returns TimestampS(lastModifiedS) + every { size } returns Bytes(sizeB) +} \ No newline at end of file diff --git a/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorterTest.kt b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorterTest.kt new file mode 100644 index 00000000..9f57e8d5 --- /dev/null +++ b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorterTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.drivelink.sorting.domain.sorter + +import me.proton.core.drive.sorting.domain.entity.By +import me.proton.core.drive.sorting.domain.entity.Direction +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +class LocaleNameSorterTest { + + private val File1 = file("File1") + private val file2 = file("file2") + private val file22 = file("file22") + private val file3 = file("file3") + private val file4 = file("file4") + private val file45 = file("file45") + private val file5 = file("file5") + + private val files = listOf( + file2, + File1, + file4, + file22, + file3, + file45, + file5, + ) + + @Test + @Config(sdk = [24]) + fun `sort files by locale names ascending`() { + + val sorted = Sorter.Factory[By.NAME].sort(files, Direction.ASCENDING) + + assertEquals( + listOf( + File1, + file2, + file3, + file4, + file5, + file22, + file45, + ).map { it.name }, + sorted.map { it.name }, + ) + } + + @Test + @Config(sdk = [24]) + fun `sort files by locale names descending`() { + + val sorted = Sorter.Factory[By.NAME].sort(files, Direction.DESCENDING) + + assertEquals( + listOf( + file45, + file22, + file5, + file4, + file3, + file2, + File1, + ).map { it.name }, + sorted.map { it.name }, + ) + } + + @Test + @Config(sdk = [23]) + fun `sort files by names ascending`() { + + val sorted = Sorter.Factory[By.NAME].sort(files, Direction.ASCENDING) + + assertEquals( + listOf( + File1, + file2, + file22, + file3, + file4, + file45, + file5, + ).map { it.name }, + sorted.map { it.name }, + ) + } + + @Test + @Config(sdk = [23]) + fun `sort files by names descending`() { + + val sorted = Sorter.Factory[By.NAME].sort(files, Direction.DESCENDING) + + assertEquals( + listOf( + file5, + file45, + file4, + file3, + file22, + file2, + File1, + ).map { it.name }, + sorted.map { it.name }, + ) + } +} \ No newline at end of file diff --git a/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/SorterTest.kt b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/SorterTest.kt index e2b5f10b..cc3aa904 100644 --- a/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/SorterTest.kt +++ b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/SorterTest.kt @@ -18,18 +18,14 @@ package me.proton.core.drive.drivelink.sorting.domain.sorter -import io.mockk.every -import io.mockk.mockk import junit.framework.TestCase.assertEquals -import me.proton.core.crypto.common.pgp.VerificationStatus -import me.proton.core.drive.base.domain.entity.Bytes -import me.proton.core.drive.base.domain.entity.TimestampS -import me.proton.core.drive.base.domain.entity.CryptoProperty -import me.proton.core.drive.drivelink.domain.entity.DriveLink import me.proton.core.drive.sorting.domain.entity.By import me.proton.core.drive.sorting.domain.entity.Direction import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +@RunWith(RobolectricTestRunner::class) class SorterTest { private val drivelinks = listOf( @@ -774,26 +770,4 @@ class SorterTest { } // endregion // endregion - - private fun file(name: String, type: String, lastModified: Long, size: Long) = mockk() - .apply(name, type, lastModified, size) - - private fun cryptedFile(name: String, type: String, lastModified: Long, size: Long) = mockk() - .apply(name, type, lastModified, size) - .apply { every { cryptoName } returns CryptoProperty.Encrypted(name) } - - private fun folder(name: String, lastModified: Long, size: Long) = mockk() - .apply(name, "Folder", lastModified, size) - - private fun cryptedFolder(name: String, lastModified: Long, size: Long) = mockk() - .apply(name, "Folder", lastModified, size) - .apply { every { cryptoName } returns CryptoProperty.Encrypted(name) } - - private fun T.apply(name: String, type: String, lastModifiedS: Long, sizeB: Long) = apply { - every { cryptoName } returns CryptoProperty.Decrypted(name, VerificationStatus.Success) - every { this@apply.name } returns name - every { mimeType } returns type - every { lastModified } returns TimestampS(lastModifiedS) - every { size } returns Bytes(sizeB) - } } diff --git a/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/db/dao/DriveLinkDao.kt b/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/db/dao/DriveLinkDao.kt index f3e9deda..4ba4d5d8 100644 --- a/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/db/dao/DriveLinkDao.kt +++ b/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/db/dao/DriveLinkDao.kt @@ -49,6 +49,18 @@ interface DriveLinkDao : LinkDao { ) fun getLink(userId: UserId, shareId: String, linkId: String?): Flow> + @Query( + """ + SELECT $DRIVE_LINK_SELECT FROM $DRIVE_LINK_ENTITY + WHERE + LinkEntity.user_id = :userId AND + LinkEntity.share_id = :shareId AND + LinkEntity.parent_id = :parentId AND + ${LinkTrashDao.NOT_TRASHED_CONDITION} + """ + ) + fun getLinks(userId: UserId, shareId: String, parentId: String): Flow> + @Query( """ SELECT $DRIVE_LINK_SELECT FROM $DRIVE_LINK_ENTITY @@ -57,9 +69,20 @@ interface DriveLinkDao : LinkDao { LinkEntity.share_id = :shareId AND LinkEntity.parent_id = :parentId AND ${LinkTrashDao.NOT_TRASHED_CONDITION} - """ + LIMIT :limit OFFSET :offset + """ ) - fun getLinks(userId: UserId, shareId: String, parentId: String?): Flow> + fun getLinks(userId: UserId, shareId: String, parentId: String?, limit: Int, offset: Int): Flow> + + @Query(""" + SELECT COUNT(*) FROM (SELECT DISTINCT LinkEntity.id FROM $DRIVE_LINK_ENTITY + WHERE + LinkEntity.user_id = :userId AND + LinkEntity.share_id = :shareId AND + LinkEntity.parent_id = :parentId AND + ${LinkTrashDao.NOT_TRASHED_CONDITION}) + """) + fun getLinksCountFlow(userId: UserId, shareId: String, parentId: String?): Flow @Query( """ diff --git a/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/repository/DriveLinkRepositoryImpl.kt b/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/repository/DriveLinkRepositoryImpl.kt index 9a32c7ee..4c9e0f4a 100644 --- a/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/repository/DriveLinkRepositoryImpl.kt +++ b/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/repository/DriveLinkRepositoryImpl.kt @@ -48,6 +48,13 @@ class DriveLinkRepositoryImpl @Inject constructor( .distinctUntilChanged() .map { entities -> entities.toDriveLinks() } + override fun getDriveLinksCount(parentId: FolderId): Flow = + driveLinkDao.getLinksCountFlow(parentId.userId, parentId.shareId.id, parentId.id) + + override fun getDriveLinks(parentId: FolderId, fromIndex: Int, count: Int): Flow> = + driveLinkDao.getLinks(parentId.userId, parentId.shareId.id, parentId.id, count, fromIndex) + .map { entities -> entities.toDriveLinks() } + override fun getDriveLinks(linkIds: List): Flow> = linkIds .groupBy({ linkId -> linkId.shareId }) { linkId -> linkId.id } diff --git a/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/repository/DriveLinkRepository.kt b/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/repository/DriveLinkRepository.kt index 866733d6..55208768 100644 --- a/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/repository/DriveLinkRepository.kt +++ b/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/repository/DriveLinkRepository.kt @@ -29,5 +29,9 @@ interface DriveLinkRepository { fun getDriveLinks(parentId: FolderId): Flow> + fun getDriveLinks(parentId: FolderId, fromIndex: Int, count: Int): Flow> + + fun getDriveLinksCount(parentId: FolderId): Flow + fun getDriveLinks(linkIds: List): Flow> } diff --git a/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/usecase/GetDriveLinksCount.kt b/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/usecase/GetDriveLinksCount.kt new file mode 100644 index 00000000..ed69abc1 --- /dev/null +++ b/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/usecase/GetDriveLinksCount.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.drivelink.domain.usecase + +import kotlinx.coroutines.flow.Flow +import me.proton.core.drive.drivelink.domain.repository.DriveLinkRepository +import me.proton.core.drive.link.domain.entity.FolderId +import javax.inject.Inject + +class GetDriveLinksCount @Inject constructor( + private val repository: DriveLinkRepository, +) { + operator fun invoke(parentId: FolderId): Flow = + repository.getDriveLinksCount(parentId) +} diff --git a/drive/event-manager/data/build.gradle.kts b/drive/event-manager/data/build.gradle.kts index 52be4aef..4d3d753b 100644 --- a/drive/event-manager/data/build.gradle.kts +++ b/drive/event-manager/data/build.gradle.kts @@ -32,4 +32,5 @@ driveModule( implementation(libs.core.dataRoom) implementation(libs.core.presentation) // AppLifecycleProvider implementation(libs.core.userSettings) + implementation(libs.core.user.data) } diff --git a/drive/file/base/domain/build.gradle.kts b/drive/file/base/domain/build.gradle.kts index 8896b74a..9eae8d83 100644 --- a/drive/file/base/domain/build.gradle.kts +++ b/drive/file/base/domain/build.gradle.kts @@ -26,5 +26,4 @@ driveModule( ) { api(project(":drive:link:domain")) implementation(project(":drive:event-manager:base:domain")) - implementation(project(":drive:base:data")) } diff --git a/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/entity/XAttr.kt b/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/entity/XAttr.kt index 33309a0a..e3df3355 100644 --- a/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/entity/XAttr.kt +++ b/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/entity/XAttr.kt @@ -17,8 +17,8 @@ */ package me.proton.core.drive.file.base.domain.entity -import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable @Serializable data class XAttr( @@ -35,6 +35,8 @@ data class XAttr( val size: Long? = null, @SerialName("BlockSizes") val blockSizes: List? = null, + @SerialName("Digests") + val digests: Map? = null, ) @Serializable diff --git a/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/usecase/CreateXAttr.kt b/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/usecase/CreateXAttr.kt index f0477ccb..1abf75ad 100644 --- a/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/usecase/CreateXAttr.kt +++ b/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/usecase/CreateXAttr.kt @@ -39,12 +39,14 @@ class CreateXAttr @Inject constructor( size: Bytes, blockSizes: List, mediaResolution: MediaResolution? = null, + digests: Map? = null, ) = XAttr( common = XAttr.Common( modificationTime = dateTimeFormatter.formatToIso8601String(modificationTime), size = size.value, - blockSizes = blockSizes.map { blockSize -> blockSize.value } + blockSizes = blockSizes.map { blockSize -> blockSize.value }, + digests = digests, ), media = mediaResolution?.let { XAttr.Media( diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/Files.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/Files.kt index 9f01c1bd..eaa976dd 100644 --- a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/Files.kt +++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/Files.kt @@ -17,11 +17,11 @@ */ package me.proton.core.drive.files.presentation.component +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState @@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import me.proton.core.compose.flow.rememberFlowWithLifecycle +import me.proton.core.compose.theme.ProtonDimens.ExtraSmallSpacing import me.proton.core.drive.base.domain.entity.Percentage import me.proton.core.drive.base.presentation.component.TopAppBar import me.proton.core.drive.drivelink.domain.entity.DriveLink @@ -125,6 +126,7 @@ private inline fun ListContent( viewEvent: FilesViewEvent, uploadFileLinkList: List, lazyListState: LazyListState, + verticalArrangement : Arrangement.Vertical = Arrangement.Top, crossinline content: LazyListScope.() -> Unit, ) { val modifier = if (viewState.listContentState is ListContentState.Content) { @@ -132,7 +134,11 @@ private inline fun ListContent( } else { Modifier } - LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) { + LazyColumn( + modifier = modifier.fillMaxSize(), + state = lazyListState, + verticalArrangement = verticalArrangement, + ) { itemsIndexed(uploadFileLinkList, key = { _, uploadFileLink -> uploadFileLink.id }) { index, uploadFileLink -> UploadItem(viewState, viewEvent, index, uploadFileLink) } @@ -188,8 +194,14 @@ private fun LazyColumnItems.DisplayAsGrid( BoxWithConstraints { val itemsPerRow = floor(maxWidth / GridItemWidth).roundToInt().coerceAtLeast(1) val lazyListState = this@DisplayAsGrid.rememberLazyListState() - ListContent(viewState, viewEvent, uploadFileLinkList, lazyListState) { - FilesGridContent(this@DisplayAsGrid, GridItemWidth, itemsPerRow) { driveLink: DriveLink -> + ListContent( + viewState = viewState, + viewEvent = viewEvent, + uploadFileLinkList = uploadFileLinkList, + lazyListState = lazyListState, + verticalArrangement = Arrangement.spacedBy(ExtraSmallSpacing) + ) { + FilesGridContent(this@DisplayAsGrid, itemsPerRow) { driveLink: DriveLink -> val selected = selectedDriveLinks.contains(driveLink.id) FilesGridItem( link = driveLink, @@ -200,7 +212,7 @@ private fun LazyColumnItems.DisplayAsGrid( isClickEnabled = viewState.isClickEnabled, isTextEnabled = viewState.isTextEnabled, transferProgressFlow = remember(driveLink.downloadState) { getTransferProgress(driveLink) }, - modifier = Modifier.width(GridItemWidth), + modifier = Modifier.weight(1F), isSelected = selected, inMultiselect = selected || selectedDriveLinks.isNotEmpty(), ) diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/CircleSelection.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/CircleSelection.kt new file mode 100644 index 00000000..79840c3e --- /dev/null +++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/CircleSelection.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 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 . + */ + + +package me.proton.core.drive.files.presentation.component.files + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import me.proton.core.compose.theme.ProtonDimens +import me.proton.core.compose.theme.ProtonTheme +import me.proton.core.drive.base.presentation.R as BasePresentation +import me.proton.core.presentation.R as CorePresentation + + +@Composable +fun CircleSelection(isSelected: Boolean) { + if (isSelected) { + Image( + modifier = Modifier.size(ProtonDimens.DefaultIconSize), + painter = painterResource(id = BasePresentation.drawable.ic_checkmark_circle_filled), + contentDescription = null + ) + } else { + Icon( + modifier = Modifier.size(ProtonDimens.DefaultIconSize), + painter = painterResource(id = CorePresentation.drawable.ic_proton_circle), + tint = ProtonTheme.colors.iconWeak, + contentDescription = null + ) + } +} \ No newline at end of file diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesGridItem.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesGridItem.kt index c1e5a97c..a668d23e 100644 --- a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesGridItem.kt +++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesGridItem.kt @@ -18,6 +18,7 @@ package me.proton.core.drive.files.presentation.component.files +import android.content.res.Configuration import androidx.compose.animation.Crossfade import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image @@ -29,14 +30,17 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Checkbox -import androidx.compose.material.CheckboxDefaults import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -44,29 +48,29 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.Flow +import me.proton.core.compose.flow.rememberFlowWithLifecycle import me.proton.core.compose.theme.ProtonDimens.DefaultButtonMinHeight import me.proton.core.compose.theme.ProtonDimens.DefaultIconSize -import me.proton.core.compose.theme.ProtonTheme -import me.proton.core.drive.drivelink.domain.entity.DriveLink -import me.proton.core.drive.linkdownload.domain.entity.DownloadState import me.proton.core.compose.theme.ProtonDimens.ExtraSmallSpacing import me.proton.core.compose.theme.ProtonDimens.SmallIconSize import me.proton.core.compose.theme.ProtonDimens.SmallSpacing +import me.proton.core.compose.theme.ProtonTheme import me.proton.core.compose.theme.defaultSmall import me.proton.core.drive.base.domain.entity.Percentage -import me.proton.core.compose.flow.rememberFlowWithLifecycle import me.proton.core.drive.base.presentation.component.EncryptedItem import me.proton.core.drive.base.presentation.component.LinearProgressIndicator -import me.proton.core.drive.base.presentation.component.protonColors import me.proton.core.drive.base.presentation.component.text.TextWithMiddleEllipsis +import me.proton.core.drive.drivelink.domain.entity.DriveLink import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted +import me.proton.core.drive.linkdownload.domain.entity.DownloadState import me.proton.core.drive.thumbnail.presentation.extension.thumbnailPainter import me.proton.core.presentation.R as CorePresentation @@ -102,6 +106,10 @@ fun FilesGridItem( onLongClick(link) }, ) + .background( + color = if (isSelected) ProtonTheme.colors.backgroundSecondary else Color.Transparent, + shape = ProtonTheme.shapes.medium, + ) .padding(SmallSpacing), ) { Column( @@ -113,16 +121,11 @@ fun FilesGridItem( Crossfade(inMultiselect) { inMultiselect -> if (inMultiselect) { Box( - modifier = Modifier - .size(IconSize) - .padding(MoreButtonPadding), + modifier = Modifier.size(DefaultButtonMinHeight), contentAlignment = Alignment.Center, ) { - Checkbox( - checked = isSelected, - onCheckedChange = null, - modifier = Modifier.scale(1.25f), - colors = CheckboxDefaults.protonColors(), + CircleSelection( + isSelected = isSelected, ) } } else { @@ -189,7 +192,9 @@ fun GridItemImage( ) { Box( modifier = modifier - .size(ImageWidth, ImageHeight) + .defaultMinSize(ImageWidth, ImageHeight) + .fillMaxWidth() + .aspectRatio(ImageRatio) .border(1.dp, ProtonTheme.colors.separatorNorm, ProtonTheme.shapes.medium) .clip(ProtonTheme.shapes.medium) ) { @@ -252,5 +257,96 @@ private fun GridItemMoreButton( private val ImageHeight = 96.dp private val ImageWidth = 158.dp +private val ImageRatio = ImageWidth/ImageHeight private val MoreButtonPadding = 12.dp private const val MoreButtonAlpha = 0.7f + +@Preview( + name = "GridItem not downloaded, not favorite, not shared in light mode", + group = "light mode", + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, +) +@Preview( + name = "GridItem not downloaded, not favorite, not shared in dark mode", + group = "dark mode", + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, +) +@Suppress("unused") +@Composable +private fun PreviewGridItem() { + ProtonTheme { + Surface(modifier = Modifier.background(MaterialTheme.colors.background)) { + FilesGridItem( + link = PREVIEW_DRIVELINK, + onClick = {}, + onLongClick = {}, + onMoreOptionsClick = {}, + isClickEnabled = { false }, + isTextEnabled = { true }, + ) + } + } +} +@Preview( + name = "GridItem in multi select, selected in light mode", + group = "light mode", + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, +) +@Preview( + name = "GridItem in multi select, selected in dark mode", + group = "dark mode", + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, +) +@Suppress("unused") +@Composable +private fun PreviewSelectedGridItem() { + ProtonTheme { + Surface(modifier = Modifier.background(MaterialTheme.colors.background)) { + FilesGridItem( + link = PREVIEW_DRIVELINK, + onClick = {}, + onLongClick = {}, + onMoreOptionsClick = {}, + isClickEnabled = { false }, + isTextEnabled = { true }, + isSelected = true, + inMultiselect = true, + ) + } + } +} +@Preview( + name = "GridItem in multi select, unselected in light mode", + group = "light mode", + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, +) +@Preview( + name = "GridItem in multi select, unselected in dark mode", + group = "dark mode", + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, +) +@Suppress("unused") +@Composable +private fun PreviewUnselectedGridItem() { + ProtonTheme { + Surface(modifier = Modifier.background(MaterialTheme.colors.background)) { + FilesGridItem( + link = PREVIEW_DRIVELINK, + onClick = {}, + onLongClick = {}, + onMoreOptionsClick = {}, + isClickEnabled = { false }, + isTextEnabled = { true }, + isSelected = false, + inMultiselect = true, + ) + } + } +} + diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesList.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesList.kt index add7fa90..34d128bb 100644 --- a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesList.kt +++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesList.kt @@ -24,11 +24,11 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items @@ -39,7 +39,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp import androidx.paging.compose.items import me.proton.core.compose.component.DeferredCircularProgressIndicator import me.proton.core.compose.component.ErrorPadding @@ -47,6 +46,7 @@ import me.proton.core.compose.component.ProtonErrorMessage import me.proton.core.compose.component.ProtonErrorMessageWithAction import me.proton.core.compose.component.ProtonSecondaryButton import me.proton.core.compose.theme.ProtonDimens.DefaultSpacing +import me.proton.core.compose.theme.ProtonDimens.ExtraSmallSpacing import me.proton.core.compose.theme.ProtonDimens.MediumSpacing import me.proton.core.compose.theme.ProtonDimens.SmallSpacing import me.proton.core.compose.theme.ProtonTheme @@ -199,22 +199,21 @@ fun LazyListScope.FilesListContent( fun LazyListScope.FilesGridContent( driveLinks: LazyColumnItems, - itemSize: Dp, itemsPerRow: Int, - onItemsIndexed: @Composable (DriveLink) -> Unit, + onItemsIndexed: @Composable RowScope.(DriveLink) -> Unit, ) { require(itemsPerRow > 0) { "itemsPerRow must be > 0, value passed $itemsPerRow" } items(ceil(driveLinks.size.toFloat() / itemsPerRow).toInt()) { row -> Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier.fillMaxWidth().padding(horizontal = ExtraSmallSpacing), + horizontalArrangement = Arrangement.spacedBy(ExtraSmallSpacing), ) { repeat(itemsPerRow) { repeat -> val driveLink = driveLinks[row * itemsPerRow + repeat] if (driveLink != null) { onItemsIndexed(driveLink) } else { - Spacer(modifier = Modifier.width(itemSize)) + Spacer(modifier = Modifier.weight(1F)) } } } diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesListItem.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesListItem.kt index 3f980510..a6a3f809 100644 --- a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesListItem.kt +++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesListItem.kt @@ -31,8 +31,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Checkbox -import androidx.compose.material.CheckboxDefaults import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -44,7 +42,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext @@ -65,31 +62,20 @@ import me.proton.core.compose.theme.ProtonDimens.SmallSpacing import me.proton.core.compose.theme.ProtonTheme import me.proton.core.compose.theme.captionWeak import me.proton.core.compose.theme.default -import me.proton.core.domain.entity.UserId -import me.proton.core.drive.base.domain.entity.Attributes -import me.proton.core.drive.base.domain.entity.Bytes import me.proton.core.drive.base.domain.entity.Percentage -import me.proton.core.drive.base.domain.entity.Permissions -import me.proton.core.drive.base.domain.entity.TimestampS import me.proton.core.drive.base.domain.extension.toPercentString import me.proton.core.drive.base.presentation.component.EncryptedItem import me.proton.core.drive.base.presentation.component.LinearProgressIndicator -import me.proton.core.drive.base.presentation.component.protonColors import me.proton.core.drive.base.presentation.component.text.TextWithMiddleEllipsis import me.proton.core.drive.base.presentation.extension.currentLocale import me.proton.core.drive.drivelink.domain.entity.DriveLink import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted -import me.proton.core.drive.link.domain.entity.FileId import me.proton.core.drive.link.domain.entity.Folder -import me.proton.core.drive.link.domain.entity.FolderId -import me.proton.core.drive.link.domain.entity.Link import me.proton.core.drive.link.domain.extension.isSharedUrlExpired import me.proton.core.drive.link.presentation.extension.getSize import me.proton.core.drive.link.presentation.extension.lastModifiedRelative import me.proton.core.drive.linkdownload.domain.entity.DownloadState -import me.proton.core.drive.share.domain.entity.ShareId import me.proton.core.drive.thumbnail.presentation.extension.thumbnailPainter -import me.proton.core.drive.volume.domain.entity.VolumeId import me.proton.core.drive.base.presentation.R as BasePresentation import me.proton.core.presentation.R as CorePresentation @@ -118,7 +104,7 @@ fun FilesListItem( Box( modifier = modifier .fillMaxWidth() - .background(color = if (isSelected) ProtonTheme.colors.interactionWeakNorm else Color.Transparent) + .background(color = if (isSelected) ProtonTheme.colors.backgroundSecondary else Color.Transparent) .combinedClickable( enabled = onClick != null && isClickEnabled(link), onClick = { onClick?.invoke(link) }, @@ -135,30 +121,14 @@ fun FilesListItem( .padding(start = StartPadding, end = EndPadding) .padding(vertical = VerticalPadding), ) { - Crossfade(isSelected) { isSelected -> - if (isSelected) { - Box( - modifier = Modifier - .size(IconSize), - contentAlignment = Alignment.Center, - ) { - Checkbox( - checked = true, - onCheckedChange = null, - modifier = Modifier.scale(1.75f), - colors = CheckboxDefaults.protonColors(), - ) - } - } else { - Image( - modifier = Modifier - .size(IconSize) - .clip(RoundedCornerShape(DefaultCornerRadius)), - painter = link.thumbnailPainter().painter, - contentDescription = null, - ) - } - } + + Image( + modifier = Modifier + .size(IconSize) + .clip(RoundedCornerShape(DefaultCornerRadius)), + painter = link.thumbnailPainter().painter, + contentDescription = null, + ) Details( modifier = Modifier .weight(1f) @@ -186,6 +156,15 @@ fun FilesListItem( ) } } + } else { + Crossfade(isSelected) { isSelected -> + Box( + modifier = Modifier.size(DefaultButtonMinHeight), + contentAlignment = Alignment.Center, + ) { + CircleSelection(isSelected) + } + } } } } @@ -409,6 +388,67 @@ fun PreviewListItem() { } } } +@Preview( + name = "ListItem in multiselect, selected, in light mode", + group = "light mode", + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, +) +@Preview( + name = "ListItem in multiselect, selected, in dark mode", + group = "dark mode", + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, +) +@Suppress("unused") +@Composable +fun PreviewSelectedListItem() { + ProtonTheme { + Surface(modifier = Modifier.background(MaterialTheme.colors.background)) { + FilesListItem( + link = PREVIEW_DRIVELINK, + onClick = {}, + onLongClick = {}, + onMoreOptionsClick = {}, + isClickEnabled = { false }, + isTextEnabled = { true }, + isSelected = true, + inMultiselect = true, + ) + } + } +} + +@Preview( + name = "ListItem in multiselect, unselected, in light mode", + group = "light mode", + uiMode = Configuration.UI_MODE_NIGHT_NO, + showBackground = true, +) +@Preview( + name = "ListItem in multiselect, unselected, in dark mode", + group = "dark mode", + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, +) +@Suppress("unused") +@Composable +fun PreviewUnselectedListItem() { + ProtonTheme { + Surface(modifier = Modifier.background(MaterialTheme.colors.background)) { + FilesListItem( + link = PREVIEW_DRIVELINK, + onClick = {}, + onLongClick = {}, + onMoreOptionsClick = {}, + isClickEnabled = { false }, + isTextEnabled = { true }, + isSelected = false, + inMultiselect = true, + ) + } + } +} @Preview( name = "ListItem downloaded but not favorite, not shared in light mode", @@ -553,49 +593,6 @@ val Height = 68.dp val TitleStartPadding = 20.dp val ExtraSmallIconSize = 12.dp -private val PREVIEW_LINK = Link.File( - id = FileId(ShareId(UserId("USER_ID"), "SHARE_ID"), "FILE_ID"), - parentId = FolderId(ShareId(UserId("USER_ID"), "SHARE_ID"), "PARENT_ID"), - name = "revision_id", - size = Bytes(0L), - lastModified = TimestampS(0), - mimeType = "text/plain", - isShared = false, - key = "", - passphrase = "", - passphraseSignature = "", - numberOfAccesses = 0L, - uploadedBy = "He-Who-Must-Not-Be-Named", - isFavorite = false, - hasThumbnail = false, - activeRevisionId = "", - xAttr = null, - contentKeyPacket = "", - contentKeyPacketSignature = "", - attributes = Attributes(0), - permissions = Permissions(0), - state = Link.State.ACTIVE, - nameSignatureEmail = "", - hash = "", - expirationTime = null, - nodeKey = "", - nodePassphrase = "", - nodePassphraseSignature = "", - signatureAddress = "", - creationTime = TimestampS(0), - trashedTime = null, - shareUrlExpirationTime = null, - shareUrlId = null, -) -private val PREVIEW_DRIVELINK = DriveLink.File( - link = PREVIEW_LINK, - volumeId = VolumeId("VOLUME_ID"), - isMarkedAsOffline = false, - isAnyAncestorMarkedAsOffline = false, - downloadState = null, - trashState = null, -) - object FilesListItemComponentTestTag { const val item = "file list item" const val folder = "folder item" diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/Previews.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/Previews.kt new file mode 100644 index 00000000..c28f165e --- /dev/null +++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/Previews.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.files.presentation.component.files + +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.domain.entity.Attributes +import me.proton.core.drive.base.domain.entity.Bytes +import me.proton.core.drive.base.domain.entity.Permissions +import me.proton.core.drive.base.domain.entity.TimestampS +import me.proton.core.drive.drivelink.domain.entity.DriveLink +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.Link +import me.proton.core.drive.share.domain.entity.ShareId +import me.proton.core.drive.volume.domain.entity.VolumeId + +internal val PREVIEW_LINK = Link.File( + id = FileId(ShareId(UserId("USER_ID"), "SHARE_ID"), "FILE_ID"), + parentId = FolderId(ShareId(UserId("USER_ID"), "SHARE_ID"), "PARENT_ID"), + name = "revision_id", + size = Bytes(0L), + lastModified = TimestampS(0), + mimeType = "text/plain", + isShared = false, + key = "", + passphrase = "", + passphraseSignature = "", + numberOfAccesses = 0L, + uploadedBy = "He-Who-Must-Not-Be-Named", + isFavorite = false, + hasThumbnail = false, + activeRevisionId = "", + xAttr = null, + contentKeyPacket = "", + contentKeyPacketSignature = "", + attributes = Attributes(0), + permissions = Permissions(0), + state = Link.State.ACTIVE, + nameSignatureEmail = "", + hash = "", + expirationTime = null, + nodeKey = "", + nodePassphrase = "", + nodePassphraseSignature = "", + signatureAddress = "", + creationTime = TimestampS(0), + trashedTime = null, + shareUrlExpirationTime = null, + shareUrlId = null, +) +internal val PREVIEW_DRIVELINK = DriveLink.File( + link = PREVIEW_LINK, + volumeId = VolumeId("VOLUME_ID"), + isMarkedAsOffline = false, + isAnyAncestorMarkedAsOffline = false, + downloadState = null, + trashState = null, +) \ No newline at end of file diff --git a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolder.kt b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolder.kt index 66d1ee02..e38c3a79 100644 --- a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolder.kt +++ b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolder.kt @@ -27,11 +27,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.coroutines.android.awaitFrame import kotlinx.coroutines.delay @@ -77,7 +81,7 @@ fun CreateFolder( fun CreateFolder( @StringRes titleResId: Int, folderName: String, - selection: IntRange, + selection: IntRange?, showProgress: Boolean, modifier: Modifier = Modifier, inputError: String? = null, @@ -116,7 +120,7 @@ fun CreateFolder( @Composable fun CreateFolderContent( folderName: String, - selection: IntRange, + selection: IntRange?, modifier: Modifier = Modifier, focusRequester: FocusRequester = remember { FocusRequester() }, inputError: String? = null, @@ -127,12 +131,38 @@ fun CreateFolderContent( .height(intrinsicSize = IntrinsicSize.Max) ) { Spacer(modifier = Modifier.size(DefaultSpacing)) + var state by remember { + mutableStateOf( + TextFieldValue( + text = folderName, + selection = if (selection == null) { + TextRange.Zero + } else { + TextRange(selection.first, selection.last) + } + ) + ) + } + LaunchedEffect(folderName, selection) { + state = state.copy( + text = folderName, + selection = if (selection != null) { + TextRange(selection.first, selection.last) + } else { + state.selection + } + ) + } OutlinedTextFieldWithError( - text = folderName, - selection = selection, + textFieldValue = state, errorText = inputError, focusRequester = focusRequester, - onValueChanged = onValueChanged, + onValueChanged = { textFieldValue -> + if (state != textFieldValue) { + state = textFieldValue + onValueChanged(textFieldValue.text) + } + }, modifier = Modifier .testTag(CreateFolderComponentTestTag.folderNameTextField), ) diff --git a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewModel.kt b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewModel.kt index 16b52a96..2f0d7118 100644 --- a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewModel.kt +++ b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewModel.kt @@ -71,7 +71,7 @@ class CreateFolderViewModel @Inject constructor( ) { name, error, inProgress -> initialViewState.copy( name = name, - selection = if (error != null) name.selectAll else name.endPosition, + selection = if (error != null) name.selectAll else null, error = error, inProgress = inProgress ) @@ -133,9 +133,6 @@ class CreateFolderViewModel @Inject constructor( private val String.selectAll get() = IntRange(0, length) - private val String.endPosition - get() = IntRange(length, length) - companion object { const val KEY_SHARE_ID = "shareId" const val KEY_PARENT_ID = "parentId" diff --git a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewState.kt b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewState.kt index ff6d8fd2..2731b5a1 100644 --- a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewState.kt +++ b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewState.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable data class CreateFolderViewState( @StringRes val titleResId: Int, val name: String, - val selection: IntRange = IntRange(name.length, name.length), + val selection: IntRange? = null, val error: String? = null, val inProgress: Boolean = false ) diff --git a/drive/link-trash/data-test/build.gradle.kts b/drive/link-trash/data-test/build.gradle.kts new file mode 100644 index 00000000..3c52a90a --- /dev/null +++ b/drive/link-trash/data-test/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 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 . + */ + +plugins { + id("com.android.library") +} + +driveModule( + hilt = true, +) { + api(project(":drive:link-trash:domain")) + implementation(project(":drive:link:data-test")) +} + +configureJacoco() diff --git a/drive/link-trash/data-test/src/main/AndroidManifest.xml b/drive/link-trash/data-test/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0b69d23f --- /dev/null +++ b/drive/link-trash/data-test/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + diff --git a/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/di/TestLinkTrashBindModule.kt b/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/di/TestLinkTrashBindModule.kt new file mode 100644 index 00000000..179c0d35 --- /dev/null +++ b/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/di/TestLinkTrashBindModule.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.linktrash.data.test.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.ExperimentalCoroutinesApi +import me.proton.core.drive.linktrash.data.test.repository.StubbedLinkTrashRepository +import me.proton.core.drive.linktrash.domain.repository.LinkTrashRepository +import javax.inject.Singleton + +@Suppress("unused") +@ExperimentalCoroutinesApi +@Module +@InstallIn(SingletonComponent::class) +interface TestLinkTrashBindModule { + @Binds + @Singleton + fun bindLinkTrashRepository(repository: StubbedLinkTrashRepository): LinkTrashRepository +} diff --git a/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepository.kt b/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepository.kt new file mode 100644 index 00000000..46a06e87 --- /dev/null +++ b/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepository.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.linktrash.data.test.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.transform +import me.proton.core.domain.arch.DataResult +import me.proton.core.domain.arch.ResponseSource +import me.proton.core.drive.link.data.test.NullableFile +import me.proton.core.drive.link.data.test.NullableFolder +import me.proton.core.drive.link.domain.entity.BaseLink +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.Link +import me.proton.core.drive.link.domain.entity.LinkId +import me.proton.core.drive.linktrash.domain.entity.TrashState +import me.proton.core.drive.linktrash.domain.repository.LinkTrashRepository +import me.proton.core.drive.share.domain.entity.ShareId +import javax.inject.Inject + +class StubbedLinkTrashRepository @Inject constructor() : LinkTrashRepository { + + private val workIds = MutableStateFlow(emptyMap>()) + private val stateFlow = MutableStateFlow(emptyMap, TrashState>()) + private var trashContent = emptyList() + + val state: Map, TrashState> + get() = stateFlow.value + + override suspend fun insertOrUpdateTrashState(linkIds: List, trashState: TrashState) { + stateFlow.value = stateFlow.value + (linkIds to trashState) + } + + override suspend fun removeTrashState(linkIds: List) { + stateFlow.value = stateFlow.value.filterKeys { it != linkIds } + } + + override suspend fun markTrashedLinkAsDeleted(shareId: ShareId) { + stateFlow.value = stateFlow.value.map { (linkIds, trashState) -> + if (linkIds.any { linkId -> linkId.shareId == shareId }) { + linkIds to TrashState.DELETED + } else { + linkIds to trashState + } + }.toMap() + } + + override fun hasTrashContent(shareId: ShareId): Flow { + return stateFlow.transform { state -> + emit( + state.filterKeys { it.any { linkId -> linkId.shareId == shareId } } + .filterValues { it in listOf(TrashState.TRASHING, TrashState.DELETED) } + .isNotEmpty() + ) + } + } + + override suspend fun hasWorkWithId(workId: String): Boolean { + return workIds.value.containsKey(workId) + } + + override suspend fun insertOrIgnoreWorkId(linkIds: List, workId: String) { + workIds.value = workIds.value + (workId to linkIds) + } + + override suspend fun insertWork(linkIds: List, retries: Int): DataResult { + val workId = "work-id-${linkIds.first().shareId.id}" + workIds.value = workIds.value + (workId to linkIds) + return DataResult.Success(ResponseSource.Local, workId) + } + + override suspend fun getLinksAndRemoveWorkFromCache(workId: String): List { + val work = workIds.value.filterKeys { id -> id == workId } + workIds.value = workIds.value.filterNot { (id, _) -> id == workId } + return work.values.map { linkIds -> + linkIds.map { linkId -> + val parentId = FolderId(linkId.shareId, "folder-id") + when (linkId) { + is FolderId -> NullableFolder(id = linkId, parentId = parentId) + is FileId -> NullableFile(id = linkId, parentId = parentId) + } as Link + } + }.flatten() + } + + override suspend fun markTrashContentAsFetched(shareId: ShareId) { + trashContent = trashContent + shareId + } + + override suspend fun shouldInitiallyFetchTrashContent(shareId: ShareId): Boolean { + return trashContent.contains(shareId).not() + } + + override suspend fun isTrashed(linkId: LinkId): Boolean { + return stateFlow.value + .filterKeys { linkIds -> linkId in linkIds } + .filterValues { trashState -> trashState.isTrashed() } + .isNotEmpty() + } + + override suspend fun isAnyTrashed(linkIds: Set): Boolean { + return linkIds.fold(false) { acc, linkId -> + acc || isTrashed(linkId) + } + } + + private fun TrashState.isTrashed(): Boolean { + return this !in listOf(TrashState.TRASHING, TrashState.DELETED) + } +} + +val LinkTrashRepository.state + get() = (this as StubbedLinkTrashRepository).state + +fun LinkTrashRepository.stateForLinks(vararg links: BaseLink): TrashState = + (this as StubbedLinkTrashRepository).state.getValue(links.map { it.id }) \ No newline at end of file diff --git a/drive/link-trash/data-test/src/test/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepositoryTest.kt b/drive/link-trash/data-test/src/test/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepositoryTest.kt new file mode 100644 index 00000000..8b36767b --- /dev/null +++ b/drive/link-trash/data-test/src/test/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepositoryTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.linktrash.data.test.repository + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.link.domain.entity.FileId +import me.proton.core.drive.link.domain.entity.FolderId +import me.proton.core.drive.linktrash.domain.entity.TrashState +import me.proton.core.drive.share.domain.entity.ShareId +import org.junit.Assert.* +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class StubbedLinkTrashRepositoryTest { + + private val repository = StubbedLinkTrashRepository() + + + private val userId = UserId("user-id") + private val shareId = ShareId(userId, "share-id") + private val folderId = FolderId(shareId, "folder-id") + private val otherShareId = ShareId(userId, "other-share-id") + private val otherFolderId = FolderId(otherShareId, "other-folder-id") + + @Test + fun insertOrUpdateTrashState() = runTest { + repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING) + + assertEquals(TrashState.TRASHING, repository.state[listOf(folderId)]) + } + + @Test + fun removeTrashState() = runTest { + repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING) + repository.insertOrUpdateTrashState(listOf(otherFolderId), TrashState.TRASHING) + + repository.removeTrashState(listOf(folderId)) + + assertEquals(mapOf(listOf(otherFolderId) to TrashState.TRASHING), repository.state) + } + + @Test + fun markTrashedLinkAsDeleted() = runTest { + repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING) + repository.insertOrUpdateTrashState(listOf(otherFolderId), TrashState.TRASHING) + + repository.markTrashedLinkAsDeleted(shareId) + + assertEquals( + mapOf( + listOf(folderId) to TrashState.DELETED, + listOf(otherFolderId) to TrashState.TRASHING, + ), repository.state + ) + } + + @Test + fun hasTrashContent() = runTest { + val hasTrashContent = repository.hasTrashContent(shareId) + assertFalse(hasTrashContent.first()) + + repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING) + + assertTrue(hasTrashContent.first()) + } + + @Test + fun hasWorkWithId() = runTest { + assertFalse(repository.hasWorkWithId("work-id-share-id")) + + repository.insertWork(listOf(folderId)) + + assertTrue(repository.hasWorkWithId("work-id-share-id")) + } + + @Test + fun insertOrIgnoreWorkId() = runTest { + val workId = "work-id" + + repository.insertOrIgnoreWorkId(listOf(folderId), workId) + + assertTrue(repository.hasWorkWithId(workId)) + } + + @Test + fun insertWork() = runTest { + repository.insertWork(listOf(folderId)) + + assertTrue(repository.hasWorkWithId("work-id-share-id")) + } + + @Test + fun `Given folder id When getLinksAndRemoveWorkFromCache Then returns a folder`() = runTest { + val workId = "work-id" + + repository.insertOrIgnoreWorkId(listOf(folderId), workId) + + val links = repository.getLinksAndRemoveWorkFromCache(workId) + + assertEquals(folderId, links.first().id) + assertFalse(repository.hasWorkWithId(workId)) + } + + @Test + fun `Given file id When getLinksAndRemoveWorkFromCache Then returns a file`() = runTest { + val workId = "work-id" + val fileId = FileId(shareId, "file-id") + + repository.insertOrIgnoreWorkId(listOf(fileId), workId) + + val links = repository.getLinksAndRemoveWorkFromCache(workId) + + assertEquals(fileId, links.first().id) + assertFalse(repository.hasWorkWithId(workId)) + } + + @Test + fun shouldInitiallyFetchTrashContent() = runTest { + assertTrue(repository.shouldInitiallyFetchTrashContent(shareId)) + + repository.markTrashContentAsFetched(shareId) + + assertFalse(repository.shouldInitiallyFetchTrashContent(shareId)) + } + + @Test + fun `Given empty When isTrashed Then returns false`() = runTest { + assertFalse(repository.isTrashed(folderId)) + } + + @Test + fun `Given trashed folder When isTrashed Then returns true`() = runTest { + repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHED) + + assertTrue(repository.isTrashed(folderId)) + } + + @Test + fun `Given trashing folder When isTrashed Then returns false`() = runTest { + repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING) + + assertFalse(repository.isTrashed(folderId)) + } + + @Test + fun `Given empty When isAnyTrashed Then returns false`() = runTest { + assertFalse(repository.isAnyTrashed(setOf(folderId))) + } + + @Test + fun `Given trashed folder When isAnyTrashed Then returns true`() = runTest { + repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHED) + + assertTrue(repository.isAnyTrashed(setOf(folderId))) + } + + @Test + fun `Given trashing folder When isAnyTrashed Then returns false`() = runTest { + repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING) + + assertFalse(repository.isAnyTrashed(setOf(folderId))) + } +} \ No newline at end of file diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/LinkUploadDao.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/LinkUploadDao.kt index bfe42840..b6632a80 100644 --- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/LinkUploadDao.kt +++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/LinkUploadDao.kt @@ -131,6 +131,12 @@ abstract class LinkUploadDao : BaseDao() { WHERE id = :id """) abstract fun updateMediaResolution(id: Long, mediaResolutionWidth: Long, mediaResolutionHeight: Long) + @Query(""" + UPDATE LinkUploadEntity SET + digests = :digests + WHERE id = :id + """) + abstract fun updateDigests(id: Long, digests: String) open fun getDistinctFlow(id: Long) = getFlow(id).distinctUntilChanged() } diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/LinkUploadEntity.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/LinkUploadEntity.kt index c5ee3479..4275d765 100644 --- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/LinkUploadEntity.kt +++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/LinkUploadEntity.kt @@ -27,6 +27,7 @@ import me.proton.core.domain.entity.UserId import me.proton.core.drive.base.data.db.Column import me.proton.core.drive.base.data.db.Column.CONTENT_KEY_PACKET import me.proton.core.drive.base.data.db.Column.CONTENT_KEY_PACKET_SIGNATURE +import me.proton.core.drive.base.data.db.Column.DIGESTS import me.proton.core.drive.base.data.db.Column.ID import me.proton.core.drive.base.data.db.Column.LAST_MODIFIED import me.proton.core.drive.base.data.db.Column.LINK_ID @@ -113,4 +114,6 @@ data class LinkUploadEntity( val mediaResolutionWidth: Long? = null, @ColumnInfo(name = MEDIA_RESOLUTION_HEIGHT, defaultValue = "NULL") val mediaResolutionHeight: Long? = null, + @ColumnInfo(name = DIGESTS, defaultValue = "NULL") + val digests: String? = null, ) diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/LinkUploadEntity.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/LinkUploadEntity.kt index 565a303a..ff0c0400 100644 --- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/LinkUploadEntity.kt +++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/LinkUploadEntity.kt @@ -17,14 +17,18 @@ */ package me.proton.core.drive.linkupload.data.extension +import kotlinx.serialization.SerializationException +import me.proton.core.data.room.BuildConfig import me.proton.core.drive.base.domain.entity.MediaResolution import me.proton.core.drive.base.domain.entity.TimestampMs import me.proton.core.drive.base.domain.extension.bytes import me.proton.core.drive.link.domain.entity.FolderId import me.proton.core.drive.linkupload.data.db.entity.LinkUploadEntity +import me.proton.core.drive.linkupload.domain.entity.UploadDigests import me.proton.core.drive.linkupload.domain.entity.UploadFileLink import me.proton.core.drive.share.domain.entity.ShareId import me.proton.core.drive.volume.domain.entity.VolumeId +import me.proton.core.util.kotlin.deserialize import me.proton.core.util.kotlin.takeIfNotEmpty fun LinkUploadEntity.toUploadFileLink() = @@ -54,5 +58,15 @@ fun LinkUploadEntity.toUploadFileLink() = width = requireNotNull(mediaResolutionWidth), height = requireNotNull(mediaResolutionHeight), ) - } + }, + digests = digests?.let { json -> + try { + UploadDigests(json.deserialize()) + }catch (e: SerializationException){ + if (BuildConfig.DEBUG) { + throw e + } + UploadDigests() + } + } ?: UploadDigests() ) diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt index a4d8a859..8e9e04e9 100644 --- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt +++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt @@ -37,10 +37,12 @@ import me.proton.core.drive.linkupload.data.extension.toUploadBulkEntity import me.proton.core.drive.linkupload.data.extension.toUploadFileLink import me.proton.core.drive.linkupload.domain.entity.UploadBlock import me.proton.core.drive.linkupload.domain.entity.UploadBulk +import me.proton.core.drive.linkupload.domain.entity.UploadDigests import me.proton.core.drive.linkupload.domain.entity.UploadFileLink import me.proton.core.drive.linkupload.domain.entity.UploadState import me.proton.core.drive.linkupload.domain.factory.UploadBlockFactory import me.proton.core.drive.linkupload.domain.repository.LinkUploadRepository +import me.proton.core.util.kotlin.serialize import javax.inject.Inject class LinkUploadRepositoryImpl @Inject constructor( @@ -143,6 +145,11 @@ class LinkUploadRepositoryImpl @Inject constructor( mediaResolutionWidth = mediaResolution.width, mediaResolutionHeight = mediaResolution.height, ) + override suspend fun updateUploadFileLinkDigests(uploadFileLinkId: Long, digests: UploadDigests) = + db.linkUploadDao.updateDigests( + id = uploadFileLinkId, + digests = digests.values.serialize() + ) override suspend fun removeUploadFileLink(uploadFileLinkId: Long) = db.linkUploadDao.delete(uploadFileLinkId) diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadDigests.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadDigests.kt new file mode 100644 index 00000000..fa3c18fd --- /dev/null +++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadDigests.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.linkupload.domain.entity + +data class UploadDigests(val values: Map = emptyMap()) diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadFileLink.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadFileLink.kt index abff3069..93d0baa1 100644 --- a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadFileLink.kt +++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadFileLink.kt @@ -47,4 +47,5 @@ data class UploadFileLink( val uriString: String? = null, val shouldDeleteSourceUri: Boolean = false, val mediaResolution: MediaResolution? = null, + val digests : UploadDigests = UploadDigests() ) diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt index 660afc12..7bc5a935 100644 --- a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt +++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt @@ -26,6 +26,7 @@ import me.proton.core.drive.link.domain.entity.FileId import me.proton.core.drive.link.domain.entity.FolderId import me.proton.core.drive.linkupload.domain.entity.UploadBlock import me.proton.core.drive.linkupload.domain.entity.UploadBulk +import me.proton.core.drive.linkupload.domain.entity.UploadDigests import me.proton.core.drive.linkupload.domain.entity.UploadFileLink import me.proton.core.drive.linkupload.domain.entity.UploadState @@ -75,6 +76,8 @@ interface LinkUploadRepository { suspend fun updateUploadFileLinkMediaResolution(uploadFileLinkId: Long, mediaResolution: MediaResolution) + suspend fun updateUploadFileLinkDigests(uploadFileLinkId: Long, digests: UploadDigests) + suspend fun removeUploadFileLink(uploadFileLinkId: Long) suspend fun insertUploadBlocks(uploadFileLink: UploadFileLink, uploadBlocks: List) diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateDigests.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateDigests.kt new file mode 100644 index 00000000..f9a62db2 --- /dev/null +++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateDigests.kt @@ -0,0 +1,35 @@ +/* + * 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 . + */ +package me.proton.core.drive.linkupload.domain.usecase + +import me.proton.core.drive.linkupload.domain.entity.UploadDigests +import me.proton.core.drive.linkupload.domain.entity.UploadFileLink +import me.proton.core.drive.linkupload.domain.repository.LinkUploadRepository +import javax.inject.Inject + +class UpdateDigests @Inject constructor( + private val linkUploadRepository: LinkUploadRepository, + private val getUploadFileLinkAfterOperation: GetUploadFileLinkAfterOperation, +) { + suspend operator fun invoke( + uploadFileLinkId: Long, + digests: UploadDigests, + ): Result = getUploadFileLinkAfterOperation(uploadFileLinkId) { + linkUploadRepository.updateUploadFileLinkDigests(uploadFileLinkId, digests) + } +} diff --git a/drive/link/data-test/build.gradle.kts b/drive/link/data-test/build.gradle.kts new file mode 100644 index 00000000..befc3d36 --- /dev/null +++ b/drive/link/data-test/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 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 . + */ + +plugins { + id("com.android.library") +} + +driveModule() { + api(project(":drive:link:domain")) +} diff --git a/drive/link/data-test/src/main/AndroidManifest.xml b/drive/link/data-test/src/main/AndroidManifest.xml new file mode 100644 index 00000000..954a3398 --- /dev/null +++ b/drive/link/data-test/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + diff --git a/drive/link/data-test/src/main/kotlin/me/proton/core/drive/link/data/test/NullableBaseLink.kt b/drive/link/data-test/src/main/kotlin/me/proton/core/drive/link/data/test/NullableBaseLink.kt new file mode 100644 index 00000000..b595e42e --- /dev/null +++ b/drive/link/data-test/src/main/kotlin/me/proton/core/drive/link/data/test/NullableBaseLink.kt @@ -0,0 +1,190 @@ +/* + * Copyright (c) 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 . + */ + +@file:Suppress("FunctionName") +/* + * Copyright (c) 2023 Proton Technologies AG + * This file is part of Proton Technologies AG and ProtonCore. + * + * ProtonCore 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. + * + * ProtonCore 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 ProtonCore. If not, see . + */ +package me.proton.core.drive.link.data.test + +import me.proton.core.drive.base.domain.entity.Attributes +import me.proton.core.drive.base.domain.entity.Bytes +import me.proton.core.drive.base.domain.entity.Permissions +import me.proton.core.drive.base.domain.entity.TimestampS +import me.proton.core.drive.link.domain.entity.File +import me.proton.core.drive.link.domain.entity.FileId +import me.proton.core.drive.link.domain.entity.Folder +import me.proton.core.drive.link.domain.entity.FolderId +import me.proton.core.drive.link.domain.entity.Link +import me.proton.core.drive.shareurl.base.domain.entity.ShareUrlId + +fun NullableFile(parentId: FolderId, filename: String = "test-file"): File { + return NullableFile(id = FileId(parentId.shareId, filename), parentId = parentId) +} + +fun NullableFile( + id: FileId, + parentId: FolderId, + name: String = "", + size: Bytes = Bytes(0L), + lastModified: TimestampS = TimestampS(0L), + mimeType: String = "", + isShared: Boolean = false, + key: String = "", + passphrase: String = "", + passphraseSignature: String = "", + numberOfAccesses: Long = 0, + shareUrlExpirationTime: TimestampS? = null, + uploadedBy: String = "", + isFavorite: Boolean = false, + attributes: Attributes = Attributes(0L), + permissions: Permissions = Permissions(0L), + state: Link.State = Link.State.ACTIVE, + nameSignatureEmail: String? = null, + hash: String = "", + expirationTime: TimestampS? = null, + nodeKey: String = "", + nodePassphrase: String = "", + nodePassphraseSignature: String = "", + signatureAddress: String = "", + creationTime: TimestampS = TimestampS(0L), + trashedTime: TimestampS? = null, + hasThumbnail: Boolean = false, + activeRevisionId: String = "", + xAttr: String? = null, + shareUrlId: ShareUrlId? = null, + contentKeyPacket: String = "", + contentKeyPacketSignature: String? = null, +): File { + return Link.File( + id = id, + parentId = parentId, + name = name, + size = size, + lastModified = lastModified, + mimeType = mimeType, + isShared = isShared, + key = key, + passphrase = passphrase, + passphraseSignature = passphraseSignature, + numberOfAccesses = numberOfAccesses, + shareUrlExpirationTime = shareUrlExpirationTime, + uploadedBy = uploadedBy, + isFavorite = isFavorite, + attributes = attributes, + permissions = permissions, + state = state, + nameSignatureEmail = nameSignatureEmail, + hash = hash, + expirationTime = expirationTime, + nodeKey = nodeKey, + nodePassphrase = nodePassphrase, + nodePassphraseSignature = nodePassphraseSignature, + signatureAddress = signatureAddress, + creationTime = creationTime, + trashedTime = trashedTime, + hasThumbnail = hasThumbnail, + activeRevisionId = activeRevisionId, + xAttr = xAttr, + shareUrlId = shareUrlId, + contentKeyPacket = contentKeyPacket, + contentKeyPacketSignature = contentKeyPacketSignature, + ) +} + +fun NullableFolder(parentId: FolderId, filename: String = "test-folder"): Folder { + return NullableFolder(id = FolderId(parentId.shareId, filename), parentId = parentId) +} + +fun NullableFolder( + id: FolderId, + parentId: FolderId, + name: String = "", + size: Bytes = Bytes(0L), + lastModified: TimestampS = TimestampS(0L), + mimeType: String = "", + isShared: Boolean = false, + key: String = "", + passphrase: String = "", + passphraseSignature: String = "", + numberOfAccesses: Long = 0, + shareUrlExpirationTime: TimestampS? = null, + uploadedBy: String = "", + isFavorite: Boolean = false, + attributes: Attributes = Attributes(0L), + permissions: Permissions = Permissions(0L), + state: Link.State = Link.State.ACTIVE, + nameSignatureEmail: String? = null, + hash: String = "", + expirationTime: TimestampS? = null, + nodeKey: String = "", + nodePassphrase: String = "", + nodePassphraseSignature: String = "", + signatureAddress: String = "", + creationTime: TimestampS = TimestampS(0L), + trashedTime: TimestampS? = null, + xAttr: String? = null, + shareUrlId: ShareUrlId? = null, + nodeHashKey: String = "", +): Folder { + return Link.Folder( + id = id, + parentId = parentId, + name = name, + size = size, + lastModified = lastModified, + mimeType = mimeType, + isShared = isShared, + key = key, + passphrase = passphrase, + passphraseSignature = passphraseSignature, + numberOfAccesses = numberOfAccesses, + shareUrlExpirationTime = shareUrlExpirationTime, + uploadedBy = uploadedBy, + isFavorite = isFavorite, + attributes = attributes, + permissions = permissions, + state = state, + nameSignatureEmail = nameSignatureEmail, + hash = hash, + expirationTime = expirationTime, + nodeKey = nodeKey, + nodePassphrase = nodePassphrase, + nodePassphraseSignature = nodePassphraseSignature, + signatureAddress = signatureAddress, + creationTime = creationTime, + trashedTime = trashedTime, + xAttr = xAttr, + shareUrlId = shareUrlId, + nodeHashKey = nodeHashKey + ) +} \ No newline at end of file diff --git a/drive/link/data/build.gradle.kts b/drive/link/data/build.gradle.kts index 907c8892..5f6a550d 100644 --- a/drive/link/data/build.gradle.kts +++ b/drive/link/data/build.gradle.kts @@ -32,5 +32,6 @@ driveModule( implementation(project(":drive:share:data")) implementation(libs.retrofit) androidTestImplementation(libs.bundles.core) + androidTestImplementation(project(":drive:db")) kaptAndroidTest(libs.androidx.room.compiler) } diff --git a/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/LinkWithPropertiesTest.kt b/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/LinkWithPropertiesTest.kt index 02c71359..996ae251 100644 --- a/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/LinkWithPropertiesTest.kt +++ b/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/LinkWithPropertiesTest.kt @@ -18,11 +18,13 @@ package me.proton.core.drive.link.data.db import android.content.Context +import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking +import me.proton.android.drive.db.DriveDatabase import me.proton.core.account.data.entity.AccountEntity import me.proton.core.account.domain.entity.AccountState import me.proton.core.domain.entity.UserId @@ -40,12 +42,12 @@ import java.io.IOException @RunWith(AndroidJUnit4::class) class LinkWithPropertiesTest { - private lateinit var db: TestDatabase + private lateinit var db: DriveDatabase @Before fun createDb() { val context = ApplicationProvider.getApplicationContext() - db = buildDatabase(context) + db = Room.inMemoryDatabaseBuilder(context, DriveDatabase::class.java).build() runBlocking { prepareDb(db) } } @@ -93,10 +95,10 @@ class LinkWithPropertiesTest { val link = insertAndGetLink(LinkWithProperties(testParentLink, testFolderProperties(testParentLink.id))) // endregion // region When - db.linkDao().delete(link) + db.linkDao.delete(link) // endregion //region Then - assertFalse(db.linkDao().hasLinkEntity(testParentLink.userId, testParentLink.shareId, testParentLink.id).first()) + assertFalse(db.linkDao.hasLinkEntity(testParentLink.userId, testParentLink.shareId, testParentLink.id).first()) // endregion } @@ -107,14 +109,14 @@ class LinkWithPropertiesTest { insertAndGetLink(LinkWithProperties(testLink, testFileProperties(testLink.id))) // endregion // region When - db.linkDao().delete(parentLink) + db.linkDao.delete(parentLink) // endregion //region Then - assertFalse(db.linkDao().hasLinkEntity(testLink.userId, testLink.shareId, testLink.id).first()) + assertFalse(db.linkDao.hasLinkEntity(testLink.userId, testLink.shareId, testLink.id).first()) // endregion } - private suspend fun insertAndGetLink(linkWithProperties: LinkWithProperties) = with (db.linkDao()) { + private suspend fun insertAndGetLink(linkWithProperties: LinkWithProperties) = with (db.linkDao) { insertOrUpdate(linkWithProperties) getLinkWithPropertiesFlow( userId = linkWithProperties.link.userId, @@ -125,9 +127,9 @@ class LinkWithPropertiesTest { .first() } - private suspend fun prepareDb(db: TestDatabase) { + private suspend fun prepareDb(db: DriveDatabase) { db.accountDao().insertOrUpdate(testAccount) - db.shareDao().insertOrUpdate(testShare) + db.shareDao.insertOrUpdate(testShare) } private val testAccount = diff --git a/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/TestDatabase.kt b/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/TestDatabase.kt deleted file mode 100644 index 120e5d36..00000000 --- a/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/TestDatabase.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2021-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 . - */ -package me.proton.core.drive.link.data.db - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import me.proton.core.account.data.db.AccountDao -import me.proton.core.account.data.entity.AccountEntity -import me.proton.core.account.data.entity.SessionEntity -import me.proton.core.data.room.db.CommonConverters -import me.proton.core.drive.link.data.db.entity.LinkEntity -import me.proton.core.drive.link.data.db.entity.LinkFilePropertiesEntity -import me.proton.core.drive.link.data.db.entity.LinkFolderPropertiesEntity -import me.proton.core.drive.share.data.db.ShareDao -import me.proton.core.drive.share.data.db.ShareEntity -import me.proton.core.user.data.db.UserConverters - -@Database( - entities = [ - AccountEntity::class, - SessionEntity::class, - LinkEntity::class, - LinkFilePropertiesEntity::class, - LinkFolderPropertiesEntity::class, - ShareEntity::class, - ], - version = 1, - exportSchema = false, -) -@TypeConverters( - CommonConverters::class, - UserConverters::class, -) -abstract class TestDatabase : RoomDatabase() { - abstract fun linkDao(): LinkDao - abstract fun accountDao(): AccountDao - abstract fun shareDao(): ShareDao -} - -fun buildDatabase(context: Context): TestDatabase = - Room.inMemoryDatabaseBuilder( - context, TestDatabase::class.java - ).build() diff --git a/drive/link/data/src/main/kotlin/me/proton/core/drive/link/data/api/extension/GetLinkResponses.kt b/drive/link/data/src/main/kotlin/me/proton/core/drive/link/data/api/extension/GetLinkResponses.kt index f193a8e4..f2800b71 100644 --- a/drive/link/data/src/main/kotlin/me/proton/core/drive/link/data/api/extension/GetLinkResponses.kt +++ b/drive/link/data/src/main/kotlin/me/proton/core/drive/link/data/api/extension/GetLinkResponses.kt @@ -18,10 +18,10 @@ package me.proton.core.drive.link.data.api.extension -import me.proton.core.data.arch.toDataResult import me.proton.core.domain.arch.DataResult import me.proton.core.domain.arch.ResponseSource import me.proton.core.drive.base.data.api.ProtonApiCode +import me.proton.core.drive.base.domain.extension.toDataResult import me.proton.core.drive.link.data.api.response.LinkResponses import me.proton.core.drive.link.domain.entity.LinkId import me.proton.core.network.domain.ApiException @@ -53,6 +53,6 @@ inline fun associateResults( try { block().mapResults(links) } catch (e: ApiException) { - val result = e.error.toDataResult() + val result = e.toDataResult() links.associateWith { result } } diff --git a/drive/navigation-drawer/presentation/src/main/kotlin/me/proton/core/drive/navigationdrawer/presentation/NavigationDrawer.kt b/drive/navigation-drawer/presentation/src/main/kotlin/me/proton/core/drive/navigationdrawer/presentation/NavigationDrawer.kt index cfb99b55..2cbe155a 100644 --- a/drive/navigation-drawer/presentation/src/main/kotlin/me/proton/core/drive/navigationdrawer/presentation/NavigationDrawer.kt +++ b/drive/navigation-drawer/presentation/src/main/kotlin/me/proton/core/drive/navigationdrawer/presentation/NavigationDrawer.kt @@ -246,6 +246,7 @@ fun NavigationDrawerListItem( ) { ProtonListItem( icon = icon, + iconTintColor = ProtonTheme.colors.iconWeak, title = title, modifier = modifier .clickable { diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/ImagePreview.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/ImagePreview.kt index 37b1eaef..514d5891 100644 --- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/ImagePreview.kt +++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/ImagePreview.kt @@ -27,41 +27,31 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import coil.annotation.ExperimentalCoilApi import coil.compose.rememberImagePainter import coil.request.ImageRequest -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import me.proton.core.compose.theme.ProtonTheme +import me.proton.core.compose.theme.isNightMode import me.proton.core.drive.files.preview.R -import me.proton.core.drive.files.preview.presentation.component.state.ZoomEffect @Composable -@OptIn(ExperimentalCoilApi::class) fun ImagePreview( uri: Uri, - zoomEffect: Flow, + transformationState: TransformationState, isFullScreen: Boolean, modifier: Modifier = Modifier, ) { val backgroundColor by animateColorAsState( - targetValue = if (isFullScreen) { + targetValue = if (isFullScreen && isNightMode().not()) { MaterialTheme.colors.onBackground } else { MaterialTheme.colors.background @@ -76,37 +66,24 @@ fun ImagePreview( ImagePreview( modifier = modifier.background(backgroundColor), painter = painter, - zoomEffect = zoomEffect, + transformationState = transformationState, ) } @Composable fun ImagePreview( painter: Painter, - zoomEffect: Flow, + transformationState: TransformationState, modifier: Modifier = Modifier, ) { - var scale by remember { mutableStateOf(1f) } - var offset by remember { mutableStateOf(Offset.Zero) } - - LaunchedEffect(LocalContext.current) { - zoomEffect - .onEach { zoomEffect -> - if (zoomEffect is ZoomEffect.Reset) { - scale = 1f - offset = Offset.Zero - } - } - .launchIn(this) - } - val state = rememberTransformableState { zoomChange, offsetChange, _ -> - scale *= zoomChange - if (scale < 1f) scale = 1f - offset += offsetChange + transformationState.scale = (transformationState.scale * zoomChange) + transformationState.addOffset(offsetChange) } Box( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .onPlaced { transformationState.containerLayout = it }, contentAlignment = Alignment.Center ) { Image( @@ -114,12 +91,13 @@ fun ImagePreview( contentDescription = stringResource(id = R.string.content_description_file_preview), contentScale = ContentScale.Fit, modifier = modifier - .fillMaxSize() + .onPlaced { transformationState.contentLayout = it } .graphicsLayer( - scaleX = scale, - scaleY = scale, - translationX = offset.x, - translationY = offset.y + scaleX = transformationState.scale, + scaleY = transformationState.scale, + translationX = transformationState.offset.x, + translationY = transformationState.offset.y, + clip = true, ) .transformable( state = state @@ -134,7 +112,7 @@ fun PreviewImagePreview() { ProtonTheme { ImagePreview( uri = Uri.parse("https://protonmail.com/images/media/live/protonmail-shot-decrypt.jpg"), - zoomEffect = emptyFlow(), + transformationState = rememberTransformationState(), isFullScreen = false, ) } diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/MediaPreview.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/MediaPreview.kt index af1d0ee5..e5797cd7 100644 --- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/MediaPreview.kt +++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/MediaPreview.kt @@ -42,6 +42,7 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView import me.proton.core.compose.theme.ProtonTheme +import me.proton.core.compose.theme.isNightMode import me.proton.core.drive.base.presentation.extension.conditional @Composable @@ -54,7 +55,7 @@ fun MediaPreview( mediaControllerVisibility: (Boolean) -> Unit = {} ) { val backgroundColor by animateColorAsState( - targetValue = if (isFullScreen) { + targetValue = if (isFullScreen && isNightMode().not()) { MaterialTheme.colors.onBackground } else { MaterialTheme.colors.background diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/PdfPreview.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/PdfPreview.kt index 5cb7721d..72f63c33 100644 --- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/PdfPreview.kt +++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/PdfPreview.kt @@ -17,68 +17,90 @@ */ package me.proton.core.drive.files.preview.presentation.component +import android.annotation.SuppressLint import android.content.Context import android.graphics.Bitmap import android.graphics.pdf.PdfRenderer import android.net.Uri -import androidx.annotation.WorkerThread +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.rememberTransformableState +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.gestures.transformable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp -import coil.annotation.ExperimentalCoilApi +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import me.proton.core.compose.theme.ProtonTheme import me.proton.core.compose.theme.overline -import me.proton.core.drive.files.preview.presentation.component.state.ZoomEffect +import java.io.IOException +import kotlin.math.min +import kotlin.math.roundToInt @Composable fun PdfPreview( uri: Uri, - zoomEffect: Flow, + transformationState: TransformationState, modifier: Modifier = Modifier, onRenderFailed: (Throwable) -> Unit, ) { - var pages by remember { mutableStateOf>(emptyList()) } val context = LocalContext.current - LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { + + val pdfReader by produceState(initialValue = null, uri) { + value = PdfReader(context).apply { try { - pages = context.renderPdf(uri) - } catch (t: Throwable) { - onRenderFailed(t) + openPdf(uri) + } catch (e: IOException){ + onRenderFailed(e) + } + } + awaitDispose { + launch { + value?.close() } } } - if (pages.isEmpty()) { + val reader = pdfReader + if (reader == null) { PdfLoading(modifier) } else { PdfPreview( modifier = modifier, - zoomEffect = zoomEffect, - pages = pages, + transformationState = transformationState, + onRenderFailed = onRenderFailed, + reader = reader, ) } } @@ -94,27 +116,73 @@ fun PdfLoading(modifier: Modifier = Modifier) { } @Composable -@OptIn(ExperimentalCoilApi::class) +@OptIn(ExperimentalFoundationApi::class) fun PdfPreview( - zoomEffect: Flow, - pages: List, + transformationState: TransformationState, + reader: PdfReader, + onRenderFailed: (Throwable) -> Unit, modifier: Modifier = Modifier, ) { + val lazyListState = rememberLazyListState() + + val density = LocalDensity.current.density + val maxWidth = with(LocalDensity.current) { + LocalConfiguration.current.smallestScreenWidthDp.dp.roundToPx() * 2 + } + LazyColumn( modifier = modifier .fillMaxSize() - .background(Color.White) + .onPlaced { transformationState.containerLayout = it } + .background(Color.White), + userScrollEnabled = !transformationState.hasScale(), + state = lazyListState, + flingBehavior = rememberSnapFlingBehavior(lazyListState) ) { - itemsIndexed(pages) { index, item -> - Column { - ImagePreview( - painter = BitmapPainter(item), - modifier = Modifier.fillParentMaxHeight(), - zoomEffect = zoomEffect - ) + items(reader.pageCount) { index -> + var item by remember { mutableStateOf(null) } + + LaunchedEffect(index) { + try { + item = reader.renderPage(index, density, maxWidth) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + onRenderFailed(e) + } + } + + val transformableState = rememberTransformableState { zoomChange, offsetChange, _ -> + transformationState.scale = transformationState.scale * zoomChange + transformationState.addOffset(offsetChange) + } + Box( + modifier = Modifier + .fillParentMaxSize() + .clipToBounds(), + contentAlignment = Alignment.Center, + ) { + if (item != null) { + Image( + painter = BitmapPainter(item as ImageBitmap), + contentDescription = null, + modifier = Modifier + .onPlaced { transformationState.contentLayout = it } + .graphicsLayer( + scaleX = transformationState.scale, + scaleY = transformationState.scale, + translationX = transformationState.offset.x, + translationY = transformationState.offset.y, + ) + .transformable(transformableState), + ) + } PageNumber( pageNumber = index + 1, - pageCount = pages.size, + pageCount = reader.pageCount, + modifier = Modifier + .navigationBarsPadding() + .align(Alignment.BottomCenter) ) } } @@ -141,27 +209,56 @@ fun PageNumber( } } -@WorkerThread -fun Context.renderPdf(uri: Uri): List { - val pfd = contentResolver.openAssetFileDescriptor(uri, "r")?.parcelFileDescriptor ?: return emptyList() - return PdfRenderer(pfd).use { pdfRenderer -> - (0 until pdfRenderer.pageCount).map { pageIndex -> - renderPage( - pdfRenderer = pdfRenderer, - pageIndex = pageIndex, - ) +class PdfReader(private val context: Context) { + private var pdfRenderer: PdfRenderer? = null + + val pageCount: Int + get() = pdfRenderer?.pageCount ?: -1 + + private val mutex = Mutex() + + @SuppressLint("Recycle") + suspend fun openPdf(uri: Uri) { + withContext(Dispatchers.IO) { + pdfRenderer = context.contentResolver.openAssetFileDescriptor(uri, "r")?.let { + PdfRenderer(it.parcelFileDescriptor) + } } } + + suspend fun close() { + withContext(Dispatchers.IO) { + mutex.withLock { + pdfRenderer?.close() + pdfRenderer = null + } + } + } + + suspend fun renderPage( + pageIndex: Int, + density: Float, + maxWidth: Int, + ): ImageBitmap { + return withContext(Dispatchers.IO) { + mutex.withLock { + val page = pdfRenderer?.openPage(pageIndex) + ?: throw IllegalStateException("Open page with index $pageIndex failed.") + page.use { + val width = min(page.width.fromPtToPx(density), maxWidth) + Bitmap.createBitmap( + width, + page.height * width / page.width, + Bitmap.Config.ARGB_8888 + ).also { bitmap -> + page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + }.asImageBitmap() + } + } + } + } + } -internal fun renderPage( - pdfRenderer: PdfRenderer, - pageIndex: Int, -): ImageBitmap { - val page = pdfRenderer.openPage(pageIndex) ?: throw IllegalStateException("Open page with index $pageIndex failed.") - return page.use { - Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888).also { bitmap -> - page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) - }.asImageBitmap() - } -} +private fun Int.fromPtToPx(density: Float): Int = + (this.toFloat() / 72F * 160F * density).roundToInt() diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/Preview.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/Preview.kt index 21b17a37..03f83ada 100644 --- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/Preview.kt +++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/Preview.kt @@ -17,13 +17,13 @@ */ package me.proton.core.drive.files.preview.presentation.component -import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets @@ -35,7 +35,9 @@ import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -45,11 +47,11 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -61,7 +63,6 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.rememberPagerState import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow @@ -80,12 +81,12 @@ import me.proton.core.drive.base.presentation.component.TopAppBar import me.proton.core.drive.base.presentation.entity.FileTypeCategory import me.proton.core.drive.base.presentation.extension.conditional import me.proton.core.drive.base.presentation.extension.debugOnly +import me.proton.core.drive.base.presentation.extension.isLandscape import me.proton.core.drive.files.preview.R import me.proton.core.drive.files.preview.presentation.component.event.PreviewViewEvent import me.proton.core.drive.files.preview.presentation.component.state.ContentState import me.proton.core.drive.files.preview.presentation.component.state.PreviewContentState import me.proton.core.drive.files.preview.presentation.component.state.PreviewViewState -import me.proton.core.drive.files.preview.presentation.component.state.ZoomEffect import me.proton.core.util.kotlin.exhaustive import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -98,19 +99,11 @@ import me.proton.core.presentation.R as CorePresentation fun Preview( viewState: PreviewViewState, viewEvent: PreviewViewEvent, - zoomEffect: Flow, modifier: Modifier = Modifier, onPageChanged: FlowCollector? = null, ) { - val detectTapGestureModifier = Modifier - .pointerInput(Unit) { - detectTapGestures( - onTap = { viewEvent.onSingleTap() }, - onDoubleTap = { viewEvent.onDoubleTap() } - ) - } val pagerState = rememberPagerState(initialPage = viewState.currentIndex) - + val userScrollEnabled = remember { mutableStateOf(true) } val isFullScreen by rememberFlowWithLifecycle(viewState.isFullscreen).collectAsState(false) onPageChanged?.let { @@ -133,16 +126,16 @@ fun Preview( HorizontalPager( state = pagerState, count = viewState.items.size, + userScrollEnabled = userScrollEnabled.value, key = { page -> viewState.items[page].key } ) { page -> PreviewContent( viewState.items[page], isFullScreen, viewEvent, - zoomEffect, with(LocalDensity.current) { topBarHeightAnimated.toDp() }, isFocused = pagerState.currentPage == page, - detectTapGestureModifier, + userScrollEnabled, ) } AnimatedVisibility( @@ -150,7 +143,7 @@ fun Preview( enter = slideInVertically(initialOffsetY = { fullHeight: Int -> -fullHeight }), exit = slideOutVertically(targetOffsetY = { fullHeight: Int -> -fullHeight }), ) { - val item = viewState.items.getOrNull(viewState.currentIndex) + val item = viewState.items.getOrNull(pagerState.currentPage) TopAppBar( modifier = Modifier .background(appBarGradient) @@ -160,7 +153,8 @@ fun Preview( } .onSizeChanged { size -> topBarHeight = size.height - }.testTag(PreviewComponentTestTag.screen), + } + .testTag(PreviewComponentTestTag.screen), navigationIcon = painterResource(id = viewState.navigationIconResId), onNavigationIcon = { viewEvent.onTopAppBarNavigation() }, title = item?.title ?: "", @@ -178,9 +172,6 @@ fun Preview( } } -private val isLandscape: Boolean @Composable get() = - LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE - private val appBarGradient: Brush @Composable get() = Brush.verticalGradient( colors = listOf(0.8f, 0.7f, 0.6f, 0.5f, 0f).map { alpha -> ProtonTheme.colors.backgroundNorm.copy(alpha = alpha) @@ -192,10 +183,9 @@ fun PreviewContent( item: PreviewViewState.Item, isFullScreen: Boolean, viewEvent: PreviewViewEvent, - zoomEffect: Flow, topBarHeight: Dp, isFocused: Boolean, - modifier: Modifier = Modifier, + userScrollEnabled: MutableState, ) { val contentState by rememberFlowWithLifecycle(item.contentState).collectAsState( ContentState.Downloading(null) @@ -215,10 +205,9 @@ fun PreviewContent( item.category.toComposable(), isFullScreen, viewEvent, - zoomEffect, topBarHeight, isFocused, - modifier, + userScrollEnabled ) ContentState.NotFound -> PreviewNotFound() is ContentState.Error -> { @@ -275,21 +264,55 @@ fun PreviewContentAvailable( previewComposable: PreviewComposable, isFullScreen: Boolean, viewEvent: PreviewViewEvent, - zoomEffect: Flow, topBarHeight: Dp, isFocused: Boolean, + userScrollEnabled: MutableState, + transformationState : TransformationState = rememberTransformationState(), + onDoubleTap : () -> Unit = { transformationState.scale = 2F }, modifier: Modifier = Modifier, ) { + val dragEnable = transformationState.hasScale() + + if (isFocused) { + DisposableEffect(dragEnable) { + userScrollEnabled.value = !dragEnable + onDispose { + userScrollEnabled.value = true + } + } + } + + val pointerInputModifier = modifier + .pointerInput(Unit) { + detectTapGestures( + onTap = { viewEvent.onSingleTap() }, + onDoubleTap = { + if (transformationState.scale == 1F) { + onDoubleTap() + } else { + transformationState.scale = 1F + transformationState.offset = Offset.Zero + } + } + ) + } + .pointerInput(Unit, dragEnable) { + if (!dragEnable) return@pointerInput + detectDragGestures { _, dragAmount -> + transformationState.addOffset(dragAmount) + } + } + when (previewComposable) { PreviewComposable.Image -> ImagePreview( - modifier = modifier, + modifier = pointerInputModifier, uri = contentState.uri, - zoomEffect = zoomEffect, + transformationState = transformationState, isFullScreen = isFullScreen, ) PreviewComposable.Sound, PreviewComposable.Video -> MediaPreview( - modifier = modifier, + modifier = pointerInputModifier, uri = contentState.uri, isFullScreen = isFullScreen, play = isFocused, @@ -297,13 +320,13 @@ fun PreviewContentAvailable( ) PreviewComposable.Pdf -> PdfPreview( uri = contentState.uri, - modifier = modifier.padding(top = topBarHeight), - zoomEffect = zoomEffect, + modifier = pointerInputModifier.padding(top = topBarHeight), + transformationState = transformationState, onRenderFailed = viewEvent.onRenderFailed, ) PreviewComposable.Text -> TextPreview( uri = contentState.uri, - modifier = modifier.padding(top = topBarHeight), + modifier = pointerInputModifier.padding(top = topBarHeight), onRenderFailed = viewEvent.onRenderFailed, ) PreviewComposable.Unknown -> UnknownPreview() @@ -448,11 +471,9 @@ fun PreviewPreviewLoadingState() { override val onTopAppBarNavigation: () -> Unit = {} override val onMoreOptions: () -> Unit = {} override val onSingleTap: () -> Unit = {} - override val onDoubleTap: () -> Unit = {} override val onRenderFailed: (Throwable) -> Unit = {} override val mediaControllerVisibility: (Boolean) -> Unit = {} }, - zoomEffect = emptyFlow(), ) } } diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/TransformationState.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/TransformationState.kt new file mode 100644 index 00000000..8320e446 --- /dev/null +++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/TransformationState.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.files.preview.presentation.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.unit.IntSize + +@Composable +fun rememberTransformationState( + initialScale: Float = 1F, + initialOffset: Offset = Offset.Zero, + initialMinScale: Float = 1F, + initialMaxScale: Float = 4F, +): TransformationState { + return rememberSaveable(saver = TransformationState.Saver) { + TransformationState( + initialScale = initialScale, + initialOffset = initialOffset, + initialMinScale = initialMinScale, + initialMaxScale = initialMaxScale, + ) + } +} + +class TransformationState( + private val initialScale: Float = 1F, + private val initialOffset: Offset = Offset.Zero, + val initialMinScale: Float = 1F, + val initialMaxScale: Float = 4F, +) { + + var containerLayout: LayoutCoordinates? = null + var contentLayout: LayoutCoordinates? = null + set(value) { + // Do not take LayoutCoordinates with a size of zero it would stop updating the offset + if(value?.size != IntSize.Zero) { + field = value + } + } + + private var _scale by mutableStateOf(initialScale) + var scale + get() = _scale + set(value) { + _scale = value.coerceIn(minScale, maxScale) + } + var offset by mutableStateOf(initialOffset) + var minScale by mutableStateOf(initialMinScale) + var maxScale by mutableStateOf(initialMaxScale) + + fun hasScale() = scale > 1F + + fun addOffset(dragAmount: Offset) { + val containerSize = containerLayout?.size + val contentSize = contentLayout?.size + if (containerSize == null || contentSize == null) { + // let the offset go outside bounds sizes are not set + offset += dragAmount + } else { + val horizontalLimit = ((contentSize.width * scale - containerSize.width) / 2F) + .coerceAtLeast(0F) + val verticalLimit = ((contentSize.height * scale - containerSize.height) / 2F) + .coerceAtLeast(0F) + + val offsetX = (offset.x + dragAmount.x) + .coerceIn(minimumValue = -horizontalLimit, maximumValue = horizontalLimit) + val offsetY = (offset.y + dragAmount.y) + .coerceIn(minimumValue = -verticalLimit, maximumValue = verticalLimit) + offset = Offset(x = offsetX, y = offsetY) + } + } + + companion object { + val Saver: Saver = listSaver( + save = { + listOf( + it.scale, + it.offset.x, + it.offset.y, + it.minScale, + it.maxScale, + ) + }, + restore = { + TransformationState( + initialScale = it[0], + initialOffset = Offset(it[1], it[2]), + initialMinScale = it[3], + initialMaxScale = it[4], + ) + } + ) + } +} \ No newline at end of file diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/event/PreviewViewEvent.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/event/PreviewViewEvent.kt index 8c66329c..2fb6a485 100644 --- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/event/PreviewViewEvent.kt +++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/event/PreviewViewEvent.kt @@ -22,7 +22,6 @@ interface PreviewViewEvent { val onTopAppBarNavigation: () -> Unit val onMoreOptions: () -> Unit val onSingleTap: () -> Unit - val onDoubleTap: () -> Unit val onRenderFailed: (Throwable) -> Unit val mediaControllerVisibility: (Boolean) -> Unit } diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/state/PreviewViewState.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/state/PreviewViewState.kt index 544523bf..e3fd1510 100644 --- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/state/PreviewViewState.kt +++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/state/PreviewViewState.kt @@ -40,10 +40,6 @@ data class PreviewViewState( ) } -sealed class ZoomEffect { - object Reset : ZoomEffect() -} - sealed interface PreviewContentState { object Loading : PreviewContentState object Empty : PreviewContentState diff --git a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/Settings.kt b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/Settings.kt index baad9947..be6a0dc6 100644 --- a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/Settings.kt +++ b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/Settings.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -46,10 +47,12 @@ import me.proton.core.drive.settings.presentation.component.DebugSettings import me.proton.core.drive.settings.presentation.component.ExternalSettingsEntry import me.proton.core.drive.settings.presentation.component.ThemeChooserDialog import me.proton.core.drive.settings.presentation.event.SettingsViewEvent +import me.proton.core.drive.settings.presentation.extension.toString import me.proton.core.drive.settings.presentation.state.LegalLink import me.proton.core.drive.settings.presentation.state.SettingsViewState import me.proton.core.usersettings.presentation.compose.view.CrashReportSettingToggleItem import me.proton.core.usersettings.presentation.compose.view.TelemetrySettingToggleItem +import kotlin.time.Duration.Companion.seconds import me.proton.core.drive.base.presentation.R as BasePresentation import me.proton.core.presentation.R as CorePresentation @@ -80,6 +83,24 @@ fun Settings( ) Column(Modifier.verticalScroll(rememberScrollState())) { + ProtonSettingsHeader(title = R.string.settings_section_security) + + ProtonSettingsItem( + name = stringResource(id = R.string.settings_app_lock), + hint = stringResource(id = viewState.appAccessSubtitleResId), + ) { + viewEvent.onAppAccess() + } + + if (viewState.isAutoLockDurationsVisible) { + ProtonSettingsItem( + name = stringResource(id = R.string.settings_auto_lock), + hint = viewState.autoLockDuration.toString(LocalContext.current), + ) { + viewEvent.onAutoLockDurations() + } + } + ProtonSettingsHeader(title = R.string.settings_section_appearance_settings) ProtonSettingsItem( @@ -89,14 +110,7 @@ fun Settings( showThemeDialog = true } - if (viewState.isAppLockEnabled) { - ProtonSettingsHeader(title = R.string.settings_section_app_settings) - ProtonSettingsItem( - name = stringResource(id = R.string.settings_pin_biomtric_lock_entry), - hint = stringResource(R.string.settings_pin_biomtric_lock_status_off), - ) - } if (viewState.legalLinks.isNotEmpty()) { ProtonSettingsHeader(title = R.string.settings_section_about) @@ -158,11 +172,16 @@ private fun SettingsPreview() { ), availableStyles = emptyList(), currentStyle = BasePresentation.string.common_cancel_action, + appAccessSubtitleResId = BasePresentation.string.common_cancel_action, + isAutoLockDurationsVisible = true, + autoLockDuration = 0.seconds, ), viewEvent = SettingsViewEvent( navigateBack = {}, onLinkClicked = {}, onThemeStyleChanged = {}, + onAppAccess = {}, + onAutoLockDurations = {} ) ) } diff --git a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/component/SettingsEntry.kt b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/component/SettingsEntry.kt index 15b87c63..7e15f65f 100644 --- a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/component/SettingsEntry.kt +++ b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/component/SettingsEntry.kt @@ -206,7 +206,7 @@ private fun PreviewExternalSettingsEntry() { modifier = Modifier.background(ProtonTheme.colors.backgroundNorm), link = LegalLink.External( text = BasePresentation.string.title_app, - url = R.string.settings_section_app_settings, + url = R.string.settings_section_security, )) { } } diff --git a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/event/SettingsViewEvent.kt b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/event/SettingsViewEvent.kt index a6b22dc5..3d24b1f1 100644 --- a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/event/SettingsViewEvent.kt +++ b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/event/SettingsViewEvent.kt @@ -24,4 +24,6 @@ data class SettingsViewEvent( val navigateBack: () -> Unit, val onLinkClicked: (LegalLink) -> Unit, val onThemeStyleChanged: (Int) -> Unit, + val onAppAccess: () -> Unit, + val onAutoLockDurations: () -> Unit, ) diff --git a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/extension/Duration.kt b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/extension/Duration.kt new file mode 100644 index 00000000..32f233ed --- /dev/null +++ b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/extension/Duration.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.settings.presentation.extension + +import android.content.Context +import me.proton.core.drive.base.presentation.extension.quantityString +import kotlin.time.Duration +import me.proton.core.drive.settings.R as SettingsPresentation + +fun Duration.toString(context: Context): String = when { + inWholeSeconds == 0L -> context.getString(SettingsPresentation.string.settings_auto_lock_durations_immediately) + inWholeSeconds < 60 -> context.quantityString(SettingsPresentation.plurals.settings_auto_lock_durations_seconds, inWholeSeconds.toInt()) + inWholeMinutes < 60 -> context.quantityString(SettingsPresentation.plurals.settings_auto_lock_durations_minutes, inWholeMinutes.toInt()) + inWholeHours < 24 -> context.quantityString(SettingsPresentation.plurals.settings_auto_lock_durations_hours, inWholeHours.toInt()) + else -> context.quantityString(SettingsPresentation.plurals.settings_auto_lock_durations_days, inWholeDays.toInt()) +} diff --git a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/state/SettingsViewState.kt b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/state/SettingsViewState.kt index b30910ff..37c4e0c5 100644 --- a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/state/SettingsViewState.kt +++ b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/state/SettingsViewState.kt @@ -21,6 +21,7 @@ package me.proton.core.drive.settings.presentation.state import androidx.annotation.DrawableRes import androidx.annotation.StringRes import me.proton.core.drive.settings.presentation.component.DebugSettingsStateAndEvent +import kotlin.time.Duration data class SettingsViewState( @DrawableRes val navigationIcon: Int, @@ -30,7 +31,9 @@ data class SettingsViewState( val availableStyles: List, @StringRes val currentStyle: Int, val debugSettingsStateAndEvent: DebugSettingsStateAndEvent? = null, - val isAppLockEnabled: Boolean = false, + @StringRes val appAccessSubtitleResId: Int, + val isAutoLockDurationsVisible: Boolean, + val autoLockDuration: Duration, ) sealed class LegalLink( diff --git a/drive/settings/src/main/res/values/strings.xml b/drive/settings/src/main/res/values/strings.xml index 6756fd7c..122cf3d8 100644 --- a/drive/settings/src/main/res/values/strings.xml +++ b/drive/settings/src/main/res/values/strings.xml @@ -17,10 +17,27 @@ --> - - Off - App settings + App Lock + Security About + Automatically lock the app + Immediately + + After %1$d second + After %1$d seconds + + + After %1$d minute + After %1$d minutes + + + After %1$d hour + After %1$d hours + + + After %1$d day + After %1$d days + + + diff --git a/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/di/TestModule.kt b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/di/TestModule.kt new file mode 100644 index 00000000..14fa858c --- /dev/null +++ b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/di/TestModule.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.share.data.test.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import me.proton.core.drive.share.data.test.repository.StubbedShareRepository +import me.proton.core.drive.share.domain.repository.ShareRepository +import javax.inject.Singleton + +@Suppress("unused") +@Module +@InstallIn(SingletonComponent::class) +interface TestModule { + @Binds + @Singleton + fun bindsShareRepository(manager: StubbedShareRepository): ShareRepository +} diff --git a/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/nullable/NullableShare.kt b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/nullable/NullableShare.kt new file mode 100644 index 00000000..c1e664d8 --- /dev/null +++ b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/nullable/NullableShare.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 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 . + */ + +@file:Suppress("FunctionName") +/* + * Copyright (c) 2023 Proton Technologies AG + * This file is part of Proton Technologies AG and ProtonCore. + * + * ProtonCore 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. + * + * ProtonCore 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 ProtonCore. If not, see . + */ +package me.proton.core.drive.share.data.test.nullable + +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.domain.entity.TimestampS +import me.proton.core.drive.share.domain.entity.Share +import me.proton.core.drive.share.domain.entity.ShareId +import me.proton.core.drive.volume.domain.entity.VolumeId +import me.proton.core.user.domain.entity.AddressId + +fun NullableShare( + id: ShareId = ShareId(UserId("user-id"), "share-id"), + volumeId: VolumeId = VolumeId("volume-id"), + rootLinkId: String = "", + addressId: AddressId? = null, + isMain: Boolean = false, + isLocked: Boolean = false, + key: String = "", + passphrase: String = "", + passphraseSignature: String = "", + creationTime: TimestampS? = null +) = Share( + id = id, + volumeId = volumeId, + rootLinkId = rootLinkId, + addressId = addressId, + isMain = isMain, + isLocked = isLocked, + key = key, + passphrase = passphrase, + passphraseSignature = passphraseSignature, + creationTime = creationTime +) \ No newline at end of file diff --git a/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/repository/StubbedShareRepository.kt b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/repository/StubbedShareRepository.kt new file mode 100644 index 00000000..94bf52e0 --- /dev/null +++ b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/repository/StubbedShareRepository.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.share.data.test.repository +/* + * Copyright (c) 2023 Proton Technologies AG + * This file is part of Proton Technologies AG and ProtonCore. + * + * ProtonCore 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. + * + * ProtonCore 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 ProtonCore. If not, see . + */ +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import me.proton.core.domain.arch.DataResult +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.domain.extension.asSuccess +import me.proton.core.drive.share.data.test.nullable.NullableShare +import me.proton.core.drive.share.domain.entity.Share +import me.proton.core.drive.share.domain.entity.ShareId +import me.proton.core.drive.share.domain.entity.ShareInfo +import me.proton.core.drive.share.domain.repository.ShareRepository +import me.proton.core.drive.volume.domain.entity.VolumeId +import javax.inject.Inject + +class StubbedShareRepository @Inject constructor() : ShareRepository { + + private val sharesFlow = MutableStateFlow( + listOf( + NullableShare( + mainShareId, + isMain = true, + key = "key", + passphrase = "passphrase", + passphraseSignature = "passphraseSignature", + ) + ) + ) + + override fun getSharesFlow(userId: UserId): Flow>> = + filterShares { share -> share.id.userId == userId }.map { it.asSuccess } + + override fun getSharesFlow(userId: UserId, volumeId: VolumeId): Flow>> = + filterShares { share -> share.id.userId == userId && share.volumeId == volumeId }.map { it.asSuccess } + + override suspend fun hasShares(userId: UserId): Boolean { + return fetchShares(userId).isNotEmpty() + } + + override suspend fun hasShares(userId: UserId, volumeId: VolumeId): Boolean { + return filterShares { share -> share.id.userId == userId }.first().isNotEmpty() + } + + override suspend fun fetchShares(userId: UserId): List { + return filterShares { share -> share.id.userId == userId }.first() + } + + override fun getShareFlow(shareId: ShareId): Flow> { + return filterShares { share -> share.id == shareId }.map { it.first().asSuccess } + } + + override suspend fun hasShare(shareId: ShareId): Boolean { + return filterShares { share -> share.id == shareId }.firstOrNull() != null + } + + override suspend fun hasShareWithKey(shareId: ShareId): Boolean { + return filterShares { share -> share.id == shareId && share.key.isNotEmpty() }.firstOrNull() != null + } + + override suspend fun fetchShare(shareId: ShareId) { + sharesFlow.value = sharesFlow.value + NullableShare(shareId) + } + + override suspend fun deleteShare(shareId: ShareId, locallyOnly: Boolean) { + deleteShares(listOf(shareId)) + } + + override suspend fun deleteShares(shareIds: List) { + sharesFlow.value = sharesFlow.value.filterNot { share -> share.id in shareIds } + } + + override suspend fun createShare( + userId: UserId, + volumeId: VolumeId, + shareInfo: ShareInfo + ): Result { + val id = ShareId(userId, "share-${shareInfo.name}") + sharesFlow.value = sharesFlow.value + NullableShare( + id = id, + volumeId = volumeId, + addressId = shareInfo.addressId, + key = shareInfo.shareKey, + passphrase = shareInfo.sharePassphrase, + passphraseSignature = shareInfo.sharePassphraseSignature, + rootLinkId = shareInfo.rootLinkId, + ) + return Result.success(id) + } + + private fun filterShares( + filter: (Share) -> Boolean + ) = sharesFlow.filter { shares -> shares.any(filter) } + + companion object { + val mainShareId = ShareId(UserId("user-id"), "share-main") + } +} \ No newline at end of file diff --git a/drive/share/data-test/src/test/kotlin/me/proton/core/drive/share/data/test/repository/StubbedShareRepositoryTest.kt b/drive/share/data-test/src/test/kotlin/me/proton/core/drive/share/data/test/repository/StubbedShareRepositoryTest.kt new file mode 100644 index 00000000..6c1959ce --- /dev/null +++ b/drive/share/data-test/src/test/kotlin/me/proton/core/drive/share/data/test/repository/StubbedShareRepositoryTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.share.data.test.repository + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import me.proton.core.domain.arch.DataResult +import me.proton.core.domain.arch.onSuccess +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.share.data.test.nullable.NullableShare +import me.proton.core.drive.share.data.test.repository.StubbedShareRepository.Companion.mainShareId +import me.proton.core.drive.share.domain.entity.ShareId +import me.proton.core.drive.share.domain.entity.ShareInfo +import me.proton.core.drive.volume.domain.entity.VolumeId +import me.proton.core.user.domain.entity.AddressId +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class StubbedShareRepositoryTest { + + private val repository = StubbedShareRepository() + + private val userId = UserId("user-id") + private val volumeId = VolumeId("volume-id") + + @Test + fun getSharesFlow() = runTest { + assertNotNull(repository.getSharesFlow(userId).first()) + } + + + @Test + fun `getSharesFlow volumeId`() = runTest { + assertNotNull(repository.getSharesFlow(userId, volumeId).first()) + } + + @Test + fun `hasShares userId`() = runTest { + assertTrue(repository.hasShares(userId)) + } + + @Test + fun `hasShares userId volumeId`() = runTest { + assertTrue(repository.hasShares(userId, volumeId)) + } + + @Test + fun `hasShare shareId`() = runTest { + assertTrue(repository.hasShare(mainShareId)) + } + + @Test + fun `hasShareWithKey shareId`() = runTest { + assertTrue(repository.hasShareWithKey(mainShareId)) + } + + @Test + fun getShareFlow() = runTest { + assertTrue(repository.getShareFlow(mainShareId).first() is DataResult.Success) + } + + @Test + fun fetchShare() = runTest { + val sharesFlow = repository.getSharesFlow(userId) + val shareId2 = ShareId(userId, "share-2") + repository.fetchShare(shareId2) + sharesFlow.first().onSuccess { shares -> + assertEquals(listOf(mainShareId, shareId2), shares.map { it.id }) + } + } + + @Test + fun createShare() = runTest { + val sharesFlow = repository.getSharesFlow(userId) + val shareIdResult = repository.createShare( + userId, volumeId, ShareInfo( + addressId = AddressId("address-id"), + name = "create", + rootLinkId = "rootLinkId", + shareKey = "shareKey", + sharePassphrase = "sharePassphrase", + sharePassphraseSignature = "sharePassphraseSignature", + passphraseKeyPacket = "", + nameKeyPacket = "" + ) + ) + sharesFlow.first().onSuccess { shares -> + assertEquals(NullableShare( + ShareId(userId, "share-create"), + volumeId, + addressId = AddressId("address-id"), + rootLinkId = "rootLinkId", + key = "shareKey", + passphrase = "sharePassphrase", + passphraseSignature = "sharePassphraseSignature", + ), shares.first { it.id == shareIdResult.getOrThrow() }) + } + } + + @Test + fun deleteShare() = runTest { + val sharesFlow = repository.getSharesFlow(userId) + val shareId2 = ShareId(userId, "share-2") + repository.fetchShare(shareId2) + repository.deleteShare(shareId2, false) + sharesFlow.first().onSuccess { shares -> + assertEquals(listOf(mainShareId), shares.map { it.id }) + } + } +} \ No newline at end of file diff --git a/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/di/ThumbnailModule.kt b/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/di/ThumbnailModule.kt index 1a58c8d0..47ce6941 100644 --- a/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/di/ThumbnailModule.kt +++ b/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/di/ThumbnailModule.kt @@ -23,15 +23,21 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet +import me.proton.core.drive.thumbnail.data.provider.AudioThumbnailProvider import me.proton.core.drive.thumbnail.data.provider.ImageThumbnailProvider import me.proton.core.drive.thumbnail.data.provider.PdfThumbnailProvider import me.proton.core.drive.thumbnail.data.provider.SvgThumbnailProvider +import me.proton.core.drive.thumbnail.data.provider.VideoThumbnailProvider import me.proton.core.drive.thumbnail.domain.usecase.CreateThumbnail @Module @InstallIn(SingletonComponent::class) interface ThumbnailModule { + @Binds + @IntoSet + fun bindsAudioThumbnailProviderIntoList(provider: AudioThumbnailProvider): CreateThumbnail.Provider + @Binds @IntoSet fun bindsImageThumbnailProviderIntoList(provider: ImageThumbnailProvider): CreateThumbnail.Provider @@ -43,4 +49,8 @@ interface ThumbnailModule { @Binds @IntoSet fun bindsSvgThumbnailProviderIntoList(provider: SvgThumbnailProvider): CreateThumbnail.Provider + + @Binds + @IntoSet + fun bindsVideoThumbnailProviderIntoList(provider: VideoThumbnailProvider): CreateThumbnail.Provider } diff --git a/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/AudioThumbnailProvider.kt b/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/AudioThumbnailProvider.kt new file mode 100644 index 00000000..2d543bdf --- /dev/null +++ b/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/AudioThumbnailProvider.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.thumbnail.data.provider + +import android.content.Context +import android.media.ThumbnailUtils +import android.os.Build +import android.util.Size +import dagger.hilt.android.qualifiers.ApplicationContext +import me.proton.core.drive.base.presentation.entity.FileTypeCategory +import java.io.File +import javax.inject.Inject + +@Suppress("BlockingMethodInNonBlockingContext") +class AudioThumbnailProvider @Inject constructor( + @ApplicationContext private val context: Context, +) : FileThumbnailProvider( + context = context, + category = FileTypeCategory.Audio, + prefix = "audio_thumbnail_", +) { + + override fun fileToBitmap(file: File, size: Size) = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ThumbnailUtils.createAudioThumbnail( + file, + size, + null + ) + } else { + null + } +} diff --git a/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/FileThumbnailProvider.kt b/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/FileThumbnailProvider.kt new file mode 100644 index 00000000..9f22e4f0 --- /dev/null +++ b/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/FileThumbnailProvider.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.thumbnail.data.provider + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.util.Size +import me.proton.core.drive.base.domain.entity.Bytes +import me.proton.core.drive.base.presentation.entity.FileTypeCategory +import me.proton.core.drive.base.presentation.entity.toFileTypeCategory +import me.proton.core.drive.base.presentation.extension.compress +import me.proton.core.drive.thumbnail.domain.usecase.CreateThumbnail +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream + +@Suppress("BlockingMethodInNonBlockingContext") +abstract class FileThumbnailProvider( + private val context: Context, + private val category: FileTypeCategory, + private val prefix: String, +) : CreateThumbnail.Provider { + + override suspend fun getThumbnail( + uriString: String, + mimeType: String, + maxWidth: Int, + maxHeight: Int, + maxSize: Bytes, + ): ByteArray? { + if (mimeType.toFileTypeCategory() != category) { + return null + } + var tmpFile: File? = null + return try { + tmpFile = File.createTempFile(prefix, "", context.cacheDir) + val uri = Uri.parse(uriString) + context.contentResolver.openFileDescriptor(uri, "r")?.use { pfd -> + FileInputStream(pfd.fileDescriptor).use { input -> + FileOutputStream(tmpFile).use { output -> + input.copyTo(output) + } + } + } + val bitmap = fileToBitmap(tmpFile, Size(maxWidth, maxHeight)) + bitmap?.compress(maxSize)?.also { + bitmap.recycle() + } + } catch (e: OutOfMemoryError) { + System.gc() + null + } catch (e: IllegalArgumentException) { + null + } finally { + tmpFile?.delete() + } + } + + abstract fun fileToBitmap(file: File, size: Size): Bitmap? +} diff --git a/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/VideoThumbnailProvider.kt b/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/VideoThumbnailProvider.kt new file mode 100644 index 00000000..2ff0cc7d --- /dev/null +++ b/drive/thumbnail/data/src/main/kotlin/me/proton/core/drive/thumbnail/data/provider/VideoThumbnailProvider.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.thumbnail.data.provider + +import android.content.Context +import android.media.ThumbnailUtils +import android.os.Build +import android.provider.MediaStore +import android.util.Size +import dagger.hilt.android.qualifiers.ApplicationContext +import me.proton.core.drive.base.presentation.entity.FileTypeCategory +import java.io.File +import javax.inject.Inject + +@Suppress("BlockingMethodInNonBlockingContext") +class VideoThumbnailProvider @Inject constructor( + @ApplicationContext private val context: Context, +) : FileThumbnailProvider( + context = context, + category = FileTypeCategory.Video, + prefix = "video_thumbnail_", +) { + + override fun fileToBitmap(file: File, size: Size) = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ThumbnailUtils.createVideoThumbnail( + file, + size, + null + ) + } else { + @Suppress("DEPRECATION") + ThumbnailUtils.createVideoThumbnail( + file.absolutePath, + MediaStore.Video.Thumbnails.MINI_KIND + ) + } +} diff --git a/drive/trash/data-test/build.gradle.kts b/drive/trash/data-test/build.gradle.kts new file mode 100644 index 00000000..0a262388 --- /dev/null +++ b/drive/trash/data-test/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 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 . + */ + +plugins { + id("com.android.library") +} + +driveModule( + hilt = true, +) { + api(project(":drive:trash:domain")) + api(project(":drive:base:data-test")) +} diff --git a/drive/trash/data-test/src/main/AndroidManifest.xml b/drive/trash/data-test/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0feeff58 --- /dev/null +++ b/drive/trash/data-test/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/drive/trash/data-test/src/main/kotlin/me/proton/core/drive/trash/data/test/di/TestDriveTrashModule.kt b/drive/trash/data-test/src/main/kotlin/me/proton/core/drive/trash/data/test/di/TestDriveTrashModule.kt new file mode 100644 index 00000000..6ab5cdbb --- /dev/null +++ b/drive/trash/data-test/src/main/kotlin/me/proton/core/drive/trash/data/test/di/TestDriveTrashModule.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.trash.data.test.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.ExperimentalCoroutinesApi +import me.proton.core.drive.trash.data.test.manager.StubbedTrashManager +import me.proton.core.drive.trash.domain.TrashManager + +@Suppress("unused") +@ExperimentalCoroutinesApi +@Module +@InstallIn(SingletonComponent::class) +interface TestDriveTrashModule { + @Binds + fun bindTrashManager(manager: StubbedTrashManager): TrashManager + +} diff --git a/drive/trash/data-test/src/main/kotlin/me/proton/core/drive/trash/data/test/manager/StubbedTrashManager.kt b/drive/trash/data-test/src/main/kotlin/me/proton/core/drive/trash/data/test/manager/StubbedTrashManager.kt new file mode 100644 index 00000000..ec224580 --- /dev/null +++ b/drive/trash/data-test/src/main/kotlin/me/proton/core/drive/trash/data/test/manager/StubbedTrashManager.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.trash.data.test.manager + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import me.proton.core.domain.arch.DataResult +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.base.data.test.manager.StubbedWorkManager +import me.proton.core.drive.link.domain.entity.FolderId +import me.proton.core.drive.link.domain.entity.LinkId +import me.proton.core.drive.linktrash.domain.repository.LinkTrashRepository +import me.proton.core.drive.share.domain.entity.ShareId +import me.proton.core.drive.trash.domain.TrashManager +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +@OptIn(ExperimentalCoroutinesApi::class) +class StubbedTrashManager @Inject constructor( + private val repository: LinkTrashRepository, + private val manager: StubbedWorkManager, +) : TrashManager { + + override suspend fun trash( + userId: UserId, + folderId: FolderId, + linkIds: List + ): DataResult = manager.add("trash", userId, folderId, linkIds) + + override suspend fun restore( + userId: UserId, + shareId: ShareId, + linkIds: List + ): DataResult = manager.add("restore", userId, shareId, linkIds) + + override suspend fun delete( + userId: UserId, + shareId: ShareId, + linkIds: List + ): DataResult = manager.add("delete", userId, shareId, linkIds) + + override fun emptyTrash(userId: UserId, shareId: ShareId) { + manager.add("emptyTrash", userId, shareId) + } + + override fun getEmptyTrashState( + userId: UserId, + shareId: ShareId + ): Flow { + return manager.works.flatMapLatest { works -> + if (works.isNotEmpty()) { + flowOf(TrashManager.EmptyTrashState.TRASHING) + } else { + repository.hasTrashContent(shareId).map { hasTrashContent -> + if (hasTrashContent) { + TrashManager.EmptyTrashState.INACTIVE + } else { + TrashManager.EmptyTrashState.NO_FILES_TO_TRASH + } + } + } + } + + } +} \ No newline at end of file diff --git a/drive/trash/domain/build.gradle.kts b/drive/trash/domain/build.gradle.kts index 53de4047..d129630f 100644 --- a/drive/trash/domain/build.gradle.kts +++ b/drive/trash/domain/build.gradle.kts @@ -26,8 +26,17 @@ driveModule( ) { api(project(":drive:link-trash:domain")) api(libs.androidx.paging.common) - implementation(project(":drive:base:presentation")) implementation(project(":drive:crypto:domain")) implementation(project(":drive:message-queue:domain")) implementation(project(":drive:share:domain")) + + testImplementation(libs.dagger.hilt.android.testing) + add("kaptTest", libs.dagger.hilt.compiler) + + testImplementation(project(":drive:trash:data-test")) + testImplementation(project(":drive:link-trash:data-test")) + testImplementation(project(":drive:link:data-test")) + testImplementation(project(":drive:share:data-test")) } + +configureJacoco() \ No newline at end of file diff --git a/drive/trash/domain/src/main/res/values/strings.xml b/drive/trash/domain/src/main/res/values/strings.xml index 11f41583..c80e8bdf 100644 --- a/drive/trash/domain/src/main/res/values/strings.xml +++ b/drive/trash/domain/src/main/res/values/strings.xml @@ -17,6 +17,6 @@ --> - @string/common_undo_action - @string/common_retry_action + + \ No newline at end of file diff --git a/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/notification/TrashExtraActionProviderTest.kt b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/notification/TrashExtraActionProviderTest.kt new file mode 100644 index 00000000..4e3ce03d --- /dev/null +++ b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/notification/TrashExtraActionProviderTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.trash.domain.notification + +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.link.domain.entity.FileId +import me.proton.core.drive.link.domain.entity.FolderId +import me.proton.core.drive.linktrash.data.test.repository.state +import me.proton.core.drive.linktrash.domain.entity.TrashState +import me.proton.core.drive.linktrash.domain.repository.LinkTrashRepository +import me.proton.core.drive.share.domain.entity.ShareId +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +class TrashExtraActionProviderTest { + @get:Rule + var hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var repository: LinkTrashRepository + + @Inject + lateinit var actionProvider: TrashExtraActionProvider + + private val userId = UserId("user-id") + private val shareId = ShareId(userId, "share-id") + private val folderId = FolderId(shareId, "folder-id") + private val fileId = FileId(shareId, "file-id") + + @Before + fun setUp() { + hiltRule.inject() + } + + @Test + fun `no exception during delete`() = runTest { + assertNull( + actionProvider.provideAction( + DeleteFilesExtra( + userId = userId, + shareId = shareId, + links = listOf(fileId), + ) + ) + ) + } + + @Test + fun `exception during delete`() = runTest { + actionProvider.provideAction( + DeleteFilesExtra( + userId = userId, + shareId = shareId, + links = listOf(fileId), + exception = RuntimeException(), + ) + )?.invoke() + + assertEquals(TrashState.DELETING, repository.state[listOf(fileId)]) + } + + @Test + fun `no exception during empty trash`() = runTest { + assertNull( + actionProvider.provideAction( + EmptyTrashExtra( + userId = userId, + shareId = shareId, + ) + ) + ) + } + + @Test + fun `exception during empty trash`() = runTest { + assertNull( + actionProvider.provideAction( + EmptyTrashExtra( + userId = userId, + shareId = shareId, + exception = RuntimeException(), + + ) + ) + ) + } + + @Test + fun `no exception during restore`() = runTest { + assertNull( + actionProvider.provideAction( + RestoreFilesExtra( + userId = userId, + shareId = shareId, + links = listOf(fileId), + ) + ) + ) + } + + @Test + fun `exception during restore`() = runTest { + actionProvider.provideAction( + RestoreFilesExtra( + userId = userId, + shareId = shareId, + links = listOf(fileId), + exception = RuntimeException(), + ) + )?.invoke() + + assertEquals(TrashState.RESTORING, repository.state[listOf(fileId)]) + } + + @Test + fun `no exception during trash`() = runTest { + actionProvider.provideAction( + TrashFilesExtra( + userId = userId, + folderId = folderId, + links = listOf(fileId), + ) + )?.invoke() + + assertEquals(TrashState.RESTORING, repository.state[listOf(fileId)]) + } + + @Test + fun `exception during trash`() = runTest { + actionProvider.provideAction( + TrashFilesExtra( + userId = userId, + folderId = folderId, + links = listOf(fileId), + exception = RuntimeException(), + ) + )?.invoke() + + assertEquals(TrashState.TRASHING, repository.state[listOf(fileId)]) + } +} \ No newline at end of file diff --git a/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/DefaultValues.kt b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/DefaultValues.kt new file mode 100644 index 00000000..0156452f --- /dev/null +++ b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/DefaultValues.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.trash.domain.usecase + +import me.proton.core.domain.entity.UserId +import me.proton.core.drive.link.domain.entity.FileId +import me.proton.core.drive.link.domain.entity.FolderId +import me.proton.core.drive.share.domain.entity.ShareId + + +internal val userId = UserId("user-id") +internal val shareId = ShareId(userId, "share-id") +internal val folderId = FolderId(shareId, "folder-id") +internal val fileId = FileId(shareId, "file-id") \ No newline at end of file diff --git a/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/DeleteFromTrashTest.kt b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/DeleteFromTrashTest.kt new file mode 100644 index 00000000..49b98979 --- /dev/null +++ b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/DeleteFromTrashTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.trash.domain.usecase + +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import me.proton.core.drive.base.data.test.manager.StubbedWorkManager +import me.proton.core.drive.linktrash.data.test.repository.state +import me.proton.core.drive.linktrash.domain.entity.TrashState +import me.proton.core.drive.linktrash.domain.repository.LinkTrashRepository +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +class DeleteFromTrashTest { + @get:Rule + var hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var repository: LinkTrashRepository + + @Inject + lateinit var manager: StubbedWorkManager + + @Inject + lateinit var deleteFromTrash: DeleteFromTrash + + @Before + fun setUp() { + hiltRule.inject() + } + + + @Test + fun success() = runTest { + deleteFromTrash(userId, fileId) + + assertEquals(TrashState.DELETING, repository.state[listOf(fileId)]) + } + + @Test + fun failing() = runTest { + manager.behavior = StubbedWorkManager.BEHAVIOR_ERROR + + deleteFromTrash(userId, fileId) + + assertEquals(TrashState.TRASHED, repository.state[listOf(fileId)]) + } +} \ No newline at end of file diff --git a/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/EmptyTrashTest.kt b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/EmptyTrashTest.kt new file mode 100644 index 00000000..38a5eb5c --- /dev/null +++ b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/EmptyTrashTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.trash.domain.usecase + +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import me.proton.core.drive.base.data.test.manager.StubbedWorkManager +import me.proton.core.drive.base.data.test.manager.assertHasWork +import me.proton.core.drive.share.data.test.repository.StubbedShareRepository.Companion.mainShareId +import me.proton.core.drive.share.domain.repository.ShareRepository +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +class EmptyTrashTest { + @get:Rule + var hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var shareRepository: ShareRepository + + @Inject + lateinit var manager: StubbedWorkManager + + @Inject + lateinit var emptyTrash: EmptyTrash + + @Before + fun setUp() { + hiltRule.inject() + } + + @Test + fun `with shareId`() = runTest { + emptyTrash(userId, shareId) + + manager.assertHasWork("emptyTrash", userId, shareId) + } + + @Test + @Suppress("DEPRECATION") + fun `without shareId`() = runTest { + emptyTrash(userId) + + manager.assertHasWork("emptyTrash", userId, mainShareId) + } +} \ No newline at end of file diff --git a/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/GetEmptyTrashTest.kt b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/GetEmptyTrashTest.kt new file mode 100644 index 00000000..4a1891fa --- /dev/null +++ b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/GetEmptyTrashTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.trash.domain.usecase + +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import me.proton.core.drive.base.data.test.manager.StubbedWorkManager +import me.proton.core.drive.link.data.test.NullableFile +import me.proton.core.drive.link.domain.entity.FileId +import me.proton.core.drive.share.data.test.repository.StubbedShareRepository.Companion.mainShareId +import me.proton.core.drive.trash.domain.TrashManager +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +class GetEmptyTrashTest { + @get:Rule + var hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var manager: StubbedWorkManager + + @Inject + lateinit var getEmptyTrashState: GetEmptyTrashState + + @Inject + lateinit var sendToTrash: SendToTrash + + @Before + fun setUp() { + hiltRule.inject() + } + + @Test + fun `with share id`() = runTest { + val state = getEmptyTrashState(userId, shareId) + + assertEquals(TrashManager.EmptyTrashState.NO_FILES_TO_TRASH, state.first()) + + sendToTrash(userId, NullableFile(folderId)) + + assertEquals(TrashManager.EmptyTrashState.TRASHING, state.first()) + + manager.execute() + + assertEquals(TrashManager.EmptyTrashState.INACTIVE, state.first()) + + } + + @Test + fun `without share id`() = runTest { + val state = getEmptyTrashState(userId) + + assertEquals(TrashManager.EmptyTrashState.NO_FILES_TO_TRASH, state.first()) + + sendToTrash( + userId = userId, + link = NullableFile( + id = FileId(shareId = mainShareId, id = "file-id"), + parentId = folderId + ) + ) + + assertEquals(TrashManager.EmptyTrashState.TRASHING, state.first()) + + manager.execute() + + assertEquals(TrashManager.EmptyTrashState.INACTIVE, state.first()) + } +} \ No newline at end of file diff --git a/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/RestoreFromTrashTest.kt b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/RestoreFromTrashTest.kt new file mode 100644 index 00000000..8d6bc964 --- /dev/null +++ b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/RestoreFromTrashTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.trash.domain.usecase + +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import me.proton.core.drive.base.data.test.manager.StubbedWorkManager +import me.proton.core.drive.linktrash.data.test.repository.state +import me.proton.core.drive.linktrash.domain.entity.TrashState +import me.proton.core.drive.linktrash.domain.repository.LinkTrashRepository +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +class RestoreFromTrashTest { + @get:Rule + var hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var repository: LinkTrashRepository + + @Inject + lateinit var manager: StubbedWorkManager + + @Inject + lateinit var restoreFromTrash: RestoreFromTrash + + @Before + fun setUp() { + hiltRule.inject() + } + + + @Test + fun success() = runTest { + restoreFromTrash(userId, fileId) + + assertEquals(TrashState.RESTORING, repository.state[listOf(fileId)]) + } + + @Test + fun failing() = runTest { + manager.behavior = StubbedWorkManager.BEHAVIOR_ERROR + + restoreFromTrash(userId, fileId) + + assertEquals(TrashState.TRASHED, repository.state[listOf(fileId)]) + } +} \ No newline at end of file diff --git a/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/SendToTrashTest.kt b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/SendToTrashTest.kt new file mode 100644 index 00000000..e1d8e2c1 --- /dev/null +++ b/drive/trash/domain/src/test/kotlin/me/proton/core/drive/trash/domain/usecase/SendToTrashTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 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 . + */ +package me.proton.core.drive.trash.domain.usecase + +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import me.proton.core.drive.base.data.test.manager.StubbedWorkManager +import me.proton.core.drive.link.data.test.NullableFile +import me.proton.core.drive.link.domain.entity.FolderId +import me.proton.core.drive.link.domain.entity.LinkId +import me.proton.core.drive.linktrash.data.test.repository.state +import me.proton.core.drive.linktrash.data.test.repository.stateForLinks +import me.proton.core.drive.linktrash.domain.entity.TrashState +import me.proton.core.drive.linktrash.domain.repository.LinkTrashRepository +import me.proton.core.drive.share.domain.entity.ShareId +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import javax.inject.Inject + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +class SendToTrashTest { + + @get:Rule + var hiltRule = HiltAndroidRule(this) + + @Inject + lateinit var repository: LinkTrashRepository + + @Inject + lateinit var manager: StubbedWorkManager + + @Inject + lateinit var sendToTrash: SendToTrash + + @Before + fun setUp() { + hiltRule.inject() + } + + @Test + fun `same folder`() = runTest { + val link1 = NullableFile(folderId, "file-1") + val link2 = NullableFile(folderId, "file-2") + + sendToTrash(userId, listOf(link1, link2)) + + assertEquals(TrashState.TRASHING, repository.stateForLinks(link1, link2)) + } + + @Test + fun `two folders`() = runTest { + + val share1 = ShareId(userId, "share-1") + val share2 = ShareId(userId, "share-2") + val folder1 = FolderId(share1, "folder-1") + val folder2 = FolderId(share2, "folder-2") + val link1 = NullableFile(folder1, "file-1") + val link2 = NullableFile(folder2, "file-2") + + sendToTrash(userId, listOf(link1, link2)) + + assertEquals(TrashState.TRASHING, repository.stateForLinks(link1)) + assertEquals(TrashState.TRASHING, repository.stateForLinks(link2)) + } + + @Test + fun failing() = runTest { + manager.behavior = StubbedWorkManager.BEHAVIOR_ERROR + + sendToTrash(userId, NullableFile(folderId)) + + assertEquals(emptyMap(), repository.state) + } +} \ No newline at end of file diff --git a/drive/trash/domain/src/test/resources/robolectric.properties b/drive/trash/domain/src/test/resources/robolectric.properties new file mode 100644 index 00000000..2484ede0 --- /dev/null +++ b/drive/trash/domain/src/test/resources/robolectric.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 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 . +# + +application = dagger.hilt.android.testing.HiltTestApplication \ No newline at end of file diff --git a/drive/trash/presentation/build.gradle.kts b/drive/trash/presentation/build.gradle.kts new file mode 100644 index 00000000..bdc422d4 --- /dev/null +++ b/drive/trash/presentation/build.gradle.kts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021-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 . + */ + +plugins { + id("com.android.library") +} + +driveModule { + implementation(project(":drive:trash:domain")) + implementation(project(":drive:base:presentation")) +} diff --git a/drive/trash/presentation/src/main/AndroidManifest.xml b/drive/trash/presentation/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e9c81fc9 --- /dev/null +++ b/drive/trash/presentation/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/drive/trash/presentation/src/main/res/values/strings.xml b/drive/trash/presentation/src/main/res/values/strings.xml new file mode 100644 index 00000000..11f41583 --- /dev/null +++ b/drive/trash/presentation/src/main/res/values/strings.xml @@ -0,0 +1,22 @@ + + + + @string/common_undo_action + @string/common_retry_action + \ No newline at end of file diff --git a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/GetBlocksUploadUrlWorker.kt b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/GetBlocksUploadUrlWorker.kt index df1cc1a6..9757d8b1 100644 --- a/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/GetBlocksUploadUrlWorker.kt +++ b/drive/upload/data/src/main/kotlin/me/proton/core/drive/upload/data/worker/GetBlocksUploadUrlWorker.kt @@ -122,13 +122,18 @@ class GetBlocksUploadUrlWorker @AssistedInject constructor( else latest + continuations.subList(requests.size, continuations.size) } }.also { continuations -> - WorkContinuation.combine(continuations) - .then( + if (continuations.isNotEmpty()) { + WorkContinuation.combine(continuations) + .then( + UpdateRevisionWorker.getWorkRequest(userId, uploadFileLinkId, uploadTag) + ) + } else { + workManager.beginWith( UpdateRevisionWorker.getWorkRequest(userId, uploadFileLinkId, uploadTag) ) - .then( - UploadSuccessCleanupWorker.getWorkRequest(userId, uploadFileLinkId, uploadTag) - ) + }.then( + UploadSuccessCleanupWorker.getWorkRequest(userId, uploadFileLinkId, uploadTag) + ) .enqueue() } } diff --git a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/extension/InputStream.kt b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/extension/InputStream.kt index 78335263..3feb49d7 100644 --- a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/extension/InputStream.kt +++ b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/extension/InputStream.kt @@ -18,9 +18,14 @@ package me.proton.core.drive.upload.domain.extension import me.proton.core.drive.base.domain.entity.Bytes +import me.proton.core.drive.base.domain.log.LogTag import me.proton.core.drive.upload.domain.outputstream.MultipleFileOutputStream +import me.proton.core.util.kotlin.CoreLogger import java.io.File import java.io.InputStream +import java.security.DigestInputStream +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException fun InputStream.saveToBlocks( destinationFolder: File, @@ -29,3 +34,18 @@ fun InputStream.saveToBlocks( MultipleFileOutputStream(destinationFolder, blockMaxSize) .apply { use { outputStream -> copyTo(outputStream) } } .files + +internal fun InputStream.injectMessageDigests(algorithms: List): Pair> { + val messageDigests = algorithms.mapNotNull { algorithm -> + try { + MessageDigest.getInstance(algorithm) + } catch (e: NoSuchAlgorithmException) { + CoreLogger.i(LogTag.UPLOAD, e, "Algorithm not supported") + null + } + } + val digestsInputStream = messageDigests.fold(this) { acc, messageDigest -> + DigestInputStream(acc, messageDigest) + } + return digestsInputStream to messageDigests +} \ No newline at end of file diff --git a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt index 24b41fe9..cd988fa9 100644 --- a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt +++ b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/SplitFileToBlocksAndEncrypt.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import me.proton.core.drive.base.domain.extension.bytes import me.proton.core.drive.base.domain.extension.size +import me.proton.core.drive.base.domain.extension.toHex import me.proton.core.drive.base.domain.provider.ConfigurationProvider import me.proton.core.drive.base.domain.usecase.GetSignatureAddress import me.proton.core.drive.base.domain.util.coRunCatching @@ -37,17 +38,19 @@ import me.proton.core.drive.key.domain.usecase.GetNodeKey import me.proton.core.drive.link.domain.entity.Link.Companion.THUMBNAIL_INDEX import me.proton.core.drive.link.domain.entity.Link.Companion.THUMBNAIL_NAME import me.proton.core.drive.linkupload.domain.entity.UploadBlock +import me.proton.core.drive.linkupload.domain.entity.UploadDigests import me.proton.core.drive.linkupload.domain.entity.UploadFileLink import me.proton.core.drive.linkupload.domain.entity.UploadState import me.proton.core.drive.linkupload.domain.factory.UploadBlockFactory +import me.proton.core.drive.linkupload.domain.usecase.UpdateDigests import me.proton.core.drive.linkupload.domain.usecase.UpdateManifestSignature import me.proton.core.drive.linkupload.domain.usecase.UpdateUploadState import me.proton.core.drive.thumbnail.domain.usecase.CreateThumbnail import me.proton.core.drive.upload.domain.extension.blockFile +import me.proton.core.drive.upload.domain.extension.injectMessageDigests import me.proton.core.drive.upload.domain.extension.saveToBlocks import me.proton.core.drive.upload.domain.provider.FileProvider import me.proton.core.drive.upload.domain.resolver.UriResolver -import me.proton.core.util.kotlin.takeIfNotEmpty import java.io.File import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -70,6 +73,7 @@ class SplitFileToBlocksAndEncrypt @Inject constructor( private val updateManifestSignature: UpdateManifestSignature, private val fileProvider: FileProvider, private val getSignatureAddress: GetSignatureAddress, + private val updateDigests: UpdateDigests, ) { suspend operator fun invoke( uploadFileLink: UploadFileLink, @@ -77,9 +81,16 @@ class SplitFileToBlocksAndEncrypt @Inject constructor( shouldDeleteSource: Boolean = false, coroutineContext: CoroutineContext = Job() + Dispatchers.IO, ): Result = coRunCatching(coroutineContext) { + val (unencryptedBlocks, digests) = uploadFileLink.splitUriToBlocks( + uriString = uriString + ) + updateDigests( + uploadFileLinkId = uploadFileLink.id, + digests = digests + ) encryptBlocks( uploadFileLink = uploadFileLink, - unencryptedBlocks = uploadFileLink.splitUriToBlocks(uriString), + unencryptedBlocks = unencryptedBlocks, uriString = uriString, coroutineContext = coroutineContext, ).getOrThrow().also { @@ -107,14 +118,14 @@ class SplitFileToBlocksAndEncrypt @Inject constructor( signKey = addressKey, coroutineContext = coroutineContext, ) + - listOfNotNull( - getThumbnailUploadBlock( - uploadFileContentKey = uploadFileContentKey, - signKey = addressKey, - uriString = uriString, - coroutineContext = coroutineContext, + listOfNotNull( + getThumbnailUploadBlock( + uploadFileContentKey = uploadFileContentKey, + signKey = addressKey, + uriString = uriString, + coroutineContext = coroutineContext, + ) ) - ) addUploadBlocks(uploadBlocks) updateManifestSignature( uploadFileLinkId = id, @@ -146,13 +157,23 @@ class SplitFileToBlocksAndEncrypt @Inject constructor( fileKey = uploadFileKey, ).getOrThrow() - private suspend fun UploadFileLink.splitUriToBlocks(uriString: String): List = + private suspend fun UploadFileLink.splitUriToBlocks( + uriString: String, + ): Pair, UploadDigests> = uriResolver.useInputStream(uriString) { inputStream -> - inputStream.saveToBlocks( + val (digestsInputStream, messageDigests) = inputStream.injectMessageDigests( + algorithms = configurationProvider.digestAlgorithms, + ) + val files = digestsInputStream.saveToBlocks( destinationFolder = getBlockFolder(userId, this).getOrThrow(), blockMaxSize = configurationProvider.blockMaxSize, - ).takeIfNotEmpty() - } ?: listOf(File(getBlockFolder(userId, this).getOrThrow(), "empty")) + ) + val uploadDigests = messageDigests + .associate { messageDigest -> + messageDigest.algorithm to messageDigest.digest().toHex() + }.let(::UploadDigests) + files to uploadDigests + } ?: (emptyList() to UploadDigests()) private suspend fun List.encryptBlocksAndDeleteSourceFiles( uploadFileContentKey: ContentKey, diff --git a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/UpdateRevision.kt b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/UpdateRevision.kt index 802ae42b..97d5683b 100644 --- a/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/UpdateRevision.kt +++ b/drive/upload/domain/src/main/kotlin/me/proton/core/drive/upload/domain/usecase/UpdateRevision.kt @@ -17,8 +17,8 @@ */ package me.proton.core.drive.upload.domain.usecase -import me.proton.core.drive.base.domain.util.coRunCatching import me.proton.core.drive.base.domain.usecase.GetSignatureAddress +import me.proton.core.drive.base.domain.util.coRunCatching import me.proton.core.drive.crypto.domain.usecase.link.EncryptAndSignXAttr import me.proton.core.drive.file.base.domain.usecase.CreateXAttr import me.proton.core.drive.file.base.domain.usecase.UpdateRevision @@ -73,6 +73,7 @@ class UpdateRevision @Inject constructor( .filterNot { uploadBlock -> uploadBlock.isThumbnail } .map { uploadBlock -> uploadBlock.rawSize }, mediaResolution = uploadFileLink.mediaResolution, + digests = uploadFileLink.digests.values, ), ).getOrThrow() ).getOrThrow() diff --git a/drive/upload/domain/src/test/kotlin/me/proton/core/drive/upload/domain/extension/InputStreamKtTest.kt b/drive/upload/domain/src/test/kotlin/me/proton/core/drive/upload/domain/extension/InputStreamKtTest.kt new file mode 100644 index 00000000..11281196 --- /dev/null +++ b/drive/upload/domain/src/test/kotlin/me/proton/core/drive/upload/domain/extension/InputStreamKtTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 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 . + */ + +package me.proton.core.drive.upload.domain.extension + +import me.proton.core.drive.base.domain.extension.toHex +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.security.MessageDigest + + +@RunWith(RobolectricTestRunner::class) +class InputStreamKtTest { + + @Test + fun injectDigests_sha1() { + val (inputStream, digests) = "".byteInputStream().injectMessageDigests(listOf("SHA1")) + + inputStream.readAllBytes() + + assertEquals(sha1_empty, digests.first().digest().toHex()) + } + + @Test + fun injectDigests_unsupported() { + val (_, digests) = "".byteInputStream().injectMessageDigests(listOf("unsupported")) + + assertEquals(emptyList(), digests) + } +} + +private const val sha1_empty = "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709" diff --git a/drive/user/presentation/src/main/kotlin/me/proton/core/drive/user/presentation/storage/Storage.kt b/drive/user/presentation/src/main/kotlin/me/proton/core/drive/user/presentation/storage/Storage.kt index 73cca91b..ee55ab30 100644 --- a/drive/user/presentation/src/main/kotlin/me/proton/core/drive/user/presentation/storage/Storage.kt +++ b/drive/user/presentation/src/main/kotlin/me/proton/core/drive/user/presentation/storage/Storage.kt @@ -61,7 +61,7 @@ fun StorageIndicator( Row { Icon( painter = painterResource(CorePresentation.drawable.ic_proton_cloud), - tint = ProtonTheme.colors.iconNorm, + tint = ProtonTheme.colors.iconWeak, contentDescription = null, ) Text( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1f044bfe..b75a7175 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ material = "1.6.1" androidx-activity = "1.5.0" androidx-annotation = "1.4.0" androidx-appCompat = "1.4.2" +androidx-biometric = "1.2.0-alpha05" androidx-compose = "1.3.2" androidx-compose-compiler = "1.3.2" androidx-compose-foundation = "1.3.1" @@ -29,11 +30,11 @@ androidx-work = "2.7.1" # Coil coil = "1.4.0" # Core -core = "9.9.3" +core = "9.12.0" # Dagger dagger = "2.44.2" # Gradle -android-gradle-plugin = "7.3.1" +android-gradle-plugin = "7.4.1" proton-detekt-plugin = "1.1.2" #Android tools android-tools = "1.1.5" @@ -54,6 +55,8 @@ retrofit = "2.9.0" # Test junit = "4.13.2" mockk = "1.12.2" +robolectric = "4.9.2" +fusion = "0.9.50" [libraries] # Gradle @@ -73,11 +76,15 @@ accompanist-pager = { module = "com.google.accompanist:accompanist-pager", versi accompanist-systemUiController = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } accompanist-swipeRefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } +accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" } # AndroidX ## Activity androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } +## Biometric +androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "androidx-biometric" } +androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "androidx-biometric" } ## Compose androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidx-compose-foundation" } androidx-compose-foundationLayout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "androidx-compose-foundation" } @@ -126,22 +133,37 @@ coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" } # Core core-account = { module = "me.proton.core:account", version.ref = "core" } +core-account-data = { module = "me.proton.core:account-data", version.ref = "core" } core-accountManager = { module = "me.proton.core:account-manager", version.ref = "core" } +core-accountManager-data = { module = "me.proton.core:account-manager-data", version.ref = "core" } +core-accountManager-domain = { module = "me.proton.core:account-manager-domain", version.ref = "core" } core-auth = { module = "me.proton.core:auth", version.ref = "core" } +core-auth-domain = { module = "me.proton.core:auth-domain", version.ref = "core" } core-challenge = { module = "me.proton.core:challenge", version.ref = "core" } +core-challenge-data = { module = "me.proton.core:challenge-data", version.ref = "core" } core-country = { module = "me.proton.core:country", version.ref = "core" } core-crypto = { module = "me.proton.core:crypto", version.ref = "core" } +core-crypto-android = { module = "me.proton.core:crypto-android", version.ref = "core" } core-cryptoCommon = { module = "me.proton.core:crypto-common", version.ref = "core" } core-cryptoValidator = { module = "me.proton.core:crypto-validator", version.ref = "core" } core-data = { module = "me.proton.core:data", version.ref = "core" } core-dataRoom = { module = "me.proton.core:data-room", version.ref = "core" } core-domain = { module = "me.proton.core:domain", version.ref = "core" } core-eventManager = { module = "me.proton.core:event-manager", version.ref = "core" } +core-eventManager-data = { module = "me.proton.core:event-manager-data", version.ref = "core" } core-featureFlag = { module = "me.proton.core:feature-flag", version.ref = "core" } +core-featureFlag-data = { module = "me.proton.core:feature-flag-data", version.ref = "core" } core-humanVerification = { module = "me.proton.core:human-verification", version.ref = "core" } +core-humanVerification-data = { module = "me.proton.core:human-verification-data", version.ref = "core" } core-key = { module = "me.proton.core:key", version.ref = "core" } +core-key-data = { module = "me.proton.core:key-data", version.ref = "core" } +core-key-domain = { module = "me.proton.core:key-domain", version.ref = "core" } core-network = { module = "me.proton.core:network", version.ref = "core" } +core-network-domain = { module = "me.proton.core:network-domain", version.ref = "core" } +core-observability = { module = "me.proton.core:observability", version.ref = "core" } +core-observability-data = { module = "me.proton.core:observability-data", version.ref = "core" } core-payment = { module = "me.proton.core:payment", version.ref = "core" } +core-payment-data = { module = "me.proton.core:payment-data", version.ref = "core" } # core-payment-iap = { module = "me.proton.core:payment-iap", version.ref = "core" } core-plan = { module = "me.proton.core:plan", version.ref = "core" } core-presentation = { module = "me.proton.core:presentation", version.ref = "core" } @@ -152,7 +174,10 @@ core-test-android-instrumented = { module = "me.proton.core:test-android-instrum core-test-kotlin = { module = "me.proton.core:test-kotlin", version.ref = "core" } core-test-quark = { module = "me.proton.core:test-quark", version.ref = "core" } core-user = { module = "me.proton.core:user", version.ref = "core" } +core-user-data = { module = "me.proton.core:user-data", version.ref = "core" } +core-user-domain = { module = "me.proton.core:user-domain", version.ref = "core" } core-userSettings = { module = "me.proton.core:user-settings", version.ref = "core" } +core-userSettings-data = { module = "me.proton.core:user-settings-data", version.ref = "core" } core-utilAndroidDagger = { module = "me.proton.core:util-android-dagger", version.ref = "core" } core-utilKotlin = { module = "me.proton.core:util-kotlin", version.ref = "core" } @@ -197,12 +222,14 @@ coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", ve junit = { module = "junit:junit", version.ref = "junit" } mockk-jvm = { module = "io.mockk:mockk", version.ref = "mockk" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric"} +fusion = { module = "me.proton.test:fusion", version.ref = "fusion"} [bundles] -accompanist = ["accompanist-insets", "accompanist-navigation-animation", "accompanist-pager", "accompanist-systemUiController", "accompanist-swipeRefresh", "accompanist-permissions"] -core = ["core-account", "core-accountManager", "core-auth", "core-challenge", "core-country", "core-crypto", "core-cryptoValidator", "core-data", "core-dataRoom", "core-domain", "core-eventManager", "core-featureFlag", "core-humanVerification", "core-key", "core-network", "core-payment", "core-plan", "core-report", "core-presentation", "core-presentation-compose", "core-user", "core-userSettings", "core-utilAndroidDagger", "core-utilKotlin"] -test-android = ["junit", "mockk-android", "coroutines-test", "androidx-test-core-ktx", "androidx-test-runner", "androidx-test-rules", "androidx-compose-ui-test", "androidx-compose-ui-test-junit", "androidx-test-uiautomator", "core-test-android-instrumented" ] -test-jvm = ["junit", "mockk-jvm", "coroutines-test", "core-test-kotlin", "core-test-quark"] +accompanist = ["accompanist-insets", "accompanist-navigation-animation", "accompanist-pager", "accompanist-systemUiController", "accompanist-swipeRefresh", "accompanist-permissions", "accompanist-drawablepainter"] +core = ["core-account", "core-accountManager", "core-auth", "core-challenge", "core-country", "core-crypto", "core-cryptoValidator", "core-data", "core-dataRoom", "core-domain", "core-eventManager", "core-featureFlag", "core-humanVerification", "core-key", "core-network", "core-observability", "core-payment", "core-plan", "core-report", "core-presentation", "core-presentation-compose", "core-user", "core-userSettings", "core-utilAndroidDagger", "core-utilKotlin"] +test-android = ["junit", "mockk-android", "coroutines-test", "androidx-test-core-ktx", "androidx-test-runner", "androidx-test-rules", "androidx-compose-ui-test", "androidx-compose-ui-test-junit", "androidx-test-uiautomator", "core-test-android-instrumented"] +test-jvm = ["junit", "mockk-jvm", "coroutines-test", "core-test-kotlin", "core-test-quark", "robolectric"] [plugins] proton-detekt = { id = "me.proton.core.gradle-plugins.detekt", version.ref = "proton-detekt-plugin" }

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 literal 0 HcmV?d00001 diff --git a/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark_land.webp b/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark_land.webp new file mode 100644 index 0000000000000000000000000000000000000000..93fc85981705011d19336aaa4d7b427d80d3a111 GIT binary patch literal 420748 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